[GH-ISSUE #7552] [Bug] list_unknowns overflow diagnostic directs callers to CLI instead of working session paging #4493

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/7552

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

On a target-free stdio MCP connection started with node scripts/rea.mjs mcp, three record_unknown requests with 4,000,014-character questions succeed. Calling list_unknowns with {} then returns isError: true and a resource_constraint diagnostic for operation list_unknowns, resource transport.

The recovery instruction begins Use complete CLI JSON output to consume the analysis. It does not mention offset, limit, or another request on the current connection, and there is no recovery field. A standalone CLI process cannot retrieve the current connection's ledger directly. The same connection remains usable: three list_unknowns calls with limit: 1 recover every original identity, revision and complete question, ending with next_offset: null.

The refusal correctly enforces the default response budget. The defect is the inapplicable consumption advice despite a supported paging route that works for these records.

Reproduction

  1. Use the source revision below with its locked dependencies and compiled runtime already prepared, and Node 24.22.1 on PATH. No installation or rebuild was performed during verification.
  2. From the checkout root, run this SDK client. It generates all input inline, starts an isolated server environment, and removes its disposable state after closing the connection:
node --input-type=module <<'JS'
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import assert from 'node:assert/strict';
const work = mkdtempSync(join(tmpdir(), 'rea-recovery-'));
const env = Object.fromEntries(Object.entries(process.env)
  .filter(([key]) => !key.startsWith('REA_')));
for (const [key, name] of Object.entries({ HOME: 'home',
  XDG_CONFIG_HOME: 'config', XDG_CACHE_HOME: 'cache',
  XDG_DATA_HOME: 'data', TMPDIR: 'temp' })) {
  env[key] = join(work, name);
  mkdirSync(env[key]);
}
const client = new Client({ name: 'recovery-check', version: '1' });
const transport = new StdioClientTransport({ command: process.execPath,
  args: ['scripts/rea.mjs', 'mcp'], cwd: process.cwd(), env, stderr: 'ignore' });
try {
  await client.connect(transport);
  const fields = { severity: 'low', domain: 'protocol',
    required_authority: 'controlled-replay', required_confidence: 'observed',
    required_environment: null, recommended_probes: [], relationships: [] };
  const created = new Set();
  for (let i = 0; i < 3; i++) {
    const reply = await client.callTool({ name: 'record_unknown',
      arguments: { ...fields, question: `Independent ${i} ` + 'a'.repeat(4_000_000) } });
    assert.notEqual(reply.isError, true);
    created.add(reply.structuredContent.result.unknown_id);
  }
  const all = await client.callTool({ name: 'list_unknowns', arguments: {} });
  assert.equal(all.isError, true);
  const error = JSON.parse(all.content[0].text).error;
  assert.equal(error.code, 'resource_constraint');
  assert.equal(error.details.resource, 'transport');
  console.log('unpaged:', error.code, error.details.operation);
  console.log('remediation:', error.remediation.action);
  console.log('recovery:', error.recovery ?? null);
  let offset = 0;
  const recovered = new Set();
  while (offset !== null) {
    const page = await client.callTool({ name: 'list_unknowns',
      arguments: { offset, limit: 1 } });
    assert.notEqual(page.isError, true);
    const result = page.structuredContent.result;
    assert.equal(result.total, 3);
    assert.equal(result.items.length, 1);
    const item = result.items[0];
    assert.match(item.question, /^Independent [0-2] a+$/);
    assert.equal(item.question.length, 4_000_014);
    assert.equal(item.revision, 1);
    recovered.add(item.unknown_id);
    console.log('page:', offset, 'total:', result.total,
      'next_offset:', result.next_offset, 'question_length:', item.question.length);
    offset = result.next_offset;
  }
  assert.deepEqual(recovered, created);
  console.log('all created identities recovered:', recovered.size);
} finally {
  await client.close();
  await transport.close();
  rmSync(work, { recursive: true, force: true });
}
JS
  1. Compare the diagnostic's CLI instruction with the successful same-connection pages. This exact script passed once; an independently written SDK probe reproduced the same behavior in another fresh session, for two successful reproductions overall.

Expected behavior

When a valid list_unknowns response exceeds the transport budget, the remediation should identify an applicable recovery route on the current connection. For this fixture, it should direct the caller to provide offset and limit, then follow next_offset while keeping the ledger unchanged. Plain actionable guidance is sufficient; a new typed recovery field is not required.

The documented MCP contract advertises complete current-head results by default and offset/limit paging for smaller replies. It also scopes retained state to a connection and explains that a standalone CLI invocation cannot resolve another connection's records. Exact contract and handler locators are in Evidence below.

Impact and scope

P2: callers encountering a large valid unknown listing receive advice that cannot directly retrieve the affected session state and no instruction for the working bounded read. Both tested sessions showed this result. Paging is a demonstrated practical workaround: every record, revision and full question was recovered without restarting or changing the server.

No lost records, host retry loop, abandoned investigation, or security-boundary crossing was observed. This verification covers three individually deliverable records, default response settings, stdio MCP and the stated platform. It does not establish behavior for a record too large to fit on a one-record page or for other tools/transports.

REA version

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

Runtime and platform

Node.js v24.22.1; macOS 27.0.1, build 26A434; arm64. The SDK client and stdio transport were loaded from the checkout's locked dependencies.

Target and artifact context

No binary, imported bundle or external provider. A fresh connection contains three generated residual-unknown records, each with a distinct prefix and 4,000,000 repeated ASCII characters.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

The exact inline script printed:

unpaged: resource_constraint list_unknowns
remediation: Use complete CLI JSON output to consume the analysis. The effective REA_MCP_MAX_RESPONSE_BYTES value is 10485760 bytes (floor 10485760 bytes, 10 MiB). A change requires restarting the REA process and a matching client receive buffer. It does not raise REA's reserved single-string limit of 536869864 UTF-16 code units or Node's single-string ceiling of 536870888 UTF-16 code units. This release has no runtime override for either limit.
recovery: null
page: 0 total: 3 next_offset: 1 question_length: 4000014
page: 1 total: 3 next_offset: 2 question_length: 4000014
page: 2 total: 3 next_offset: null question_length: 4000014
all created identities recovered: 3

recovery: null is the probe's rendering of the absent field. Assertions additionally check successful creation, the transport classification, revision 1, full question shape/length and equality of the created/recovered identity sets.

At the stated revision:

  • docs/mcp-contracts.md:304-308 defines complete individual-record paging, matching-head totals, stable ID order and traversal without concurrent ledger changes. The live tools/list description independently advertised the same paging parameters in the first verification session.
  • docs/mcp-contracts.md:141-157 directs callers to follow the returned remediation and defines bounded recovery as optional.
  • docs/mcp-contracts.md:582-594 defines connection-scoped retained records and the limits of a standalone CLI invocation. src/server/registerSessionRecordTools.ts:151-169 reads the unknown page from that handler's session and delivers it without recovery metadata.
  • src/server/ToolResultDelivery.ts:221-233 supplies special advice for committed mutations, retained analysis and Evidence bundles, then selects the generic CLI instruction used by this list reply.

Evidence provenance: the error and complete recovery pages were executed through the public stdio entry point; the explanation of branch selection and session ownership is source-derived. No checkout edits or synthetic response-budget overrides were used.

Typed outcome or recovery expected

Keep the bounded resource_constraint result and correct transport details. Provide a same-connection list_unknowns paging instruction, optionally accompanied by a ready-to-call bounded request. Do not imply that restarting or invoking a separate CLI process can directly read the live ledger.

Analysis or suspected cause

Source-derived explanation: the list handler supplies no recovery metadata to result delivery. The success-overflow fallback has no branch for the list tool, so its diagnostic inherits analysis-oriented CLI guidance. A smaller list request can already complete; the missing behavior is selecting truthful recovery guidance at delivery.

Additional context

Duplicate searches covered open/closed issues, issue comments and all-state PRs. Closed #6453 concerns committed mutation identifiers and their recovery receipt; this reproduction uses three fully delivered mutations followed by an oversized read. Closed #6106 concerns invalid cross-field inputs, and open #7327 concerns the provenance exception list. None has the same closure criterion. Related oversized validation/analysis and general recovery work was also checked; no matching report was found. No last-known-good version or regression range was established.

Proposed restoration check, not executed: rerun the inline script and require the unpaged error to recommend the supported paging route while preserving the demonstrated complete three-page recovery.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Two real stdio sessions returned inapplicable CLI consumption advice for a valid oversized unknown listing even though complete same-session paging succeeded.
  • Workaround status: available
  • Workaround basis: The executed offset/limit traversal recovered all three created identities at revision 1 and all 4,000,014 characters of each question without changing or restarting the server.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7552 ### 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 On a target-free stdio MCP connection started with `node scripts/rea.mjs mcp`, three `record_unknown` requests with 4,000,014-character questions succeed. Calling `list_unknowns` with `{}` then returns `isError: true` and a `resource_constraint` diagnostic for operation `list_unknowns`, resource `transport`. The recovery instruction begins `Use complete CLI JSON output to consume the analysis.` It does not mention `offset`, `limit`, or another request on the current connection, and there is no `recovery` field. A standalone CLI process cannot retrieve the current connection's ledger directly. The same connection remains usable: three `list_unknowns` calls with `limit: 1` recover every original identity, revision and complete question, ending with `next_offset: null`. The refusal correctly enforces the default response budget. The defect is the inapplicable consumption advice despite a supported paging route that works for these records. ### Reproduction 1. Use the source revision below with its locked dependencies and compiled runtime already prepared, and Node 24.22.1 on PATH. No installation or rebuild was performed during verification. 2. From the checkout root, run this SDK client. It generates all input inline, starts an isolated server environment, and removes its disposable state after closing the connection: ```sh node --input-type=module <<'JS' import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import assert from 'node:assert/strict'; const work = mkdtempSync(join(tmpdir(), 'rea-recovery-')); const env = Object.fromEntries(Object.entries(process.env) .filter(([key]) => !key.startsWith('REA_'))); for (const [key, name] of Object.entries({ HOME: 'home', XDG_CONFIG_HOME: 'config', XDG_CACHE_HOME: 'cache', XDG_DATA_HOME: 'data', TMPDIR: 'temp' })) { env[key] = join(work, name); mkdirSync(env[key]); } const client = new Client({ name: 'recovery-check', version: '1' }); const transport = new StdioClientTransport({ command: process.execPath, args: ['scripts/rea.mjs', 'mcp'], cwd: process.cwd(), env, stderr: 'ignore' }); try { await client.connect(transport); const fields = { severity: 'low', domain: 'protocol', required_authority: 'controlled-replay', required_confidence: 'observed', required_environment: null, recommended_probes: [], relationships: [] }; const created = new Set(); for (let i = 0; i < 3; i++) { const reply = await client.callTool({ name: 'record_unknown', arguments: { ...fields, question: `Independent ${i} ` + 'a'.repeat(4_000_000) } }); assert.notEqual(reply.isError, true); created.add(reply.structuredContent.result.unknown_id); } const all = await client.callTool({ name: 'list_unknowns', arguments: {} }); assert.equal(all.isError, true); const error = JSON.parse(all.content[0].text).error; assert.equal(error.code, 'resource_constraint'); assert.equal(error.details.resource, 'transport'); console.log('unpaged:', error.code, error.details.operation); console.log('remediation:', error.remediation.action); console.log('recovery:', error.recovery ?? null); let offset = 0; const recovered = new Set(); while (offset !== null) { const page = await client.callTool({ name: 'list_unknowns', arguments: { offset, limit: 1 } }); assert.notEqual(page.isError, true); const result = page.structuredContent.result; assert.equal(result.total, 3); assert.equal(result.items.length, 1); const item = result.items[0]; assert.match(item.question, /^Independent [0-2] a+$/); assert.equal(item.question.length, 4_000_014); assert.equal(item.revision, 1); recovered.add(item.unknown_id); console.log('page:', offset, 'total:', result.total, 'next_offset:', result.next_offset, 'question_length:', item.question.length); offset = result.next_offset; } assert.deepEqual(recovered, created); console.log('all created identities recovered:', recovered.size); } finally { await client.close(); await transport.close(); rmSync(work, { recursive: true, force: true }); } JS ``` 3. Compare the diagnostic's CLI instruction with the successful same-connection pages. This exact script passed once; an independently written SDK probe reproduced the same behavior in another fresh session, for two successful reproductions overall. ### Expected behavior When a valid `list_unknowns` response exceeds the transport budget, the remediation should identify an applicable recovery route on the current connection. For this fixture, it should direct the caller to provide `offset` and `limit`, then follow `next_offset` while keeping the ledger unchanged. Plain actionable guidance is sufficient; a new typed recovery field is not required. The documented MCP contract advertises complete current-head results by default and offset/limit paging for smaller replies. It also scopes retained state to a connection and explains that a standalone CLI invocation cannot resolve another connection's records. Exact contract and handler locators are in Evidence below. ### Impact and scope P2: callers encountering a large valid unknown listing receive advice that cannot directly retrieve the affected session state and no instruction for the working bounded read. Both tested sessions showed this result. Paging is a demonstrated practical workaround: every record, revision and full question was recovered without restarting or changing the server. No lost records, host retry loop, abandoned investigation, or security-boundary crossing was observed. This verification covers three individually deliverable records, default response settings, stdio MCP and the stated platform. It does not establish behavior for a record too large to fit on a one-record page or for other tools/transports. ### REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source revision: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1, build 26A434; arm64. The SDK client and stdio transport were loaded from the checkout's locked dependencies. ### Target and artifact context No binary, imported bundle or external provider. A fresh connection contains three generated residual-unknown records, each with a distinct prefix and 4,000,000 repeated ASCII characters. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence The exact inline script printed: ```text unpaged: resource_constraint list_unknowns remediation: Use complete CLI JSON output to consume the analysis. The effective REA_MCP_MAX_RESPONSE_BYTES value is 10485760 bytes (floor 10485760 bytes, 10 MiB). A change requires restarting the REA process and a matching client receive buffer. It does not raise REA's reserved single-string limit of 536869864 UTF-16 code units or Node's single-string ceiling of 536870888 UTF-16 code units. This release has no runtime override for either limit. recovery: null page: 0 total: 3 next_offset: 1 question_length: 4000014 page: 1 total: 3 next_offset: 2 question_length: 4000014 page: 2 total: 3 next_offset: null question_length: 4000014 all created identities recovered: 3 ``` `recovery: null` is the probe's rendering of the absent field. Assertions additionally check successful creation, the transport classification, revision 1, full question shape/length and equality of the created/recovered identity sets. At the stated revision: - `docs/mcp-contracts.md:304-308` defines complete individual-record paging, matching-head totals, stable ID order and traversal without concurrent ledger changes. The live `tools/list` description independently advertised the same paging parameters in the first verification session. - `docs/mcp-contracts.md:141-157` directs callers to follow the returned remediation and defines bounded `recovery` as optional. - `docs/mcp-contracts.md:582-594` defines connection-scoped retained records and the limits of a standalone CLI invocation. `src/server/registerSessionRecordTools.ts:151-169` reads the unknown page from that handler's session and delivers it without recovery metadata. - `src/server/ToolResultDelivery.ts:221-233` supplies special advice for committed mutations, retained analysis and Evidence bundles, then selects the generic CLI instruction used by this list reply. Evidence provenance: the error and complete recovery pages were executed through the public stdio entry point; the explanation of branch selection and session ownership is source-derived. No checkout edits or synthetic response-budget overrides were used. ### Typed outcome or recovery expected Keep the bounded `resource_constraint` result and correct transport details. Provide a same-connection `list_unknowns` paging instruction, optionally accompanied by a ready-to-call bounded request. Do not imply that restarting or invoking a separate CLI process can directly read the live ledger. ### Analysis or suspected cause Source-derived explanation: the list handler supplies no recovery metadata to result delivery. The success-overflow fallback has no branch for the list tool, so its diagnostic inherits analysis-oriented CLI guidance. A smaller list request can already complete; the missing behavior is selecting truthful recovery guidance at delivery. ### Additional context Duplicate searches covered open/closed issues, issue comments and all-state PRs. Closed [#6453](https://github.com/morluto/rea/issues/6453) concerns committed mutation identifiers and their recovery receipt; this reproduction uses three fully delivered mutations followed by an oversized read. Closed [#6106](https://github.com/morluto/rea/issues/6106) concerns invalid cross-field inputs, and open [#7327](https://github.com/morluto/rea/issues/7327) concerns the provenance exception list. None has the same closure criterion. Related oversized validation/analysis and general recovery work was also checked; no matching report was found. No last-known-good version or regression range was established. Proposed restoration check, not executed: rerun the inline script and require the unpaged error to recommend the supported paging route while preserving the demonstrated complete three-page recovery. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Two real stdio sessions returned inapplicable CLI consumption advice for a valid oversized unknown listing even though complete same-session paging succeeded. - Workaround status: available - Workaround basis: The executed offset/limit traversal recovered all three created identities at revision 1 and all 4,000,014 characters of each question without changing or restarting the server.
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#4493
No description provided.