[GH-ISSUE #7553] [Bug] record_unknown overflow receipt recommends paging a head that cannot fit one page #4494

Closed
opened 2026-10-31 00:50:15 +00:00 by bsakaguchi · 0 comments
Owner

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

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

In a stdio MCP session started with node scripts/rea.mjs mcp, record_unknown accepted a question consisting of 5,500,000 ASCII Q characters. Its reply reported a completed mutation at revision 1 through resource_constraint, included the committed identity, and recommended either exporting the session or paging list_unknowns.

Following that paging recommendation with {"offset":0,"limit":1} failed with another resource_constraint. Increasing the limit to 2 also failed. Moving the offset to 1 returned an empty page and skipped the only head. The record cannot be split into smaller pieces through this paging interface at the default response limit.

Exporting the same live session succeeded, and reading the exported file recovered the exact question and committed identity. The defect is the inapplicable paging alternative in the mutation recovery instruction; the bounded overflow refusal and successful export preserve the data.

Reproduction

  1. Use the baseline below with its locked dependencies and built runtime already prepared, with Node 24.22.1 on PATH. The verifier did not reinstall or rebuild the prepared checkout.
  2. From the checkout root, run this SDK client. It generates the input inline, isolates the server's HOME/XDG/temp directories, verifies full export/readback, and removes its temporary state after closing the connection.
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const { Client } = await import(process.cwd() + '/node_modules/@modelcontextprotocol/client/dist/index.mjs');
const { StdioClientTransport } = await import(process.cwd() + '/node_modules/@modelcontextprotocol/client/dist/stdio.mjs');
const work = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-unknown-'));
const env = { PATH: process.env.PATH, HOME: path.join(work, 'home'), REA_LOG_LEVEL: 'silent' };
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'TMPDIR']) {
  env[key] ??= path.join(work, key.toLowerCase());
  fs.mkdirSync(env[key], { recursive: true });
}
const client = new Client({ name: 'unknown-recovery-check', version: '1' });
const transport = new StdioClientTransport({ command: process.execPath, args: ['scripts/rea.mjs', 'mcp'], cwd: process.cwd(), env, stderr: 'ignore' });
const call = (name, args) => client.callTool({ name, arguments: args });
const error = r => JSON.parse(r.content[0].text).error;
try {
  await client.connect(transport);
  const tools = await client.listTools();
  console.log('question schema:', JSON.stringify(tools.tools.find(t => t.name === 'record_unknown').inputSchema.properties.question));
  const question = 'Q'.repeat(5_500_000);
  const recorded = await call('record_unknown', {
    question, domain: 'large', severity: 'low', required_authority: 'controlled-replay',
    required_confidence: 'observed', required_environment: null, recommended_probes: [], relationships: []
  });
  assert.equal(recorded.isError, true);
  const receipt = error(recorded).details.reported_limits.committed_unknown;
  console.log('record:', error(recorded).code, 'revision:', receipt.revision);
  console.log('recovery:', error(recorded).remediation.action);
  for (const limit of [1, 2]) {
    const page = await call('list_unknowns', { offset: 0, limit });
    assert.equal(page.isError, true);
    console.log('page:', limit, error(page).code, error(page).details.reported_limits.response_bytes_at_least);
  }
  const skipped = await call('list_unknowns', { offset: 1, limit: 1 });
  console.log('skip:', JSON.stringify(skipped.structuredContent.result));
  const summary = await call('get_evidence_bundle', { detail: 'summary' });
  console.log('summary:', summary.structuredContent.result.records.length, 'question:', Object.hasOwn(summary.structuredContent.result.records[0], 'question'));
  console.log('complete bundle:', error(await call('get_evidence_bundle', {})).code);
  const view = await call('inspect_analysis_view', { source: { kind: 'retained-evidence', evidence_id: receipt.mutation_evidence_ids[0] }, view: { kind: 'summary' } });
  console.log('retained view:', error(view).code);
  const destination = path.join(work, 'bundle.json');
  const exported = await call('export_evidence_bundle', { path: destination });
  if (exported.isError) console.log('export error:', JSON.stringify(error(exported)));
  assert.notEqual(exported.isError, true);
  const bundle = JSON.parse(fs.readFileSync(destination, 'utf8'));
  assert.equal(bundle.unknowns[0].unknown_id, receipt.unknown_id);
  assert.equal(bundle.unknowns[0].question, question);
  console.log('export:', bundle.records.length, bundle.unknowns.length, bundle.unknowns[0].question.length);
} finally {
  await client.close();
  await transport.close();
  fs.rmSync(work, { recursive: true, force: true });
}
JS

The exact inline client above completed once. An earlier independent probe also reproduced both paging failures, confirmed a small-record paging control, and successfully exported the large head. Additional sessions confirmed the same paging refusal; they are not counted as additional successful executions of this complete inline recipe.

Expected behavior

The postcommit recovery instruction should identify a route that can retrieve this committed result under the live connection's limits. If a whole head already exceeds the response budget, direct the caller to the working export route, or provide a supported bounded representation before recommending paging for that head.

Ordinary offset/limit paging can reduce the number of complete records returned, but cannot reduce an individual record. This request is admitted through stdio and the advertised question schema has no maximum length. A general requirement to reject large questions at admission, truncate retained data, or deliver arbitrary accepted input inline is not asserted here.

Impact and scope

P2: callers following an explicitly advertised recovery step encounter another transport refusal even at the smallest nonempty page size. The affected case is a large caller-supplied unknown in the default stdio configuration. No downstream automated retry loop was measured.

A safe workaround is available and demonstrated: call export_evidence_bundle on the same session and inspect its file. The full question and receipt identity matched. No data loss or connection loss was observed. A small-record paging control succeeded. Updates, HTTP delivery, other platforms, and larger matched client/server response budgets were not tested; this report does not claim the head is undeliverable under every possible configuration.

REA version

Exact node scripts/rea.mjs --version output: 6.8.0.
Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin arm64. Installed locked MCP SDK client/server, using their stdio transport and the default 10 MiB response setting. No external analysis provider.

Target and artifact context

One session-local residual unknown with a generated ASCII question, plus its immutable mutation Evidence. No binary, imported bundle, external target, or target execution.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Selected output from the completed inline reproduction:

question schema: {"type":"string","minLength":1}
record: resource_constraint revision: 1
page: 1 resource_constraint 10600966
page: 2 resource_constraint 10600966
skip: {"items":[],"total":1,"offset":1,"next_offset":null}
summary: 1 question: false
complete bundle: resource_constraint
retained view: unsupported_target
export: 1 1 5500000

The mutation's remediation begins: Export this session with export_evidence_bundle to inspect the committed unknown and its mutation Evidence, or page list_unknowns using the reported identity. Do not repeat the completed mutation. The page errors report result_budget_bytes: 10484736 and response_bytes_at_least: 10600966. The latter is an early admission lower bound, not a measurement of the complete response size.

Expected sources at the baseline:

  • docs/mcp-contracts.md:304-308 defines offset/limit paging with complete records. Lines 319-324 recommend export or paging after an oversized committed mutation. Lines 189-200 describe bounded response admission and complete export; lines 333-342 document larger matched buffers after restart.
  • The live advertised question schema above agrees with src/domain/residualUnknown.ts:13,91,243-260: nonempty trimmed text without a question-length maximum. The installed stdio SDK's default request buffer is 10,485,760 bytes, defined in node_modules/@modelcontextprotocol/server/dist/src-Cqbh3MYc.mjs:7159-7174. The independent probe's representative JSON-RPC request was 5,500,289 bytes, and actual SDK admission/commit confirms it fits. The 4 MiB HTTP body limit documented at docs/mcp-contracts.md:832-833 concerns another transport.

Failure sources at the baseline:

  • src/server/ToolResultDelivery.ts:204-224 budgets text and structured representations and chooses the paging recommendation whenever committed_unknown exists, without establishing that one head can fit.
  • src/contracts/sessionToolSchemas.ts:56-75 provides whole-record filters and offset/positive limit, with no text-slice or field projection. src/application/investigation/EvidenceLedger.ts:343-377 selects and clones complete heads. The two question copies alone exceed the default response budget. Any page including this head must include that payload; skipping it cannot recover it.
  • src/server/registerSessionRecordTools.ts:175-191,234-247 supplies the committed receipt after a successful mutation. The observed receipt includes both identity and retained Evidence reference.

Typed outcome or recovery expected

Preserve the committed identity, revision, mutation Evidence IDs, completion statement, and instruction not to repeat the mutation. Recommend same-session export for a head that cannot fit a one-record page. Advertising an alternate bounded view would require that view to support this Evidence and payload.

Analysis or suspected cause

Source-derived: offset/limit operates on record count while delivery checks the complete serialized page. The mutation overflow branch treats the existence of a receipt as sufficient to recommend paging. These facts explain why reducing the page to one record cannot resolve this case. No change to input admission or stored question content is required to make the recovery instruction accurate.

Additional context

Closed #6453 reported missing committed-state identifiers in mutation overflow replies. This baseline supplies those identifiers, and its export workaround succeeds. This report concerns the additional paging recommendation for an individually oversized head, rather than a regression of the missing-receipt defect. Closed #6106 concerns invalid-input classification, and open #7327 concerns provenance documentation; neither has the same restoration criterion.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A valid committed large question receives a paging recovery instruction that fails even at one head per page. The failure is bounded to this payload regime; data remains retained.
  • Workaround status: available
  • Workaround basis: Same-session export succeeded in the completed replay, and parsing its file verified the exact 5,500,000-character question and committed identity.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7553 ### 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 In a stdio MCP session started with `node scripts/rea.mjs mcp`, `record_unknown` accepted a question consisting of 5,500,000 ASCII `Q` characters. Its reply reported a completed mutation at revision 1 through `resource_constraint`, included the committed identity, and recommended either exporting the session or paging `list_unknowns`. Following that paging recommendation with `{"offset":0,"limit":1}` failed with another `resource_constraint`. Increasing the limit to 2 also failed. Moving the offset to 1 returned an empty page and skipped the only head. The record cannot be split into smaller pieces through this paging interface at the default response limit. Exporting the same live session succeeded, and reading the exported file recovered the exact question and committed identity. The defect is the inapplicable paging alternative in the mutation recovery instruction; the bounded overflow refusal and successful export preserve the data. ### Reproduction 1. Use the baseline below with its locked dependencies and built runtime already prepared, with Node 24.22.1 on PATH. The verifier did not reinstall or rebuild the prepared checkout. 2. From the checkout root, run this SDK client. It generates the input inline, isolates the server's HOME/XDG/temp directories, verifies full export/readback, and removes its temporary state after closing the connection. ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; const { Client } = await import(process.cwd() + '/node_modules/@modelcontextprotocol/client/dist/index.mjs'); const { StdioClientTransport } = await import(process.cwd() + '/node_modules/@modelcontextprotocol/client/dist/stdio.mjs'); const work = fs.mkdtempSync(path.join(os.tmpdir(), 'rea-unknown-')); const env = { PATH: process.env.PATH, HOME: path.join(work, 'home'), REA_LOG_LEVEL: 'silent' }; for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'TMPDIR']) { env[key] ??= path.join(work, key.toLowerCase()); fs.mkdirSync(env[key], { recursive: true }); } const client = new Client({ name: 'unknown-recovery-check', version: '1' }); const transport = new StdioClientTransport({ command: process.execPath, args: ['scripts/rea.mjs', 'mcp'], cwd: process.cwd(), env, stderr: 'ignore' }); const call = (name, args) => client.callTool({ name, arguments: args }); const error = r => JSON.parse(r.content[0].text).error; try { await client.connect(transport); const tools = await client.listTools(); console.log('question schema:', JSON.stringify(tools.tools.find(t => t.name === 'record_unknown').inputSchema.properties.question)); const question = 'Q'.repeat(5_500_000); const recorded = await call('record_unknown', { question, domain: 'large', severity: 'low', required_authority: 'controlled-replay', required_confidence: 'observed', required_environment: null, recommended_probes: [], relationships: [] }); assert.equal(recorded.isError, true); const receipt = error(recorded).details.reported_limits.committed_unknown; console.log('record:', error(recorded).code, 'revision:', receipt.revision); console.log('recovery:', error(recorded).remediation.action); for (const limit of [1, 2]) { const page = await call('list_unknowns', { offset: 0, limit }); assert.equal(page.isError, true); console.log('page:', limit, error(page).code, error(page).details.reported_limits.response_bytes_at_least); } const skipped = await call('list_unknowns', { offset: 1, limit: 1 }); console.log('skip:', JSON.stringify(skipped.structuredContent.result)); const summary = await call('get_evidence_bundle', { detail: 'summary' }); console.log('summary:', summary.structuredContent.result.records.length, 'question:', Object.hasOwn(summary.structuredContent.result.records[0], 'question')); console.log('complete bundle:', error(await call('get_evidence_bundle', {})).code); const view = await call('inspect_analysis_view', { source: { kind: 'retained-evidence', evidence_id: receipt.mutation_evidence_ids[0] }, view: { kind: 'summary' } }); console.log('retained view:', error(view).code); const destination = path.join(work, 'bundle.json'); const exported = await call('export_evidence_bundle', { path: destination }); if (exported.isError) console.log('export error:', JSON.stringify(error(exported))); assert.notEqual(exported.isError, true); const bundle = JSON.parse(fs.readFileSync(destination, 'utf8')); assert.equal(bundle.unknowns[0].unknown_id, receipt.unknown_id); assert.equal(bundle.unknowns[0].question, question); console.log('export:', bundle.records.length, bundle.unknowns.length, bundle.unknowns[0].question.length); } finally { await client.close(); await transport.close(); fs.rmSync(work, { recursive: true, force: true }); } JS ``` The exact inline client above completed once. An earlier independent probe also reproduced both paging failures, confirmed a small-record paging control, and successfully exported the large head. Additional sessions confirmed the same paging refusal; they are not counted as additional successful executions of this complete inline recipe. ### Expected behavior The postcommit recovery instruction should identify a route that can retrieve this committed result under the live connection's limits. If a whole head already exceeds the response budget, direct the caller to the working export route, or provide a supported bounded representation before recommending paging for that head. Ordinary offset/limit paging can reduce the number of complete records returned, but cannot reduce an individual record. This request is admitted through stdio and the advertised question schema has no maximum length. A general requirement to reject large questions at admission, truncate retained data, or deliver arbitrary accepted input inline is not asserted here. ### Impact and scope P2: callers following an explicitly advertised recovery step encounter another transport refusal even at the smallest nonempty page size. The affected case is a large caller-supplied unknown in the default stdio configuration. No downstream automated retry loop was measured. A safe workaround is available and demonstrated: call `export_evidence_bundle` on the same session and inspect its file. The full question and receipt identity matched. No data loss or connection loss was observed. A small-record paging control succeeded. Updates, HTTP delivery, other platforms, and larger matched client/server response budgets were not tested; this report does not claim the head is undeliverable under every possible configuration. ### REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; Darwin arm64. Installed locked MCP SDK client/server, using their stdio transport and the default 10 MiB response setting. No external analysis provider. ### Target and artifact context One session-local residual unknown with a generated ASCII question, plus its immutable mutation Evidence. No binary, imported bundle, external target, or target execution. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Selected output from the completed inline reproduction: ```text question schema: {"type":"string","minLength":1} record: resource_constraint revision: 1 page: 1 resource_constraint 10600966 page: 2 resource_constraint 10600966 skip: {"items":[],"total":1,"offset":1,"next_offset":null} summary: 1 question: false complete bundle: resource_constraint retained view: unsupported_target export: 1 1 5500000 ``` The mutation's remediation begins: `Export this session with export_evidence_bundle to inspect the committed unknown and its mutation Evidence, or page list_unknowns using the reported identity. Do not repeat the completed mutation.` The page errors report `result_budget_bytes: 10484736` and `response_bytes_at_least: 10600966`. The latter is an early admission lower bound, not a measurement of the complete response size. Expected sources at the baseline: - `docs/mcp-contracts.md:304-308` defines offset/limit paging with complete records. Lines 319-324 recommend export or paging after an oversized committed mutation. Lines 189-200 describe bounded response admission and complete export; lines 333-342 document larger matched buffers after restart. - The live advertised question schema above agrees with `src/domain/residualUnknown.ts:13,91,243-260`: nonempty trimmed text without a question-length maximum. The installed stdio SDK's default request buffer is 10,485,760 bytes, defined in `node_modules/@modelcontextprotocol/server/dist/src-Cqbh3MYc.mjs:7159-7174`. The independent probe's representative JSON-RPC request was 5,500,289 bytes, and actual SDK admission/commit confirms it fits. The 4 MiB HTTP body limit documented at `docs/mcp-contracts.md:832-833` concerns another transport. Failure sources at the baseline: - `src/server/ToolResultDelivery.ts:204-224` budgets text and structured representations and chooses the paging recommendation whenever `committed_unknown` exists, without establishing that one head can fit. - `src/contracts/sessionToolSchemas.ts:56-75` provides whole-record filters and offset/positive limit, with no text-slice or field projection. `src/application/investigation/EvidenceLedger.ts:343-377` selects and clones complete heads. The two question copies alone exceed the default response budget. Any page including this head must include that payload; skipping it cannot recover it. - `src/server/registerSessionRecordTools.ts:175-191,234-247` supplies the committed receipt after a successful mutation. The observed receipt includes both identity and retained Evidence reference. ### Typed outcome or recovery expected Preserve the committed identity, revision, mutation Evidence IDs, completion statement, and instruction not to repeat the mutation. Recommend same-session export for a head that cannot fit a one-record page. Advertising an alternate bounded view would require that view to support this Evidence and payload. ### Analysis or suspected cause Source-derived: offset/limit operates on record count while delivery checks the complete serialized page. The mutation overflow branch treats the existence of a receipt as sufficient to recommend paging. These facts explain why reducing the page to one record cannot resolve this case. No change to input admission or stored question content is required to make the recovery instruction accurate. ### Additional context Closed [#6453](https://github.com/morluto/rea/issues/6453) reported missing committed-state identifiers in mutation overflow replies. This baseline supplies those identifiers, and its export workaround succeeds. This report concerns the additional paging recommendation for an individually oversized head, rather than a regression of the missing-receipt defect. Closed [#6106](https://github.com/morluto/rea/issues/6106) concerns invalid-input classification, and open [#7327](https://github.com/morluto/rea/issues/7327) concerns provenance documentation; neither has the same restoration criterion. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A valid committed large question receives a paging recovery instruction that fails even at one head per page. The failure is bounded to this payload regime; data remains retained. - Workaround status: available - Workaround basis: Same-session export succeeded in the completed replay, and parsing its file verified the exact 5,500,000-character question and committed identity.
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#4494
No description provided.