Skip to content

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

Closed

Description

@objectstack-fleet

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

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_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.

Dedupe words: MCP create_record run_action tool error DUPLICATE_RECORD UNIQUE_VIOLATION thrownRefusalEnvelope errorResultWithCode wire spelling.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    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).

    finding leaves on grading.

  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main 395ca21cc7; stop on a breach and explain it in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
    ]
    }

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    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

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions