Found while building #8194's consumer-seam invariant, which enumerates every seam that performs a by-id write on a caller-supplied id and answers a receipt. Four seams exist; three reach the repo's ONE not-found envelope and one does not.
What the seam scan measured
seam [local] packages/mcp/src/stdio-data-bridge.ts:337 update() — by-id update
seam [local] packages/mcp/src/stdio-data-bridge.ts:347 remove() — by-id delete
seam [shared] packages/metadata-protocol/src/protocol.ts:7698 updateData()
seam [shared] packages/metadata-protocol/src/protocol.ts:7712 deleteData()
seam [shared] packages/runtime/src/action-execution.ts:205 callData() — update
seam [shared] packages/runtime/src/action-execution.ts:241 callData() — delete
All six refuse before answering, so existence coherence itself is intact — this is not a silent-success defect. The divergence is the ENVELOPE. packages/mcp/src/stdio-data-bridge.ts:130:
functionrecordNotFound(object: string,id: string): Error{returnnewError(`Record "${id}" not found in "${object}"`);}versus packages/core/src/utils/record-not-found.ts, which sets code = 'RECORD_NOT_FOUND' and status = 404 on the error it returns.
Why that is worth a card
record-not-found.ts's own header is the argument, and it was written against exactly this shape twice already: the factory was "extracted so the READ and the two WRITE paths cannot disagree about it" (#4435), then EXPORTED because callData's fallback "had reinvented this fact three incompatible ways" (#5138), then moved down to @objectstack/core so a third producer could reach it (#7867). Its sentence "re-spelling the envelope there would have been a second not-found envelope" applies here unchanged — this is the fourth spelling.
Concretely, the two MCP transports answer a missing id differently. The HTTP bridge routes its data verbs through callData, which throws recordNotFoundError (code + 404); the stdio bridge throws a bare Error, so a stdio caller gets a message with no machine-readable code and nothing that maps to 404. That is the same two-transports-one-operation divergence class as #8083 (stdio skipped the ADR-0049 exposure gate HTTP applied) and #8266 (the stdio record resource reads through its own seam) — both on this same file, neither covering the error envelope.
The local factory's comment explains why it throws rather than returns (registerObjectTools turns a throw into a tool error), which is right and is not the question here. The question is only which error object it throws.
Suggested fix
Import recordNotFoundError from @objectstack/core and delete the local mint. Worth checking whether @objectstack/mcp already depends on @objectstack/core (metadata-protocol re-exports it too, per packages/metadata-protocol/src/index.ts).
Why this is filed rather than fixed in #8194's PR
#8194's declared file surface is scripts/check-engine-double-contract.mjs only; packages/mcp was explicitly out of scope, so the gate reports the two rows as local rather than reddening on a defect its own PR did not introduce. That gate has a SHARED_ONLY-shaped tightening noted in its header: once this card lands, requiring the shared envelope becomes a one-line change and the seam list is already complete in both directions.
Dedup: searched open issues for the file path, for the envelope factory by name, and for the not-found/MCP divergence wording. The nearest hits are #8266 and #8083, both stdio-vs-HTTP seam divergences on this file, neither about the error envelope.
Refs #8194, #8083, #8266, #7867, #5138, #4435, packages/core/src/utils/record-not-found.ts.
Generated by Claude Code
Found while building #8194's consumer-seam invariant, which enumerates every seam that performs a by-id write on a caller-supplied id and answers a receipt. Four seams exist; three reach the repo's ONE not-found envelope and one does not.
What the seam scan measured
All six refuse before answering, so existence coherence itself is intact — this is not a silent-success defect. The divergence is the ENVELOPE.
packages/mcp/src/stdio-data-bridge.ts:130:versus
packages/core/src/utils/record-not-found.ts, which setscode = 'RECORD_NOT_FOUND'andstatus = 404on the error it returns.Why that is worth a card
record-not-found.ts's own header is the argument, and it was written against exactly this shape twice already: the factory was "extracted so the READ and the two WRITE paths cannot disagree about it" (#4435), then EXPORTED becausecallData's fallback "had reinvented this fact three incompatible ways" (#5138), then moved down to@objectstack/coreso a third producer could reach it (#7867). Its sentence "re-spelling the envelope there would have been a second not-found envelope" applies here unchanged — this is the fourth spelling.Concretely, the two MCP transports answer a missing id differently. The HTTP bridge routes its data verbs through
callData, which throwsrecordNotFoundError(code + 404); the stdio bridge throws a bareError, so a stdio caller gets a message with no machine-readable code and nothing that maps to 404. That is the same two-transports-one-operation divergence class as #8083 (stdio skipped the ADR-0049 exposure gate HTTP applied) and #8266 (the stdio record resource reads through its own seam) — both on this same file, neither covering the error envelope.The local factory's comment explains why it throws rather than returns (
registerObjectToolsturns a throw into a tool error), which is right and is not the question here. The question is only which error object it throws.Suggested fix
Import
recordNotFoundErrorfrom@objectstack/coreand delete the local mint. Worth checking whether@objectstack/mcpalready depends on@objectstack/core(metadata-protocolre-exports it too, perpackages/metadata-protocol/src/index.ts).Why this is filed rather than fixed in #8194's PR
#8194's declared file surface is
scripts/check-engine-double-contract.mjsonly;packages/mcpwas explicitly out of scope, so the gate reports the two rows aslocalrather than reddening on a defect its own PR did not introduce. That gate has aSHARED_ONLY-shaped tightening noted in its header: once this card lands, requiring the shared envelope becomes a one-line change and the seam list is already complete in both directions.Dedup: searched open issues for the file path, for the envelope factory by name, and for the not-found/MCP divergence wording. The nearest hits are #8266 and #8083, both stdio-vs-HTTP seam divergences on this file, neither about the error envelope.
Refs #8194, #8083, #8266, #7867, #5138, #4435,
packages/core/src/utils/record-not-found.ts.Generated by Claude Code