You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
runtime: errorResponseBase never surfaces userMessage, so the dispatcher exit has no author-facing text channel — the one the 5xx prose withhold assumes as its compensation #13241
Found while implementing #12281 (draft PR #13240), which made errorResponseBase withhold the message of every declared 5xx. Out of scope there: that card is fenced to the message ternary, and this is a different field on the envelope.
The gap
The 2026-08-27 maintainer ruling on #12509 (option D), propagated to #12281, says:
errorResponseBase adopts the structural withhold for every declared 5xx message, aligning to /data's rule; the author-facing text channel is userMessage (#9934), never the raw message.
That sentence names userMessage as the channel a producer uses when its refusal text is genuinely addressed to a human — the compensation for the withheld prose. At this exit that channel does not exist.
Measured on origin/main at a81aa9dd:
packages/runtime/src/dispatcher-plugin.ts — errorResponseBase calls buildApiError({ message, httpStatus, code, details, extra }). userMessage appears nowhere in the function. It resolves the thrown error through resolveThrownHttpError, whose result does carry userMessage, and then does not read that field.
packages/runtime/src/http-dispatcher.ts — errorFromThrown, the caught-path sibling, does carry it: ...(thrown.userMessage !== undefined ? { userMessage: thrown.userMessage } : {}).
packages/rest/src/error-response.ts — the /data door carries it too, via boundedDeclaredUserMessage.
packages/spec/src/api/contract.zod.ts — ApiErrorSchema.userMessage is a declared optional field, so the envelope already has the slot.
So of the boundaries that answer with an ADR-0112 error envelope, the dispatcher's throw-transparent exit is the only one that drops the field. buildApiError already supports it through extra, which is how declaredCode reaches the wire from this same function — so the omission is not a structural obstacle.
Before #12281, a producer that declared a 5xx without a code kept its raw prose at this door, so author text reached the caller by accident, through the diagnostic channel. #12281 closes that — correctly, per the ruling. The result is that a declared-5xx producer at this exit now has no way to address the caller: the diagnostic channel is withheld and the author channel is not plumbed.
⚠️This is a latent seam, not a live regression. The #12281 measurement established that the population of declared 5xx reaching this door is empty today, so nothing currently loses text. The consequence is for the next producer, not for today's tree.
packages/runtime/src/dispatcher-plugin.ts (errorResponseBase) · possibly packages/runtime/src/error-envelope.ts (buildApiError) if the shared shape is preferred.
⚠️packages/runtime/src/http-dispatcher.ts is #7898's on-hold trigger file. It is named above only as the sibling that already carries the field — a repair should not need to touch it.
Found while implementing #12281 (draft PR #13240), which made
errorResponseBasewithhold the message of every declared 5xx. Out of scope there: that card is fenced to the message ternary, and this is a different field on the envelope.The gap
The 2026-08-27 maintainer ruling on #12509 (option D), propagated to #12281, says:
That sentence names
userMessageas the channel a producer uses when its refusal text is genuinely addressed to a human — the compensation for the withheld prose. At this exit that channel does not exist.Measured on
origin/mainata81aa9dd:packages/runtime/src/dispatcher-plugin.ts—errorResponseBasecallsbuildApiError({ message, httpStatus, code, details, extra }).userMessageappears nowhere in the function. It resolves the thrown error throughresolveThrownHttpError, whose result does carryuserMessage, and then does not read that field.packages/runtime/src/http-dispatcher.ts—errorFromThrown, the caught-path sibling, does carry it:...(thrown.userMessage !== undefined ? { userMessage: thrown.userMessage } : {}).packages/rest/src/error-response.ts— the/datadoor carries it too, viaboundedDeclaredUserMessage.packages/spec/src/api/contract.zod.ts—ApiErrorSchema.userMessageis a declared optional field, so the envelope already has the slot.So of the boundaries that answer with an ADR-0112 error envelope, the dispatcher's throw-transparent exit is the only one that drops the field.
buildApiErroralready supports it throughextra, which is howdeclaredCodereaches the wire from this same function — so the omission is not a structural obstacle.Why it matters more after #12281
Before #12281, a producer that declared a 5xx without a code kept its raw prose at this door, so author text reached the caller by accident, through the diagnostic channel. #12281 closes that — correctly, per the ruling. The result is that a declared-5xx producer at this exit now has no way to address the caller: the diagnostic channel is withheld and the author channel is not plumbed.
Not established here
userMessageon a 5xx that exits this way. contract: a hook refusal has no way to mark its message user-facing — the console's 403 substitution (ruled in #3821) needs a producer-side opt-in channel #9934 made the field status-agnostic on purpose ("a 400, 403, 409 or 503 refusal may all carry it"), but the throw-transparent dispatcher routes are/analytics/meta,/mcp/skill,/notifications,/i18nand/automation, and none was checked for a producer that marks text.userMessageat this exit, or to make the sharedbuildApiErrorcarry it for every caller so a door cannot forget it again. The second shape matches the "one rule, every door inherits" direction [Decision] Is ADR-0112'sdeclaredCodechannel in scope for 5xx sanitisation at all — and the answer must be applied to all three doors at once #12509 established for the code channel, and would be worth measuring against the otherbuildApiErrorcallers before choosing.Region
packages/runtime/src/dispatcher-plugin.ts(errorResponseBase) · possiblypackages/runtime/src/error-envelope.ts(buildApiError) if the shared shape is preferred.packages/runtime/src/http-dispatcher.tsis #7898's on-hold trigger file. It is named above only as the sibling that already carries the field — a repair should not need to touch it.Refs
codekeeps its prose on/analytics/querywhere/datawithholds it unconditionally #12281 / PR fix(runtime): withhold the message of every declared 5xx at the dispatcher exit (#12281) #13240 — the prose withhold that makes this the only remaining channeldeclaredCodechannel in scope for 5xx sanitisation at all — and the answer must be applied to all three doors at once #12509 — the ruling naminguserMessageas the author-facing channel