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
mcp: a tool call that repeats a unique value answers the engine's in-process DUPLICATE_RECORD in its error result, not the ruled wire spelling UNIQUE_VIOLATION (create_record, run_action, resume_run) #23701
Filing gate ①, class (b). reach:measured at a public door by #23686's dev, using a real MCPServerRuntime from packages/mcp over a real ObjectQL + SqlDriver, with the dispatcher at ab389b338. POST /api/v1/mcptools/callcreate_record repeated a unique value. It answered 200 with a JSON-RPC result isError: true, whose text was {"error":{"code":"DUPLICATE_RECORD","message":"Duplicate record refused on 'udoc': …","status":409}} (os-dev-report6197200915 on #23686, out-of-scope finding 1).
Filed by domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG, from that report. ⛔ Not a claim. Grade and lane are triage's.
Governing text
The maintainer ruling recorded in commit 65846bc46, restated at packages/metadata-protocol/src/protocol.ts (near :3384): "one wire spelling on every route, UNIQUE_VIOLATION". The published pages (error-handling.mdx, error-catalog.mdx) say "the in-process spelling never crosses HTTP". The engine's in-process DuplicateRecordError.code stays DUPLICATE_RECORD. Only what crosses is translated, keyed on the registered code AND the class (isEngineDuplicateRecordEnvelope, @objectstack/types).
Where it crosses
Who writes it:thrownRefusalEnvelope (packages/mcp/src/mcp-http-tools.ts, called near :334 and :364). It is reached by errorResultWithCode (the data tools) and errorResultFromThrown (run_action, resume_run), and relays err.code verbatim.
Triage grades it and routes it. The landing is packages/mcp (domain:cli), in thrownRefusalEnvelope, on the same shared key. ⛔ Not a second translator with its own predicate.
Pin: create_record and run_action repeating a unique value carry UNIQUE_VIOLATION.
CONTROL: a tool body that throws the registered DUPLICATE_RECORD without being the engine's class keeps it.
Dedupe
MCP search_issues on objectstack-ai/objectstack, closed included: MCP tool error create_record run_action relays DUPLICATE_RECORD instead of UNIQUE_VIOLATION thrownRefusalEnvelope gave 4 hits. The hits are #23686 (open; the dispatcher's writer, not this one), #23509 (closed; the data tools rendered a refusal's message without its code) and two closed docs findings. None is this.
Triage: first grade · bug · priority:p2 · domain:cli · pm:queue · land after PR #23700 · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T16:57Z
Read fresh. An MCP tools/call that repeats a unique value (create_record, run_action, resume_run) answers an isError result whose code is the engine's in-process DUPLICATE_RECORD, not the ruled wire spelling UNIQUE_VIOLATION (65846bc46). The writer is @objectstack/mcp's own thrownRefusalEnvelope (mcp-http-tools.ts, near :334 and :364). Measured at the door by #23686's dev.
Grade p2: the same family and grade as #23686. A client or AI agent matching the catalog's code misses it on MCP.
Not folded into #23686: that card is in flight (PR #23700), and its writer is the dispatcher's, not this one.
Lane domain:cli (packages/mcp).
Order: it lands after PR #23700. Reuse the one key, isEngineDuplicateRecordEnvelope (@objectstack/types), ⛔ not a second translator.
Pins: each of the three tools answers UNIQUE_VIOLATION. CONTROL: a hook that throws the registered code without being the engine's class keeps its own code (the ruling's own exception).
Claim: PM loop round 4, serial dispatch (the maintainer's 「当前任务处理完,后续改为串行派发」, recorded in seat post #6024 §1): the one card in flight. Label order: priority:p2 is the highest open rank in domain:cli's queue (no priority:p0, pm:blocking, open target: or p1 card; #23497 is pm:blocked), and #23701 is the older of the two p2 bugs. #23701: an MCP tool call that repeats a unique value answers the ruled wire spelling UNIQUE_VIOLATION · 2026-10-15T17:42Z
Session: session_01FLwgbTSmGMrqsXkBDsiwfG
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23701-mcp-unique-violation
Worktree: objectstack-issue-23701
Domain: domain:cli
Seat: domain:cli#1 (seat post #6024)
File surface (read on origin/main395ca21cc7; stop on a breach and explain it in the report):
packages/mcp/src/mcp-http-tools.ts: thrownRefusalEnvelope (:392), the one writer errorResultFromThrown (:333) and errorResultWithCode (:363) share.
Pins under packages/mcp/src/**, and one changeset.
⛔ Not packages/types (the key, isEngineDuplicateRecordEnvelope, is reused as it is), not packages/runtime, and not packages/spec.
Container & model: M (one writer, three tools to pin), mode:subagent, model: TIER_DEFAULT (dispatch-gates --tier --repo objectstack-ai/objectstack packages/mcp/src/mcp-http-tools.ts at 395ca21cc7: "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, and no packages/spec text moves. The MCP tool result's code is brought to the maintainer's ruled wire spelling (65846bc46: "one wire spelling on every route, UNIQUE_VIOLATION"), which the published error pages state for every route. The engine's in-process DuplicateRecordError.code stays DUPLICATE_RECORD. No export is added; thrownRefusalEnvelope's signature is unchanged.
Responsibility: @objectstack/mcp's tool-result writer, platform code in this lane | none: the REST, Hono and dispatcher doors translate, but an MCP client reaches only this writer | an MCP client, AI agents included, whose tool call repeats a unique value; measured at POST /api/v1/mcptools/call create_record by runtime: the dispatcher's own catch arms answer the engine's DuplicateRecordError as 409 DUPLICATE_RECORD, not the ruled wire spelling UNIQUE_VIOLATION (measured at POST /api/v1/actions/:object/:action) #23686's dev (6197200915)
Thread-read: 6197606607
Serial constraints cleared, and the landing order:
os-dev-report
{
"issue": 23701,
"status": "done",
"branch": "claude/issue-23701-mcp-unique-violation",
"pr": "#23713",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (subagent: the parent's id)",
"premise_still_valid": true,
"summary": "thrownRefusalEnvelope (packages/mcp/src/mcp-http-tools.ts), the one reader errorResultWithCode, errorResultFromThrown and the record resource share, now answers the engine's DuplicateRecordError as UNIQUE_VIOLATION: keyed on isEngineDuplicateRecordEnvelope, the code read off structuredCodeAnswer's arm (both existing @objectstack/types exports). Status 409, message, isError and the envelope's keys (code, message, status) are unchanged; a producer merely speaking DUPLICATE_RECORD keeps it. The envelope has no context slot (the data tools withhold details by design; run_action relays only the thrown value's own details, which the class lacks), so the code alone is carried, per the dispatch. H1 confirmed on a real engine (create_record, update_record, run_action handler + declarative update all DUPLICATE_RECORD before, UNIQUE_VIOLATION after); H2 resume_run pinned at the writer, NOT MEASURED on an engine; H3 update_record crosses, no batch tool exists.",
"tests": "At final head da084ab (origin/main still = merge base 69351e9): pnpm --filter @objectstack/mcp test → 'Test Files 40 passed (40)', 'Tests 478 passed (478)'; pnpm --filter @objectstack/mcp typecheck → exit 0, 'check:test-typecheck: OK' (no new debt; tsc -p tsconfig.test.json --listFiles lists the new test file); eslint --no-inline-config packages/mcp → exit 0, empty output; pnpm --filter @objectstack/mcp build → exit 0 (check-dts-emitted 2/2). New pins packages/mcp/src/mcp-unique-violation-wire-code.test.ts: 11 rows, all green. ABLATION at da084ab via node scripts/ablation-replace.mjs (wrap mode, outer trap EXIT INT TERM restoring the absolute path with git checkout HEAD --): anchor 'if (!isEngineDuplicateRecordEnvelope(err)) return thrownCode;' replaced by 'return thrownCode; // ABLATION_23701_NO_WIRE_MAP'; on disk anchor 1→0, marker 0→1, blob 378ec1bff879→05d1d36e1f14; pins and rig import mcp source by relative path (no dist in the resolution path; @objectstack/types unmutated), so no rebuild; pins 'Tests 5 failed | 6 passed (11)', exactly the 5 positive rows red ('expected DUPLICATE_RECORD to be UNIQUE_VIOLATION'), all 6 controls green; rig byte-identical to the before run at the merge base; restore: blob == HEAD 378ec1bff879, git diff HEAD empty. The same ablation at the first spelling 3368361 read the same (5 red / 6 green).",
"gates": "Local, final head da084ab: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 63 commands from 3 paths vs merge base 69351e9; all 63 run, exit codes captured before any pipe, all 63 exit 0; --ran: '63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN' (derived zero). First pass at 3368361: check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET → 0 after cache-backed 'pnpm turbo run build --filter=!@objectstack/docs --concurrency=2' (75 successful, 74 cached); check-comment-mask-corpus exit 1 = ENOENT on a scratch rig file deleted mid-sweep (not a gate verdict) → 0 on rerun; both 0 on the first pass at the final head. Extra, all exit 0 at final head: the 4 artifact-roster gates whose roster sits in a touched directory (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity) and pnpm --filter @objectstack/spec run check:error-code-provenance ('OK - every registered-code stamp site is listed ...'; it was RED on the first spelling, see deviations). eslint over packages/mcp exit 0. CI check-runs on da084ab at report time (read once, not waited on): 23 runs; 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; the other 21 in_progress, including Lint · ESLint, Lint · PM dispatch-gates, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Type Check · workspace / source gates / consumer gates / debt ledger, Check Changeset, Governed Surface Queue Guard, The card this PR closes must claim this branch.",
"line_budget": "n/a",
"mcp_calls": "0",
"api_writes": "2 relay strokes (scripts/pm/fleet-write/dispatch.mjs, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23713/assignees [os-bill]; read-back '15959 bytes sent, 15959 stored - identical', assignee os-bill; (2) comment → POST /repos//issues/23701/comments (this report). Plus git push x3 (empty-branch probe, 3368361, da084ab), not REST. No label-write, no PR-body PATCH.",
"files_changed": [
"packages/mcp/src/mcp-http-tools.ts (+52 / -3)",
".changeset/23701-mcp-unique-violation.md (+16, new; @objectstack/mcp patch, FROM DUPLICATE_RECORD TO UNIQUE_VIOLATION, Clause-② line)",
"packages/mcp/src/mcp-unique-violation-wire-code.test.ts (+237, new)"
],
"before_after": [
"rig: real MCPServerRuntime (mcp source) behind real HttpDispatcher /mcp over real ObjectQL + SqlDriver better-sqlite3 + real ObjectStackProtocolImplementation; udoc.email unique; before = merge base 69351e9, after = da084ab",
"create_record repeating email: before 200 isError {error:{code:DUPLICATE_RECORD, message:engine sentence, status:409}} → after same body, code UNIQUE_VIOLATION",
"update_record giving r2 r1's email: before DUPLICATE_RECORD/409 → after UNIQUE_VIOLATION/409",
"run_action, registered handler whose ctx.engine.insert repeats email: before DUPLICATE_RECORD/409 → after UNIQUE_VIOLATION/409",
"run_action, declarative operation:update action on r2: before DUPLICATE_RECORD/409 → after UNIQUE_VIOLATION/409",
"diff of before vs after outputs: exactly these 4 code lines; all other bytes identical; keys code,message,status both sides; store unchanged both runs",
"resume_run: NOT MEASURED on an engine (writer pinned: before = ablation red, after green)"
],
"tools": [
"create_record (errorResultWithCode): crossing, measured + pinned",
"update_record (errorResultWithCode): crossing, measured + pinned",
"run_action (errorResultFromThrown), handler write / declarative update: crossing, measured + pinned",
"resume_run (errorResultFromThrown): writer pinned; NOT MEASURED on an engine - packages/mcp composes no engine, packages/runtime no automation service; by reading a resumed run's node write failure is rethrown as plain Error ('Node x failed') and answered 400 FLOW_FAILED by classifyResumeResult, so it does not reach the writer as the engine's class",
"run_action on a type:flow action: not crossing, by reading (same FLOW_FAILED path)",
"run_action over a sandboxed body: crossing the writer but NOT translated - measured after the fix: DUPLICATE_RECORD (SandboxError relay drops the class name) - #23702's",
"delete_record, get_record, query_records, aggregate_records, describe_object, validate_expression, list_objects, list_actions (errorResultWithCode): translate by construction, no call writes a unique value, not measured",
"record resource (MCPServerRuntime.bridgeResources reads thrownRefusalEnvelope): translates by construction, a read; pinned",
"stdio face: same tools via wireBridgeTools, stdio-data-bridge create/update call engine.insert/update → same writer; by reading, not measured",
"batch tool: none exists"
],
"controls": [
"run_action, handler throws {code:DUPLICATE_RECORD,status:409} (not the class): rig before DUPLICATE_RECORD → after DUPLICATE_RECORD; pin green",
"create_record, beforeInsert hook throws {code:DUPLICATE_RECORD,status:409}: rig before DUPLICATE_RECORD → after DUPLICATE_RECORD; pin green",
"run_action, handler's ctx.engine.delete hits restrict lookup: rig before DELETE_RESTRICTED → after DELETE_RESTRICTED; pin green",
"delete_record on restricted parent: rig before DELETE_RESTRICTED → after DELETE_RESTRICTED; pin green",
"pin only: class name DuplicateRecordError with code CONCURRENT_UPDATE keeps CONCURRENT_UPDATE; record resource speaker keeps DUPLICATE_RECORD; all green before (ablation) and after"
],
"deviations": [
"First spelling (3368361) stamped the code as a module constant ('UNIQUE_VIOLATION' satisfies ErrorCode); check:error-code-provenance went RED ('@objectstack/mcp stamps UNIQUE_VIOLATION (constdef) ... not listed under its own owner key'), whose remedy is a ledger row or PROVENANCE_WAIVERS entry in packages/spec, outside the declared surface. Second commit da084ab reads the code off structuredCodeAnswer (@objectstack/types, the arm RestServer relays) behind the same key, so mcp spells no copy of its own; gate green. Not the gate-blind ternary literal the two landed doors use - that spelling was rejected as routing around the gate. Spelling choice is open_questions[1].",
"Scratch measurement rigs were placed temporarily as untracked files under packages/runtime/src (runtime composes the engine; mcp imported by relative path), never committed, deleted; one deletion raced the first gate pass (check-comment-mask-corpus ENOENT, rerun 0).",
"Attribution: commits carry the model-free trailer pair and the PR body the AGENTS.md session-URL footer, per the dispatch, not the harness reminder's model-named trailer / alternate footer."
],
"pr_body_changes": [],
"open_questions": [
{
"question": "Context slot for this refusal on MCP: carry the code alone (this PR), or add the REST arm's object/field?",
"options": [
"A: code alone, keys code/message/status on every tool (this PR; the dispatch's rule where no slot exists)",
"B: arm object/field in details on run_action/resume_run only, where a details member exists - two bodies for one refusal across tools",
"C: on every tool - reverses errorResultWithCode's declared withholding for this class, a second wire change"
],
"recommendation": "A, because no measured reader needs object/field on MCP (business need unmeasured), message already carries the developerMessage text, and B/C widen a surface at startup stage; A keeps one envelope shape per surface (no new capability declared)."
},
{
"question": "Spelling of the wire code at this door: relay structuredCodeAnswer's arm (this PR), or stamp a literal with a provenance row/waiver for @objectstack/mcp in packages/spec?",
"options": [
"A: relay the arm (this PR) - one statement of the code in @objectstack/types, no spec edit, Clause-② basis unchanged",
"B: literal + ERROR_CODE_LEDGER row under @objectstack/mcp or a PROVENANCE_WAIVERS entry - a packages/spec edit, Clause-② re-judged"
],
"recommendation": "A, because it single-sources the wire code with the classification REST already relays (contract-first, no second spelling an AI could drift), adds no declaration surface, and keeps the provenance gate green honestly rather than through its ternary blind spot."
}
],
"out_of_scope_findings": [
"class: b · reach: named producers - HttpDispatcher.errorFromThrown (packages/runtime/src/http-dispatcher.ts) and declaredEnvelopeForThrow (packages/plugins/plugin-hono-server/src/adapter.ts) stamp 'UNIQUE_VIOLATION' on the wire (measured at POST /actions by PR #23700 and the Hono doors by PR #23682) · evidence: ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts) lists UNIQUE_VIOLATION only under @objectstack/types, driver-memory, driver-sql; its header: 'a code emitted by several packages is listed once per emitting package'; check:error-code-provenance's header: 'every stamp site of a registered code in packages/** non-test source must be listed under the stamping package's own owner key, or carry a recorded PROVENANCE_WAIVERS entry'; both sites are inside a ternary, which the gate declares itself blind to, so it stays green; the same gate went red on this PR's first (constdef) spelling. Seam: spec:ERROR_CODE_LEDGER UNIQUE_VIOLATION provenance → runtime:HttpDispatcher.errorFromThrown + plugin-hono-server declaredEnvelopeForThrow | consumer: check:error-code-provenance (blind to ternary) · dedupe words: error-code-provenance UNIQUE_VIOLATION ledger row runtime plugin-hono-server ternary stamp blind",
"carrier: #23702 · run_action over a sandboxed action body repeating a unique value still answers DUPLICATE_RECORD after this fix (measured at da084ab: {error:{code:DUPLICATE_RECORD, message:"action 'sbx_duplicate' threw: DuplicateRecordError: Duplicate record refused on 'udoc': ...", status:409}}); noted, not filed",
"carrier: none (no carrier) · observation: a resumed or flow-typed run whose write repeats a unique value answers 400 FLOW_FAILED (by reading: the flow engine rethrows a node failure as a plain Error), never the unique-violation code; noted, not filed"
]
}
Landed: PR #23713 → 4c21e57169 (#23701). The card is closed completed, and pm:dispatched is stripped
domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T19:32Z · holder of claim 6198461167, released by this landing.
reading
value
queue
added_to_merge_queue 19:11Z, merged 19:32Z
origin/main
4c21e57169: one parent (a queue squash), an ancestor of origin/main, 3 files +305 / −3 as reviewed at da084ab11; no queue branch left
card
closed completed by the PR's Fixes #23701; pm:dispatched removed by targeted delete
Review chain: ACCEPT 6199920441 on a true-green head. No contract review was owed.
What landed:
@objectstack/mcp's thrownRefusalEnvelope, the one reader behind the data tools, run_action / resume_run and the record resource, answers the engine's DuplicateRecordError with UNIQUE_VIOLATION.
The code is read off structuredCodeAnswer's arm, so the package stamps no copy of it. Only the code moves: status 409, message and the envelope's keys are unchanged.
Measured: create_record, update_record, and run_action (handler write and declarative update).
The family on main now: the REST /data arm, the batch and import rows, both Hono doors, the runtime dispatcher's writer, and now the MCP tool result all answer UNIQUE_VIOLATION.
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p2·domain:cli·pm:queue· land after PR #23700 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T16:57ZRead fresh. An MCP
tools/callthat repeats a unique value (create_record,run_action,resume_run) answers anisErrorresult whose code is the engine's in-processDUPLICATE_RECORD, not the ruled wire spellingUNIQUE_VIOLATION(65846bc46). The writer is@objectstack/mcp's ownthrownRefusalEnvelope(mcp-http-tools.ts, near:334and:364). Measured at the door by #23686's dev.Grade p2: the same family and grade as #23686. A client or AI agent matching the catalog's code misses it on MCP.
Not folded into #23686: that card is in flight (PR #23700), and its writer is the dispatcher's, not this one.
Lane
domain:cli(packages/mcp).Order: it lands after PR #23700. Reuse the one key,
isEngineDuplicateRecordEnvelope(@objectstack/types), ⛔ not a second translator.Pins: each of the three tools answers
UNIQUE_VIOLATION. CONTROL: a hook that throws the registered code without being the engine's class keeps its own code (the ruling's own exception).findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 4, serial dispatch (the maintainer's 「当前任务处理完,后续改为串行派发」, recorded in seat post #6024 §1): the one card in flight. Label order:
priority:p2is the highest open rank indomain:cli's queue (nopriority:p0,pm:blocking, opentarget:or p1 card; #23497 ispm:blocked), and #23701 is the older of the two p2 bugs. #23701: an MCP tool call that repeats a unique value answers the ruled wire spellingUNIQUE_VIOLATION· 2026-10-15T17:42ZSession:
session_01FLwgbTSmGMrqsXkBDsiwfGAccount:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23701-mcp-unique-violationWorktree:
objectstack-issue-23701Domain:
domain:cliSeat:
domain:cli#1(seat post #6024)File surface (read on
origin/main395ca21cc7; stop on a breach and explain it in the report):packages/mcp/src/mcp-http-tools.ts:thrownRefusalEnvelope(:392), the one writererrorResultFromThrown(:333) anderrorResultWithCode(:363) share.packages/mcp/src/**, and one changeset.packages/types(the key,isEngineDuplicateRecordEnvelope, is reused as it is), notpackages/runtime, and notpackages/spec.Container & model:
M(one writer, three tools to pin),mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier --repo objectstack-ai/objectstack packages/mcp/src/mcp-http-tools.tsat395ca21cc7: "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, and no
packages/spectext moves. The MCP tool result's code is brought to the maintainer's ruled wire spelling (65846bc46: "one wire spelling on every route,UNIQUE_VIOLATION"), which the published error pages state for every route. The engine's in-processDuplicateRecordError.codestaysDUPLICATE_RECORD. No export is added;thrownRefusalEnvelope's signature is unchanged.Responsibility:
@objectstack/mcp's tool-result writer, platform code in this lane | none: the REST, Hono and dispatcher doors translate, but an MCP client reaches only this writer | an MCP client, AI agents included, whose tool call repeats a unique value; measured atPOST /api/v1/mcptools/call create_recordby runtime: the dispatcher's own catch arms answer the engine's DuplicateRecordError as409 DUPLICATE_RECORD, not the ruled wire spellingUNIQUE_VIOLATION(measured atPOST /api/v1/actions/:object/:action) #23686's dev (6197200915)Thread-read: 6197606607
Serial constraints cleared, and the landing order:
409 DUPLICATE_RECORD, not the ruled wire spellingUNIQUE_VIOLATION(measured atPOST /api/v1/actions/:object/:action) #23686) landed at395ca21cc7.packages/mcp/**: no open PR edits it (every open PR's file list was read in this act), and nopm:dispatchedclaim names it.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23701,
"status": "done",
"branch": "claude/issue-23701-mcp-unique-violation",
"pr": "#23713",
"session": "session_01FLwgbTSmGMrqsXkBDsiwfG (subagent: the parent's id)",
"premise_still_valid": true,
"summary": "thrownRefusalEnvelope (packages/mcp/src/mcp-http-tools.ts), the one reader errorResultWithCode, errorResultFromThrown and the record resource share, now answers the engine's DuplicateRecordError as UNIQUE_VIOLATION: keyed on isEngineDuplicateRecordEnvelope, the code read off structuredCodeAnswer's arm (both existing @objectstack/types exports). Status 409, message, isError and the envelope's keys (code, message, status) are unchanged; a producer merely speaking DUPLICATE_RECORD keeps it. The envelope has no context slot (the data tools withhold details by design; run_action relays only the thrown value's own details, which the class lacks), so the code alone is carried, per the dispatch. H1 confirmed on a real engine (create_record, update_record, run_action handler + declarative update all DUPLICATE_RECORD before, UNIQUE_VIOLATION after); H2 resume_run pinned at the writer, NOT MEASURED on an engine; H3 update_record crosses, no batch tool exists.",
"tests": "At final head da084ab (origin/main still = merge base 69351e9): pnpm --filter @objectstack/mcp test → 'Test Files 40 passed (40)', 'Tests 478 passed (478)'; pnpm --filter @objectstack/mcp typecheck → exit 0, 'check:test-typecheck: OK' (no new debt; tsc -p tsconfig.test.json --listFiles lists the new test file); eslint --no-inline-config packages/mcp → exit 0, empty output; pnpm --filter @objectstack/mcp build → exit 0 (check-dts-emitted 2/2). New pins packages/mcp/src/mcp-unique-violation-wire-code.test.ts: 11 rows, all green. ABLATION at da084ab via node scripts/ablation-replace.mjs (wrap mode, outer trap EXIT INT TERM restoring the absolute path with git checkout HEAD --): anchor 'if (!isEngineDuplicateRecordEnvelope(err)) return thrownCode;' replaced by 'return thrownCode; // ABLATION_23701_NO_WIRE_MAP'; on disk anchor 1→0, marker 0→1, blob 378ec1bff879→05d1d36e1f14; pins and rig import mcp source by relative path (no dist in the resolution path; @objectstack/types unmutated), so no rebuild; pins 'Tests 5 failed | 6 passed (11)', exactly the 5 positive rows red ('expected DUPLICATE_RECORD to be UNIQUE_VIOLATION'), all 6 controls green; rig byte-identical to the before run at the merge base; restore: blob == HEAD 378ec1bff879, git diff HEAD empty. The same ablation at the first spelling 3368361 read the same (5 red / 6 green).",
"gates": "Local, final head da084ab: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 63 commands from 3 paths vs merge base 69351e9; all 63 run, exit codes captured before any pipe, all 63 exit 0; --ran: '63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN' (derived zero). First pass at 3368361: check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET → 0 after cache-backed 'pnpm turbo run build --filter=!@objectstack/docs --concurrency=2' (75 successful, 74 cached); check-comment-mask-corpus exit 1 = ENOENT on a scratch rig file deleted mid-sweep (not a gate verdict) → 0 on rerun; both 0 on the first pass at the final head. Extra, all exit 0 at final head: the 4 artifact-roster gates whose roster sits in a touched directory (check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity) and pnpm --filter @objectstack/spec run check:error-code-provenance ('OK - every registered-code stamp site is listed ...'; it was RED on the first spelling, see deviations). eslint over packages/mcp exit 0. CI check-runs on da084ab at report time (read once, not waited on): 23 runs; 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; the other 21 in_progress, including Lint · ESLint, Lint · PM dispatch-gates, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Type Check · workspace / source gates / consumer gates / debt ledger, Check Changeset, Governed Surface Queue Guard, The card this PR closes must claim this branch.",
"line_budget": "n/a",
"mcp_calls": "0",
"api_writes": "2 relay strokes (scripts/pm/fleet-write/dispatch.mjs, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23713/assignees [os-bill]; read-back '15959 bytes sent, 15959 stored - identical', assignee os-bill; (2) comment → POST /repos//issues/23701/comments (this report). Plus git push x3 (empty-branch probe, 3368361, da084ab), not REST. No label-write, no PR-body PATCH.",
"files_changed": [
"packages/mcp/src/mcp-http-tools.ts (+52 / -3)",
".changeset/23701-mcp-unique-violation.md (+16, new; @objectstack/mcp patch, FROM DUPLICATE_RECORD TO UNIQUE_VIOLATION, Clause-② line)",
"packages/mcp/src/mcp-unique-violation-wire-code.test.ts (+237, new)"
],
"before_after": [
"rig: real MCPServerRuntime (mcp source) behind real HttpDispatcher /mcp over real ObjectQL + SqlDriver better-sqlite3 + real ObjectStackProtocolImplementation; udoc.email unique; before = merge base 69351e9, after = da084ab",
"create_record repeating email: before 200 isError {error:{code:DUPLICATE_RECORD, message:engine sentence, status:409}} → after same body, code UNIQUE_VIOLATION",
"update_record giving r2 r1's email: before DUPLICATE_RECORD/409 → after UNIQUE_VIOLATION/409",
"run_action, registered handler whose ctx.engine.insert repeats email: before DUPLICATE_RECORD/409 → after UNIQUE_VIOLATION/409",
"run_action, declarative operation:update action on r2: before DUPLICATE_RECORD/409 → after UNIQUE_VIOLATION/409",
"diff of before vs after outputs: exactly these 4 code lines; all other bytes identical; keys code,message,status both sides; store unchanged both runs",
"resume_run: NOT MEASURED on an engine (writer pinned: before = ablation red, after green)"
],
"tools": [
"create_record (errorResultWithCode): crossing, measured + pinned",
"update_record (errorResultWithCode): crossing, measured + pinned",
"run_action (errorResultFromThrown), handler write / declarative update: crossing, measured + pinned",
"resume_run (errorResultFromThrown): writer pinned; NOT MEASURED on an engine - packages/mcp composes no engine, packages/runtime no automation service; by reading a resumed run's node write failure is rethrown as plain Error ('Node x failed') and answered 400 FLOW_FAILED by classifyResumeResult, so it does not reach the writer as the engine's class",
"run_action on a type:flow action: not crossing, by reading (same FLOW_FAILED path)",
"run_action over a sandboxed body: crossing the writer but NOT translated - measured after the fix: DUPLICATE_RECORD (SandboxError relay drops the class name) - #23702's",
"delete_record, get_record, query_records, aggregate_records, describe_object, validate_expression, list_objects, list_actions (errorResultWithCode): translate by construction, no call writes a unique value, not measured",
"record resource (MCPServerRuntime.bridgeResources reads thrownRefusalEnvelope): translates by construction, a read; pinned",
"stdio face: same tools via wireBridgeTools, stdio-data-bridge create/update call engine.insert/update → same writer; by reading, not measured",
"batch tool: none exists"
],
"controls": [
"run_action, handler throws {code:DUPLICATE_RECORD,status:409} (not the class): rig before DUPLICATE_RECORD → after DUPLICATE_RECORD; pin green",
"create_record, beforeInsert hook throws {code:DUPLICATE_RECORD,status:409}: rig before DUPLICATE_RECORD → after DUPLICATE_RECORD; pin green",
"run_action, handler's ctx.engine.delete hits restrict lookup: rig before DELETE_RESTRICTED → after DELETE_RESTRICTED; pin green",
"delete_record on restricted parent: rig before DELETE_RESTRICTED → after DELETE_RESTRICTED; pin green",
"pin only: class name DuplicateRecordError with code CONCURRENT_UPDATE keeps CONCURRENT_UPDATE; record resource speaker keeps DUPLICATE_RECORD; all green before (ablation) and after"
],
"deviations": [
"First spelling (3368361) stamped the code as a module constant ('UNIQUE_VIOLATION' satisfies ErrorCode); check:error-code-provenance went RED ('@objectstack/mcp stamps UNIQUE_VIOLATION (constdef) ... not listed under its own owner key'), whose remedy is a ledger row or PROVENANCE_WAIVERS entry in packages/spec, outside the declared surface. Second commit da084ab reads the code off structuredCodeAnswer (@objectstack/types, the arm RestServer relays) behind the same key, so mcp spells no copy of its own; gate green. Not the gate-blind ternary literal the two landed doors use - that spelling was rejected as routing around the gate. Spelling choice is open_questions[1].",
"Scratch measurement rigs were placed temporarily as untracked files under packages/runtime/src (runtime composes the engine; mcp imported by relative path), never committed, deleted; one deletion raced the first gate pass (check-comment-mask-corpus ENOENT, rerun 0).",
"Attribution: commits carry the model-free trailer pair and the PR body the AGENTS.md session-URL footer, per the dispatch, not the harness reminder's model-named trailer / alternate footer."
],
"pr_body_changes": [],
"open_questions": [
{
"question": "Context slot for this refusal on MCP: carry the code alone (this PR), or add the REST arm's object/field?",
"options": [
"A: code alone, keys code/message/status on every tool (this PR; the dispatch's rule where no slot exists)",
"B: arm object/field in details on run_action/resume_run only, where a details member exists - two bodies for one refusal across tools",
"C: on every tool - reverses errorResultWithCode's declared withholding for this class, a second wire change"
],
"recommendation": "A, because no measured reader needs object/field on MCP (business need unmeasured), message already carries the developerMessage text, and B/C widen a surface at startup stage; A keeps one envelope shape per surface (no new capability declared)."
},
{
"question": "Spelling of the wire code at this door: relay structuredCodeAnswer's arm (this PR), or stamp a literal with a provenance row/waiver for @objectstack/mcp in packages/spec?",
"options": [
"A: relay the arm (this PR) - one statement of the code in @objectstack/types, no spec edit, Clause-② basis unchanged",
"B: literal + ERROR_CODE_LEDGER row under @objectstack/mcp or a PROVENANCE_WAIVERS entry - a packages/spec edit, Clause-② re-judged"
],
"recommendation": "A, because it single-sources the wire code with the classification REST already relays (contract-first, no second spelling an AI could drift), adds no declaration surface, and keeps the provenance gate green honestly rather than through its ternary blind spot."
}
],
"out_of_scope_findings": [
"class: b · reach: named producers - HttpDispatcher.errorFromThrown (packages/runtime/src/http-dispatcher.ts) and declaredEnvelopeForThrow (packages/plugins/plugin-hono-server/src/adapter.ts) stamp 'UNIQUE_VIOLATION' on the wire (measured at POST /actions by PR #23700 and the Hono doors by PR #23682) · evidence: ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts) lists UNIQUE_VIOLATION only under @objectstack/types, driver-memory, driver-sql; its header: 'a code emitted by several packages is listed once per emitting package'; check:error-code-provenance's header: 'every stamp site of a registered code in packages/** non-test source must be listed under the stamping package's own owner key, or carry a recorded PROVENANCE_WAIVERS entry'; both sites are inside a ternary, which the gate declares itself blind to, so it stays green; the same gate went red on this PR's first (constdef) spelling. Seam: spec:ERROR_CODE_LEDGER UNIQUE_VIOLATION provenance → runtime:HttpDispatcher.errorFromThrown + plugin-hono-server declaredEnvelopeForThrow | consumer: check:error-code-provenance (blind to ternary) · dedupe words: error-code-provenance UNIQUE_VIOLATION ledger row runtime plugin-hono-server ternary stamp blind",
"carrier: #23702 · run_action over a sandboxed action body repeating a unique value still answers DUPLICATE_RECORD after this fix (measured at da084ab: {error:{code:DUPLICATE_RECORD, message:"action 'sbx_duplicate' threw: DuplicateRecordError: Duplicate record refused on 'udoc': ...", status:409}}); noted, not filed",
"carrier: none (no carrier) · observation: a resumed or flow-typed run whose write repeats a unique value answers 400 FLOW_FAILED (by reading: the flow engine rethrows a node failure as a plain Error), never the unique-violation code; noted, not filed"
]
}
objectstack-fleet commentedon Oct 15, 2026
Landed: PR #23713 →
4c21e57169(#23701). The card is closedcompleted, andpm:dispatchedis strippeddomain:cliseat 1 (#6024) ·os-bill· sessionsession_01FLwgbTSmGMrqsXkBDsiwfG· 2026-10-15T19:32Z · holder of claim6198461167, released by this landing.added_to_merge_queue19:11Z, merged 19:32Zorigin/main4c21e57169: one parent (a queue squash), an ancestor oforigin/main, 3 files +305 / −3 as reviewed atda084ab11; no queue branch leftcompletedby the PR'sFixes #23701;pm:dispatchedremoved by targeted delete6199920441on a true-green head. No contract review was owed.@objectstack/mcp'sthrownRefusalEnvelope, the one reader behind the data tools,run_action/resume_runand the record resource, answers the engine'sDuplicateRecordErrorwithUNIQUE_VIOLATION.structuredCodeAnswer's arm, so the package stamps no copy of it. Only the code moves:status409,messageand the envelope's keys are unchanged.create_record,update_record, andrun_action(handler write and declarative update).mainnow: the REST/dataarm, the batch and import rows, both Hono doors, the runtime dispatcher's writer, and now the MCP tool result all answerUNIQUE_VIOLATION.DuplicateRecordErrora sandboxed script body lets propagate leaves the VM asSandboxError, so no door recognises it and it answers409 DUPLICATE_RECORD, not the ruledUNIQUE_VIOLATION#23702: the sandbox relay, under triage direction A. It is the next card under this seat's serial dispatch.UNIQUE_VIOLATIONas a literal inside a ternary thatcheck:error-code-provenancecannot see, and the ledger has no row for either package #23714: the two earlier doors stamp the literal inside a ternary the provenance gate cannot see. One remedy is this PR's arm relay.packages/mcp/src/mcp-http-tools.ts.Generated by Claude Code