Skip to content

protocol/json_rpc: own and export the response-envelope constructors - #1720

Merged
sergey-shandar merged 10 commits into
mainfrom
claude/open-issue-fix-76mxy8
Aug 26, 2026
Merged

protocol/json_rpc: own and export the response-envelope constructors#1720
sergey-shandar merged 10 commits into
mainfrom
claude/open-issue-fix-76mxy8

Conversation

@sergey-shandar

@sergey-shandar sergey-shandar commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes fjs/protocol/json_rpc/todo/response-constructors.md, deleted here.

json_rpc owns jsonrpc, Id, RpcError and the Response schema — but not,
until now, the two constructors that build a response from them. Both existed
privately here and were re-rolled downstream, four copies in all:

Where What
json_rpc/module.f.mjs:82 errorResponseOf, private
json_rpc/module.f.mjs:113 the success shape, inlined in dispatch
mcp/module.f.mjs:254,257 _errResponse / _okResponse, byte-identical rebuilds
mcp/stdio/module.f.mjs:51,56 parseErrorResponse, internalErrorResponse

The error values already crossed the module boundary — mcp imports
jsonrpc, rpcError, invalidRequest, invalidParams, methodNotFound — so
only the envelope constructors had to be reinvented, and only because they were
private. errorResponseOf and successResponseOf are now exported and every
copy is gone.

The two stdio constants fold onto the same export exactly as the issue
predicted: parseErrorResponse is errorResponseOf(null)(parseError)
evaluated once, and internalErrorResponse is errorResponseOf specialised to
internalError.

Naming

…Of, matching the errorResponseOf that already existed, and pairing with the
successResponse / errorResponse schemas that were already exported — so
each constructor is named after the schema whose shape it builds a value of.

The branch types

SuccessResponse and ErrorResponse are now exported from types.ts, derived
from those same schemas exactly as Response is from response.

The constructors still return the union, deliberately. Returning the branch
was tried: resultStep(e, f) infers its result type from f, and mcpStep's
initialize and gated-method arms are multi-return functions whose early
returns are all error responses and whose last is branch-dependent. The arm's
type then becomes a union of two Effect types differing only in their value
parameter, which does not unify, and tsc rejects it. Widening only the mixed
return is not enough — inference takes the first — so all 13 return sites in
mcpStep would need an explicit annotation. Of the four consumers in the tree,
three (dispatch, mcpStep, the stdio transport) are dispatchers that answer
either arm, and Handle's result type is Response | null. The branch types
are there for a caller that genuinely holds one; both constructors' JSDoc
records the reasoning.

Proof

dispatch covered both constructors transitively already. They are public API
now, so responseOf.success / responseOf.error prove them directly, and each
parses its result back through this module's own response schema. That is the
fact worth pinning: the constructor and the schema describing it cannot drift
apart — a change to either that broke the correspondence would fail here rather
than at some consumer. responseOf.error uses a non-null id so it can see
an id-forcing mutation; errorResponseOf(null) stays covered by
dispatch.invalidRequest.

Reference repairs

Deleting the issue file, and renaming the private constructors out of existence,
would have left stale references behind:

  • json_rpc/todo/effectful-dispatch-skeleton.md cited the deleted issue as "the
    envelope constructors" — now points at the exports themselves.
  • todo/retired-issue-identifiers.md counted i665-mcp as appearing "twice as
    a working link", one of them inside the deleted file. Corrected to once. That
    document's own check script still reports its stated 18 bare citations.
  • _errResponse / _okResponse survived in 18 places: mcp/types.ts:70 (the
    only one in shipped source), 15 in mcp/todo/README.md — whose header asserts
    "the code below is the handler as it stands" — and one snippet each in
    map-step-combinator.md and effectful-dispatch-skeleton.md. All retargeted,
    and the 66D header now also warns against rebuilding a private pair.
  • Two mcp/module.f.mjs line ranges cited by those last two files were already
    wrong before this branch and had drifted 6 further; retargeted to 314-346
    (the whole quoted block, elision included) and 411-414.

Checks

No behaviour change: every construction site builds the same object it built
before.

Changelog:

  • protocol/json_rpc: errorResponseOf is now exported and joined by
    successResponseOf, so protocols layered on this module build response
    envelopes with the owner's constructors instead of their own.
  • protocol/json_rpc/types: new SuccessResponse and ErrorResponse — the two
    branches of the Response union, for a caller that holds one and wants
    .result or .error without an in check.

🤖 Generated with Claude Code

https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg

claude added 2 commits August 26, 2026 16:26
Export errorResponseOf, add successResponseOf, and drop the four
downstream copies in protocol/mcp and its stdio transport.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
functionalscript 30c7259 Commit Preview URL

Branch Preview URL
Aug 26 2026, 06:29 PM

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d75c8837d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/protocol/json_rpc/module.f.mjs
Comment thread fjs/protocol/mcp/module.f.mjs
claude and others added 2 commits August 26, 2026 16:49
_errResponse / _okResponse no longer exist; 18 references across the 66D
design, two other todo files and mcp/types.ts named them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4da1f904ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread fjs/protocol/json_rpc/module.f.mjs
claude added 2 commits August 26, 2026 17:03
Name the two branches of the Response union so a caller holding one can
say so. The constructors keep answering the union, which is what every
dispatcher consumer needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The move is exact and neither caller changed. All five base construction sites build the same two shapes in the same key order, expression-for-expression including how jsonrpc is bound. I measured it rather than reading it: 56 cases through mcpStep and dispatch at base and head — success, empty result, all five standard codes plus -32002, ids string/number/null, notification, errors with and without data — comparing Object.keys order, insertion-order stringify, and the stdio wire string and its UTF-8 bytes. Identical in all 56.

On the wire question I raised for myself: stringifyJson = stringify(sort), so stdio sorts keys and field order was never wire-observable there anyway. The stdio proof is byte-identical base to head and still builds its expected lines from independent literals, so it stays an independent oracle.

Mutation testing came out better than equivalence. Ten mutations at base and head, and row 5 goes the good way: forcing id to null in the error constructor survived at base — mcp's own proofs missed it — and is killed at head by dispatch.handlerError plus the stdio proofs, because the one shared expression now sits under the dispatch, mcp and stdio proofs simultaneously. Nothing killed at base survives at head. Row 9 (dropping data from the error schema) survives on both sides — pre-existing gap, not a regression.

Coverage didn't move out of the callers: mcp 82 → 82 asserts, stdio 19 → 19 (file byte-identical), json_rpc 28 → 36. Layering holds — json_rpc's import block is identical base to head, no dependency on fjs/protocol/mcp/** or fjs/mcp/**, and jsonrpc is correctly dropped from both mcp files. The deleted todo's four tasks are all genuinely done, the retargeted references all point at symbols that exist, and I ran retired-issue-identifiers.md's own check script at both revisions — 18 bare citations across 13 files, unchanged, so its count still holds and the i665-mcp correction is right.

One thing to fix:

6455286cc exports two new public types — SuccessResponse and ErrorResponse — and neither changelog/unreleased/1720.md nor the body's Changelog: block mentions them. Both still describe only the two constructors. The file and the body match each other byte-for-byte, so the block isn't malformed; the scope is just understated. The body's fjs test — 3400 is stale too (3402 now).

Two nits, neither blocking. effectful-dispatch-skeleton.md:23 cites mcp/module.f.mjs:264-287 and map-step-combinator.md:26 cites :347-350; both were already wrong at base (264-287 is _noParams/uninitializedState, and mcpStep's body starts at 320) and this PR shifted them 6 further while editing the identifiers inside those very snippets — cheap to fix while the files are open. And the new responseOf.error proof uses id = null as its input, so it alone can't catch an id-forcing mutation; responseOf.success uses id = 1 and does.

Gates: npm test 3398 → 3400 on the branch's own work, 3402 at the current head with main merged in; tsc --noEmit exit 0; purge matched nothing; no casts or try/catch introduced. No **BREAKING CHANGES:** needed — purely additive, no caller output moved. No HTML entities in the body this time.

The changelog entry named only the constructors, not SuccessResponse and
ErrorResponse. Also retargets two mcp line ranges that were already wrong
before this branch, and gives responseOf.error a non-null id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg

Copy link
Copy Markdown
Contributor Author

All three fixed in eeaf549. Thanks for the 56-case differential and the mutation table — row 5 is a better argument for this change than anything in the description, so I've noted it below.

The changelog gap (the blocking one). You're right that 6455286 widened the public surface and neither the entry file nor the body followed. Both now carry a second item naming SuccessResponse and ErrorResponse, and the two are still byte-identical to each other. The body's stale 3400 is now 3402, and the description also gained a short section explaining why the constructors keep returning the union — that reasoning previously existed only in a review thread, which is the wrong place for it.

The two line citations. Fixed rather than left: effectful-dispatch-skeleton.md now cites mcp/module.f.mjs:314-322 (decodeRequest through the notification split) and map-step-combinator.md cites 411-414 (the toolsList projection). You're right that both were wrong at base — and since this PR edited the identifiers inside those snippets, leaving the ranges wrong would have meant touching a quote and knowingly leaving it misaddressed. I left the step/pure spellings alone: map-step-combinator uses that vocabulary generically across all its sites, so changing one snippet would desynchronise it from its siblings.

The id = null proof. Good catch, and it's the same hole your row 5 found from the other side. responseOf.error now builds with errorResponseOf('abc') and asserts r.id === 'abc', so an id-forcing mutation dies there too — and a string id exercises the other arm of Id besides. errorResponseOf(null) stays covered by dispatch.invalidRequest and by both stdio constants, so nothing lost coverage.

On row 5: that a mutation surviving at base dies at head is the concrete payoff of the extraction — five sites collapsing to one expression means one expression sitting under the dispatch, mcp and stdio proofs at once. Row 9 (dropping data from the error schema) I left alone as you suggested — a pre-existing gap in the schema's own coverage, not this cutover's to close.

npx tsc clean, fjs test 3402 pass / 0 fail at eeaf549.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Both findings are closed.

The entry file now carries the second bullet for the type exports, and the body's Changelog: block is byte-identical to it — same md5, same 413 bytes, last section before the trailer. The test count reads 3402 and npm test gives exactly 3402/0.

Both line citations are content-correct now, which I checked by reading the ranges rather than trusting the commit message. effectful-dispatch-skeleton.md:23mcp/module.f.mjs:314-322, and 314 really is const [t, message] = decodeRequest(value), the first line of mcpStep's body, with the invalidRequest arm and the destructure following in order. map-step-combinator.md:26:411-414 is exact, line for line.

The proof change does what it claims: swapping errorResponseOf(null) for errorResponseOf('abc') means the id-forcing mutation now fails responseOf.error by name — I ran it, 3396/6 with that proof among them, where at the previous head it was blind to it. Flipping the new assertion gives a single failure naming its own row, so it is load-bearing.

Nothing else moved on the branch side: git diff 6914572458ae..HEAD -- fjs/protocol/json_rpc/module.f.mjs is empty, so the constructor expressions are exactly what I already verified as an exact move. No casts, no try/catch introduced anywhere in the PR.

Two one-line residuals, neither blocking. The 314-322 range stops at the snippet's fourth line — the elided remainder maps to 323-346, so 314-346 would cover the whole quoted block; the start is exact either way. And both doc snippets write step/pure where the source says ioStep/pureOk — that predates this branch (origin/main has the same naming), so not yours, just adjacent.

Gates: npm test 3402/0, tsc --noEmit exit 0, purge matched nothing.

…r lines

The snippet's elided remainder runs to 346; 314-322 stopped short.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg

Copy link
Copy Markdown
Contributor Author

Thanks — and I took the first residual rather than leaving it, in 8c7ee62: effectful-dispatch-skeleton.md now cites mcp/module.f.mjs:314-346.

You're right about where the block ends. I read the range rather than trusting the arithmetic: the snippet's last two quoted lines, return pure(null) and its closing brace, are lines 345 and 346, with 323-344 the elided notifications/initialized body. So 314-322 covered the four lines before the elision and stopped. Since half this PR is about citations that stopped being true, shipping one that stops four lines in would have been a poor note to end on — and it costs a re-review, which is the right way round.

The second residual I'm leaving deliberately, and it isn't only that it predates the branch. map-step-combinator.md uses step/pure as its vocabulary throughout — that's the shape the whole issue is about converting, and it spells its fjs/cli, fjs/website and fjs/djs sites the same way. Rewriting this one snippet to ioStep/pureOk would make it the only site quoted in the module's real spelling and desynchronise it from its siblings, which reads as an inconsistency rather than a correction. It wants one pass over the whole document, by whoever does that conversion — the file's own Tasks list already has fjs/protocol/mcp as a checkbox, so it will be reread then.

Head is 8c7ee62; the only change since the head you reviewed is that one line-range in a todo/ file. tsc --noEmit exit 0.


Generated by Claude Code

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit 7bf9849 Aug 26, 2026
19 checks passed

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retarget is right, verified line for line rather than by trusting the message: 314-346 maps exactly — 314 the decodeRequest destructure, 315-317 the error arm, 318 the destructure, 322 the id === undefined guard, 323-344 the body the snippet elides as { ... }, 345 return pureOk(null), 346 the closing brace. The sibling retarget in map-step-combinator.md (411-414) is exact too.

But there is a third stale citation in the same document, and this one the PR did cause. Line 12:

Pure `dispatch` (`fjs/protocol/json_rpc/module.f.mjs:95-110`):

At head, 95-110 is the JSDoc for errorResponseOf/successResponseOf — the constructors this PR adds. dispatch moved to 126, and the quoted snippet spans 126-141. On origin/main, 95-110 is exactly right; inserting the two constructors above dispatch shifted it 31 lines. It was already wrong at 739e54ecd, the SHA I approved — eeaf549ce fixed the two mcp citations and missed this one, 8c7ee62df fixed an endpoint. Third of three, and now the only one left.

Delta is docs-only on the branch side — one commit, one file, one line. The Rust files in the diffstat all arrive via #1699 from main.

On the step/pure naming I flagged: confirmed pre-existing on origin/main in both snippets, so not yours. Worth knowing they're real but different functions — pure at effects/module.f.mjs:113 and step at :342 both exist and differ from pureOk at :268 and ioStep, so a reader can follow them to the wrong place.

Gates: npm test 3402/0 matching the body, tsc --noEmit exit 0, purge matched nothing. Changelog: still byte-identical to the entry file and last before the trailer.

@sergey-shandar
sergey-shandar deleted the claude/open-issue-fix-76mxy8 branch August 26, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants