[GH-ISSUE #5396] [Bug] Duplicate record_unknown calls are reported as retryable revision conflicts #3266

Closed
opened 2026-10-31 00:38:34 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @coygeek on GitHub (Oct 20, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5396

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

MCP or agent integration

Observed behavior

Repeating an explicit record_unknown request in one stdio MCP session correctly rejects the duplicate, but reports revision_conflict, retryable: true, and “Refresh the current state and try again.” The request has no expected revision, and the retained identity has not changed. Both an unchanged request and a severity-only variant produced this diagnostic in an independent production-entry-point reproduction.

Reproduction

Use the built source revision below and start node scripts/rea.mjs --mcp. The independent verifier used isolated disposable HOME, XDG configuration/data/cache/state directories, configuration path, and temporary directory; no provider or binary was opened.

Send these newline-delimited JSON-RPC messages through one server process. Wait for the initialize response before sending the notification and tool calls:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"ledger-verifier","version":"1.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"record_unknown","arguments":{"question":"Which build number produced this artifact?","domain":"general","severity":"medium","required_authority":null,"required_confidence":"observed","required_environment":null,"recommended_probes":[],"relationships":[],"supporting_evidence_ids":[],"contradicting_evidence_ids":[]}}}

The creation succeeds at revision 1. Repeat the final message with a new JSON-RPC ID and severity changed to high; then repeat the original arguments unchanged with another ID. Both duplicates fail with the diagnostic below. These are the shortest creation/duplicate steps from the completed replay; no autonomous retry loop was run.

Expected behavior

A duplicate should remain rejected while identifying the existing identity conflict and avoiding a retryable stale-revision diagnosis or an ineffective refresh-and-repeat instruction. A genuine stale update_unknown revision should remain retryable.

src/contracts/sessionToolContracts.ts:145-153 advertises deterministic duplicate rejection and exact expected-revision matching for updates; the live tools/list advertisement matched. docs/mcp-contracts.md:81-87 makes the canonical diagnostic the source of error code and recovery guidance. AGENTS.md requires preserving meaningful failure reasons. The replacement code and diagnostic structure are implementation choices.

Impact and scope

Supported P2: MCP clients receive a false machine-readable retry signal for a deterministic rejection. Both duplicate variants failed in the completed replay. An actual downstream retry loop or exhausted retry budget was not observed; no data loss was observed.

Demonstrated workaround: inspect details.reason, call list_unknowns, and use a complete update_unknown with the existing head's current revision when intending to change severity. That update succeeded at revision 2. Rejected writes preserved the original head. Other REA versions and native providers were not tested.

REA version

6.7.0, reported by MCP initialize; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node.js v26.12.0; darwin/arm64. No Hopper or Ghidra required.

Target and artifact context

Session-local residual-unknown registry, with no open binary, provider, database, or exported artifact.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Both duplicate calls returned isError: true, no success structured content, and this JSON in text content:

{"error":{"code":"revision_conflict","category":"execution_failure","message":"Evidence state changed before the update completed. Refresh the current state and try again.","retryable":true,"remediation":{"action":"Evidence state changed before the update completed. Refresh the current state and try again."},"details":{"reason":"already-exists"}}}

The independent replay also exercised these controls through complete full-state update_unknown requests:

  • Invalid contradicted transition without contradicting evidence: execution_failure, retryable: false, reason invalid-transition, but the same state-changed message/remediation.
  • Absent relationship target: evidence_integrity_mismatch, retryable: false, reason integrity, with that same message/remediation.
  • Missing update head: correct not-found-specific message.
  • Valid severity update: revision 2; subsequent expected_revision: 1 update: genuine revision_conflict, retryable: true, reason revision-conflict.

Lists confirmed rejected writes did not advance the head. The server exited 0 with empty stderr. The transition/relationship cases are related message-projection evidence; only the duplicate incorrectly advertises retryability.

Typed outcome or recovery expected

A truthful non-retryable duplicate diagnostic; an achievable recovery action such as updating the existing head when appropriate. Preserve duplicate rejection, meaningful transition/integrity diagnostics, valid full-state updates, and the retryable diagnosis for genuinely stale revisions.

Analysis or suspected cause

Source-derived reasoning: src/application/investigation/EvidenceLedger.ts:238-239 rejects duplicate identity as already-exists. src/domain/analysisErrorProjection.ts:194-202 maps that reason to revision_conflict, and :573-580 makes that code retryable. src/domain/analysisErrorPresentation.ts:272-275 gives every unknown-registry reason except not-found the state-changed message; remediation falls back to that message.

src/domain/residualUnknown.ts:23-40 derives identity from normalized question, trimmed domain, scope digest, and optional comparison conclusion scope. The tested request has no open-target scope and omits comparison conclusion scope; severity does not change its identity.

Additional context

Prepared with AI assistance and independently reproduced through the production MCP stdio entry point. The submission check fetched upstream main d532b18123916a9e88f98c7880ba4f984e23d715; none of the cited contract/failure files changed after the tested baseline. No new runtime replay on that later revision is claimed.

Related reports #3333, #719, and #3925 concern automatic workflow registration/replay collisions, where the requested operation should succeed. This report concerns correct rejection of an explicit duplicate with an incorrect recovery signal. #1724 concerns mutation Evidence content; #3005 concerns an update integrity failure. PRs #1869 and #4829 address other default recovery messages and do not resolve this duplicate projection.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5396 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area MCP or agent integration ### Observed behavior Repeating an explicit `record_unknown` request in one stdio MCP session correctly rejects the duplicate, but reports `revision_conflict`, `retryable: true`, and “Refresh the current state and try again.” The request has no expected revision, and the retained identity has not changed. Both an unchanged request and a severity-only variant produced this diagnostic in an independent production-entry-point reproduction. ### Reproduction Use the built source revision below and start `node scripts/rea.mjs --mcp`. The independent verifier used isolated disposable HOME, XDG configuration/data/cache/state directories, configuration path, and temporary directory; no provider or binary was opened. Send these newline-delimited JSON-RPC messages through one server process. Wait for the initialize response before sending the notification and tool calls: ```json {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"ledger-verifier","version":"1.0"}}} {"jsonrpc":"2.0","method":"notifications/initialized"} {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"record_unknown","arguments":{"question":"Which build number produced this artifact?","domain":"general","severity":"medium","required_authority":null,"required_confidence":"observed","required_environment":null,"recommended_probes":[],"relationships":[],"supporting_evidence_ids":[],"contradicting_evidence_ids":[]}}} ``` The creation succeeds at revision 1. Repeat the final message with a new JSON-RPC ID and `severity` changed to `high`; then repeat the original arguments unchanged with another ID. Both duplicates fail with the diagnostic below. These are the shortest creation/duplicate steps from the completed replay; no autonomous retry loop was run. ### Expected behavior A duplicate should remain rejected while identifying the existing identity conflict and avoiding a retryable stale-revision diagnosis or an ineffective refresh-and-repeat instruction. A genuine stale `update_unknown` revision should remain retryable. `src/contracts/sessionToolContracts.ts:145-153` advertises deterministic duplicate rejection and exact expected-revision matching for updates; the live `tools/list` advertisement matched. `docs/mcp-contracts.md:81-87` makes the canonical diagnostic the source of error code and recovery guidance. `AGENTS.md` requires preserving meaningful failure reasons. The replacement code and diagnostic structure are implementation choices. ### Impact and scope Supported P2: MCP clients receive a false machine-readable retry signal for a deterministic rejection. Both duplicate variants failed in the completed replay. An actual downstream retry loop or exhausted retry budget was not observed; no data loss was observed. Demonstrated workaround: inspect `details.reason`, call `list_unknowns`, and use a complete `update_unknown` with the existing head's current revision when intending to change severity. That update succeeded at revision 2. Rejected writes preserved the original head. Other REA versions and native providers were not tested. ### REA version 6.7.0, reported by MCP initialize; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node.js v26.12.0; darwin/arm64. No Hopper or Ghidra required. ### Target and artifact context Session-local residual-unknown registry, with no open binary, provider, database, or exported artifact. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Both duplicate calls returned `isError: true`, no success structured content, and this JSON in text content: ```json {"error":{"code":"revision_conflict","category":"execution_failure","message":"Evidence state changed before the update completed. Refresh the current state and try again.","retryable":true,"remediation":{"action":"Evidence state changed before the update completed. Refresh the current state and try again."},"details":{"reason":"already-exists"}}} ``` The independent replay also exercised these controls through complete full-state `update_unknown` requests: - Invalid `contradicted` transition without contradicting evidence: `execution_failure`, `retryable: false`, reason `invalid-transition`, but the same state-changed message/remediation. - Absent relationship target: `evidence_integrity_mismatch`, `retryable: false`, reason `integrity`, with that same message/remediation. - Missing update head: correct not-found-specific message. - Valid severity update: revision 2; subsequent `expected_revision: 1` update: genuine `revision_conflict`, `retryable: true`, reason `revision-conflict`. Lists confirmed rejected writes did not advance the head. The server exited 0 with empty stderr. The transition/relationship cases are related message-projection evidence; only the duplicate incorrectly advertises retryability. ### Typed outcome or recovery expected A truthful non-retryable duplicate diagnostic; an achievable recovery action such as updating the existing head when appropriate. Preserve duplicate rejection, meaningful transition/integrity diagnostics, valid full-state updates, and the retryable diagnosis for genuinely stale revisions. ### Analysis or suspected cause Source-derived reasoning: `src/application/investigation/EvidenceLedger.ts:238-239` rejects duplicate identity as `already-exists`. `src/domain/analysisErrorProjection.ts:194-202` maps that reason to `revision_conflict`, and `:573-580` makes that code retryable. `src/domain/analysisErrorPresentation.ts:272-275` gives every unknown-registry reason except not-found the state-changed message; remediation falls back to that message. `src/domain/residualUnknown.ts:23-40` derives identity from normalized question, trimmed domain, scope digest, and optional comparison conclusion scope. The tested request has no open-target scope and omits comparison conclusion scope; severity does not change its identity. ### Additional context Prepared with AI assistance and independently reproduced through the production MCP stdio entry point. The submission check fetched upstream main `d532b18123916a9e88f98c7880ba4f984e23d715`; none of the cited contract/failure files changed after the tested baseline. No new runtime replay on that later revision is claimed. Related reports #3333, #719, and #3925 concern automatic workflow registration/replay collisions, where the requested operation should succeed. This report concerns correct rejection of an explicit duplicate with an incorrect recovery signal. #1724 concerns mutation Evidence content; #3005 concerns an update integrity failure. PRs #1869 and #4829 address other default recovery messages and do not resolve this duplicate projection.
Author
Owner

@coygeek commented on GitHub (Oct 20, 2026):

Additional reproduced cases (same producing guard)

Reproduced through node scripts/rea.mjs mcp at 074768901f3cdc8205b61773fb1faf6f6c7b3ca8 (MCP reports REA 6.7.0), in one initialized session with isolated disposable state and no open binary or provider.

First create this unknown with a tools/call request:

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"record_unknown","arguments":{"question":"Does the branch require an unavailable service?","severity":"low","domain":"protocol","required_authority":"controlled-replay","required_confidence":"observed","required_environment":null,"recommended_probes":[],"relationships":[]}}}

Creation succeeds at revision 1.

  1. Normalization-equivalent duplicate. Submit this request in the same session; only question whitespace and domain padding differ from creation:
{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"record_unknown","arguments":{"question":"  Does  the branch require an unavailable service?  ","severity":"low","domain":" protocol ","required_authority":"controlled-replay","required_confidence":"observed","required_environment":null,"recommended_probes":[],"relationships":[]}}}

Observed: isError: true, error.code: "revision_conflict", error.category: "execution_failure", error.retryable: true, and error.details: {"reason":"already-exists"}. The message and remediation still say to refresh state and try again.

  1. Existing ID omitted from the duplicate diagnostic. Repeat the original creation arguments:
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"record_unknown","arguments":{"question":"Does the branch require an unavailable service?","severity":"low","domain":"protocol","required_authority":"controlled-replay","required_confidence":"observed","required_environment":null,"recommended_probes":[],"relationships":[]}}}

All three identical retries returned the fields above, with no unknown_id anywhere in the tool result and no success structuredContent. The whitespace variant also omitted it. list_unknowns with {} returned the single retained head at revision 1, including its existing unknown_id, providing a demonstrated recovery lookup for callers that did not retain the creation response. This is a recovery observation about the same diagnostic, not a separately established requirement that the error include the ID.

Both cases share the duplicate ledger guard in src/application/investigation/EvidenceLedger.ts:238-239 and diagnostic projection in src/domain/analysisErrorProjection.ts:194-202; normalization is in src/domain/residualUnknown.ts:23-40.

<!-- gh-comment-id:6288626930 --> @coygeek commented on GitHub (Oct 20, 2026): Additional reproduced cases (same producing guard) Reproduced through `node scripts/rea.mjs mcp` at `074768901f3cdc8205b61773fb1faf6f6c7b3ca8` (MCP reports REA 6.7.0), in one initialized session with isolated disposable state and no open binary or provider. First create this unknown with a `tools/call` request: ```json {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"record_unknown","arguments":{"question":"Does the branch require an unavailable service?","severity":"low","domain":"protocol","required_authority":"controlled-replay","required_confidence":"observed","required_environment":null,"recommended_probes":[],"relationships":[]}}} ``` Creation succeeds at revision 1. 1. **Normalization-equivalent duplicate.** Submit this request in the same session; only question whitespace and domain padding differ from creation: ```json {"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"record_unknown","arguments":{"question":" Does the branch require an unavailable service? ","severity":"low","domain":" protocol ","required_authority":"controlled-replay","required_confidence":"observed","required_environment":null,"recommended_probes":[],"relationships":[]}}} ``` Observed: `isError: true`, `error.code: "revision_conflict"`, `error.category: "execution_failure"`, `error.retryable: true`, and `error.details: {"reason":"already-exists"}`. The message and remediation still say to refresh state and try again. 2. **Existing ID omitted from the duplicate diagnostic.** Repeat the original creation arguments: ```json {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"record_unknown","arguments":{"question":"Does the branch require an unavailable service?","severity":"low","domain":"protocol","required_authority":"controlled-replay","required_confidence":"observed","required_environment":null,"recommended_probes":[],"relationships":[]}}} ``` All three identical retries returned the fields above, with no `unknown_id` anywhere in the tool result and no success `structuredContent`. The whitespace variant also omitted it. `list_unknowns` with `{}` returned the single retained head at revision 1, including its existing `unknown_id`, providing a demonstrated recovery lookup for callers that did not retain the creation response. This is a recovery observation about the same diagnostic, not a separately established requirement that the error include the ID. Both cases share the duplicate ledger guard in `src/application/investigation/EvidenceLedger.ts:238-239` and diagnostic projection in `src/domain/analysisErrorProjection.ts:194-202`; normalization is in `src/domain/residualUnknown.ts:23-40`.
Author
Owner

@CassieFox23 commented on GitHub (Oct 21, 2026):

Thanks, coygeek, for the JSON-RPC replay. From reading main at 01788bac8, not a run: still valid. The file src/application/investigation/EvidenceLedger.ts:237-238 returns UnknownRegistryError("already-exists") for a repeated identity. The file src/domain/analysisErrorProjection.ts:227-229 maps both "revision-conflict" and "already-exists" to revision_conflict. The code revision_conflict sits in RETRYABLE_CODES at lines 604-610. The refresh wording comes from src/domain/analysisErrorPresentation.ts:285. A fix would give already-exists its own non-retryable code, or a details reason the caller can tell apart. Related: #3333 (closed) and #6106. Area: MCP session tools, error projection.

🤖 Written with Claude Code (Fable 5.1, verified by Opus 5.5), posted with my OK 🦊

<!-- gh-comment-id:6321740431 --> @CassieFox23 commented on GitHub (Oct 21, 2026): Thanks, coygeek, for the JSON-RPC replay. From reading main at 01788bac8, not a run: still valid. The file `src/application/investigation/EvidenceLedger.ts:237-238` returns `UnknownRegistryError("already-exists")` for a repeated identity. The file `src/domain/analysisErrorProjection.ts:227-229` maps both `"revision-conflict"` and `"already-exists"` to `revision_conflict`. The code `revision_conflict` sits in `RETRYABLE_CODES` at lines 604-610. The refresh wording comes from `src/domain/analysisErrorPresentation.ts:285`. A fix would give already-exists its own non-retryable code, or a details reason the caller can tell apart. Related: #3333 (closed) and #6106. Area: MCP session tools, error projection. 🤖 Written with Claude Code (Fable 5.1, verified by Opus 5.5), posted with my OK 🦊
Author
Owner

@morluto commented on GitHub (Oct 23, 2026):

Resolved on main in e713c7532. already-exists maps to nonretryable invalid_request and actionable duplicate recovery; revision-conflict remains retryable. The exact executed projector control covers both branches. Validation: src/domain/analysisErrorProjection.test.ts — distinguishes deterministic duplicate identities from stale update revisions; recorded execution at b03a15e6d. All 68 projection cases passed within a group with unrelated failures; no fresh stdio MCP run. Existing registry producer reason is preserved through the shared projector.

<!-- gh-comment-id:6376137659 --> @morluto commented on GitHub (Oct 23, 2026): Resolved on main in [e713c7532](https://github.com/morluto/rea/commit/e713c753292b358fa4a3bfcf4d395390c8eda4de). already-exists maps to nonretryable invalid_request and actionable duplicate recovery; revision-conflict remains retryable. The exact executed projector control covers both branches. Validation: `src/domain/analysisErrorProjection.test.ts` — distinguishes deterministic duplicate identities from stale update revisions; recorded execution at `b03a15e6d`. All 68 projection cases passed within a group with unrelated failures; no fresh stdio MCP run. Existing registry producer reason is preserved through the shared projector.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3266
No description provided.