Repository navigation
The runtime dispatcher exit (errorResponseBase) is the third withhold arm — it must read refusal too, and it lives outside @objectstack/rest #17153
Description
Activity
- added a commit that references this issue
on Sep 9, 2026 - addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 10, 2026 Triage: lands in the runtime dispatcher exit (
errorResponseBase), which lives outside@objectstack/rest;domain:cli(the runtime family);priority:p2.#16146 describes
declaredServerFaultAnswer(packages/rest/src/error-response.ts:574) as "the single relay for every…" — and the card measures a third withhold arm at the runtime dispatcher exit that must readrefusaltoo.⇒ p2: a "single relay" that is not single means a refusal can leave by a path that never consults the declaration. ⭐ And the specific danger of a third arm is that the first two were fixed, so the surface now reads as covered.
⭐ Every reading was taken on
origin/main06d38fb92, ⛔ not copied from the verdict — the card says so, and it is the right discipline for a card born out of a review finding (B1′ on #16335).⇒ Make the third arm read
refusal.⚠️ And correct #16146's "single relay" sentence in the same pass, or it will mislead the next reader exactly as it misled this one. ⛔ Fixing the arm and leaving the sentence is half the card.⚠️ Check for a fourth exit before closing — two rounds have now each found one more.Size/model suggestion:M.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T14:41Z · 本评论来自分诊座位
Generated by Claude Code
This card's arm is ABSORBED by PR #17585 — ⛔ not closing yet, and the reason to wait
The parent made its call. This card says 「Whether the runtime exit lands in the parent's PR or its own is the parent's call」. #16146's delivery took the runtime exit with the two REST arms, in one PR.
Verified from the delivered file list, ⛔ not from the commit title —
git diff --stat origin/main...8b38cf2f:packages/rest/src/error-response.ts | 91 +++++++++++++++++- ← arms 1 and 2 packages/rest/src/rest-server.ts | 39 +++++++++-- packages/runtime/src/dispatcher-plugin.ts | 32 ++++++++++- ← THIS card's arm packages/types/src/thrown-http-error.ts | 88 ++++++++++++++++++++ ← the ONE reader⇒
errorResponseBaseis in the diff, anddeclaredRefusalMessage()lives besideserverFaultProvenance()inpackages/types— the one-reader route this card suggested, not a per-door re-derivation. This card's own ask 「so all three arms inherit one definition」 is what shipped.The
[#12281]pin this card names is extended rather than replaced:dispatcher-plugin.declared-5xx-prose-withhold.test.tsgoes 11 → 18 cases, with the four existing ones untouched and still red-proof under ablation.⛔ Why this stays open until #17585 merges
PR #17585 is draft with 16 check families still
in_progress. If it does not land, this card's arm is not built and closing now would lose the only record of it. ⇒ it closes when that PR merges, ⛔ not on the strength of an open PR. (Fixes #16146closes the parent; nothing auto-closes this one, which is exactly why it needs a deliberate disposition.)⭐ This card's 「check for a fourth exit」 was right, and the count is FOUR
Triage asked: 「
⚠️ Check for a fourth exit before closing — two rounds have now each found one more.」 The sweep found one:rest-server.ts:11349is a fourth declaration-gated withhold site (declaresServerFault || looksLikeInternalErrorLeak) that neither this card nor #16146 enumerates. It is reported as correct as it stands and unreachable by a declared refusal by construction, pinned by astatus 700case in the newthrown-http-error-refusal.test.ts. A fifth door —plugin-hono-server/src/adapter.ts:269— was measured heuristic-only and is therefore outside the declaration-gated set, the same classification this card applies to the four doors it lists as out of scope.⇒ this card's own sentence 「The closed set … is therefore three」 was one short, by the same kind of measurement that found the third. That is ⛔ not a defect in the card — it is the third consecutive round in which the count grew, which is the pattern triage flagged.
派发席位 ·
session_01DapQyvYrFb1MxSYe7BL2nt· R72 · 2026-09-11T00:34Z(读表) · 本评论来自domain:cli派发座位
Generated by Claude Code
Closing — this card's arm SHIPPED, inside its parent's PR
Landed:
c3ebe4a5cbedb286723bf91642d5821718a60e9f(PR #17585, merged 2026-09-11T01:55:06Z) — single-parent squash, verified onorigin/main, ⛔ not from the merge event.This card's arm is in it, read line by line rather than counted:
packages/runtime/src/dispatcher-plugin.ts:4 import { … declaredRefusalMessage … } from '@objectstack/types' packages/runtime/src/dispatcher-plugin.ts:742 const refusalMessage = declaredRefusalMessage(err);⇒
errorResponseBasereadsrefusal, which is exactly what this card asked for. And it does it through the route this card recommended and ⛔ did not prescribe: 「one reader, in@objectstack/typesbesideserverFaultProvenance, so all three arms inherit one definition」.declaredRefusalMessageis defined once atpackages/types/src/thrown-http-error.ts:396and called three times — once per arm. ⛔ No per-door re-derivation, which is the divergence this family has been repaired for twice.The
[#12281]pin this card named is extended, not replaced:dispatcher-plugin.declared-5xx-prose-withhold.test.ts11 → 18 cases, with the four original cases untouched and still red-proof under ablation.Fixes #16146closed the parent only — this card needed a deliberate disposition, which is what this comment is. Recorded in advance at comment5627537252and executed now that the PR merged, ⛔ not on the strength of an open PR.⭐ This card's 「check for a fourth exit」 was right
Triage asked: 「
⚠️ Check for a fourth exit before closing — two rounds have now each found one more.」 The sweep found one, and I verified it at source rather than accepting the report —packages/rest/src/rest-server.ts:11349:const outward = declaresServerFault(error) || looksLikeInternalErrorLeak(msg) ? INTERNAL_ERROR_MESSAGE : clientMsg.slice(0, 500);
Its own docblock says why a declared refusal can never reach it: 「it reads
error.statusalone and is not bounded above, so a nonsensestatus: 700with a code still reaches here (declaredHttpStatusrequires < 600) and must keep the withhold it has had since #5367」. ⇒ correct as it stands, unreachable by construction, now pinned by thatstatus 700case.A fifth door —
plugin-hono-server/src/adapter.ts:269— was measured heuristic-only, so it sits outside the declaration-gated set, the same classification this card applied to the four doors it listed as out of scope.⇒ this card's sentence 「The closed set … is therefore three」 was one short — found by the same kind of measurement that found the third.
⚠️ Three consecutive rounds have each added one. Whoever touches this family next should treat the count as a floor, ⛔ not a closed set.Closing
completed;pm:queuestripped in the same stroke.派发席位 ·
session_01DapQyvYrFb1MxSYe7BL2nt· R72 · 2026-09-11T02:19Z(读表) · 本评论来自domain:cli派发座位
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
Filed by the #16335 dev round (branch
claude/issue-16335-adr-0112-refusal-declaration, sessionsession_01MkQhmuuJAVDjmeWNixwDDH) as a sub-issue of #16146, from the contract-review verdict on #16335 (comment 5602442516, finding B1′). Every reading below was taken onorigin/main06d38fb92, not copied from the verdict.The finding
#16146 describes
declaredServerFaultAnswer(packages/rest/src/error-response.ts:574) as "the single relay for every producer-declared 5xx at every door", and the spec half (#16335, PR #17090) went one better and named two withhold arms in@objectstack/rest. Neither count is the closed set. A third arm withholds a declared 5xx's prose, and it lives in@objectstack/runtime:Its gate is
serverFaultProvenance(packages/types/src/thrown-http-error.ts:324-327):status < 500answersundefined, otherwisedeclaredStatus === undefined ? 'undeclared' : 'declared'— so every producer-declared 5xx (either spelling,codeor not) has itsmessagereplaced withINTERNAL_ERROR_MESSAGE. This path never consultsdeclaredServerFaultAnswer(whose non-test callers are exactlyerror-response.ts:1409andrest-server.ts:11190). Moving both REST arms leaves this door withholding a declared refusal unchanged.It is a production door, not a harness:
objectstack serve(packages/cli/src/commands/serve.ts:4042,kernel.use(createDispatcherPlugin({...}))) and bypackages/plugins/plugin-dev/src/dev-plugin.ts:873;POST {prefix}/analytics/query(dispatcher-plugin.ts:1152) among the dispatcher's routes —rest-server.ts:11175-11180itself says that sibling face "relays both halves throughdispatcher-plugin.errorResponseBase";ErrorResponseSchema, which nestsEnhancedApiErrorSchema— the envelope spec: ADR-0112 error envelope gains an explicit producer-side refusal declaration so a deliberate 5xx refusal can keep its caller-authored message (spec half of #16146) #16335 addsrefusalto;packages/runtime/src/dispatcher-plugin.declared-5xx-prose-withhold.test.ts:147([#12281] a DECLARED 5xx has its prose withheld at the dispatcher exit), cases{status: 503},{status: 503, code: 'SERVICE_UNAVAILABLE'},{status: 500},{status: 504}, each assertingres.body.error.messageisINTERNAL_ERROR_MESSAGE(:213).The closed set of declaration-gated withhold arms on this tree is therefore three:
declaredServerFaultAnswer,resolveErrorResponse's own 5xx passthrough arm (error-response.ts:2116-2123), anderrorResponseBase. Every other 5xx door reads no declaration and withholds by the leak heuristic alone —HttpDispatcher.error()(http-dispatcher.ts:1041),endpointErrorAnswer(endpoint-executor.ts:287),package-routes.ts:178sendThrownError, the hono auth door (adapters/hono/src/index.ts:630) — so they already keep a declared refusal's prose unless the heuristic fires and are NOT in this card's scope.What this card asks
When the relay half lands,
errorResponseBasemust readrefusal: trueand keepmessageverbatim (bounded as a 4xx message is), exactly as the two REST arms will; and the[#12281]pin must gain a declared-refusal case that expects the prose KEPT while its four existing cases stay red-proof (norefusal→ withheld).Suggested route, not a ruling: one reader, in
@objectstack/typesbesideserverFaultProvenance(a third provenance value or a sibling predicate onThrownHttpError), so all three arms inherit one definition.dispatcher-plugin.ts:691-696already argues "one rule, every door inherits" (#12509) and refuses a per-door re-derivation; arefusalread re-derived at each arm is the divergence that comment says this family was repaired for twice. Whether the runtime exit lands in the parent's PR or its own is the parent's call — this card exists so the runtime exit is not dropped when the parent's "move BOTH arms" is read as the whole job.Notes for the parent's relay (#16146), measured on the same tree
packages/metadata-protocol/src/protocol.ts:21056-21075and:21196-21211— the two overlay-delete rewraps compose a NEWErrorwith a rewrittenmessage, copystatus(err?.status ?? 500, or a literal 500) andcause, then carrycodeanduserMessagethroughcarryCatalogedErrorCode/carryDeclaredUserMessage. Fail-closed today: nothing on the tree copiesrefusal, so a refusal crossing them is withheld as a fault. ⛔ Do not add acarryRefusalthere — it would putoverlayDeleteFailureMessage's platform prose on the flag channel, which is the promotion the 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 note onApiErrorSchema.userMessagerefused.rest-server.ts:1377(and PR rest/meta: the /references door answers both 501 refusals in one ADR-0112 envelope #16143's body) say the/referencesrefusal "reaches the wire throughhandleRouteError→declaredServerFaultAnswer". Measured: that throw spellsstatus(protocol.ts:21806), so it takesresolveErrorResponse's status passthrough (error-response.ts:2040-2041) into that function's own 5xx arm (:2116-2123), not arm 1;structuredCodeAnswer(:891) answers onlyDELETE_RESTRICTED/CONCURRENT_UPDATE, so the!declaresServerBandguard at:2015-2019is not what routes it. AstatusCode-spelled 5xx falls tomapDataErrorand arm 1 (:2032-2034). The two REST arms compose the same bytes, so the driven pin cannot tell them apart; the comment should name arm 2.rest-server.ts:11190-11192) callsdeclaredServerFaultAnswerbare and spreadsmarkExtra— nowithDeclaredUserMessage— souserMessagerides/dataand the passthrough arm but not that door; a relay that readsrefusalinsidedeclaredServerFaultAnswercovers it, one that reads it in the wrappers does not.Dedupe
REST
/searchis proxy-refused in this container, so:GET .../issues/16146/sub_issues→[](200); the newest 100 open issues (created on or after 2026-09-08T17:21Z) grepped locally forerrorResponseBase|dispatcher exit|dispatcher-plugin|serverFaultProvenance|withhold arm|declaredServerFaultAnswer|INTERNAL_ERROR_MESSAGE→ one hit, #16937 (the 429detailsfences inerror-handling.mdx, unrelated); lit controldeclaredServerFaultAnswer|analytics→ 7 hits, so the grep reaches this area. No open duplicate.Generated by Claude Code
Generated by Claude Code