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
Description
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/mcp tools/call create_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-report 6197200915 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:334and:364). It is reached byerrorResultWithCode(the data tools) anderrorResultFromThrown(run_action,resume_run), and relayserr.codeverbatim. - Not this writer: the dispatcher's MCP transport catch is never reached by a tool failure.
@objectstack/mcprenders the tool error itself, which is why PR fix(runtime): the dispatcher answers the engine's DuplicateRecordError as 409 UNIQUE_VIOLATION, with the arm's fields (#23686) #23700 (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) does not cover it. - Already translating:
RestServer's/dataarm, the batch and import rows, both Hono doors (PR fix(plugin-hono-server): a Hono door answers the engine's DuplicateRecordError as 409 UNIQUE_VIOLATION, with the arm's fields (#23587) #23682, 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) and the dispatcher's thrown-error writer (PR fix(runtime): the dispatcher answers the engine's DuplicateRecordError as 409 UNIQUE_VIOLATION, with the arm's fields (#23686) #23700, 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).
Reader who acts
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_recordandrun_actionrepeating a unique value carryUNIQUE_VIOLATION. - CONTROL: a tool body that throws the registered
DUPLICATE_RECORDwithout 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.
Dedupe words: MCP create_record run_action tool error DUPLICATE_RECORD UNIQUE_VIOLATION thrownRefusalEnvelope errorResultWithCode wire spelling.
Generated by Claude Code
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