[GH-ISSUE #6829] [Bug] Importing unrelated Evidence display metadata disables the active snapshot cache #4175

Closed
opened 2026-10-31 00:47:26 +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/6829

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

Importing an unrelated Evidence record again with only its display name/path changed succeeds with imported: 0, but disables the active target's snapshot cache and snapshot export until the session resets. The imported record has a different target digest and is absent from the active snapshot. Active-target Evidence remains exactly unchanged.

Observed through production MCP registration and BinarySession, using an in-process fixture provider for inert results. No real Hopper/Ghidra engine or target process ran. The finding concerns shared import/cache ownership, not external provider behavior.

Reproduction

Run from the specified source checkout with locked dependencies. This portable transcription of the executed MCP probe changes only scratch/import paths. Save the following as /tmp/rea-repro/probe.mts after creating /tmp/rea-repro. The repository's harness creates an SDK in-memory MCP connection to the production server and composes the production session/router with the declared fixture provider.

import assert from 'node:assert/strict';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const repo = process.env.REA_REPO;
assert(repo);
const load = (path: string) => import(pathToFileURL(join(repo, path)).href);
const { createApplicationMcpHarness } = await load('tests/fixtures/applicationMcpHarness.ts');
const { createTestBinarySession } = await load('tests/fixtures/binarySession.ts');
const { createAnalysisExecution } = await load('src/application/AnalysisProvider.ts');
const { createAnalysisProfile } = await load('src/domain/analysisProfile.ts');
const { createEvidence } = await load('src/domain/evidence.ts');
const { createEvidenceBundle, serializeEvidenceBundle } = await load('src/domain/evidenceBundle.ts');
const { ok } = await load('src/domain/result.ts');
const { silentLogger } = await load('src/logger.ts');
const root='/tmp/rea-repro';
const identity={id:'fixture',name:'Fixture',version:'1'};
const profile=createAnalysisProfile(identity,{fixture:true});
let segmentCalls=0;
const provider={
 identity:()=>identity,
 capabilities:()=>[{operation:'list_segments',provider:identity,available:true,reason:null,cachePolicy:'snapshot',effects:{mutatesArtifact:false,launchesProcess:false,mayShowUi:false,mayAccessNetwork:false,mayWriteFilesystem:false,changesPermissions:false,requiresRoot:false},limitations:[]}],
 resolveAnalysisProfile:async()=>ok({profile}),
 createClient:()=>({execute:async(operation)=>{if(operation==='list_segments')segmentCalls++;return ok(createAnalysisExecution(operation==='list_segments'?[]:null,identity));},close:async()=>ok(null)})
};
const session=createTestBinarySession(provider,{resolveAnalysisProfile:async()=>ok({profile})});
const harness=await createApplicationMcpHarness({session,serverOptions:{logger:silentLogger,providerEnvironment:{}}});
const call=async(name:string,args:Record<string,unknown>)=>{
 const reply=await harness.client.callTool({name,arguments:args});
 assert(!reply.isError,JSON.stringify(reply));return reply;
};
try{
 const targetPath=root+'/inert.hop';await writeFile(targetPath,'inert test database');
 await call('open_binary',{path:targetPath});
 for(let i=0;i<2;i++)await call('list_segments',{document:'fixture'});
 assert.equal(segmentCalls,1);
 const baseline=session.exportAnalysisSnapshot();assert(baseline.ok);assert.equal(baseline.value.entries.length,1);
 await writeFile(root+'/mcp-baseline-snapshot.json',JSON.stringify(baseline.value));
 const foreign={path:root+'/foreign.elf',sha256:'b'.repeat(64),format:'elf' as const,architecture:'x86_64' as const};
 const record=createEvidence(foreign,identity,{operation:'observe',parameters:{},result:{value:1}});
 const relocated={...record,subject:{...record.subject!,name:'renamed.elf',local_path:root+'/relocated/renamed.elf'}};
 const inputs=[['initial',record],['duplicate-control',record],['foreign-display-change',relocated]] as const;
 for(const [label,incoming] of inputs){
  const path=root+'/mcp-'+label+'.json';await writeFile(path,serializeEvidenceBundle(createEvidenceBundle([incoming])));
  const imported=await call('import_evidence_bundle',{path});
  const snapshot=session.exportAnalysisSnapshot();
  const beforeCalls=segmentCalls;
  await call('list_segments',{document:'fixture'});await call('list_segments',{document:'fixture'});
  const activeAfter=session.exportEvidenceBundle().records.filter(r=>r.subject?.digest.sha256===baseline.value.target.sha256);
  assert.deepEqual(activeAfter,baseline.value.evidence_bundle.records);
  console.log(JSON.stringify({label,import:imported.structuredContent,snapshot_ok:snapshot.ok,...(snapshot.ok?{snapshot_entries:snapshot.value.entries.length}:{error_tag:snapshot.error._tag,error:snapshot.error.message}),provider_calls_for_two_identical_queries:segmentCalls-beforeCalls,active_evidence_unchanged:true}));
 }
 const preservedPath=root+'/mcp-preserved-evidence.json';
 await call('export_evidence_bundle',{path:preservedPath,overwrite:true});
 await call('close_binary',{});
 await call('open_binary',{path:targetPath});
 await call('import_evidence_bundle',{path:preservedPath});
 const beforeRecoveryCalls=segmentCalls;
 await call('list_segments',{document:'fixture'});await call('list_segments',{document:'fixture'});
 const recovered=session.exportAnalysisSnapshot();assert(recovered.ok);assert.equal(recovered.value.entries.length,1);
 assert(session.exportEvidenceBundle().records.some(r=>r.evidence_id===record.evidence_id&&r.subject?.local_path===relocated.subject.local_path));
 console.log(JSON.stringify({label:'export-close-reopen-reimport-recovery',snapshot_ok:recovered.ok,snapshot_entries:recovered.value.entries.length,provider_calls_for_two_identical_queries:segmentCalls-beforeRecoveryCalls,foreign_display_metadata_preserved:true}));
}finally{await harness.close();}

REA_REPO="$PWD" node --import "$PWD/node_modules/tsx/dist/loader.mjs" /tmp/rea-repro/probe.mts

The complete probe was executed with unchanged-import and recovery controls. It does not load or execute the authored inert.hop file.

Expected behavior

Retain the active target's snapshot and cache eligibility when importing display metadata for a record belonging to another target. No target bytes, provider settings, operation, parameters, or active observation changed. Snapshot documentation promises reuse of exact results and explains invalidation for dispatched metadata writes, which did not occur here.

Impact and scope

The bounded fixture loses snapshot persistence and reruns every subsequent identical analysis query. Two identical calls reached the fixture provider twice after the display-only import, versus zero times after the unchanged import. Actual expensive-engine costs and real-provider sessions were not measured.

Workaround verified in the same probe: export Evidence, close/reopen the target, reimport the preserved bundle, and query again. The first query ran once, the second used the restored cache, snapshot export succeeded, and the foreign display metadata remained preserved.

REA version

6.7.0; source revision 7fd722e15bea35255f5eadbd406948e28538d25f. The latest published release is 6.7.0; its packaged runtime was not separately tested.

Runtime and platform

Node.js v26.12.1; Linux x64; repository-locked tsx 4.23.15 and MCP SDK. No external engine.

Target and artifact context

An inert local .hop file handled by an in-process fixture provider, plus valid Evidence bundles for a different SHA-256 subject. The incoming observation, provider identity, parameters, and Evidence ID stay unchanged; only subject.name and subject.local_path change.

Interface

MCP

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

Evidence provenance: observed production MCP/common-session reproduction with a fixture provider. Local verification: reproduced. Reproduction completeness: complete for the shared import and snapshot boundary; real engines and external MCP transport were not tested.

initial import:                 imported=1 snapshot_ok=true  provider_calls_for_two_identical_queries=0
unchanged-record control:       imported=0 snapshot_ok=true  provider_calls_for_two_identical_queries=0
foreign display-only import:    imported=0 snapshot_ok=false provider_calls_for_two_identical_queries=2
error: EvidenceIntegrityError: Analysis snapshots are unavailable because analysis metadata may have changed
active_evidence_unchanged=true for every import
export/reopen/reimport recovery: snapshot_ok=true snapshot_entries=1 provider_calls_for_two_identical_queries=1
foreign_display_metadata_preserved=true

A separate exact-production-owner control also reproduced the failure with a name-only change. Importing a genuinely different foreign observation added a record without invalidating the snapshot. That distinguishes display replacement from ordinary record additions.

Typed outcome or recovery expected

The successful unrelated import should leave exportAnalysisSnapshot() successful and identical list_segments queries eligible for the existing cache. Do not require a target reset for a display-only import.

Analysis or suspected cause

Source-derived: the ledger marks replacement display metadata as metadataChanged; BinarySessionRecords.importEvidenceBundle unconditionally passes that flag to whole-session invalidation. Invalidation clears the cache; lookup and recording remain disabled.

Invalidating a changed cached dependency could be reasonable. This foreign record is outside the active snapshot and changes no active analysis dependency, so that interpretation does not explain the observed reset requirement.

Additional context

Open and closed searches covered import/snapshot metadata, BinarySessionRecords invalidation, Evidence display caching, and metadataChanged. No matching report was found. A fresh open-issue listing was also checked immediately before submission. No regression range was established.

AI assistance disclosure: a Codex research agent prepared the fixture and inspected the production implementation; another Codex agent inspected the saved scripts, outputs, and relevant source before preparing this report. The in-process provider is explicitly synthetic. No fix was made, no real user configuration changed, and no external target ran.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A successful display-only foreign Evidence import demonstrably prevents active-target snapshot export and makes identical queries repeatedly execute, despite unchanged active Evidence.
  • Workaround status: available
  • Workaround basis: Exporting Evidence, closing/reopening the target, and reimporting the preserved bundle restored cache reuse and snapshot export while preserving the foreign display metadata in the executed probe.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6829 ### 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 Evidence, artifacts, or reconstruction ### Observed behavior Importing an unrelated Evidence record again with only its display name/path changed succeeds with `imported: 0`, but disables the active target's snapshot cache and snapshot export until the session resets. The imported record has a different target digest and is absent from the active snapshot. Active-target Evidence remains exactly unchanged. Observed through production MCP registration and `BinarySession`, using an in-process fixture provider for inert results. No real Hopper/Ghidra engine or target process ran. The finding concerns shared import/cache ownership, not external provider behavior. ### Reproduction Run from the specified source checkout with locked dependencies. This portable transcription of the executed MCP probe changes only scratch/import paths. Save the following as `/tmp/rea-repro/probe.mts` after creating `/tmp/rea-repro`. The repository's harness creates an SDK in-memory MCP connection to the production server and composes the production session/router with the declared fixture provider. ```typescript import assert from 'node:assert/strict'; import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; const repo = process.env.REA_REPO; assert(repo); const load = (path: string) => import(pathToFileURL(join(repo, path)).href); const { createApplicationMcpHarness } = await load('tests/fixtures/applicationMcpHarness.ts'); const { createTestBinarySession } = await load('tests/fixtures/binarySession.ts'); const { createAnalysisExecution } = await load('src/application/AnalysisProvider.ts'); const { createAnalysisProfile } = await load('src/domain/analysisProfile.ts'); const { createEvidence } = await load('src/domain/evidence.ts'); const { createEvidenceBundle, serializeEvidenceBundle } = await load('src/domain/evidenceBundle.ts'); const { ok } = await load('src/domain/result.ts'); const { silentLogger } = await load('src/logger.ts'); const root='/tmp/rea-repro'; const identity={id:'fixture',name:'Fixture',version:'1'}; const profile=createAnalysisProfile(identity,{fixture:true}); let segmentCalls=0; const provider={ identity:()=>identity, capabilities:()=>[{operation:'list_segments',provider:identity,available:true,reason:null,cachePolicy:'snapshot',effects:{mutatesArtifact:false,launchesProcess:false,mayShowUi:false,mayAccessNetwork:false,mayWriteFilesystem:false,changesPermissions:false,requiresRoot:false},limitations:[]}], resolveAnalysisProfile:async()=>ok({profile}), createClient:()=>({execute:async(operation)=>{if(operation==='list_segments')segmentCalls++;return ok(createAnalysisExecution(operation==='list_segments'?[]:null,identity));},close:async()=>ok(null)}) }; const session=createTestBinarySession(provider,{resolveAnalysisProfile:async()=>ok({profile})}); const harness=await createApplicationMcpHarness({session,serverOptions:{logger:silentLogger,providerEnvironment:{}}}); const call=async(name:string,args:Record<string,unknown>)=>{ const reply=await harness.client.callTool({name,arguments:args}); assert(!reply.isError,JSON.stringify(reply));return reply; }; try{ const targetPath=root+'/inert.hop';await writeFile(targetPath,'inert test database'); await call('open_binary',{path:targetPath}); for(let i=0;i<2;i++)await call('list_segments',{document:'fixture'}); assert.equal(segmentCalls,1); const baseline=session.exportAnalysisSnapshot();assert(baseline.ok);assert.equal(baseline.value.entries.length,1); await writeFile(root+'/mcp-baseline-snapshot.json',JSON.stringify(baseline.value)); const foreign={path:root+'/foreign.elf',sha256:'b'.repeat(64),format:'elf' as const,architecture:'x86_64' as const}; const record=createEvidence(foreign,identity,{operation:'observe',parameters:{},result:{value:1}}); const relocated={...record,subject:{...record.subject!,name:'renamed.elf',local_path:root+'/relocated/renamed.elf'}}; const inputs=[['initial',record],['duplicate-control',record],['foreign-display-change',relocated]] as const; for(const [label,incoming] of inputs){ const path=root+'/mcp-'+label+'.json';await writeFile(path,serializeEvidenceBundle(createEvidenceBundle([incoming]))); const imported=await call('import_evidence_bundle',{path}); const snapshot=session.exportAnalysisSnapshot(); const beforeCalls=segmentCalls; await call('list_segments',{document:'fixture'});await call('list_segments',{document:'fixture'}); const activeAfter=session.exportEvidenceBundle().records.filter(r=>r.subject?.digest.sha256===baseline.value.target.sha256); assert.deepEqual(activeAfter,baseline.value.evidence_bundle.records); console.log(JSON.stringify({label,import:imported.structuredContent,snapshot_ok:snapshot.ok,...(snapshot.ok?{snapshot_entries:snapshot.value.entries.length}:{error_tag:snapshot.error._tag,error:snapshot.error.message}),provider_calls_for_two_identical_queries:segmentCalls-beforeCalls,active_evidence_unchanged:true})); } const preservedPath=root+'/mcp-preserved-evidence.json'; await call('export_evidence_bundle',{path:preservedPath,overwrite:true}); await call('close_binary',{}); await call('open_binary',{path:targetPath}); await call('import_evidence_bundle',{path:preservedPath}); const beforeRecoveryCalls=segmentCalls; await call('list_segments',{document:'fixture'});await call('list_segments',{document:'fixture'}); const recovered=session.exportAnalysisSnapshot();assert(recovered.ok);assert.equal(recovered.value.entries.length,1); assert(session.exportEvidenceBundle().records.some(r=>r.evidence_id===record.evidence_id&&r.subject?.local_path===relocated.subject.local_path)); console.log(JSON.stringify({label:'export-close-reopen-reimport-recovery',snapshot_ok:recovered.ok,snapshot_entries:recovered.value.entries.length,provider_calls_for_two_identical_queries:segmentCalls-beforeRecoveryCalls,foreign_display_metadata_preserved:true})); }finally{await harness.close();} ``` ```sh REA_REPO="$PWD" node --import "$PWD/node_modules/tsx/dist/loader.mjs" /tmp/rea-repro/probe.mts ``` The complete probe was executed with unchanged-import and recovery controls. It does not load or execute the authored `inert.hop` file. ### Expected behavior Retain the active target's snapshot and cache eligibility when importing display metadata for a record belonging to another target. No target bytes, provider settings, operation, parameters, or active observation changed. [Snapshot documentation](https://github.com/morluto/rea/blob/7fd722e15bea35255f5eadbd406948e28538d25f/docs/cli.md#L305-L315) promises reuse of exact results and explains invalidation for dispatched metadata writes, which did not occur here. ### Impact and scope The bounded fixture loses snapshot persistence and reruns every subsequent identical analysis query. Two identical calls reached the fixture provider twice after the display-only import, versus zero times after the unchanged import. Actual expensive-engine costs and real-provider sessions were not measured. Workaround verified in the same probe: export Evidence, close/reopen the target, reimport the preserved bundle, and query again. The first query ran once, the second used the restored cache, snapshot export succeeded, and the foreign display metadata remained preserved. ### REA version 6.7.0; source revision `7fd722e15bea35255f5eadbd406948e28538d25f`. The latest published release is 6.7.0; its packaged runtime was not separately tested. ### Runtime and platform Node.js v26.12.1; Linux x64; repository-locked tsx 4.23.15 and MCP SDK. No external engine. ### Target and artifact context An inert local `.hop` file handled by an in-process fixture provider, plus valid Evidence bundles for a different SHA-256 subject. The incoming observation, provider identity, parameters, and Evidence ID stay unchanged; only `subject.name` and `subject.local_path` change. ### Interface MCP ### Earliest failure stage Artifact or evidence registration ### Relevant output or evidence Evidence provenance: observed production MCP/common-session reproduction with a fixture provider. Local verification: reproduced. Reproduction completeness: complete for the shared import and snapshot boundary; real engines and external MCP transport were not tested. ```text initial import: imported=1 snapshot_ok=true provider_calls_for_two_identical_queries=0 unchanged-record control: imported=0 snapshot_ok=true provider_calls_for_two_identical_queries=0 foreign display-only import: imported=0 snapshot_ok=false provider_calls_for_two_identical_queries=2 error: EvidenceIntegrityError: Analysis snapshots are unavailable because analysis metadata may have changed active_evidence_unchanged=true for every import export/reopen/reimport recovery: snapshot_ok=true snapshot_entries=1 provider_calls_for_two_identical_queries=1 foreign_display_metadata_preserved=true ``` A separate exact-production-owner control also reproduced the failure with a name-only change. Importing a genuinely different foreign observation added a record without invalidating the snapshot. That distinguishes display replacement from ordinary record additions. ### Typed outcome or recovery expected The successful unrelated import should leave `exportAnalysisSnapshot()` successful and identical `list_segments` queries eligible for the existing cache. Do not require a target reset for a display-only import. ### Analysis or suspected cause Source-derived: the [ledger marks replacement display metadata](https://github.com/morluto/rea/blob/7fd722e15bea35255f5eadbd406948e28538d25f/src/application/investigation/EvidenceLedger.ts#L100-L123) as `metadataChanged`; [`BinarySessionRecords.importEvidenceBundle`](https://github.com/morluto/rea/blob/7fd722e15bea35255f5eadbd406948e28538d25f/src/application/binary/BinarySessionRecords.ts#L148-L155) unconditionally passes that flag to whole-session invalidation. Invalidation [clears the cache](https://github.com/morluto/rea/blob/7fd722e15bea35255f5eadbd406948e28538d25f/src/application/binary/BinarySessionRecords.ts#L331-L334); [lookup and recording remain disabled](https://github.com/morluto/rea/blob/7fd722e15bea35255f5eadbd406948e28538d25f/src/application/binary/BinarySessionRecords.ts#L263-L279). Invalidating a changed cached dependency could be reasonable. This foreign record is outside the active snapshot and changes no active analysis dependency, so that interpretation does not explain the observed reset requirement. ### Additional context Open and closed searches covered import/snapshot metadata, BinarySessionRecords invalidation, Evidence display caching, and metadataChanged. No matching report was found. A fresh open-issue listing was also checked immediately before submission. No regression range was established. AI assistance disclosure: a Codex research agent prepared the fixture and inspected the production implementation; another Codex agent inspected the saved scripts, outputs, and relevant source before preparing this report. The in-process provider is explicitly synthetic. No fix was made, no real user configuration changed, and no external target ran. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A successful display-only foreign Evidence import demonstrably prevents active-target snapshot export and makes identical queries repeatedly execute, despite unchanged active Evidence. - Workaround status: available - Workaround basis: Exporting Evidence, closing/reopening the target, and reimporting the preserved bundle restored cache reuse and snapshot export while preserving the foreign display metadata in the executed probe.
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#4175
No description provided.