[GH-ISSUE #7614] [Bug] Resource-session snapshot refusal recommends an inapplicable snapshot save #4544

Closed
opened 2026-10-31 00:50:39 +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/7614

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

After opening a generated app with open_bundle_resources, the MCP request close_binary with {"snapshot_path":"<absolute output path>/saved.json"} correctly refuses to save a binary analysis snapshot. Its message explains that the resource session has no executable identity or replay binding. However, remediation.action tells the caller to run analysis and save a fresh snapshot with the intended artifact, provider and analysis profile, which is unavailable in this session kind.

The snapshot file does not exist afterward. The session remains open and usable. Retrying with close_binary {} succeeds. The defect is the suggested recovery action, rather than the snapshot refusal or failed-save session retention.

Reproduction

  1. Start in the built baseline checkout with its locked dependencies present and Node.js 24.22.1 on PATH. Run this block from the repository root. It creates its own app bundle without an executable, uses isolated HOME/XDG directories, and deletes the fixture after closing the stdio client.
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, access, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Client } from './node_modules/@modelcontextprotocol/client/dist/index.mjs';
import { StdioClientTransport } from './node_modules/@modelcontextprotocol/client/dist/stdio.mjs';

const repo = process.cwd();
const work = await mkdtemp(join(tmpdir(), 'resource-close-check-'));
const env = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent' };
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME',
                   'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR']) {
  env[key] = join(work, key.toLowerCase());
  await mkdir(env[key]);
}
const app = join(work, 'Fixture.app');
await mkdir(join(app, 'Contents/Resources'), { recursive: true });
const plist = value => `<?xml version="1.0"?><plist version="1.0"><dict>${value}</dict></plist>`;
await writeFile(join(app, 'Contents/Info.plist'),
  plist('<key>CFBundleIdentifier</key><string>example.verifier.resources</string>'));
await writeFile(join(app, 'Contents/Resources/settings.plist'),
  plist('<key>Mode</key><string>test</string>'));
const client = new Client({ name: 'resource-close-check', version: '1' });
const transport = new StdioClientTransport({
  command: process.execPath, args: [join(repo, 'scripts/rea.mjs'), '--mcp'],
  cwd: work, env, stderr: 'pipe',
});
const deadline = setTimeout(() => transport.close(), 45000);
const call = async (name, args = {}) => {
  const reply = await client.callTool({ name, arguments: args });
  return { isError: reply.isError ?? false,
    body: reply.structuredContent ?? JSON.parse(reply.content.find(p => p.type === 'text').text) };
};
try {
  await client.connect(transport);
  const opened = await call('open_bundle_resources', { path: app });
  assert.equal(opened.isError, false);
  assert.equal(opened.body.result.executable_sha256, null);
  const snapshot = join(work, 'saved.json');
  const refused = await call('close_binary', { snapshot_path: snapshot });
  assert.equal(refused.isError, true);
  console.log('refused', JSON.stringify(refused.body));
  const exists = await access(snapshot).then(() => true, () => false);
  assert.equal(exists, false);
  console.log('snapshot_exists', exists);
  const status = (await call('binary_session')).body.result;
  assert.equal(status.open, true);
  assert.equal(status.session_kind, 'bundle_resources');
  assert.equal(status.cleanup_pending, false);
  console.log('after_refusal', JSON.stringify({ open: status.open,
    session_kind: status.session_kind, cleanup_pending: status.cleanup_pending }));
  const closed = await call('close_binary');
  assert.equal(closed.isError, false);
  assert.equal(closed.body.result, null);
  const final = (await call('binary_session')).body.result;
  assert.equal(final.open, false);
  console.log('plain_close', JSON.stringify({ ...closed.body, open_after: final.open }));
} finally {
  clearTimeout(deadline);
  await client.close();
  await rm(work, { recursive: true, force: true });
  assert.equal(await access(work).then(() => true, () => false), false);
  console.log('fixture_removed', true);
}
JS
  1. Compare the refusal's accurate message with its fresh-analysis-and-snapshot remediation.action. The assertions also check that no snapshot exists, the resource session survives the refusal, and ordinary close succeeds.

The behavior reproduced in two fresh server processes with separately generated bundles. The exact reduced block above was then executed once more and passed all assertions with the same refusal and recovery results. Only open_bundle_resources was used to admit a target; no binary or analysis provider was opened.

Expected behavior

The advertised close-tool description explicitly tells callers to close resource-only app sessions without snapshot_path, because those sessions have no binary snapshot binding. The bundle-resource contract also says binary analysis snapshots do not apply to this session kind. The returned remediation should offer that applicable next action: call close_binary without snapshot_path.

Keep rejecting the unsupported snapshot save and retaining the live session. This does not request binary snapshot support for resource sessions or acceptance of an invalid binding. Contract source locations appear under Evidence below.

Impact and scope

P3: the demonstrated defect is recovery wording. Following the returned action cannot create an analysis snapshot while retaining the current resource-only session. No failed automated recovery, target modification, data loss, invalid replay or output corruption was demonstrated. The public tool description supplies a practical workaround, and the verification exercised it successfully: omit snapshot_path to close.

The tested input was an otherwise empty resource session on macOS. Populated resource ledgers, other platforms and other snapshot refusal causes were not exercised. Snapshot persistence for this lane is expressly outside the contract.

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, arm64. Production stdio MCP through node scripts/rea.mjs --mcp, driven with the checkout's locked MCP SDK client. HOME, XDG config/data/cache/state and temporary directories were isolated. No Hopper or other GUI application was launched.

Target and artifact context

An authored .app directory containing an XML Contents/Info.plist and Contents/Resources/settings.plist. There is no executable declaration or executable file. Admission reports executable_sha256: null. No existing snapshot, imported Evidence or external target is needed.

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

Reduced stdout from the exact reproduction block:

refused {"error":{"code":"evidence_integrity_mismatch","category":"integrity_mismatch","message":"Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding.","retryable":false,"remediation":{"action":"Run analysis without this snapshot, then save a fresh snapshot using the intended artifact, provider, and analysis profile."},"details":{"reason":"Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding."}}}
snapshot_exists false
after_refusal {"open":true,"session_kind":"bundle_resources","cleanup_pending":false}
plain_close {"result":null,"open_after":false}
fixture_removed true

This shows the contradictory action and confirms that the documented ordinary-close workaround succeeds.

Source checks at the baseline, corroborating the observed public behavior:

  • Expected: src/contracts/sessionToolContracts.ts:73-79 declares the separate resource lane and explicitly directs resource-only sessions to close without snapshot_path. The server's tools/list returned this close description in both initial trials. docs/mcp-contracts.md:694-703 excludes binary snapshots from this lane and permits ordinary close. AGENTS.md, Architecture and Cleanup, requires meaningful failure reasons.
  • Failure path: src/server/registerCloseLifecycleTool.ts:90-120 selects snapshot close when a destination is supplied. src/application/binary/BinarySession.ts:814-828 attempts export before writing or closing; :1075-1082 returns the resource-specific applicability refusal as AnalysisSnapshotMismatchError.
  • Recovery projection: src/domain/analysisErrorPresentation.ts:81-82 gives that class the fresh-analysis-and-snapshot action regardless of the particular refusal reason. The matching compiled runtime was also inspected. These are source observations, separate from the executed MCP calls.

Typed outcome or recovery expected

Return the existing resource-applicability refusal with an action that names close_binary without snapshot_path. Preserve the live session after refusal and successful ordinary closure. No new snapshot capability or error-code change is required to resolve this report.

Analysis or suspected cause

Source-derived explanation: the resource-only export guard uses the same error class as replay-binding mismatches. The presentation layer assigns one action to that class. Reanalyzing a suitable executable can address a replay mismatch, whereas this guard states that the current session has no binary snapshot binding at all. A remediation that distinguishes those causes would address the observed contradiction.

Additional context

Related open #7311 concerns a fresh archive session incorrectly diagnosed as invalid Evidence with a profileless-native reason. Its resource-only control establishes a correct applicability message, but does not check this resource-session remediation. Correcting the archive diagnosis can leave the action reported here unchanged; correcting this action would leave the archive diagnosis unresolved. These are different closure signals.

Closed #5945 fixed a wrong-target snapshot-import producer; its closure comment identifies target-mismatch regression assertions. The fresh-snapshot action is applicable to that case. This report does not claim a regression of that fix. #7427 concerns a publication receipt lost after cleanup failure, and #7290 concerns DOL persistence; neither involves an intentionally inapplicable resource snapshot. Open/closed issues, all-state PRs including merged PRs, and relevant comments were searched; no exact duplicate was found. The first affected release is unknown.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7614 ### 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 After opening a generated app with `open_bundle_resources`, the MCP request `close_binary` with `{"snapshot_path":"<absolute output path>/saved.json"}` correctly refuses to save a binary analysis snapshot. Its message explains that the resource session has no executable identity or replay binding. However, `remediation.action` tells the caller to run analysis and save a fresh snapshot with the intended artifact, provider and analysis profile, which is unavailable in this session kind. The snapshot file does not exist afterward. The session remains open and usable. Retrying with `close_binary {}` succeeds. The defect is the suggested recovery action, rather than the snapshot refusal or failed-save session retention. ### Reproduction 1. Start in the built baseline checkout with its locked dependencies present and Node.js 24.22.1 on PATH. Run this block from the repository root. It creates its own app bundle without an executable, uses isolated HOME/XDG directories, and deletes the fixture after closing the stdio client. ```sh node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { mkdtemp, mkdir, writeFile, access, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Client } from './node_modules/@modelcontextprotocol/client/dist/index.mjs'; import { StdioClientTransport } from './node_modules/@modelcontextprotocol/client/dist/stdio.mjs'; const repo = process.cwd(); const work = await mkdtemp(join(tmpdir(), 'resource-close-check-')); const env = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent' }; for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR']) { env[key] = join(work, key.toLowerCase()); await mkdir(env[key]); } const app = join(work, 'Fixture.app'); await mkdir(join(app, 'Contents/Resources'), { recursive: true }); const plist = value => `<?xml version="1.0"?><plist version="1.0"><dict>${value}</dict></plist>`; await writeFile(join(app, 'Contents/Info.plist'), plist('<key>CFBundleIdentifier</key><string>example.verifier.resources</string>')); await writeFile(join(app, 'Contents/Resources/settings.plist'), plist('<key>Mode</key><string>test</string>')); const client = new Client({ name: 'resource-close-check', version: '1' }); const transport = new StdioClientTransport({ command: process.execPath, args: [join(repo, 'scripts/rea.mjs'), '--mcp'], cwd: work, env, stderr: 'pipe', }); const deadline = setTimeout(() => transport.close(), 45000); const call = async (name, args = {}) => { const reply = await client.callTool({ name, arguments: args }); return { isError: reply.isError ?? false, body: reply.structuredContent ?? JSON.parse(reply.content.find(p => p.type === 'text').text) }; }; try { await client.connect(transport); const opened = await call('open_bundle_resources', { path: app }); assert.equal(opened.isError, false); assert.equal(opened.body.result.executable_sha256, null); const snapshot = join(work, 'saved.json'); const refused = await call('close_binary', { snapshot_path: snapshot }); assert.equal(refused.isError, true); console.log('refused', JSON.stringify(refused.body)); const exists = await access(snapshot).then(() => true, () => false); assert.equal(exists, false); console.log('snapshot_exists', exists); const status = (await call('binary_session')).body.result; assert.equal(status.open, true); assert.equal(status.session_kind, 'bundle_resources'); assert.equal(status.cleanup_pending, false); console.log('after_refusal', JSON.stringify({ open: status.open, session_kind: status.session_kind, cleanup_pending: status.cleanup_pending })); const closed = await call('close_binary'); assert.equal(closed.isError, false); assert.equal(closed.body.result, null); const final = (await call('binary_session')).body.result; assert.equal(final.open, false); console.log('plain_close', JSON.stringify({ ...closed.body, open_after: final.open })); } finally { clearTimeout(deadline); await client.close(); await rm(work, { recursive: true, force: true }); assert.equal(await access(work).then(() => true, () => false), false); console.log('fixture_removed', true); } JS ``` 2. Compare the refusal's accurate `message` with its fresh-analysis-and-snapshot `remediation.action`. The assertions also check that no snapshot exists, the resource session survives the refusal, and ordinary close succeeds. The behavior reproduced in two fresh server processes with separately generated bundles. The exact reduced block above was then executed once more and passed all assertions with the same refusal and recovery results. Only `open_bundle_resources` was used to admit a target; no binary or analysis provider was opened. ### Expected behavior The advertised close-tool description explicitly tells callers to close resource-only app sessions without `snapshot_path`, because those sessions have no binary snapshot binding. The bundle-resource contract also says binary analysis snapshots do not apply to this session kind. The returned remediation should offer that applicable next action: call `close_binary` without `snapshot_path`. Keep rejecting the unsupported snapshot save and retaining the live session. This does not request binary snapshot support for resource sessions or acceptance of an invalid binding. Contract source locations appear under Evidence below. ### Impact and scope P3: the demonstrated defect is recovery wording. Following the returned action cannot create an analysis snapshot while retaining the current resource-only session. No failed automated recovery, target modification, data loss, invalid replay or output corruption was demonstrated. The public tool description supplies a practical workaround, and the verification exercised it successfully: omit `snapshot_path` to close. The tested input was an otherwise empty resource session on macOS. Populated resource ledgers, other platforms and other snapshot refusal causes were not exercised. Snapshot persistence for this lane is expressly outside the contract. ### REA version Exact `node scripts/rea.mjs --version` output: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`, arm64. Production stdio MCP through `node scripts/rea.mjs --mcp`, driven with the checkout's locked MCP SDK client. HOME, XDG config/data/cache/state and temporary directories were isolated. No Hopper or other GUI application was launched. ### Target and artifact context An authored `.app` directory containing an XML `Contents/Info.plist` and `Contents/Resources/settings.plist`. There is no executable declaration or executable file. Admission reports `executable_sha256: null`. No existing snapshot, imported Evidence or external target is needed. ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence Reduced stdout from the exact reproduction block: ```text refused {"error":{"code":"evidence_integrity_mismatch","category":"integrity_mismatch","message":"Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding.","retryable":false,"remediation":{"action":"Run analysis without this snapshot, then save a fresh snapshot using the intended artifact, provider, and analysis profile."},"details":{"reason":"Analysis snapshots do not apply to a resource-only app session because it has no executable identity or replay binding."}}} snapshot_exists false after_refusal {"open":true,"session_kind":"bundle_resources","cleanup_pending":false} plain_close {"result":null,"open_after":false} fixture_removed true ``` This shows the contradictory action and confirms that the documented ordinary-close workaround succeeds. Source checks at the baseline, corroborating the observed public behavior: - Expected: `src/contracts/sessionToolContracts.ts:73-79` declares the separate resource lane and explicitly directs resource-only sessions to close without `snapshot_path`. The server's `tools/list` returned this close description in both initial trials. `docs/mcp-contracts.md:694-703` excludes binary snapshots from this lane and permits ordinary close. `AGENTS.md`, Architecture and Cleanup, requires meaningful failure reasons. - Failure path: `src/server/registerCloseLifecycleTool.ts:90-120` selects snapshot close when a destination is supplied. `src/application/binary/BinarySession.ts:814-828` attempts export before writing or closing; `:1075-1082` returns the resource-specific applicability refusal as `AnalysisSnapshotMismatchError`. - Recovery projection: `src/domain/analysisErrorPresentation.ts:81-82` gives that class the fresh-analysis-and-snapshot action regardless of the particular refusal reason. The matching compiled runtime was also inspected. These are source observations, separate from the executed MCP calls. ### Typed outcome or recovery expected Return the existing resource-applicability refusal with an action that names `close_binary` without `snapshot_path`. Preserve the live session after refusal and successful ordinary closure. No new snapshot capability or error-code change is required to resolve this report. ### Analysis or suspected cause Source-derived explanation: the resource-only export guard uses the same error class as replay-binding mismatches. The presentation layer assigns one action to that class. Reanalyzing a suitable executable can address a replay mismatch, whereas this guard states that the current session has no binary snapshot binding at all. A remediation that distinguishes those causes would address the observed contradiction. ### Additional context Related open [#7311](https://github.com/morluto/rea/issues/7311) concerns a fresh archive session incorrectly diagnosed as invalid Evidence with a profileless-native reason. Its resource-only control establishes a correct applicability message, but does not check this resource-session remediation. Correcting the archive diagnosis can leave the action reported here unchanged; correcting this action would leave the archive diagnosis unresolved. These are different closure signals. Closed [#5945](https://github.com/morluto/rea/issues/5945) fixed a wrong-target snapshot-import producer; its closure comment identifies target-mismatch regression assertions. The fresh-snapshot action is applicable to that case. This report does not claim a regression of that fix. [#7427](https://github.com/morluto/rea/issues/7427) concerns a publication receipt lost after cleanup failure, and [#7290](https://github.com/morluto/rea/issues/7290) concerns DOL persistence; neither involves an intentionally inapplicable resource snapshot. Open/closed issues, all-state PRs including merged PRs, and relevant comments were searched; no exact duplicate was found. The first affected release is unknown.
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#4544
No description provided.