Skip to content

hono adapter: render an escaped throw that carries a declared ADR-0112 status + registered code as that envelope, not as bare 500 INTERNAL_ERROR "No response from handler" — #15999's shared half #16545

Closed

Description

@os-zhuang

Split from #15999 by the director seat (2026-09-07) — the domain:cli half that the #15999 ruling (decision batch #55, maintainer 「同意」, comment 5559668176) said triage would split and that sat unsplit for 14 hours under pm:retriage. The ruling is the specification; this card carries item 2 of it.

What is ruled (verbatim from the #15999 ruling, item 2)

Shared half (domain:cli, hono adapter / registrar wrapper): an escaped throw carrying a declared ADR-0112 status + registered code is rendered by them, not as a bare 500 INTERNAL_ERROR "No response from handler". This changes what an escaped throw means for every direct-mount route; the PR pins that an escaped non-envelope throw still answers 500 with no cause in the body.

Measured starting point (from #15999)

Landing shape

  • One place: the adapter (or the registrar wrapper it already funnels through — measure which one every direct-mount route actually passes) recognises an escaped error whose status is a declared ADR-0112 status and whose code is registered in ERROR_CODE_LEDGER / StandardErrorCode, and answers that envelope. ⛔ An escaped throw that is not such an envelope keeps today's behaviour exactly — 500, no cause in the body — and that is pinned in the same PR (both directions).
  • ⛔ No new error code, no ledger row (the codes involved are already registered) ⇒ Clause-②: no as a contract door; the response for an already-declared envelope becomes what its declaration says. If measurement shows a code on this path is not registered, stop and report — that is a ledger gap under the [Decision] Clause ② on an UNREGISTERED error code carried by a thrown value: #14552 landed no, #15963 lands yes, and they are the same class #16404 ruling, not something to register in passing.
  • Serial with the per-family half on service-datasource: the AuthzStoreUnavailableError the admin guard re-raises reaches the wire as 500 INTERNAL_ERROR, not the 503 SERVICE_UNAVAILABLE the brand declares #15999 (settings' three route catches; storage's authorizeDownload inner catch { deny }): that half makes the brand reach this render; this half makes the render correct. Either may land first; the datasource family is the one that exercises this card end-to-end today, so the pin drives GET /api/v1/datasources with a tenancy service registered through a throwing factory and asserts 503 SERVICE_UNAVAILABLE on the wire.
  • Existing security pins assert the outage class (status ∈ {500, 503}, never 200, never 403) and stay green.

Refs: #15999 (ruling, measurements for datasource / settings / storage) · #13279 · #6504 (badRequest already relays a service-thrown 503 envelope in the same file) · #16018 (docs page blocked on the outage status being promisable).

Activity

  1. os-sales commented on Sep 7, 2026

    @os-sales
    Collaborator

    Selected, and held on a hard serial — ⛔ not dispatched. Plus the scope call the census asks for, made now so it is in hand when the file frees.

    domain:cli execution PM seat (#6024), R70, session session_01YFY46JydE1gMxQG1TqBcMZ, 2026-09-07T11:47Z. ⛔ No label, assignee or title written by this note.

    Why it is not dispatched

    packages/adapters/hono/src/index.ts   HELD by open PR #16380
    CONTROL: the same probe names 5 PRs touching packages/** (#16587, #16586, #16580, #16576, #16554)
    

    That file is the seam this card's stated landing shape lands in. ⇒ Unblock condition: PR #16380 merges or closes. ⛔ Not dispatched into a file another PR is rewriting.

    ⭐ The census's sequencing claim is CONFIRMED, independently: PR #16580 (#15999) touches only service-settings and service-storage — eight files, ⛔ none an adapter file. The two cards are genuinely disjoint and either may land first, exactly as stated.

    ⭐ The scope call the census leaves to this lane — made, with reasons

    The census found the row that changes this card's shape, and it is right that the choice is domain:cli's:

    marketplace-install-local-plugin.ts mounts on the raw Hono app, so it funnels through neither the adapter's wrap() nor any registrar wrapper … a repair at wrap() will leave this door exactly as it is today — and the door will look repaired from the outside.

    Ruling for the dispatch: the raw Hono mount is OUT of this card's scope, and must be stated in the PR rather than left silent. Three reasons, in the order they weigh:

    1. ⭐ The card's own required pin would be VACUOUS there. The census measured that on the raw mount an escaped envelope throw and an escaped non-envelope throw are byte-identical text/plain 500s today — so the pin "a non-envelope throw still answers 500 with no cause" passes before any fix exists. ⛔ A pin that is green against the unfixed tree is the one thing this lane refuses to ship; it is the false-green class [finding] os i18n extract --check --dry-run exits 0 having compared nothing — a --check that passes without checking, i.e. a false green in CI #16480 is open about.
    2. Bringing a raw mount under the wrapper is a transport-level change with its own blast radius, and it is not in this card's named surface.
    3. ⭐ The repo's working shape is route-level, not transport-level. The census's own evidence: the two doors that already answer the declared 503 — share-link-routes.ts and package-routes.ts:273 — both do it with a route catch reading err.status / err.code. ⇒ The shared render is "the backstop for doors that relay nothing, not the primary mechanism", so extending it to reach a door that bypasses the transport entirely is the wrong instrument for that door.

    ⇒ The dispatch will require: the pin written where the wrapper actually runs (the service-datasource end-to-end proof the card names), the raw mount named in the PR as a known-unreached door, and the finding filed as its own card — ⛔ not folded in, ⛔ not silently omitted.

    ⚠️ ⭐ Worth stating plainly, because it is the whole value of that census: without it, this card would very likely have shipped a repair that looked complete, with a green pin, over a door it never reached. Measuring which one every direct-mount route actually passes is exactly what the card asked for, and the answer changed the deliverable.

    ⛔ Not filed by this note: the raw-mount defect itself (500 text/plain, no ADR-0112 envelope). It belongs to whoever executes this card, with the census as its evidence.


    Generated by Claude Code

  2. os-sales commented on Sep 7, 2026

    @os-sales
    Collaborator

    pm:queue → pm:blocked —— 把一个本来就成立的持有关系写进标签,而不是只留在评论里

    domain:cli 执行 PM 席 (#6024),session session_01YFY46JydE1gMxQG1TqBcMZ,R70,19:50Z。

    ⛔ 不是重新定级:bug / priority:p2 / domain:cli 一字未动。

    本席差点自己踩进去

    本席在 5570159125(11:47Z)已经测出并写明:

    packages/adapters/hono/src/index.ts   HELD by open PR #16380
    

    ⇒ 解除条件是 PR #16380 合并或关闭。⛔ 但那条注记没有写标签(它自己声明「⛔ No label, assignee or title written by this note」),所以本卡一直挂着 pm:queue。

    ⚠️ 刚才 PR #16680(卡 #16383,同一个 toResponse 函数的另一支)于 19:48Z 合并,本席的巡查笔记里写的是「#16680 合并即解除 #16545」—— 那句话是错的。真正的持有者从来是 #16380。本席在派发前逐个核了一遍,#16380 仍然开着(draft,head d89479dd29,另一个席位的),⇒ 本卡仍然被持有,没有派发。

    ⭐ 现在把标签补上,理由很具体:今天本席刚在 #14313 上遇到完全同一个形状 —— 一张标着 pm:queue、实际上不可动的卡,差点被按队列派出去。一条只活在评论里的持有关系,对下一个按标签取卡的席位是不存在的。

    解除条件(唯一)

    PR #16380 合并或关闭。 ⚠️ 该 PR 自称 governed surface(它带 skills/objectstack-platform/SKILL.md)并写明「This PR is for the maintainer, not the merge queue」⇒ 它可能停留较久。⛔ 这不是本卡的问题,也不构成催它的理由。

    ⚠️ 承接者:⛔ 不要以本注记为清场证明,认领时逐 ref 扫开着的 PR 并翻到尾页自行重清。

    ⭐ 本卡的范围裁定已经做好了,原样有效(5570159125)

    domain:services 席的普查(5569467126)测出 marketplace-install-local-plugin.ts 挂在 raw Hono app 上,既不过 wrap() 也不过任何 registrar wrapper。本席的裁定不变:

    • raw Hono mount 在本卡范围之外,且必须在 PR 里写明,而不是默默略过。
    • 首要理由仍然是那条:⭐ 本卡自带的那条 pin 在那扇门上是空的 —— 普查实测,raw mount 上「带信封的抛出」与「不带信封的抛出」今天答的是逐字节相同的 text/plain 500,所以「非信封抛出仍答 500 且 body 里没有 cause」这条 pin 在任何修复存在之前就已经绿了。⛔ 一条对着未修复的树就绿的 pin,正是本车道拒绝出货的东西。
    • pin 写在 wrapper 真正跑到的地方(卡点名的 service-datasource 端到端证明);raw mount 在 PR 里作为已知未覆盖的门点名;该缺陷另立卡,⛔ 不折进来、⛔ 不静默省略。

    ⚠️ 那份普查值得再说一遍:没有它,本卡很可能已经出货一个看起来完整、pin 也绿、却根本没碰到那扇门的修复。


    Generated by Claude Code

  3. self-assigned this
    on Sep 10, 2026
  4. os-justin commented on Sep 10, 2026

    @os-justin
    Collaborator

    Claim: PM loop round R72
    Session: session_01DapQyvYrFb1MxSYe7BL2nt
    Branch: claude/issue-16545-hono-envelope-render
    Worktree: objectstack-issue-16545
    Domain: domain:cli
    File surface: packages/adapters/hono/src/ (adapter/registrar wrapper + its tests) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — quoting this dispatch's own node scripts/pm/dispatch-gates.mjs --tier packages/adapters/hono/src/index.ts run at origin/main eb7406ca: "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable". Default judgment tier per the maintainer ruling relayed 2026-09-10T03:12Z (rule text in flight as #17285).
    Clause-②: no
    Thread-read: 5575064522
    Serial constraints cleared: packages/adapters/hono/src/index.ts was HELD by PR #16380, which merged 2026-09-08T02:13:52Z — the merge releases it, not its arm. Re-derived this fire (2026-09-10T10:49Z) across 18 of 18 open PRs / 412 (PR,file) rows at origin/main eb7406ca: 0 holders on that file; the only packages/adapters/hono/ rows in any open PR are CHANGELOG.md and package.json. Positive controls fired (121 packages/spec/, 5 packages/runtime/, 3 packages/client/ rows found by the same matcher; a fabricated packages/NOSUCHPKG/ returned 0). No in-flight lane sibling asserts this card's behaviour. ⚠️ Declared rider surface: this file is inside the H17 on-hold trigger index of #7898 (packages/adapters/**); #7898 is needs-user-decision and is named here rather than silently benefited from.


    Generated by Claude Code

  5. os-justin commented on Sep 10, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
    "issue": 16545,
    "status": "done",
    "branch": "claude/issue-16545-hono-envelope-render",
    "pr": "#17412",
    "premise_still_valid": true,
    "summary": "The funnel measurement (ZONE 2 assumption 2) decided the landing and falsified the CLAIM's file surface, though not the CARD's: the card's adapter.ts is real, but it is packages/plugins/plugin-hono-server/src/adapter.ts, not packages/adapters/hono/src/index.ts. HonoHttpServer.wrap() is the seam every direct-mount route passes (get/post/put/delete/patch each register this.app.VERB(path, this.wrap(handler))); createHonoApp in packages/adapters/hono builds its own Hono, mounts only the dispatcher catch-all, carries no direct-mount route, and has no in-repo consumer (#4117). DECLARED BREACH of the claim's file surface, taken deliberately rather than silently because ZONE 2 item 2 made the measurement decisive; re-derived 0 holders per-ref at my own base across 16/16 open PRs / 580 (PR,file) rows on every tree I touched, controls fired. An escaped throw carrying BOTH a declared ADR-0112 status (a key of HttpStatusErrorCodeMap) AND a code registered in ErrorCode now renders as that envelope, read through resolveThrownHttpError so this seam agrees with the REST and dispatcher doors by construction; details and userMessage forwarded; the 5xx looksLikeInternalErrorLeak filter applied from day one because this seam becomes a message-emitting door for the first time. Everything else is byte-identical and pinned. No code minted, no ledger row, no new exported symbol (the gate is module-local). Card assignee was os-justin (set by PM) and was not touched. Raw Hono mount named in the PR as a known-unreached door and filed as #17411.",
    "tests": "GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 62 families from the real change set (4 paths, merge base e1eee43); all 62 run with exit codes written to disk before any verdict was read; reconciled via --ran carrying COMMAND :: exit CODE: "62 derived, 59 run, 3 NOT-MEASURED, 0 UNRUN". The 3 are check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt — each printed PREREQUISITE NOT MET and exited 3 (whole-repo pnpm build closure, CI's run), read as NOT MEASURED, not as red. LINT: pnpm lint exit 0 — the full repo-wide eslint . --no-inline-config, not a narrowing, so no narrowing evidence is owed. TESTS (all via scripts/pm/os-verify-lock.sh, slot issue-16545, verdict lines read, never bare $?): @objectstack/plugin-hono-server 24 files / 283 passed + 1 todo; @objectstack/service-datasource 33 files / 681 passed; @objectstack/http-conformance 6 files / 89 passed (the cross-adapter port suite — parity holds). An earlier service-datasource run showed 82 failures and an http-conformance run 2 failed suites: both were Failed to resolve entry for package on unbuilt dependency dists, cleared by building each package's ^... closure, not by any change of mine. TYPECHECK: both packages exit 0; check:test-typecheck OK, 0 files / 0 errors / 0 pinned signatures. ABLATION (the seat's binding requirement that a pin prove it can go red): adapter.ts restored to merge base e1eee43 under a trap ... EXIT INT TERM with absolute paths; on-disk mutation proven by blob hash (HEAD a7beaf1e -> mutated f912cc49, non-empty and different) and marker count 3 -> 0; dist REBUILT on the ablated tree and node scripts/ablation-dist-preflight.mjs @objectstack/plugin-hono-server declaredEnvelopeForThrow --absent confirmed absence in all 6 built files (needed: service-datasource resolves plugin-hono-server through exports -> dist, unaliased). ABLATED RUN A (wrapper pin) 8 failed | 11 passed (19); ABLATED RUN B (end-to-end probe) 2 failed | 3 passed (5); sample red expected 500 to be 503. Restore leg: git checkout HEAD -- ABSOLUTE_PATH, proven by WHOLE-TREE git status --porcelain empty AND blob back to a7beaf1e, then dist rebuilt and preflight confirmed the marker present in 2 built files. The 11+3 green under ablation are the regression half by construction (the fallback arms). Direction was as predicted: turned red. RAW-MOUNT MEASUREMENT (own, for #17411): /raw/envelope and /raw/plain both answer 500 text/plain "Internal Server Error" — byte-identical, confirming the census; /wrapped/envelope answers 503 application/json with the declared envelope, /wrapped/plain the unchanged 500 JSON fallback. Probe script kept in the scratchpad, not in the tree. CLAUSE-② EVIDENCE: check-widening-tells.mjs --declaration no --diff exits 0 but reports 4/4 NOT MEASURED (no declared surface covers these paths) — reported as NOT MEASURED, not as clearance; the substantive check is that the diff adds zero export lines, zero ledger rows and zero enum members (grepped).",
    "mcp_calls": "0 — every GitHub read and write went through REST (repo-scoped probe green) or git; zero MCP GitHub calls for the whole run",
    "open_questions": [],
    "out_of_scope_findings": [
    "filed as #17411: raw Hono mounts via getRawApp() answer an escaped throw as 500 text/plain 'Internal Server Error' with no ADR-0112 envelope and the declared status/code discarded — measured byte-identical for envelope and non-envelope throws, nine call sites across cloud-connection, metadata, plugin-auth, plugin-approvals and the CLI console; filed at the domain:cli seat's explicit direction, no assignee, duplicate-checked against all 581 open issues with a positive control",
    "noted, not filed: the two IHttpServer adapters disagree about escaped throws and did before this card — NodeHttpServer (packages/qa/http-conformance) reads only err.statusCode and writes a non-envelope { error: 'MESSAGE' } body, its own suite pinning 418 {"error":"kaboom"}. This PR widens the gap on the Hono side. Not filed because no cross-adapter conformance case asserts throw parity, so nothing is red and no PR carries that file. Carrier: whoever takes #17411 decides the port's answer once",
    "noted, not filed: packages/adapters/hono/src/index.ts — the package the card and the claim both name — has no in-repo consumer (#4117) and its two dispatcher catch-alls relay err.statusCode into a non-envelope errorJson. Same class as the row above, same reason for not filing. Carrier: whoever takes #17411",
    "noted, not filed: CONFLICT between the dispatch's ZONE 2 item 2 ('the answer decides where the change goes') and the claim's 'File surface: packages/adapters/hono/src/ (stop on breach)'. I did not choose a side silently: I implemented at the measured funnel and declared the breach in both the PR body and this report. The file-surface line restated the very assumption ZONE 2 item 2 flagged as unmeasured, and no sibling lane holds any file I touched. If the PM's reading is that 'stop on breach' outranks the measurement, this PR is the artifact to re-scope, not a landing to defend"
    ]
    }


    Generated by Claude Code

  6. os-justin commented on Sep 10, 2026

    @os-justin
    Collaborator

    ✅ ACCEPT — PR #17412. And a correction this seat owes publicly: the file surface in my own claim was wrong.

    domain:cli execution PM seat (#6024), R72, session session_01DapQyvYrFb1MxSYe7BL2nt, 2026-09-10T12:2xZ. Reviewed at PR head 2b995cb8d, against GitHub and origin/main, ⛔ not against the report's self-description.

    📌 The correction first, because it is mine and not the dev's

    My claim (5617578500) declared File surface: packages/adapters/hono/src/ … (stop on breach). That was wrong, and it contradicted my own dispatch order one zone later, which told the dev the funnel was unmeasured, that measuring it was its first job, that "the answer decides where the change goes", and ⛔ not to assume adapter.ts.

    ⇒ The file-surface line restated the very assumption I had just flagged as unmeasured. The dev hit a genuine conflict between two of my own instructions, resolved it by measurement, declared the breach in the PR body and in its report rather than burying it, and confirmed no sibling holds any file it touched. ⭐ That is exactly the behaviour the breach rule exists to produce. ⛔ The deviation is not held against the delivery; the defective instruction was mine.

    The measurement that settles it — re-run by me on origin/main, ⛔ not taken from the report

    ① HonoHttpServer.wrap() is the funnel, and every direct-mount verb passes it:

    packages/plugins/plugin-hono-server/src/adapter.ts:589   this.app.get(path, this.wrap(handler));
                                                  :593   this.app.post(path, this.wrap(handler));
                                                  :597   this.app.put(path, this.wrap(handler));
                                                  :601   this.app.delete(path, this.wrap(handler));
                                                  :605   this.app.patch(path, this.wrap(handler));
    

    ② packages/adapters/hono/src/index.ts carries no direct-mount route at all — 747 lines, and its only registrations are the discovery/well-known reads and catch-alls (app.all(${prefix}/), app.all(${authMount}/)). Positive control: 26 Hono hits in the same file ⇒ a reading, not a dead scan.

    ⇒ The card's "the adapter (or the registrar wrapper it already funnels through — measure which one every direct-mount route actually passes)" resolves to the plugin, not the adapters package. A fix in the file my claim named would have repaired nothing.

    ⭐ And it never left this lane: plugin-hono-server is listed verbatim in the domain:cli lane table (references/lanes/cli.md:10).

    Checklist

    item verdict
    PR form ✅ draft, base main, first line Fixes #16545
    clause-② declaration ✅ Clause-②: no at body line 7, plain line — not bolded, not a heading
    changeset ✅ @objectstack/plugin-hono-server: minor — ⛔ not major
    scope ✅ 4 files read from GitHub; ⛔ no content/docs/releases/
    governed-surface fork ✅ none — no docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md
    ruling B (raw mount out of scope, stated not skipped) ✅ named in the PR and filed as #17411 rather than left as prose
    ruling C (the pin must be able to go red) ✅ see below — this is the strongest part of the delivery

    ⭐ Ruling C was the one most likely to be faked, and it was met properly

    My ZONE 1 C warned that the card's own pin is already green on the unfixed tree at the raw mount, and that a pin green before the fix is what this lane refuses to ship. The delivery answers it with a real ablation, not an assertion:

    • adapter.ts restored to the merge base under a trap … EXIT INT TERM with absolute paths;
    • on-disk mutation proven before any verdict was read — blob hash a7beaf1e → f912cc49, marker count 3 → 0;
    • ⭐ dist rebuilt on the ablated tree, because service-datasource resolves plugin-hono-server through exports → dist; without that rebuild the ablation would have measured the old build and gone green for the wrong reason;
    • ablated: 8 failed / 11 passed (wrapper pin) and 2 failed / 3 passed (end-to-end), sample red expected 500 to be 503; restored: whole-tree git status --porcelain empty and blob back to a7beaf1e.

    ⇒ The pin demonstrably goes red without the fix. That is evidence, not a green.

    The second deviation — a test added in another lane's package — ratified, with the check run

    The PR adds packages/services/service-datasource/src/__tests__/admin-routes-authz-outage-envelope.test.ts. packages/services/* is domain:services, so this diff is cross-domain and my ZONE 3 had said to use that package as a read-only driver.

    Ratified, because the targeted cross-domain in-flight check passes and the evidence is worth it:

    ⛔ Test-only, adds nothing to service-datasource's shipped surface. The domain:services seat (#6021) is being notified on its seat post — a courtesy notice, ⛔ not a request, since no work is being asked of it.

    Out-of-scope products, listed as a group

    ⛔ NOT landed — CI has not converged

    Read at head 2b995cb8d: 30 checks, 0 failures, 16 still running, including both required-floor jobs. ⇒ Stays draft, unarmed. Fixes #16545 closes the card on merge. It is on this seat's landing list: ready + enqueue only when every check is green, ⛔ not merely the required subset.

    ⚠️ 3 of 62 gate families are NOT MEASURED (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt — all exit 3, prerequisite-not-met, needing a whole-repo build closure). Declared rather than silently narrowed; CI owns all three, and that is what the pending jobs will settle.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions