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
{{ message }}
Repository navigation
hono: a 409 DELETE_RESTRICTED answered by createHonoApp carries no developerMessage, dependentObject, dependentCount or object, which RestServer's structuredCodeAnswer arm carries (the #23531 family beyond CONCURRENT_UPDATE) #23587
Filing gate ①, class (b). reach:measured at a public door by the #23531 dev at d7484b8621, with a scratch test that was not committed (the os-dev-report on #23531, finding 2). The rig was a real createHonoApp with the real dispatcher and protocol over SQLite WASM.
Filed by domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU. ⛔ Not a claim. Lane and grade are triage's.
What was measured
Request:DELETE /api/v1/data/parent/ID, where a child lookup with deleteBehavior: 'restrict' references the parent.
Answer:409 {success:false,error:{code:DELETE_RESTRICTED,message:'This Parent is still referenced by 1 Child record(s) through “Parent”. Delete or reassign them first.'}}.
What is missing:developerMessage, dependentObject, dependentCount and object. RestServer answers the same refusal through structuredCodeAnswer's DELETE_RESTRICTED arm (packages/types/src/data-error-classification.ts, just above the CONCURRENT_UPDATE arm), which carries all four.
Not measured:METADATA_CONFLICT's currentVersion and INVALID_FIELD's field, at the same door.
declaredEnvelopeForThrow (packages/plugins/plugin-hono-server/src/adapter.ts) forwards only the details that resolveThrownHttpError derives. PR #23582 adds declaredConflictContext for CONCURRENT_UPDATE only, by #23531's scope.
The mechanical shape is to widen that helper from one code to "the arm agrees on status and code". ⚠️ Read #23586 first: a blanket widening would also forward CONCURRENT_UPDATE's currentRecord, which this seat holds back until that card is answered.
Triage: first grade · bug · priority:p3 · domain:cli · pm:queue · after PR #23582, and never forwarding currentRecord before #23586 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T02:56Z
Read fresh. A 409 DELETE_RESTRICTED answered by createHonoApp carries none of developerMessage, dependentObject, dependentCount or object, which RestServer's structuredCodeAnswer arm carries. This was measured on a real createHonoApp. METADATA_CONFLICT and INVALID_FIELD were not measured.
Claim: PM loop round 2 (#23587: a 409 DELETE_RESTRICTED answered by the Hono doors carries RestServer's structured fields, and CONCURRENT_UPDATE's currentRecord stays withheld until #23586 is answered) · 2026-10-15T03:31Z
Session: session_01FLwgbTSmGMrqsXkBDsiwfG
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23587-hono-delete-restricted-fields
Worktree: objectstack-issue-23587
Domain: domain:cli
Seat: domain:cli#1 (seat post #6024)
File surface: packages/plugins/plugin-hono-server/src/adapter.ts, declaredEnvelopeForThrow and declaredConflictContext (near :304-:430 on origin/main2372e0a7b7) only; its pins in packages/plugins/plugin-hono-server and packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts; one changeset. Stop on a breach and explain it in the report. ⛔ packages/types/src/data-error-classification.ts (structuredCodeAnswer) is read and called, not edited, unless a call needs an export (then the declaration below is re-judged before any edit). ⛔ packages/metadata-protocol (#23586's producer) is not touched.
Container & model: M, mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier --repo objectstack-ai/objectstack packages/plugins/plugin-hono-server/src/adapter.ts: no path-derived mandate; the default tier is kept for the security-adjacent constraint below)
Clause-②: no
Responsibility: plugin-hono-server's recogniser, platform code in this lane | RestServer's structuredCodeAnswer arm already carries the four fields; the Hono doors forward only what resolveThrownHttpError derives plus PR #23582's CONCURRENT_UPDATE context | any caller of a createHonoApp or HonoHttpServer host whose delete is restricted; measured once by #23531's dev on a real createHonoApp (finding 2); no consumer reading the fields was measured
Thread-read: 6183354733
Serial constraints cleared: adapter.ts: none of the open PRs edits it (#23481, the Version Packages PR, carries only .changeset/pre/23365-*), and no pm:dispatched claim names it. PR #23582 (#23531's currentVersion half), the order triage named, landed as 19c554ddc1. #23531's remainder (currentRecord) is released to pm:queue and ordered after #23586, so it waits behind this claim on the same function if it is claimed before this lands. ⛔ Hard constraint, triage 6183354733: CONCURRENT_UPDATE's currentRecord is not forwarded while #23586 (p1, security, domain:spec) is open.
os-dev-report
{
"issue": 23587,
"status": "done",
"branch": "claude/issue-23587-hono-delete-restricted-fields",
"pr": "#23616",
"head": "d1a39b380ce6a026db1a826312f9d915eb53c2ac",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (this run's harness-stamped id; subagent, so it is the parent's)",
"premise_still_valid": true,
"summary": "Premise confirmed on origin/main 16abea8 with a scratch rig (real createHonoApp, real engine and protocol over SQLite WASM, not committed). DELETE /api/v1/data/parent/ID, where a child's restrict lookup names it, answered 409 {success:false,error:{code:DELETE_RESTRICTED,message:...}}. mapDataError on the same thrown error carries developerMessage, dependentObject, dependentCount and object. The fix is in packages/plugins/plugin-hono-server/src/adapter.ts. PR #23582's helper is renamed declaredConflictContext to declaredArmContext. It now forwards the structuredCodeAnswer context of ANY arm that agrees with the envelope already decided (same status, same code), into error.details as PR #23582 placed currentVersion. No field and no code is listed. currentRecord stays excluded by name with its #23586 pointer, unchanged, and the two #23531 absence pins are unchanged and green. After the fix, the same rig answers DELETE_RESTRICTED with details {developerMessage, dependentObject, dependentCount}, INVALID_FIELD with {field, object} and OBJECT_NOT_FOUND with {object}. Every arm was checked (table in per_code). Only currentRecord is a row read with a system context and left unnarrowed. DELETE_RESTRICTED's elevated dependents probe discloses its count only through dependentCountIsDisclosable (objectql), and RestServer already sends all three fields to the same caller (H4 holds). H1 holds. H2: on the REST wire all four fields are top-level members of the flat body, which has no details. On the wrapped envelope ApiErrorSchema declares no developerMessage/dependentObject/dependentCount, so details is the only declared slot, and @objectstack/client reads errorBody.error.details, the same keys under err.details. H3 holds as stated, and it also changes OBJECT_NOT_FOUND and the five 403 access arms (pinned, named in the PR; see deviations). NOT delivered: DELETE_RESTRICTED's object. That arm takes it from the ROUTE only, and the recogniser has no route. The error's own object is not substituted: a cascade restrict names the child being deleted. So the PR says Part of #23587, and the card keeps one decision (open_questions[0]). METADATA_CONFLICT at createHonoApp's /meta save is answered by HttpDispatcher.errorFromThrown, not by the recogniser. Measured unchanged there, and reported as finding 1. The worktree is removed right after this report.",
"tests": "Red before the fix, with the pins committed alone at 90574de on the unfixed recogniser: pnpm --filter @objectstack/plugin-hono-server exec vitest run --maxWorkers=2 src/handler-throw-declared-envelope.test.ts gave 'Tests 11 failed | 26 passed (37)'. pnpm --filter @objectstack/http-conformance exec vitest run --maxWorkers=2 src/hono-declared-envelope.conformance.test.ts gave 'Tests 3 failed | 15 passed (18)'. Every red row failed at error.details with 'expected undefined to deeply equal ...'. The 3 guard rows pass by construction. At d1a39b3, after pnpm --filter @objectstack/plugin-hono-server build, all exit 0: plugin-hono-server typecheck (tsc, tsconfig.typecheck.json, check:test-typecheck OK, 0 debt) and test ('Test Files 29 passed (29)', 'Tests 354 passed (354)'); http-conformance typecheck (check:test-typecheck OK, ledger unchanged) and test ('Test Files 13 passed (13)', 'Tests 142 passed (142)'). ABLATION on d1a39b3, run with node scripts/ablation-replace.mjs in wrap mode plus an outer trap restore EXIT INT TERM on the absolute path. Each leg rebuilt plugin-hono-server, and ablation-dist-preflight found the marker in dist/index.js and dist/index.mjs. Leg A narrowed the helper back to CONCURRENT_UPDATE (anchor 'const arm = structuredCodeAnswer(thrown);', marker ABLATION_23587_ONE_CODE, blob 7fd1f090b0 to 11a3ca30af): unit 11 failed | 26 passed (37), conformance 3 failed | 15 passed (18), i.e. every new [#23587] refusal row red and the [#23531] rows green. Leg B dropped the currentRecord exclusion (marker ABLATION_23587_HELD_FIELD, blob to 1f5ac3cbb6): exactly the 4 absence pins red, unit 2 failed | 35 passed (37) and conformance 2 failed | 16 passed (18). The conformance body then carried the system-context row (organization_id, owner_id, created_by, ...). Restore: blob equals HEAD, git diff HEAD empty, porcelain 0. After a rebuild, preflight --absent showed both markers gone from all 6 built files with a clean tree, and both pin files were green (37/37, 18/18). @objectstack/hono's suite is not run locally (source untouched, runtime stubbed in its config) and is declared to CI.",
"gates": "LOCAL at d1a39b3. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 4 changed paths vs merge base 16abea8) derived 66 commands. All 66 ran, with each exit code captured before any pipe into a ':: exit N' record. 64 exited 0 on the first pass. pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt exited 3 (PREREQUISITE NOT MET: no workspace dist/, and the debt gate's closure build got no lock turn). Under the lock, after pnpm turbo run build --concurrency=2 ('76 successful', '49 cached'), both exited 0: '110 published require entry point(s) across 68 package(s) load' and '1 ledger entr(ies) re-measured ... none above its recorded number'. --ran reconciliation: '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Dispatch-named gates, all exit 0: check:route-envelope and check:dispatcher-error-vocabulary (both in the derived list), and check:durability-log-level (not derived, run separately: '46 durability-critical catch seam(s), all loud'). Roster gates under touched directories, all exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing, pnpm check:filter-alias-parity. Lint: eslint --no-inline-config packages/plugins/plugin-hono-server packages/qa/http-conformance exited 0 with no findings; eslint.config.mjs is unchanged between merge base and origin/main. git merge-tree --write-tree HEAD origin/main (dd4ef38) is clean, and no incoming commit touches these packages, so the branch was not merged. CI on d1a39b3, read once at PR open: 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, and the other 21 check-runs in_progress, among them Lint · ESLint, Type Check · workspace, Check Changeset, Governed Surface Queue Guard and 'Part-of PR must not also close its card'. CI convergence is the PM's.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches. Stroke 1, pr_create (run 38685988212): POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23616/assignees ['os-bill']; read back 17768 bytes identical. Stroke 2: POST /repos//issues/23587/comments (this report). label-write: 0 (no label named; a published fix owes a changeset, not skip-changeset). git push x3 (empty branch probe, 90574de, d1a39b3), not REST. Reads were REST GETs through gh api.",
"line_budget": "n/a",
"files_changed": [
"packages/plugins/plugin-hono-server/src/adapter.ts",
"packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts",
"packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts",
".changeset/23587-hono-structured-arm-fields.md"
],
"per_code": [
"DELETE_RESTRICTED 409 · RestServer: developerMessage, dependentObject, dependentCount, object(route) · Hono before: none (measured) · after: developerMessage, dependentObject, dependentCount (measured) · not forwarded: object (route-only arm input; recogniser has no route)",
"CONCURRENT_UPDATE 409 · RestServer: currentVersion, currentRecord, object(route) · before: currentVersion · after: currentVersion (unchanged) · not forwarded: currentRecord (held by name, #23586), object (route-only)",
"METADATA_CONFLICT 409 · RestServer: currentVersion, object(route) · before: none · after: currentVersion on a HonoHttpServer route (unit pin); createHonoApp /meta save unchanged (measured: answered by HttpDispatcher.errorFromThrown, never reaches the recogniser) · not forwarded: object (route-only)",
"INVALID_FIELD 400 · RestServer: field, object · before: none (measured) · after: field, object (measured) · none excluded",
"OBJECT_NOT_FOUND 404 · RestServer: object · before: none (measured) · after: object (measured) · none excluded (REST's fixed sentence not taken; message channel untouched)",
"FEEDS_DISABLED, FILES_DISABLED, ATTACHMENT_PARENT_ACCESS, ATTACHMENT_DELETE_DENIED, RECORD_NOT_ACCESSIBLE 403 · RestServer: object · before: none · after: object (unit pins) · none excluded",
"VALIDATION_FAILED 400 · RestServer: fields, object(route) · before: fields (resolver) · after: fields, same array, byte-identical · not forwarded: object (route-only)",
"DUPLICATE_RECORD 409 (REST answers UNIQUE_VIOLATION) · RestServer: developerMessage, field, object · before: none, code DUPLICATE_RECORD (measured) · after: unchanged (measured) · excluded: arm disagrees on code (agreement guard); finding 2",
"ERR_DATASOURCE_UNAVAILABLE 503 · RestServer: datasource, reason, object(route) · before/after: recogniser declines (its one producer, DatasourceUnavailableError, declares no status) · unreachable",
"OBJECT_API_DISABLED, OBJECT_API_METHOD_NOT_ALLOWED · RestServer: object, allowed (only when error.object differs from the route's) · before/after: arm never fires without a route object · unreachable"
],
"deviations": [
"Scope, by the bounded in-place rule (same defect class, mechanical with the agreement guard already pinned, same file, same gate family): the widened rule also changes OBJECT_NOT_FOUND (+details.object) and the five 403 access arms (+details.object), which triage's scope did not name. Each has a pin (one per arm), and the PR body names them with the reading. This is H3's generic rule, checked against every arm.",
"Zone 3 suggested four fields. Three land; DELETE_RESTRICTED's object does not (reason in summary). The PR therefore opens with 'Part of #23587', not a closing line, so the card keeps the object decision (open_questions[0]).",
"Helper renamed declaredConflictContext to declaredArmContext (the PM's 'rename it to what it now does'). #23531's Landed comment 6183626856 and #23586's body name the old name. The exclusion line and its #23586 comment are unchanged.",
"Noted tension: seat 2's Release comment 6183630660 on #23531 says 'Do not widen the helper's code guard while #23586 is open'. Triage's grade 6183354733 (Zone 1) permits a widening that keeps currentRecord out. I followed Zone 1 and the dispatch: the exclusion is unchanged and proven by ablation leg B.",
"Fresh worktree: two derived gates refused with PREREQUISITE NOT MET (exit 3) and were re-run under the lock after a cache-backed full build. Both exited 0, and the --ran record carries the re-run codes; the first-pass record is kept in the scratchpad."
],
"open_questions": [
{
"question": "DELETE_RESTRICTED's object (and CONCURRENT_UPDATE's and METADATA_CONFLICT's) is the object the ROUTE named, and declaredEnvelopeForThrow has no route. Close #23587 on this PR without it, or thread a route into the recogniser?",
"options": [
"A: close the card with this PR's reading. The caller named that object in its own URL. RestServer's object echoes req.params.object. No consumer reads it (objectui git grep at 28fb2a0: 0 hits for dependentObject/dependentCount). Cost: none; the Hono body stays one key short of REST's on three arms.",
"B: thread an optional route object into declaredEnvelopeForThrow. createHonoApp's catch-all would need to parse /data/:object out of the sub-path, a second route parser beside the dispatcher's; HonoHttpServer.wrap would read a ':object' param by naming convention, which is not a contract. Touches @objectstack/hono's source too. Medium effort, and a new seam to keep in step.",
"C: substitute the thrown error's own object. Rejected: the engine's cascade recurses through the public delete(childName, ...), so a deeper restrict names the child being deleted, and this door would answer differently from RestServer."
],
"recommendation": "A. The field carries nothing the caller does not already hold, no reader exists, and B buys one echoed key with a second route parser. If an API client ever needs it, B can be its own card."
}
],
"out_of_scope_findings": [
"class: b · reach: measured at a public door: createHonoApp PUT /api/v1/meta/view/case_grid with a stale If-Match at 16abea8 answers 409 {success:false,error:{code:METADATA_CONFLICT,message:'... (current is hmac-sha256:...)',httpStatus:409}} with no currentVersion · evidence: spec MetadataConflictErrorSchema.currentVersion ('Send it as If-Match to overwrite deliberately ... Opaque — echo it verbatim, never parse it'); mapDataError on the same thrown error carries currentVersion; the httpStatus member shows the answer is HttpDispatcher.errorFromThrown's (packages/runtime/src/http-dispatcher.ts), which forwards only resolveThrownHttpError's details · Seam: spec:MetadataConflictErrorSchema.currentVersion → runtime:HttpDispatcher.errorFromThrown | consumer: not measured · family: the #23531/#23587 family (a door skipping structuredCodeAnswer's arm fields), on a different door, so its own card · dedupe words: METADATA_CONFLICT currentVersion errorFromThrown HttpDispatcher createHonoApp meta save If-Match",
"class: b · reach: measured at a public door: createHonoApp POST /api/v1/data/udoc repeating a unique email at 16abea8 answers 409 DUPLICATE_RECORD, where RestServer answers 409 UNIQUE_VIOLATION · evidence: the maintainer ruling recorded at structuredCodeAnswer (2026-09-03, commit 65846bc): 'a unique-constraint refusal has ONE wire spelling on every route, UNIQUE_VIOLATION'; the engine's own developerMessage on that throw: 'over the HTTP API this refusal is reported as code === UNIQUE_VIOLATION on every route' · Seam: spec:ErrorCode UNIQUE_VIOLATION (ruling 65846bc) → runtime:plugin-hono-server declaredEnvelopeForThrow (createHonoApp thrownJson, HonoHttpServer wrap/onError) | consumer: not measured · same doors and same function as #23587, so a candidate sub-issue of it; a change of code is outside this PR's agreement rule · dedupe words: DUPLICATE_RECORD UNIQUE_VIOLATION createHonoApp declaredEnvelopeForThrow 409 unique constraint Hono door wire code",
"carrier: none (承接者:无) · noted, not filed; in the PR's Acceptance notes only: OBJECT_NOT_FOUND's sentence differs between the doors (REST arm 'Object X is not registered', Hono keeps the thrown 'Object X not found'); the two REST doors differ on a declared-5xx arm (mapDataError takes ERR_DATASOURCE_UNAVAILABLE's, resolveErrorResponse does not), unreachable today because no producer declares that status"
]
}
Seat pointer: REWORK round 1 on PR #23616 (d1a39b380c), with the claim's file surface revised in the same round · domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T05:31Z · holder of claim 6183773313. Thread-read: 6185043669.
The verdict lives on the PR. One item: correct the statements in content/docs/protocol/kernel/error-handling.mdx (:130-:132, :541-:542, :636-:637 on origin/main) that this PR makes false, plus any other content/docs/** statement about a code whose Hono-door body this PR changes.
File surface, revised this round: claim 6183773313's surface plus the content/docs/** pages that item corrects. ⛔ content/docs/releases/ is excepted. No open PR edits those pages; the seat read all 15 open PRs' file lists at this stamp.
The miss:createHonoApp and HonoHttpServer routes answer the engine's DuplicateRecordError as 409 DUPLICATE_RECORD. RestServer answers UNIQUE_VIOLATION, with developerMessage, field and object.
Measured at a public door by this card's dev at 16abea884: POST /api/v1/data/udoc repeating a unique email.
Governing text: the maintainer ruling recorded at commit 65846bc46, one wire spelling on every route, UNIQUE_VIOLATION (restated at packages/metadata-protocol/src/protocol.ts near :3414).
Where:declaredEnvelopeForThrow in packages/plugins/plugin-hono-server/src/adapter.ts.
Cross-card note from domain:spec seat 2 · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-15T09:02Z · ⛔ not a claim
#23586 landed as 7dc7d2213 (PR #23628). metadata-protocol now reads a conflicting row under the caller's own context before it reports a version conflict. A row the caller cannot read answers 404 with no currentVersion and no currentRecord, and a readable row's currentRecord is the caller's own field-masked read.
So the hold triage set on this card's Hono doors (declaredConflictContext forwarding no currentRecord until #23586 lands, 6184070020) no longer guards anything: forwarding the producer's currentRecord is now safe at those doors. Lifting it is this card's remainder, for whichever seat claims it.
Claim: PM loop round 4, label order: the oldest bug at priority:p3 in domain:cli's open queue (the lane's queue holds no priority:p0, pm:blocking, target: or p1/p2 card that is open for dispatch). #23587's folded remainder: both Hono doors answer the engine's DuplicateRecordError with the ruled wire spelling, 409 UNIQUE_VIOLATION · 2026-10-15T12:55Z
Session: session_01FLwgbTSmGMrqsXkBDsiwfG
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23587-hono-unique-violation
Worktree: objectstack-issue-23587
Domain: domain:cli
Seat: domain:cli#1 (seat post #6024)
File surface (read on origin/main7e6e4e1da; stop on a breach and explain it in the report):
packages/plugins/plugin-hono-server/src/adapter.ts: declaredEnvelopeForThrow and the arm-agreement rule (its docblock near :375-:391 names this exact case).
Its pins, under packages/plugins/plugin-hono-server/src/**.
One changeset.
Container & model: M (one mapping at one seam, measured at two doors, with prose to reconcile), mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier --repo objectstack-ai/objectstack over the three surface paths at 7e6e4e1da: "no path-derived mandate … The tier stays the PM's per-card judgment call")
Clause-②: no
Basis for the Clause-② line: no accepted input moves. The maintainer ruled one wire spelling on every route, UNIQUE_VIOLATION (recorded in commit 65846bc46, restated at packages/metadata-protocol/src/protocol.ts near :3371-:3398 and packages/core/src/utils/import-runner.ts:382). The published error pages already state that spelling for every route. The Hono doors are brought to the ruling; the engine's in-process DuplicateRecordError.code stays DUPLICATE_RECORD, as the ruling says. No export is added, and no packages/spec text moves.
Responsibility: plugin-hono-server's adapter, platform code in this lane | RestServer's door already answers UNIQUE_VIOLATION, so a client reaching the platform through it is unaffected | a client of a createHonoApp or HonoHttpServer route that repeats a unique value; measured at POST /api/v1/data/udoc by this card's earlier dev at 16abea884 (6185197160)
Thread-read: 6188445836
Serial constraints cleared, and the landing order:
os-dev-report
{
"issue": 23587,
"round": "the folded remainder (claim 6192578085, PM loop round 4)",
"status": "done",
"branch": "claude/issue-23587-hono-unique-violation",
"pr": "#23682",
"head": "3b01195d174f54d4c445c6e9f241db52dec120a1",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (this run's harness-stamped id; subagent, so it is the parent's)",
"premise_still_valid": true,
"summary": "H1 holds, measured: at 7e6e4e1 both Hono doors (createHonoApp's dispatch() catch-all; a HonoHttpServer route and its raw mount) answered a repeated unique value from the real engine as 409 DUPLICATE_RECORD with code and message only, because declaredArmContext consults RestServer's arm only when status and code already agree. H2 holds: the fix decides the envelope's code before the arm is consulted, in declaredEnvelopeForThrow (packages/plugins/plugin-hono-server/src/adapter.ts): isEngineDuplicateRecordEnvelope(thrown) ? 'UNIQUE_VIOLATION' : resolved.code. The key is the registered code AND the class name, the same predicate RestServer's arm spells and the import runner already imports from @objectstack/types (an existing export; no new export, no packages/spec edit). The agreement rule is unchanged and is asked with the decided code, so the duplicate arm now agrees and its developerMessage, object and (when the driver named a column) field ride error.details. message stays the thrown engine sentence; declaredCode stays absent. Measured after on the same rig: all three doors 409 UNIQUE_VIOLATION with details {developerMessage, object}; no field because the tenancy-scoped unique index is composite (uniq_udoc_organization_id_email); unit pins cover a producer-named field. Every control is unchanged before/after: a hook throwing {code:'DUPLICATE_RECORD',status:409} keeps DUPLICATE_RECORD on both doors; DELETE_RESTRICTED, INVALID_FIELD, CONCURRENT_UPDATE and OBJECT_NOT_FOUND keep code and details. H3 partly holds: the published 'every route answers UNIQUE_VIOLATION' sentences become true at the Hono doors, but two statements became FALSE with this change and are corrected (error-handling.mdx corollary 1 'UNIQUE_VIOLATION ... always flat'; its #### UNIQUE_VIOLATION Envelope line 'never nested'), and the Hono doors table gains a row. The 'every route' claim is still false on a third door: the runtime dispatcher's own catch arms (HttpDispatcher.errorFromThrown), measured at POST /api/v1/actions/udoc/dup answering 409 DUPLICATE_RECORD with httpStatus (out_of_scope_findings[0]). Base merged with origin/main 09dd537 before opening (no overlap). The worktree is removed right after this report.",
"tests": "Pins: packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts, new [#23587] block, 4 rows x 3 doors (HonoHttpServer route, its raw mount, declaredEnvelopeForThrow as createHonoApp's renderer). Red before the fix with the pins committed alone at df28bfd: 'Tests 6 failed | 43 passed (49)', each red row "expected 'DUPLICATE_RECORD' to be 'UNIQUE_VIOLATION'", the 6 control rows green. After the fix at 66aed12: 49 passed. At the final head 3b01195: pnpm --filter @objectstack/plugin-hono-server test 'Test Files 29 passed (29)', 'Tests 366 passed (366)'; typecheck exit 0 (tsc, tsconfig.typecheck.json, 'check:test-typecheck: OK', 0 debt; the test layer is compiled there). ABLATION at 66aed12 with node scripts/ablation-replace.mjs (wrap mode) under an outer trap restore EXIT INT TERM on the absolute path: fix line replaced by 'const code: string = resolved.code ?? (isEngineDuplicateRecordEnvelope(thrown) ? ABLATION_23587_NO_WIRE_MAP : ...)' (anchor 1 to 0, blob 401887f362 to 0630ef01a7; on disk marker 1, fix line 0); plugin-hono-server rebuilt; ablation-dist-preflight: marker present in dist/index.js and dist/index.mjs. Unit: 'Tests 6 failed | 43 passed (49)', exactly the 6 positive rows red, all controls green. Real rig: the three doors back to 409 DUPLICATE_RECORD without details; every control unchanged. Restore: blob == HEAD 401887f362, git diff HEAD empty; after rebuild, preflight --absent: marker absent from all 6 built files, 'working tree clean against HEAD'. Consumer suites (the wire shape of a public door changed), under the lock, exit 0: @objectstack/verify unique-scope + handle.observe-writes 'Tests 24 passed (24)'; @objectstack/http-conformance '13 passed' files, 'Tests 144 passed (144)'; @objectstack/hono '8 passed' files, 'Tests 161 passed (161)'. Readings came from an uncommitted scratch rig in packages/qa/http-conformance/src (real createHonoApp, real engine and protocol over SQLite WASM, plus a HonoHttpServer route and raw mount on the same kernel), deleted before the restore proof; a copy is in the session scratchpad.",
"gates": "LOCAL at the final head 3b01195. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 5 changed paths vs merge base 09dd537) derived 95 commands; all 95 ran sequentially, each exit code recorded before any pipe. 93 exited 0 on the first pass; pnpm --filter @objectstack/spec run check:skill-examples and pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: no packages/client-react/dist and no workspace dist/ after the merge). After pnpm turbo run build --filter='!@objectstack/docs' --concurrency=2 under the lock ('75 successful', '74 cached'), both exited 0: '263 prose examples type-check across 3 surface(s)' and the cjs-loads provenance 'entries/packages ... 110/68'. --ran: 'Run reconciliation — 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN.' (exit 0; ran.list carries the re-run codes, first-pass record kept in the scratchpad). Named among them, all exit 0: check:route-envelope, check:dispatcher-error-vocabulary, check:error-status-conformance ('every derivable runtime status is documented, and every documented status is reachable'), check:doc-anchors ('518 internal #fragment link(s) ... all resolve'), @objectstack/spec check:docs, check:nul-bytes, check:type-check-debt ('none above its recorded number'). Roster gates under touched directories, exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing, pnpm check:filter-alias-parity; plus pnpm check:durability-log-level exit 0. Lint lane blind spot: eslint --no-inline-config packages/plugins/plugin-hono-server exit 0, no findings (eslint.config.mjs unchanged by the merge); repo-wide pnpm lint is CI's. Post-merge (packages/spec moved on main's side): pnpm --filter @objectstack/spec build && check:generated exit 0, 'All 14 generated artifacts are up to date'. CI on 3b01195, read once at PR open: 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, the other 22 check-runs in_progress (among them Check Changeset, Lint · ESLint, Type Check · workspace, Governed Surface Queue Guard, 'The card this PR closes must claim this branch'). CI convergence is the PM's.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches. Stroke 1, pr_create (run 38750288654): POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23682/assignees ['os-bill']; read back 13729 bytes identical, assignees os-bill. Stroke 2: POST /repos//issues/23587/comments (this report). PR-body PATCH: 0. label-write: 0. git push x5 (empty-branch probe, df28bfd, 66aed12, 9a6754f, 3b01195), not REST. Reads were REST GETs through gh api.",
"line_budget": "n/a",
"files_changed": [
"packages/plugins/plugin-hono-server/src/adapter.ts",
"packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts",
"content/docs/protocol/kernel/error-handling.mdx",
"content/docs/api/error-catalog.mdx",
".changeset/23587-hono-unique-violation.md"
],
"before_after": [
"createHonoApp POST /api/v1/data/udoc repeating a unique email · before (7e6e4e1): 409 {success:false,error:{code:DUPLICATE_RECORD,message:"Duplicate record refused on 'udoc': a unique constraint already holds these values. No record was written."}} · after (66aed12 rebuilt): 409 {success:false,error:{code:UNIQUE_VIOLATION,message:(the same sentence),details:{developerMessage:(the same sentence),object:udoc}}}",
"HonoHttpServer route (wrap) whose handler lets engine.insert's throw escape · before: the same DUPLICATE_RECORD body · after: the same UNIQUE_VIOLATION body",
"HonoHttpServer raw mount (transport onError) · before: the same DUPLICATE_RECORD body · after: the same UNIQUE_VIOLATION body",
"field: absent in every real reading (composite tenancy-scoped index uniq_udoc_organization_id_email, no single column named); with a producer-named field the unit pins read details keys exactly developerMessage, field, object on all three doors"
],
"controls": [
"hook-thrown DUPLICATE_RECORD (beforeInsert hook, {code,status:409}, not the engine's class) · createHonoApp: before DUPLICATE_RECORD, keys code+message · after DUPLICATE_RECORD, keys code+message",
"hook-thrown DUPLICATE_RECORD · HonoHttpServer route: before DUPLICATE_RECORD · after DUPLICATE_RECORD",
"DELETE_RESTRICTED (restrict lookup) · before 409 DELETE_RESTRICTED, details developerMessage/dependentObject/dependentCount · after identical",
"INVALID_FIELD (?nosuchfield=1) · before 400 INVALID_FIELD, details field/object · after identical",
"CONCURRENT_UPDATE (stale If-Match) · before 409 CONCURRENT_UPDATE, details currentVersion/currentRecord · after identical",
"OBJECT_NOT_FOUND (/data/ghost) · before 404 OBJECT_NOT_FOUND, details object · after identical",
"unit: class name DuplicateRecordError with code RESOURCE_CONFLICT keeps RESOURCE_CONFLICT (3 doors); the existing [#23587]/[#23531] arm rows and both agreement-rule controls stay green"
],
"deviations": [
"origin/main 09dd537 was merged before opening (AGENTS.md section 10; 3 incoming commits, none touching the 5 changed paths), as merge commit 3b01195 with git's default message and no trailer lines. packages/spec moved on main's side, so spec was rebuilt and check:generated ran (exit 0).",
"Two derived gates first exited 3 (PREREQUISITE NOT MET) and were re-run after a cache-backed build; ran.list carries the re-run codes.",
"Beyond the package's own suites, three consumer suites ran (verify, http-conformance, hono), because the wire shape of a public door changed for one class.",
"createHonoApp's door is pinned through declaredEnvelopeForThrow directly (the function its catch arms call): plugin-hono-server cannot import @objectstack/hono, which depends on it, and packages/qa/http-conformance is outside this file surface. The real createHonoApp door is covered by the uncommitted rig's before/after/ablation readings.",
"Docs: the 'every route answers the wire code UNIQUE_VIOLATION' sentences were kept and now name the Hono doors beside the REST door. They were not narrowed, though measured still false on the runtime dispatcher's own door; that door is out_of_scope_findings[0], and the sentences move with it.",
"The first package-test call got no lock turn (exit 99 behind the CLI dev's unit run) and the harness moved the call to the background; its typecheck half was waited on with tail --pid. Both test and typecheck were then re-run in the foreground at the final head, and only those readings are cited.",
"The unreleased .changeset/23587-hono-structured-arm-fields.md says a duplicate insert's DUPLICATE_RECORD 'gains nothing' at these doors, which this change contradicts. Not rewritten, per the dispatch; noted for release-text review 6191938402 (out_of_scope_findings[1])."
],
"open_questions": [
{
"question": "message for the engine's duplicate at the Hono doors: keep the thrown engine sentence (then details.developerMessage repeats it byte for byte), or take RestServer's arm sentence ('A record with this email already exists')?",
"options": [
"A: keep the thrown sentence (this PR). One message rule at these doors, the published Hono-door text ('the message is the thrown one'), and the batch-row precedent (toRowApiError's message is the engine's sentence). Cost: developerMessage duplicates message for this one code.",
"B: use the arm's curated sentence as message for a translated code, making the body the exact nested image of RestServer's. Cost: an exception to the thrown-message rule in declaredEnvelopeForThrow, a second wire change in the same release, and docs to re-state."
],
"recommendation": "A: the ruling moves the code, not the sentence; A adds no second rule and no second wire change, and nothing measured reads the sentence (no consumer measured). B can be its own card if a client needs the curated text."
}
],
"out_of_scope_findings": [
"class: b · reach: measured at a public door: on a createHonoApp host at 9a6754f (runtime dist at 7e6e4e1), POST /api/v1/actions/udoc/dup, a code-registered script action whose body repeats a unique email, answered 409 {success:false,error:{code:DUPLICATE_RECORD,message:'Duplicate record refused on udoc: ...',httpStatus:409}} · evidence: the maintainer ruling recorded in commit 65846bc ('one wire spelling on every route, UNIQUE_VIOLATION'); httpStatus shows the writer is HttpDispatcher.errorFromThrown (packages/runtime/src/http-dispatcher.ts), reached from the actions domain's declared-status arm (packages/runtime/src/domains/actions.ts), which never reaches declaredEnvelopeForThrow; the same writer serves the other dispatcher domains' catch arms (meta, automation, mcp, catalog-activation), not measured there · Seam: spec:ErrorCode UNIQUE_VIOLATION (ruling 65846bc) → runtime:HttpDispatcher.errorFromThrown | consumer: not measured · carries the published 'every route answers the wire code UNIQUE_VIOLATION' sentences (error-handling.mdx #### UNIQUE_VIOLATION, error-catalog.mdx ### UNIQUE_VIOLATION) and DuplicateRecordError.developerMessage's 'on every route' · same ruling family as this card, different door and package (packages/runtime), so its own card · dedupe words: DUPLICATE_RECORD UNIQUE_VIOLATION errorFromThrown HttpDispatcher actions dispatcher httpStatus 409 wire spelling",
"carrier: release-text review 6191938402 (the seat's one-story question) · noted, not filed: .changeset/23587-hono-structured-arm-fields.md (unreleased) says a duplicate insert's DUPLICATE_RECORD 'gains nothing' at the Hono doors; .changeset/23587-hono-unique-violation.md (this PR) moves that code to UNIQUE_VIOLATION, so a release carrying both prints two stories"
],
"pr_body_changes": []
}
Landed: PR #23682 → c9810dd9d1 (#23587's remainder, Fixes #23587). With PR #23616's first half, the card is done, and pm:dispatched is stripped
domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T15:08Z · holder of claim 6192578085, released by this landing.
reading
value
queue
added_to_merge_queue 14:46Z, merged 15:07Z
origin/main
c9810dd9d1: one parent (a queue squash), an ancestor of origin/main, 5 files +212 / −24 as reviewed at 3b01195d1; no queue branch left
card
closed completed by the PR's Fixes #23587 (15:07:47Z); pm:dispatched removed by targeted delete
Both Hono doors (createHonoApp's catch-all, and a HonoHttpServer route or its raw mount) answer the engine's DuplicateRecordError as 409 UNIQUE_VIOLATION, with developerMessage, object, and field where the driver named it. This follows the maintainer's ruling recorded in 65846bc46.
A hook that only speaks DUPLICATE_RECORD keeps its code. The engine's in-process code is unchanged.
The changeset states the wire change and the one-line client fix.
Carried to the release-text review 6191938402: the unreleased .changeset/23587-hono-structured-arm-fields.md says a duplicate's code "gains nothing" at these doors, while .changeset/23587-hono-unique-violation.md moves it. A release carrying both tells two stories.
Holds released:packages/plugins/plugin-hono-server/src/adapter.ts and its pin file, and the two docs pages.
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p3·domain:cli·pm:queue· after PR #23582, and never forwardingcurrentRecordbefore #23586 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T02:56ZRead fresh. A
409 DELETE_RESTRICTEDanswered bycreateHonoAppcarries none ofdeveloperMessage,dependentObject,dependentCountorobject, whichRestServer'sstructuredCodeAnswerarm carries. This was measured on a realcreateHonoApp.METADATA_CONFLICTandINVALID_FIELDwere not measured.priority:p3: a declared structured answer is incomplete on a measured door, but no consumer reading those fields is measured. The refusal and its message still arrive. hono: a409 CONCURRENT_UPDATErendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries nocurrentVersionorcurrentRecord, which the wire contract and objectui's conflict bar read #23531 was p2 because a shipped renderer reads its fields.domain:cli, hono: a409 CONCURRENT_UPDATErendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries nocurrentVersionorcurrentRecord, which the wire contract and objectui's conflict bar read #23531's family (plugin-hono-server'sdeclaredEnvelopeForThrow). It lands after PR fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentVersion (#23531) #23582.CONCURRENT_UPDATE'scurrentRecordout until metadata-protocol: a 409 CONCURRENT_UPDATE carries currentRecord as read with a system context (probeRecord), so a caller who sends a stale version may receive a row, or fields, that row- or field-level security hides from them #23586 lands. ⛔ No blanket forward.DELETE_RESTRICTED, plusMETADATA_CONFLICTandINVALID_FIELDif the seat measures the same gap. One pin per arm.findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 2 (#23587: a
409 DELETE_RESTRICTEDanswered by the Hono doors carriesRestServer's structured fields, andCONCURRENT_UPDATE'scurrentRecordstays withheld until #23586 is answered) · 2026-10-15T03:31ZSession:
session_01FLwgbTSmGMrqsXkBDsiwfGAccount:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23587-hono-delete-restricted-fieldsWorktree:
objectstack-issue-23587Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface:
packages/plugins/plugin-hono-server/src/adapter.ts,declaredEnvelopeForThrowanddeclaredConflictContext(near:304-:430onorigin/main2372e0a7b7) only; its pins inpackages/plugins/plugin-hono-serverandpackages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts; one changeset. Stop on a breach and explain it in the report. ⛔packages/types/src/data-error-classification.ts(structuredCodeAnswer) is read and called, not edited, unless a call needs an export (then the declaration below is re-judged before any edit). ⛔packages/metadata-protocol(#23586's producer) is not touched.Container & model:
M,mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier --repo objectstack-ai/objectstack packages/plugins/plugin-hono-server/src/adapter.ts: no path-derived mandate; the default tier is kept for the security-adjacent constraint below)Clause-②: no
Responsibility:
plugin-hono-server's recogniser, platform code in this lane |RestServer'sstructuredCodeAnswerarm already carries the four fields; the Hono doors forward only whatresolveThrownHttpErrorderives plus PR #23582'sCONCURRENT_UPDATEcontext | any caller of acreateHonoApporHonoHttpServerhost whose delete is restricted; measured once by #23531's dev on a realcreateHonoApp(finding 2); no consumer reading the fields was measuredThread-read: 6183354733
Serial constraints cleared:
adapter.ts: none of the open PRs edits it (#23481, the Version Packages PR, carries only.changeset/pre/23365-*), and nopm:dispatchedclaim names it. PR #23582 (#23531'scurrentVersionhalf), the order triage named, landed as19c554ddc1. #23531's remainder (currentRecord) is released topm:queueand ordered after #23586, so it waits behind this claim on the same function if it is claimed before this lands. ⛔ Hard constraint, triage6183354733:CONCURRENT_UPDATE'scurrentRecordis not forwarded while #23586 (p1,security,domain:spec) is open.Generated by Claude Code
test(plugin-hono-server): pin RestServer's structured-arm fields at b…
fix(plugin-hono-server): a Hono door's structured refusal carries Res…
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23587,
"status": "done",
"branch": "claude/issue-23587-hono-delete-restricted-fields",
"pr": "#23616",
"head": "d1a39b380ce6a026db1a826312f9d915eb53c2ac",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (this run's harness-stamped id; subagent, so it is the parent's)",
"premise_still_valid": true,
"summary": "Premise confirmed on origin/main 16abea8 with a scratch rig (real createHonoApp, real engine and protocol over SQLite WASM, not committed). DELETE /api/v1/data/parent/ID, where a child's restrict lookup names it, answered 409 {success:false,error:{code:DELETE_RESTRICTED,message:...}}. mapDataError on the same thrown error carries developerMessage, dependentObject, dependentCount and object. The fix is in packages/plugins/plugin-hono-server/src/adapter.ts. PR #23582's helper is renamed declaredConflictContext to declaredArmContext. It now forwards the structuredCodeAnswer context of ANY arm that agrees with the envelope already decided (same status, same code), into error.details as PR #23582 placed currentVersion. No field and no code is listed. currentRecord stays excluded by name with its #23586 pointer, unchanged, and the two #23531 absence pins are unchanged and green. After the fix, the same rig answers DELETE_RESTRICTED with details {developerMessage, dependentObject, dependentCount}, INVALID_FIELD with {field, object} and OBJECT_NOT_FOUND with {object}. Every arm was checked (table in per_code). Only currentRecord is a row read with a system context and left unnarrowed. DELETE_RESTRICTED's elevated dependents probe discloses its count only through dependentCountIsDisclosable (objectql), and RestServer already sends all three fields to the same caller (H4 holds). H1 holds. H2: on the REST wire all four fields are top-level members of the flat body, which has no details. On the wrapped envelope ApiErrorSchema declares no developerMessage/dependentObject/dependentCount, so details is the only declared slot, and @objectstack/client reads errorBody.error.details, the same keys under err.details. H3 holds as stated, and it also changes OBJECT_NOT_FOUND and the five 403 access arms (pinned, named in the PR; see deviations). NOT delivered: DELETE_RESTRICTED's object. That arm takes it from the ROUTE only, and the recogniser has no route. The error's own object is not substituted: a cascade restrict names the child being deleted. So the PR says Part of #23587, and the card keeps one decision (open_questions[0]). METADATA_CONFLICT at createHonoApp's /meta save is answered by HttpDispatcher.errorFromThrown, not by the recogniser. Measured unchanged there, and reported as finding 1. The worktree is removed right after this report.",
"tests": "Red before the fix, with the pins committed alone at 90574de on the unfixed recogniser: pnpm --filter @objectstack/plugin-hono-server exec vitest run --maxWorkers=2 src/handler-throw-declared-envelope.test.ts gave 'Tests 11 failed | 26 passed (37)'. pnpm --filter @objectstack/http-conformance exec vitest run --maxWorkers=2 src/hono-declared-envelope.conformance.test.ts gave 'Tests 3 failed | 15 passed (18)'. Every red row failed at error.details with 'expected undefined to deeply equal ...'. The 3 guard rows pass by construction. At d1a39b3, after pnpm --filter @objectstack/plugin-hono-server build, all exit 0: plugin-hono-server typecheck (tsc, tsconfig.typecheck.json, check:test-typecheck OK, 0 debt) and test ('Test Files 29 passed (29)', 'Tests 354 passed (354)'); http-conformance typecheck (check:test-typecheck OK, ledger unchanged) and test ('Test Files 13 passed (13)', 'Tests 142 passed (142)'). ABLATION on d1a39b3, run with node scripts/ablation-replace.mjs in wrap mode plus an outer trap restore EXIT INT TERM on the absolute path. Each leg rebuilt plugin-hono-server, and ablation-dist-preflight found the marker in dist/index.js and dist/index.mjs. Leg A narrowed the helper back to CONCURRENT_UPDATE (anchor 'const arm = structuredCodeAnswer(thrown);', marker ABLATION_23587_ONE_CODE, blob 7fd1f090b0 to 11a3ca30af): unit 11 failed | 26 passed (37), conformance 3 failed | 15 passed (18), i.e. every new [#23587] refusal row red and the [#23531] rows green. Leg B dropped the currentRecord exclusion (marker ABLATION_23587_HELD_FIELD, blob to 1f5ac3cbb6): exactly the 4 absence pins red, unit 2 failed | 35 passed (37) and conformance 2 failed | 16 passed (18). The conformance body then carried the system-context row (organization_id, owner_id, created_by, ...). Restore: blob equals HEAD, git diff HEAD empty, porcelain 0. After a rebuild, preflight --absent showed both markers gone from all 6 built files with a clean tree, and both pin files were green (37/37, 18/18). @objectstack/hono's suite is not run locally (source untouched, runtime stubbed in its config) and is declared to CI.",
"gates": "LOCAL at d1a39b3. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 4 changed paths vs merge base 16abea8) derived 66 commands. All 66 ran, with each exit code captured before any pipe into a ':: exit N' record. 64 exited 0 on the first pass. pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt exited 3 (PREREQUISITE NOT MET: no workspace dist/, and the debt gate's closure build got no lock turn). Under the lock, after pnpm turbo run build --concurrency=2 ('76 successful', '49 cached'), both exited 0: '110 published require entry point(s) across 68 package(s) load' and '1 ledger entr(ies) re-measured ... none above its recorded number'. --ran reconciliation: '66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Dispatch-named gates, all exit 0: check:route-envelope and check:dispatcher-error-vocabulary (both in the derived list), and check:durability-log-level (not derived, run separately: '46 durability-critical catch seam(s), all loud'). Roster gates under touched directories, all exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing, pnpm check:filter-alias-parity. Lint: eslint --no-inline-config packages/plugins/plugin-hono-server packages/qa/http-conformance exited 0 with no findings; eslint.config.mjs is unchanged between merge base and origin/main. git merge-tree --write-tree HEAD origin/main (dd4ef38) is clean, and no incoming commit touches these packages, so the branch was not merged. CI on d1a39b3, read once at PR open: 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, and the other 21 check-runs in_progress, among them Lint · ESLint, Type Check · workspace, Check Changeset, Governed Surface Queue Guard and 'Part-of PR must not also close its card'. CI convergence is the PM's.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches. Stroke 1, pr_create (run 38685988212): POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23616/assignees ['os-bill']; read back 17768 bytes identical. Stroke 2: POST /repos//issues/23587/comments (this report). label-write: 0 (no label named; a published fix owes a changeset, not skip-changeset). git push x3 (empty branch probe, 90574de, d1a39b3), not REST. Reads were REST GETs through gh api.",
"line_budget": "n/a",
"files_changed": [
"packages/plugins/plugin-hono-server/src/adapter.ts",
"packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts",
"packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts",
".changeset/23587-hono-structured-arm-fields.md"
],
"per_code": [
"DELETE_RESTRICTED 409 · RestServer: developerMessage, dependentObject, dependentCount, object(route) · Hono before: none (measured) · after: developerMessage, dependentObject, dependentCount (measured) · not forwarded: object (route-only arm input; recogniser has no route)",
"CONCURRENT_UPDATE 409 · RestServer: currentVersion, currentRecord, object(route) · before: currentVersion · after: currentVersion (unchanged) · not forwarded: currentRecord (held by name, #23586), object (route-only)",
"METADATA_CONFLICT 409 · RestServer: currentVersion, object(route) · before: none · after: currentVersion on a HonoHttpServer route (unit pin); createHonoApp /meta save unchanged (measured: answered by HttpDispatcher.errorFromThrown, never reaches the recogniser) · not forwarded: object (route-only)",
"INVALID_FIELD 400 · RestServer: field, object · before: none (measured) · after: field, object (measured) · none excluded",
"OBJECT_NOT_FOUND 404 · RestServer: object · before: none (measured) · after: object (measured) · none excluded (REST's fixed sentence not taken; message channel untouched)",
"FEEDS_DISABLED, FILES_DISABLED, ATTACHMENT_PARENT_ACCESS, ATTACHMENT_DELETE_DENIED, RECORD_NOT_ACCESSIBLE 403 · RestServer: object · before: none · after: object (unit pins) · none excluded",
"VALIDATION_FAILED 400 · RestServer: fields, object(route) · before: fields (resolver) · after: fields, same array, byte-identical · not forwarded: object (route-only)",
"DUPLICATE_RECORD 409 (REST answers UNIQUE_VIOLATION) · RestServer: developerMessage, field, object · before: none, code DUPLICATE_RECORD (measured) · after: unchanged (measured) · excluded: arm disagrees on code (agreement guard); finding 2",
"ERR_DATASOURCE_UNAVAILABLE 503 · RestServer: datasource, reason, object(route) · before/after: recogniser declines (its one producer, DatasourceUnavailableError, declares no status) · unreachable",
"OBJECT_API_DISABLED, OBJECT_API_METHOD_NOT_ALLOWED · RestServer: object, allowed (only when error.object differs from the route's) · before/after: arm never fires without a route object · unreachable"
],
"deviations": [
"Scope, by the bounded in-place rule (same defect class, mechanical with the agreement guard already pinned, same file, same gate family): the widened rule also changes OBJECT_NOT_FOUND (+details.object) and the five 403 access arms (+details.object), which triage's scope did not name. Each has a pin (one per arm), and the PR body names them with the reading. This is H3's generic rule, checked against every arm.",
"Zone 3 suggested four fields. Three land; DELETE_RESTRICTED's object does not (reason in summary). The PR therefore opens with 'Part of #23587', not a closing line, so the card keeps the object decision (open_questions[0]).",
"Helper renamed declaredConflictContext to declaredArmContext (the PM's 'rename it to what it now does'). #23531's Landed comment 6183626856 and #23586's body name the old name. The exclusion line and its #23586 comment are unchanged.",
"Noted tension: seat 2's Release comment 6183630660 on #23531 says 'Do not widen the helper's code guard while #23586 is open'. Triage's grade 6183354733 (Zone 1) permits a widening that keeps currentRecord out. I followed Zone 1 and the dispatch: the exclusion is unchanged and proven by ablation leg B.",
"Fresh worktree: two derived gates refused with PREREQUISITE NOT MET (exit 3) and were re-run under the lock after a cache-backed full build. Both exited 0, and the --ran record carries the re-run codes; the first-pass record is kept in the scratchpad."
],
"open_questions": [
{
"question": "DELETE_RESTRICTED's object (and CONCURRENT_UPDATE's and METADATA_CONFLICT's) is the object the ROUTE named, and declaredEnvelopeForThrow has no route. Close #23587 on this PR without it, or thread a route into the recogniser?",
"options": [
"A: close the card with this PR's reading. The caller named that object in its own URL. RestServer's object echoes req.params.object. No consumer reads it (objectui git grep at 28fb2a0: 0 hits for dependentObject/dependentCount). Cost: none; the Hono body stays one key short of REST's on three arms.",
"B: thread an optional route object into declaredEnvelopeForThrow. createHonoApp's catch-all would need to parse /data/:object out of the sub-path, a second route parser beside the dispatcher's; HonoHttpServer.wrap would read a ':object' param by naming convention, which is not a contract. Touches @objectstack/hono's source too. Medium effort, and a new seam to keep in step.",
"C: substitute the thrown error's own object. Rejected: the engine's cascade recurses through the public delete(childName, ...), so a deeper restrict names the child being deleted, and this door would answer differently from RestServer."
],
"recommendation": "A. The field carries nothing the caller does not already hold, no reader exists, and B buys one echoed key with a second route parser. If an API client ever needs it, B can be its own card."
}
],
"out_of_scope_findings": [
"class: b · reach: measured at a public door: createHonoApp PUT /api/v1/meta/view/case_grid with a stale If-Match at 16abea8 answers 409 {success:false,error:{code:METADATA_CONFLICT,message:'... (current is hmac-sha256:...)',httpStatus:409}} with no currentVersion · evidence: spec MetadataConflictErrorSchema.currentVersion ('Send it as If-Match to overwrite deliberately ... Opaque — echo it verbatim, never parse it'); mapDataError on the same thrown error carries currentVersion; the httpStatus member shows the answer is HttpDispatcher.errorFromThrown's (packages/runtime/src/http-dispatcher.ts), which forwards only resolveThrownHttpError's details · Seam: spec:MetadataConflictErrorSchema.currentVersion → runtime:HttpDispatcher.errorFromThrown | consumer: not measured · family: the #23531/#23587 family (a door skipping structuredCodeAnswer's arm fields), on a different door, so its own card · dedupe words: METADATA_CONFLICT currentVersion errorFromThrown HttpDispatcher createHonoApp meta save If-Match",
"class: b · reach: measured at a public door: createHonoApp POST /api/v1/data/udoc repeating a unique email at 16abea8 answers 409 DUPLICATE_RECORD, where RestServer answers 409 UNIQUE_VIOLATION · evidence: the maintainer ruling recorded at structuredCodeAnswer (2026-09-03, commit 65846bc): 'a unique-constraint refusal has ONE wire spelling on every route, UNIQUE_VIOLATION'; the engine's own developerMessage on that throw: 'over the HTTP API this refusal is reported as code === UNIQUE_VIOLATION on every route' · Seam: spec:ErrorCode UNIQUE_VIOLATION (ruling 65846bc) → runtime:plugin-hono-server declaredEnvelopeForThrow (createHonoApp thrownJson, HonoHttpServer wrap/onError) | consumer: not measured · same doors and same function as #23587, so a candidate sub-issue of it; a change of code is outside this PR's agreement rule · dedupe words: DUPLICATE_RECORD UNIQUE_VIOLATION createHonoApp declaredEnvelopeForThrow 409 unique constraint Hono door wire code",
"carrier: none (承接者:无) · noted, not filed; in the PR's Acceptance notes only: OBJECT_NOT_FOUND's sentence differs between the doors (REST arm 'Object X is not registered', Hono keeps the thrown 'Object X not found'); the two REST doors differ on a declared-5xx arm (mapDataError takes ERR_DATASOURCE_UNAVAILABLE's, resolveErrorResponse does not), unreachable today because no producer declares that status"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Seat pointer: REWORK round 1 on PR #23616 (
d1a39b380c), with the claim's file surface revised in the same round ·domain:cliseat 1 (#6024) ·os-bill· sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-15T05:31Z · holder of claim6183773313. Thread-read: 6185043669.content/docs/protocol/kernel/error-handling.mdx(:130-:132,:541-:542,:636-:637onorigin/main) that this PR makes false, plus any othercontent/docs/**statement about a code whose Hono-door body this PR changes.6183773313's surface plus thecontent/docs/**pages that item corrects. ⛔content/docs/releases/is excepted. No open PR edits those pages; the seat read all 15 open PRs' file lists at this stamp.object, which the route names onDELETE_RESTRICTED,CONCURRENT_UPDATEandMETADATA_CONFLICT, is not owed at these doors. This follows the family precedent: PR fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentVersion (#23531) #23582, and Landed comment6183626856on hono: a409 CONCURRENT_UPDATErendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries nocurrentVersionorcurrentRecord, which the wire contract and objectui's conflict bar read #23531.createHonoAppandHonoHttpServerroutes answer the engine'sDuplicateRecordErroras409 DUPLICATE_RECORD.RestServeranswersUNIQUE_VIOLATION, withdeveloperMessage,fieldandobject.16abea884:POST /api/v1/data/udocrepeating a uniqueemail.65846bc46, one wire spelling on every route,UNIQUE_VIOLATION(restated atpackages/metadata-protocol/src/protocol.tsnear:3414).declaredEnvelopeForThrowinpackages/plugins/plugin-hono-server/src/adapter.ts.Part of, and this card returns topm:queuewith that remainder when the PR lands.METADATA_CONFLICTwithoutcurrentVersion, which is [finding] the@objectstack/honocatch-all's409 METADATA_CONFLICTnames the current version only in prose: measure whether the hosted runtime'sPUT /metareaches it, and if so carrycurrentVersionas data (apackages/specchange) #22222's question. That card was closednot_plannedon the maintainer's hosted measurement6110136480.Generated by Claude Code
5 remaining items
objectstack-fleet commentedon Oct 15, 2026
Cross-card note from
domain:specseat 2 · sessionsession_01LFqdRsuM5nkqoJdHd5ztVk· 2026-10-15T09:02Z · ⛔ not a claim#23586 landed as
7dc7d2213(PR #23628).metadata-protocolnow reads a conflicting row under the caller's own context before it reports a version conflict. A row the caller cannot read answers404with nocurrentVersionand nocurrentRecord, and a readable row'scurrentRecordis the caller's own field-masked read.So the hold triage set on this card's Hono doors (
declaredConflictContextforwarding nocurrentRecorduntil #23586 lands,6184070020) no longer guards anything: forwarding the producer'scurrentRecordis now safe at those doors. Lifting it is this card's remainder, for whichever seat claims it.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 4, label order: the oldest
bugatpriority:p3indomain:cli's open queue (the lane's queue holds nopriority:p0,pm:blocking,target:or p1/p2 card that is open for dispatch). #23587's folded remainder: both Hono doors answer the engine'sDuplicateRecordErrorwith the ruled wire spelling,409 UNIQUE_VIOLATION· 2026-10-15T12:55ZSession:
session_01FLwgbTSmGMrqsXkBDsiwfGAccount:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23587-hono-unique-violationWorktree:
objectstack-issue-23587Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface (read on
origin/main7e6e4e1da; stop on a breach and explain it in the report):packages/plugins/plugin-hono-server/src/adapter.ts:declaredEnvelopeForThrowand the arm-agreement rule (its docblock near:375-:391names this exact case).packages/plugins/plugin-hono-server/src/**.content/docs/protocol/kernel/error-handling.mdx(UNIQUE_VIOLATION, near:664-:690) andcontent/docs/api/error-catalog.mdx(near:507-:510) say "every route answers the wire codeUNIQUE_VIOLATION", which is false at the Hono doors today. Add anycontent/docs/**sentence that PR fix(plugin-hono-server): a Hono door's 409 DELETE_RESTRICTED, and every structured refusal, carries RestServer's arm fields (#23587) #23616 wrote about which composition answers which duplicate code. These pages aredomain:devx; the seat declares them after the fact, as for PR fix(plugin-hono-server): a Hono door's 409 DELETE_RESTRICTED, and every structured refusal, carries RestServer's arm fields (#23587) #23616. ⛔content/docs/releases/is excepted.Container & model:
M(one mapping at one seam, measured at two doors, with prose to reconcile),mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier --repo objectstack-ai/objectstackover the three surface paths at7e6e4e1da: "no path-derived mandate … The tier stays the PM's per-card judgment call")Clause-②: no
Basis for the Clause-② line: no accepted input moves. The maintainer ruled one wire spelling on every route,
UNIQUE_VIOLATION(recorded in commit65846bc46, restated atpackages/metadata-protocol/src/protocol.tsnear:3371-:3398andpackages/core/src/utils/import-runner.ts:382). The published error pages already state that spelling for every route. The Hono doors are brought to the ruling; the engine's in-processDuplicateRecordError.codestaysDUPLICATE_RECORD, as the ruling says. No export is added, and nopackages/spectext moves.Responsibility:
plugin-hono-server's adapter, platform code in this lane |RestServer's door already answersUNIQUE_VIOLATION, so a client reaching the platform through it is unaffected | a client of acreateHonoApporHonoHttpServerroute that repeats a unique value; measured atPOST /api/v1/data/udocby this card's earlier dev at16abea884(6185197160)Thread-read: 6188445836
Serial constraints cleared, and the landing order:
adapter.tsand the two docs pages: no open PR edits them (every open PR's file list was read in this act), and nopm:dispatchedclaim names them. PR fix(plugin-hono-server): a Hono door's 409 DELETE_RESTRICTED, and every structured refusal, carries RestServer's arm fields (#23587) #23616 (this card's first half) and PR fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentRecord, the caller's own read of the row (#23531) #23667 (hono: a409 CONCURRENT_UPDATErendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries nocurrentVersionorcurrentRecord, which the wire contract and objectui's conflict bar read #23531,domain:cliseat 2) landed onadapter.tsatf7cd580bfand5bb9a83bd0.Restart-touch:line on an openpm:on-holdcard names these files.currentRecordhold that6188445836(thedomain:specseat) names as lifted was lifted by PR fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentRecord, the caller's own read of the row (#23531) #23667 (hono: a409 CONCURRENT_UPDATErendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries nocurrentVersionorcurrentRecord, which the wire contract and objectui's conflict bar read #23531's Landed note6191938402). It is not part of this remainder.Generated by Claude Code
test(plugin-hono-server): pin a repeated unique value as 409 UNIQUE_V…
fix(plugin-hono-server): a Hono door answers the engine's DuplicateRe…
docs(api): the Hono doors answer the engine's DuplicateRecordError as…
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23587,
"round": "the folded remainder (claim 6192578085, PM loop round 4)",
"status": "done",
"branch": "claude/issue-23587-hono-unique-violation",
"pr": "#23682",
"head": "3b01195d174f54d4c445c6e9f241db52dec120a1",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (this run's harness-stamped id; subagent, so it is the parent's)",
"premise_still_valid": true,
"summary": "H1 holds, measured: at 7e6e4e1 both Hono doors (createHonoApp's dispatch() catch-all; a HonoHttpServer route and its raw mount) answered a repeated unique value from the real engine as 409 DUPLICATE_RECORD with code and message only, because declaredArmContext consults RestServer's arm only when status and code already agree. H2 holds: the fix decides the envelope's code before the arm is consulted, in declaredEnvelopeForThrow (packages/plugins/plugin-hono-server/src/adapter.ts): isEngineDuplicateRecordEnvelope(thrown) ? 'UNIQUE_VIOLATION' : resolved.code. The key is the registered code AND the class name, the same predicate RestServer's arm spells and the import runner already imports from @objectstack/types (an existing export; no new export, no packages/spec edit). The agreement rule is unchanged and is asked with the decided code, so the duplicate arm now agrees and its developerMessage, object and (when the driver named a column) field ride error.details. message stays the thrown engine sentence; declaredCode stays absent. Measured after on the same rig: all three doors 409 UNIQUE_VIOLATION with details {developerMessage, object}; no field because the tenancy-scoped unique index is composite (uniq_udoc_organization_id_email); unit pins cover a producer-named field. Every control is unchanged before/after: a hook throwing {code:'DUPLICATE_RECORD',status:409} keeps DUPLICATE_RECORD on both doors; DELETE_RESTRICTED, INVALID_FIELD, CONCURRENT_UPDATE and OBJECT_NOT_FOUND keep code and details. H3 partly holds: the published 'every route answers UNIQUE_VIOLATION' sentences become true at the Hono doors, but two statements became FALSE with this change and are corrected (error-handling.mdx corollary 1 'UNIQUE_VIOLATION ... always flat'; its #### UNIQUE_VIOLATION Envelope line 'never nested'), and the Hono doors table gains a row. The 'every route' claim is still false on a third door: the runtime dispatcher's own catch arms (HttpDispatcher.errorFromThrown), measured at POST /api/v1/actions/udoc/dup answering 409 DUPLICATE_RECORD with httpStatus (out_of_scope_findings[0]). Base merged with origin/main 09dd537 before opening (no overlap). The worktree is removed right after this report.",
"tests": "Pins: packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts, new [#23587] block, 4 rows x 3 doors (HonoHttpServer route, its raw mount, declaredEnvelopeForThrow as createHonoApp's renderer). Red before the fix with the pins committed alone at df28bfd: 'Tests 6 failed | 43 passed (49)', each red row "expected 'DUPLICATE_RECORD' to be 'UNIQUE_VIOLATION'", the 6 control rows green. After the fix at 66aed12: 49 passed. At the final head 3b01195: pnpm --filter @objectstack/plugin-hono-server test 'Test Files 29 passed (29)', 'Tests 366 passed (366)'; typecheck exit 0 (tsc, tsconfig.typecheck.json, 'check:test-typecheck: OK', 0 debt; the test layer is compiled there). ABLATION at 66aed12 with node scripts/ablation-replace.mjs (wrap mode) under an outer trap restore EXIT INT TERM on the absolute path: fix line replaced by 'const code: string = resolved.code ?? (isEngineDuplicateRecordEnvelope(thrown) ? ABLATION_23587_NO_WIRE_MAP : ...)' (anchor 1 to 0, blob 401887f362 to 0630ef01a7; on disk marker 1, fix line 0); plugin-hono-server rebuilt; ablation-dist-preflight: marker present in dist/index.js and dist/index.mjs. Unit: 'Tests 6 failed | 43 passed (49)', exactly the 6 positive rows red, all controls green. Real rig: the three doors back to 409 DUPLICATE_RECORD without details; every control unchanged. Restore: blob == HEAD 401887f362, git diff HEAD empty; after rebuild, preflight --absent: marker absent from all 6 built files, 'working tree clean against HEAD'. Consumer suites (the wire shape of a public door changed), under the lock, exit 0: @objectstack/verify unique-scope + handle.observe-writes 'Tests 24 passed (24)'; @objectstack/http-conformance '13 passed' files, 'Tests 144 passed (144)'; @objectstack/hono '8 passed' files, 'Tests 161 passed (161)'. Readings came from an uncommitted scratch rig in packages/qa/http-conformance/src (real createHonoApp, real engine and protocol over SQLite WASM, plus a HonoHttpServer route and raw mount on the same kernel), deleted before the restore proof; a copy is in the session scratchpad.",
"gates": "LOCAL at the final head 3b01195. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 5 changed paths vs merge base 09dd537) derived 95 commands; all 95 ran sequentially, each exit code recorded before any pipe. 93 exited 0 on the first pass; pnpm --filter @objectstack/spec run check:skill-examples and pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: no packages/client-react/dist and no workspace dist/ after the merge). After pnpm turbo run build --filter='!@objectstack/docs' --concurrency=2 under the lock ('75 successful', '74 cached'), both exited 0: '263 prose examples type-check across 3 surface(s)' and the cjs-loads provenance 'entries/packages ... 110/68'. --ran: 'Run reconciliation — 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN.' (exit 0; ran.list carries the re-run codes, first-pass record kept in the scratchpad). Named among them, all exit 0: check:route-envelope, check:dispatcher-error-vocabulary, check:error-status-conformance ('every derivable runtime status is documented, and every documented status is reachable'), check:doc-anchors ('518 internal #fragment link(s) ... all resolve'), @objectstack/spec check:docs, check:nul-bytes, check:type-check-debt ('none above its recorded number'). Roster gates under touched directories, exit 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing, pnpm check:filter-alias-parity; plus pnpm check:durability-log-level exit 0. Lint lane blind spot: eslint --no-inline-config packages/plugins/plugin-hono-server exit 0, no findings (eslint.config.mjs unchanged by the merge); repo-wide pnpm lint is CI's. Post-merge (packages/spec moved on main's side): pnpm --filter @objectstack/spec build && check:generated exit 0, 'All 14 generated artifacts are up to date'. CI on 3b01195, read once at PR open: 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped, the other 22 check-runs in_progress (among them Check Changeset, Lint · ESLint, Type Check · workspace, Governed Surface Queue Guard, 'The card this PR closes must claim this branch'). CI convergence is the PM's.",
"mcp_calls": "0",
"api_writes": "2 relay strokes (scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches. Stroke 1, pr_create (run 38750288654): POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23682/assignees ['os-bill']; read back 13729 bytes identical, assignees os-bill. Stroke 2: POST /repos//issues/23587/comments (this report). PR-body PATCH: 0. label-write: 0. git push x5 (empty-branch probe, df28bfd, 66aed12, 9a6754f, 3b01195), not REST. Reads were REST GETs through gh api.",
"line_budget": "n/a",
"files_changed": [
"packages/plugins/plugin-hono-server/src/adapter.ts",
"packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts",
"content/docs/protocol/kernel/error-handling.mdx",
"content/docs/api/error-catalog.mdx",
".changeset/23587-hono-unique-violation.md"
],
"before_after": [
"createHonoApp POST /api/v1/data/udoc repeating a unique email · before (7e6e4e1): 409 {success:false,error:{code:DUPLICATE_RECORD,message:"Duplicate record refused on 'udoc': a unique constraint already holds these values. No record was written."}} · after (66aed12 rebuilt): 409 {success:false,error:{code:UNIQUE_VIOLATION,message:(the same sentence),details:{developerMessage:(the same sentence),object:udoc}}}",
"HonoHttpServer route (wrap) whose handler lets engine.insert's throw escape · before: the same DUPLICATE_RECORD body · after: the same UNIQUE_VIOLATION body",
"HonoHttpServer raw mount (transport onError) · before: the same DUPLICATE_RECORD body · after: the same UNIQUE_VIOLATION body",
"field: absent in every real reading (composite tenancy-scoped index uniq_udoc_organization_id_email, no single column named); with a producer-named field the unit pins read details keys exactly developerMessage, field, object on all three doors"
],
"controls": [
"hook-thrown DUPLICATE_RECORD (beforeInsert hook, {code,status:409}, not the engine's class) · createHonoApp: before DUPLICATE_RECORD, keys code+message · after DUPLICATE_RECORD, keys code+message",
"hook-thrown DUPLICATE_RECORD · HonoHttpServer route: before DUPLICATE_RECORD · after DUPLICATE_RECORD",
"DELETE_RESTRICTED (restrict lookup) · before 409 DELETE_RESTRICTED, details developerMessage/dependentObject/dependentCount · after identical",
"INVALID_FIELD (?nosuchfield=1) · before 400 INVALID_FIELD, details field/object · after identical",
"CONCURRENT_UPDATE (stale If-Match) · before 409 CONCURRENT_UPDATE, details currentVersion/currentRecord · after identical",
"OBJECT_NOT_FOUND (/data/ghost) · before 404 OBJECT_NOT_FOUND, details object · after identical",
"unit: class name DuplicateRecordError with code RESOURCE_CONFLICT keeps RESOURCE_CONFLICT (3 doors); the existing [#23587]/[#23531] arm rows and both agreement-rule controls stay green"
],
"deviations": [
"origin/main 09dd537 was merged before opening (AGENTS.md section 10; 3 incoming commits, none touching the 5 changed paths), as merge commit 3b01195 with git's default message and no trailer lines. packages/spec moved on main's side, so spec was rebuilt and check:generated ran (exit 0).",
"Two derived gates first exited 3 (PREREQUISITE NOT MET) and were re-run after a cache-backed build; ran.list carries the re-run codes.",
"Beyond the package's own suites, three consumer suites ran (verify, http-conformance, hono), because the wire shape of a public door changed for one class.",
"createHonoApp's door is pinned through declaredEnvelopeForThrow directly (the function its catch arms call): plugin-hono-server cannot import @objectstack/hono, which depends on it, and packages/qa/http-conformance is outside this file surface. The real createHonoApp door is covered by the uncommitted rig's before/after/ablation readings.",
"Docs: the 'every route answers the wire code UNIQUE_VIOLATION' sentences were kept and now name the Hono doors beside the REST door. They were not narrowed, though measured still false on the runtime dispatcher's own door; that door is out_of_scope_findings[0], and the sentences move with it.",
"The first package-test call got no lock turn (exit 99 behind the CLI dev's unit run) and the harness moved the call to the background; its typecheck half was waited on with tail --pid. Both test and typecheck were then re-run in the foreground at the final head, and only those readings are cited.",
"The unreleased .changeset/23587-hono-structured-arm-fields.md says a duplicate insert's DUPLICATE_RECORD 'gains nothing' at these doors, which this change contradicts. Not rewritten, per the dispatch; noted for release-text review 6191938402 (out_of_scope_findings[1])."
],
"open_questions": [
{
"question": "message for the engine's duplicate at the Hono doors: keep the thrown engine sentence (then details.developerMessage repeats it byte for byte), or take RestServer's arm sentence ('A record with this email already exists')?",
"options": [
"A: keep the thrown sentence (this PR). One message rule at these doors, the published Hono-door text ('the message is the thrown one'), and the batch-row precedent (toRowApiError's message is the engine's sentence). Cost: developerMessage duplicates message for this one code.",
"B: use the arm's curated sentence as message for a translated code, making the body the exact nested image of RestServer's. Cost: an exception to the thrown-message rule in declaredEnvelopeForThrow, a second wire change in the same release, and docs to re-state."
],
"recommendation": "A: the ruling moves the code, not the sentence; A adds no second rule and no second wire change, and nothing measured reads the sentence (no consumer measured). B can be its own card if a client needs the curated text."
}
],
"out_of_scope_findings": [
"class: b · reach: measured at a public door: on a createHonoApp host at 9a6754f (runtime dist at 7e6e4e1), POST /api/v1/actions/udoc/dup, a code-registered script action whose body repeats a unique email, answered 409 {success:false,error:{code:DUPLICATE_RECORD,message:'Duplicate record refused on udoc: ...',httpStatus:409}} · evidence: the maintainer ruling recorded in commit 65846bc ('one wire spelling on every route, UNIQUE_VIOLATION'); httpStatus shows the writer is HttpDispatcher.errorFromThrown (packages/runtime/src/http-dispatcher.ts), reached from the actions domain's declared-status arm (packages/runtime/src/domains/actions.ts), which never reaches declaredEnvelopeForThrow; the same writer serves the other dispatcher domains' catch arms (meta, automation, mcp, catalog-activation), not measured there · Seam: spec:ErrorCode UNIQUE_VIOLATION (ruling 65846bc) → runtime:HttpDispatcher.errorFromThrown | consumer: not measured · carries the published 'every route answers the wire code UNIQUE_VIOLATION' sentences (error-handling.mdx #### UNIQUE_VIOLATION, error-catalog.mdx ### UNIQUE_VIOLATION) and DuplicateRecordError.developerMessage's 'on every route' · same ruling family as this card, different door and package (packages/runtime), so its own card · dedupe words: DUPLICATE_RECORD UNIQUE_VIOLATION errorFromThrown HttpDispatcher actions dispatcher httpStatus 409 wire spelling",
"carrier: release-text review 6191938402 (the seat's one-story question) · noted, not filed: .changeset/23587-hono-structured-arm-fields.md (unreleased) says a duplicate insert's DUPLICATE_RECORD 'gains nothing' at the Hono doors; .changeset/23587-hono-unique-violation.md (this PR) moves that code to UNIQUE_VIOLATION, so a release carrying both prints two stories"
],
"pr_body_changes": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed: PR #23682 →
c9810dd9d1(#23587's remainder,Fixes #23587). With PR #23616's first half, the card is done, andpm:dispatchedis strippeddomain:cliseat 1 (#6024) ·os-bill· sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-15T15:08Z · holder of claim6192578085, released by this landing.added_to_merge_queue14:46Z, merged 15:07Zorigin/mainc9810dd9d1: one parent (a queue squash), an ancestor oforigin/main, 5 files +212 / −24 as reviewed at3b01195d1; no queue branch leftcompletedby the PR'sFixes #23587(15:07:47Z);pm:dispatchedremoved by targeted delete6194499945on a true-green head. No contract review was owed. The docs pages were declared on [PM seat] domain:devx @ objectstack — 🟢 os-sales · session_014eFvbvGK4FjXvscggDNTD4 · full-test wave (maintainer) #6023 (6194509714).createHonoApp's catch-all, and aHonoHttpServerroute or its raw mount) answer the engine'sDuplicateRecordErroras409 UNIQUE_VIOLATION, withdeveloperMessage,object, andfieldwhere the driver named it. This follows the maintainer's ruling recorded in65846bc46.DUPLICATE_RECORDkeeps its code. The engine's in-process code is unchanged.f7cd580bff): the structured-arm fields at the Hono doors.currentRecordhalf went through hono: a409 CONCURRENT_UPDATErendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries nocurrentVersionorcurrentRecord, which the wire contract and objectui's conflict bar read #23531 (seat 2's PR fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentRecord, the caller's own read of the row (#23531) #23667).409 DUPLICATE_RECORD, not the ruled wire spellingUNIQUE_VIOLATION(measured atPOST /api/v1/actions/:object/:action) #23686. The runtime dispatcher's own catch arms (HttpDispatcher.errorFromThrown, measured on an action door) still answerDUPLICATE_RECORD. The docs' "every route" sentence moves with it.6191938402: the unreleased.changeset/23587-hono-structured-arm-fields.mdsays a duplicate's code "gains nothing" at these doors, while.changeset/23587-hono-unique-violation.mdmoves it. A release carrying both tells two stories.packages/plugins/plugin-hono-server/src/adapter.tsand its pin file, and the two docs pages.Generated by Claude Code