[GH-ISSUE #6453] [Bug] record_unknown and update_unknown overflow replies omit committed-state identifiers #3867

Closed
opened 2026-10-31 00:44:46 +00:00 by bsakaguchi · 0 comments
Owner

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

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

With the default 10 MiB MCP response limit, an accepted record_unknown or update_unknown commits its mutation and then returns isError: true with resource_constraint. The message correctly says the operation completed, but the reply omits the Unknown ID, committed revision, mutation Evidence IDs and documented retained-Evidence reference. Its remediation says to use complete CLI JSON, without identifying how to recover this live MCP session's state.

Both mutations remained complete in the session and were recovered by calling export_evidence_bundle on that same connection. No corruption was observed.

Reproduction

From a fresh checkout of morluto/rea:

git checkout 9eb829aca34be36ff112826337996f6139f82eff
npm ci && npm run build:cached
export WORK="$(mktemp -d)"
node --input-type=module <<'JS'
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { mkdirSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import assert from 'node:assert/strict';
const work = resolve(process.env.WORK);
mkdirSync(`${work}/home`, { recursive: true });
const common = { severity: 'low', domain: 'd', required_authority: null,
  required_confidence: 'observed', required_environment: null,
  recommended_probes: [], relationships: [] };
for (const mode of ['record', 'update']) {
  const env = { ...process.env, HOME: `${work}/home` };
  delete env.REA_MCP_MAX_RESPONSE_BYTES;
  const client = new Client({ name: 'overflow-example', version: '1' });
  const transport = new StdioClientTransport({ command: process.execPath,
    args: ['scripts/rea.mjs', 'mcp'], env, stderr: 'inherit' });
  const call = async (name, args) => {
    const reply = await client.callTool({ name, arguments: args });
    return { reply, value: JSON.parse(reply.content[0].text) };
  };
  try {
    await client.connect(transport);
    let result, update;
    if (mode === 'record') {
      result = await call('record_unknown', { ...common, question: 'Q'.repeat(9949184) });
    } else {
      const initial = await call('record_unknown', { ...common, question: 'Can this state be recovered?' });
      update = { unknown_id: initial.value.result.unknown_id, expected_revision: 1,
        status: 'investigating', severity: 'low', supporting_evidence_ids: [],
        contradicting_evidence_ids: [], required_authority: null,
        required_confidence: 'observed', required_environment: null, relationships: [],
        resolution: null, recommended_probes: [{ operation: 'analyze_function', rationale: 'R'.repeat(9949184) }] };
      result = await call('update_unknown', update);
    }
    assert.equal(result.reply.isError, true);
    assert.equal(result.value.error.code, 'resource_constraint');
    assert.equal(result.value.error.details.reported_limits.evidence_reference, undefined);
    console.log(JSON.stringify({ mode, error: result.value.error }));
    const path = `${work}/${mode}.json`;
    const exported = await call('export_evidence_bundle', { path });
    assert.notEqual(exported.reply.isError, true);
    const bundle = JSON.parse(readFileSync(path, 'utf8'));
    const head = bundle.unknowns.at(-1);
    assert.equal(head.revision, mode === 'record' ? 1 : 2);
    assert.equal(mode === 'record' ? head.question.length : head.recommended_probes[0].rationale.length, 9949184);
    console.log(JSON.stringify({ mode, saved_revision: head.revision,
      unknown_id: head.unknown_id, mutation_evidence_ids: head.mutation_evidence_ids }));
    if (update) {
      const stale = await call('update_unknown', update);
      assert.equal(stale.value.error.code, 'revision_conflict');
      const next = await call('update_unknown', { ...update, expected_revision: 2, recommended_probes: [] });
      assert.equal(next.value.result.revision, 3);
      console.log(JSON.stringify({ stale: stale.value.error.code, next_revision: next.value.result.revision }));
    }
  } finally {
    await client.close();
    await transport.close();
  }
}
JS

The inline fixtures are a 9,949,184-character question and, in a separate session, a short question followed by an update with a 9,949,184-character probe rationale. Both requests fit the default 10 MiB input frame. No binary or provider is needed.

Both cases reproduced twice: once through raw JSON-RPC and once with the SDK probe above. The locked dependencies and prepared build were already present for these runs; installation/build were not rerun during reproduction. The probe leaves the exported bundles in $WORK for inspection.

Expected behavior

Keep the bounded transport refusal while preserving the committed mutation's identity and a usable same-session recovery route. The MCP overflow contract promises complete retained Evidence, its exact details.reported_limits.evidence_reference, and export/view recovery when a result cannot fit.

The tool contracts specify immutable creation/update mutation Evidence and exact expected-revision matching. Successful small replies provide the Unknown ID, revision and mutation Evidence IDs. A bounded overflow receipt should identify the committed state without echoing the oversized fields; its exact schema is an implementation choice.

Impact and scope

P2: large accepted session mutations lose their machine-readable committed-state identity and useful recovery instruction at delivery. This reproduced in 2/2 record cases and 2/2 update cases. The reply does state completion and retryable: false; no automatic retry loop or data loss was observed.

Demonstrated workaround: export the bundle from the still-live MCP session and inspect its Unknown revisions and mutation Evidence. A subsequent revision-1 update correctly conflicts after the oversized update committed revision 2; a small revision-2 update succeeds at revision 3. Small creation/update controls succeeded. Other platforms and response-limit settings were not tested.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v26.12.1; macOS 27.0.1; arm64. Locked MCP SDK 2.3.1. The repository's Node 24.18.0 development pin was not exercised.

Target and artifact context

Fresh session-local residual-unknown ledger; generated ASCII text only. Exported canonical Evidence bundles retain the full question/rationale, Unknown revision history and mutation Evidence. No external provider or analysis target.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Selected fields observed in both overflow responses:

{
  "isError": true,
  "error": {
    "code": "resource_constraint",
    "retryable": false,
    "details": {
      "resource": "transport",
      "reported_limits": {
        "boundary": "mcp-response",
        "default_receive_buffer_bytes": 10485760,
        "result_budget_bytes": 10484736,
        "constraint": "receive-buffer"
      }
    }
  }
}

The error JSON is inside text content. Its message begins The operation completed, but its complete MCP response exceeds the stdio response budget. The remediation begins Use complete CLI JSON output to consume the analysis. Neither result contains evidence_reference, unknown_id, revision or mutation_evidence_ids.

response_bytes_at_least was 10,600,867 for creation and 10,601,704 for the update above. These are lower bounds reported by admission, not measured complete response sizes. Raw request lines were 9,949,454 and 9,949,684 bytes respectively, including newline. Export/readback verified revisions 1 and 2 and all 9,949,184 input characters. The unchanged update retry returned revision_conflict; the next small update returned revision 3.

Typed outcome or recovery expected

Retain the explicit completion statement, transport classification and safe size refusal. Include a bounded exact reference/receipt for the committed mutation and direct the caller to export_evidence_bundle on the current session, or another supported way to recover that exact state. Preserve normal small replies and genuine stale-revision rejection.

Analysis or suspected cause

Source-derived: both handlers call the session mutation before toCallToolResult. That delivery path supplies no recovery metadata, while the Evidence-specific path supplies acknowledged retained-Evidence references. Overflow handling therefore receives an empty recovery object and chooses generic CLI advice.

Additional context

Related #5396 concerns duplicate creation diagnostics; #1724 concerns omitted mutation intent; #1053 concerns oversized analysis serialization; #6095 concerns a CLI export receipt lost during cancellation. None describes this postcommit Unknown-response overflow. No last-known-good revision or regression range was established.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6453 ### 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 With the default 10 MiB MCP response limit, an accepted `record_unknown` or `update_unknown` commits its mutation and then returns `isError: true` with `resource_constraint`. The message correctly says the operation completed, but the reply omits the Unknown ID, committed revision, mutation Evidence IDs and documented retained-Evidence reference. Its remediation says to use complete CLI JSON, without identifying how to recover this live MCP session's state. Both mutations remained complete in the session and were recovered by calling `export_evidence_bundle` on that same connection. No corruption was observed. ### Reproduction From a fresh checkout of `morluto/rea`: ```sh git checkout 9eb829aca34be36ff112826337996f6139f82eff npm ci && npm run build:cached export WORK="$(mktemp -d)" node --input-type=module <<'JS' import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { mkdirSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import assert from 'node:assert/strict'; const work = resolve(process.env.WORK); mkdirSync(`${work}/home`, { recursive: true }); const common = { severity: 'low', domain: 'd', required_authority: null, required_confidence: 'observed', required_environment: null, recommended_probes: [], relationships: [] }; for (const mode of ['record', 'update']) { const env = { ...process.env, HOME: `${work}/home` }; delete env.REA_MCP_MAX_RESPONSE_BYTES; const client = new Client({ name: 'overflow-example', version: '1' }); const transport = new StdioClientTransport({ command: process.execPath, args: ['scripts/rea.mjs', 'mcp'], env, stderr: 'inherit' }); const call = async (name, args) => { const reply = await client.callTool({ name, arguments: args }); return { reply, value: JSON.parse(reply.content[0].text) }; }; try { await client.connect(transport); let result, update; if (mode === 'record') { result = await call('record_unknown', { ...common, question: 'Q'.repeat(9949184) }); } else { const initial = await call('record_unknown', { ...common, question: 'Can this state be recovered?' }); update = { unknown_id: initial.value.result.unknown_id, expected_revision: 1, status: 'investigating', severity: 'low', supporting_evidence_ids: [], contradicting_evidence_ids: [], required_authority: null, required_confidence: 'observed', required_environment: null, relationships: [], resolution: null, recommended_probes: [{ operation: 'analyze_function', rationale: 'R'.repeat(9949184) }] }; result = await call('update_unknown', update); } assert.equal(result.reply.isError, true); assert.equal(result.value.error.code, 'resource_constraint'); assert.equal(result.value.error.details.reported_limits.evidence_reference, undefined); console.log(JSON.stringify({ mode, error: result.value.error })); const path = `${work}/${mode}.json`; const exported = await call('export_evidence_bundle', { path }); assert.notEqual(exported.reply.isError, true); const bundle = JSON.parse(readFileSync(path, 'utf8')); const head = bundle.unknowns.at(-1); assert.equal(head.revision, mode === 'record' ? 1 : 2); assert.equal(mode === 'record' ? head.question.length : head.recommended_probes[0].rationale.length, 9949184); console.log(JSON.stringify({ mode, saved_revision: head.revision, unknown_id: head.unknown_id, mutation_evidence_ids: head.mutation_evidence_ids })); if (update) { const stale = await call('update_unknown', update); assert.equal(stale.value.error.code, 'revision_conflict'); const next = await call('update_unknown', { ...update, expected_revision: 2, recommended_probes: [] }); assert.equal(next.value.result.revision, 3); console.log(JSON.stringify({ stale: stale.value.error.code, next_revision: next.value.result.revision })); } } finally { await client.close(); await transport.close(); } } JS ``` The inline fixtures are a 9,949,184-character question and, in a separate session, a short question followed by an update with a 9,949,184-character probe rationale. Both requests fit the default 10 MiB input frame. No binary or provider is needed. Both cases reproduced twice: once through raw JSON-RPC and once with the SDK probe above. The locked dependencies and prepared build were already present for these runs; installation/build were not rerun during reproduction. The probe leaves the exported bundles in `$WORK` for inspection. ### Expected behavior Keep the bounded transport refusal while preserving the committed mutation's identity and a usable same-session recovery route. The [MCP overflow contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/mcp-contracts.md#L134-L146) promises complete retained Evidence, its exact `details.reported_limits.evidence_reference`, and export/view recovery when a result cannot fit. The [tool contracts](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/contracts/sessionToolContracts.ts#L151-L159) specify immutable creation/update mutation Evidence and exact expected-revision matching. Successful small replies provide the Unknown ID, revision and mutation Evidence IDs. A bounded overflow receipt should identify the committed state without echoing the oversized fields; its exact schema is an implementation choice. ### Impact and scope P2: large accepted session mutations lose their machine-readable committed-state identity and useful recovery instruction at delivery. This reproduced in 2/2 record cases and 2/2 update cases. The reply does state completion and `retryable: false`; no automatic retry loop or data loss was observed. Demonstrated workaround: export the bundle from the still-live MCP session and inspect its Unknown revisions and mutation Evidence. A subsequent revision-1 update correctly conflicts after the oversized update committed revision 2; a small revision-2 update succeeds at revision 3. Small creation/update controls succeeded. Other platforms and response-limit settings were not tested. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v26.12.1; macOS 27.0.1; arm64. Locked MCP SDK 2.3.1. The repository's Node 24.18.0 development pin was not exercised. ### Target and artifact context Fresh session-local residual-unknown ledger; generated ASCII text only. Exported canonical Evidence bundles retain the full question/rationale, Unknown revision history and mutation Evidence. No external provider or analysis target. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Selected fields observed in both overflow responses: ```json { "isError": true, "error": { "code": "resource_constraint", "retryable": false, "details": { "resource": "transport", "reported_limits": { "boundary": "mcp-response", "default_receive_buffer_bytes": 10485760, "result_budget_bytes": 10484736, "constraint": "receive-buffer" } } } } ``` The error JSON is inside text content. Its message begins `The operation completed, but its complete MCP response exceeds the stdio response budget.` The remediation begins `Use complete CLI JSON output to consume the analysis.` Neither result contains `evidence_reference`, `unknown_id`, `revision` or `mutation_evidence_ids`. `response_bytes_at_least` was 10,600,867 for creation and 10,601,704 for the update above. These are lower bounds reported by admission, not measured complete response sizes. Raw request lines were 9,949,454 and 9,949,684 bytes respectively, including newline. Export/readback verified revisions 1 and 2 and all 9,949,184 input characters. The unchanged update retry returned `revision_conflict`; the next small update returned revision 3. ### Typed outcome or recovery expected Retain the explicit completion statement, transport classification and safe size refusal. Include a bounded exact reference/receipt for the committed mutation and direct the caller to `export_evidence_bundle` on the current session, or another supported way to recover that exact state. Preserve normal small replies and genuine stale-revision rejection. ### Analysis or suspected cause Source-derived: [both handlers](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/server/registerSessionRecordTools.ts#L185-L206) call the session mutation before `toCallToolResult`. [That delivery path](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/server/ToolResultDelivery.ts#L96-L128) supplies no recovery metadata, while the Evidence-specific path supplies acknowledged retained-Evidence references. [Overflow handling](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/server/ToolResultDelivery.ts#L194-L245) therefore receives an empty recovery object and chooses generic CLI advice. ### Additional context Related [#5396](https://github.com/morluto/rea/issues/5396) concerns duplicate creation diagnostics; [#1724](https://github.com/morluto/rea/issues/1724) concerns omitted mutation intent; [#1053](https://github.com/morluto/rea/issues/1053) concerns oversized analysis serialization; [#6095](https://github.com/morluto/rea/issues/6095) concerns a CLI export receipt lost during cancellation. None describes this postcommit Unknown-response overflow. No last-known-good revision or regression range was established.
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#3867
No description provided.