[GH-ISSUE #7383] [Bug] find_changed_behavior reports an imported process contradiction as observed unchanged #4389

Open
opened 2026-10-31 00:49:25 +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/7383

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

After import_evidence_bundle accepts a caller-authored process-comparison record with both aggregate and terminal status contradiction, MCP find_changed_behavior returns behavior_status: "observed_unchanged". Its terminal finding retains raw status contradiction but is classified as unresolved_branch; summary.contradictions is zero. The defect reproduced in two completed stdio MCP workflows. Unchanged and changed controls returned their corresponding observed statuses.

Reproduction

Use Node 24.22.1 in a fresh checkout at the source revision below and run npm ci && npm run build:cached. Verification used existing locked dependencies and rebuilt the runtime; the lockfile was unchanged. Run this from the checkout root. The script constructs synthetic capture sources and caller-authored comparison Evidence inline; it starts REA's stdio server and launches no target or analysis provider. It prints actual results and exits successfully even when the bug occurs.

export WORK="$PWD/work"
mkdir -p "$WORK"/home "$WORK"/config "$WORK"/data "$WORK"/cache "$WORK"/state "$WORK"/tmp
export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" XDG_DATA_HOME="$WORK/data" XDG_CACHE_HOME="$WORK/cache" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp"
node --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { createEvidence } from './dist/domain/evidence.js';
import { createEvidenceBundle, serializeEvidenceBundle } from './dist/domain/evidenceBundle.js';
import { PROCESS_PROVIDER } from './dist/domain/process/processEvidenceProvider.js';
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from './dist/domain/process/processCaptureExample.js';

const work = process.env.WORK;
const env = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent', NODE_DISABLE_COMPILE_CACHE: '1' };
for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR']) {
  env[key] = join(work, key.toLowerCase());
  mkdirSync(env[key], { recursive: true });
}
const sources = ['left', 'right'].map(side => createEvidence(undefined, PROCESS_PROVIDER, {
  predicateType: 'rea.process-capture', operation: 'capture_process_scenario', parameters: { side },
  result: EMPTY_PROCESS_CAPTURE_EXAMPLE, confidence: 'observed', authority: 'controlled-replay',
  environment: { id: 'fixture', platform: 'fixture', architecture: 'fixture', isolation: 'process' },
}));
const comparisons = ['unchanged', 'changed', 'contradiction'].map(status => createEvidence(undefined, PROCESS_PROVIDER, {
  predicateType: 'rea.process-comparison', operation: 'compare_process_captures',
  parameters: { left_evidence_id: sources[0].evidence_id, right_evidence_id: sources[1].evidence_id },
  result: { status, terminal: status, interaction: 'unchanged', exit: 'unchanged', filesystem: 'unchanged', process: 'unchanged',
    first_divergence: status === 'unchanged' ? { status: 'none' } : { status: 'unknown', reason: 'Synthetic comparison localization unavailable.' }, limitations: [] },
  confidence: 'derived', authority: 'analyst-inference', evidenceLinks: sources.map(record => record.evidence_id),
}));
const bundle = resolve(work, 'bundle.json');
writeFileSync(bundle, serializeEvidenceBundle(createEvidenceBundle([...sources, ...comparisons])));
const client = new Client({ name: 'comparison-check', version: '1' });
const transport = new StdioClientTransport({ command: process.execPath, args: [resolve('scripts/rea.mjs'), 'mcp'], env, stderr: 'pipe' });
transport.stderr?.on('data', () => {});
try {
  await client.connect(transport);
  const imported = await client.callTool({ name: 'import_evidence_bundle', arguments: { path: bundle } });
  assert(!imported.isError, JSON.stringify(imported));
  console.log('import_evidence_bundle: accepted');
  for (const comparison of comparisons) {
    const reply = await client.callTool({ name: 'find_changed_behavior', arguments: { comparisons: [comparison] } });
    assert(!reply.isError, JSON.stringify(reply));
    const result = reply.structuredContent.normalized_result;
    console.log(JSON.stringify({ input_status: comparison.normalized_result.status, behavior_status: result.behavior_status, summary: result.summary,
      findings: result.findings.items.map(({ dimension, classification, status }) => ({ dimension, classification, status })) }));
  }
} finally {
  await client.close();
  await transport.close();
}
JS

Expected behavior

Preserve the terminal finding as classification: "contradiction", count one contradiction, and return a non-equality aggregate. unknown fits the current aggregate enum. Keep the unchanged and changed controls correct. At the stated revision, src/domain/process/processComparison.ts:26-108 admits contradiction and requires aggregate/dimension consistency; src/domain/changedBehavior.ts:35-64 defines contradiction separately from unresolved findings and exposes its own counter. The public tool contract is src/contracts/sessionToolContracts.ts:126-130.

Impact and scope

P2: a bounded result-correctness defect lets callers relying on the aggregate and summary miss a retained conflict. Both executions used digest-valid imported comparisons; no native producer emission of contradiction or downstream incident was demonstrated. A safe workaround is available in the actual response: inspect each finding's raw status and its source comparison rather than relying on classification/counts alone. No security boundary crossing was observed.

REA version

Exact node scripts/rea.mjs --version output: 6.8.0. Source commit: 081f2e2559db4802ffdb8783f6bc8d6803eb8c3d (current main when verified).

Runtime and platform

Node.js v24.22.1; macOS 27.0.1 build 26A434; arm64, measured locally. No Hopper, Ghidra, IDA, device or live external analysis service was used.

Target and artifact context

Synthetic canonical Evidence bundle containing two process-capture records based on EMPTY_PROCESS_CAPTURE_EXAMPLE and three analyst-inference process-comparison records. Content IDs and source citations are valid; they do not attest that a native producer emitted the comparison outcomes. No real target is involved.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The independent workflow printed the following after the import was accepted. The first workflow separately asserted that the contradiction must not be observed equality and failed that assertion.

import_evidence_bundle: accepted
{"input_status":"unchanged","behavior_status":"observed_unchanged","summary":{"observed_changes":0,"static_candidates":0,"contradictions":0,"unresolved":0},"findings":[]}
{"input_status":"changed","behavior_status":"observed_changed","summary":{"observed_changes":1,"static_candidates":0,"contradictions":0,"unresolved":0},"findings":[{"dimension":"terminal","classification":"observed_change","status":"changed"}]}
{"input_status":"contradiction","behavior_status":"observed_unchanged","summary":{"observed_changes":0,"static_candidates":0,"contradictions":0,"unresolved":1},"findings":[{"dimension":"terminal","classification":"unresolved_branch","status":"contradiction"}]}

Typed outcome or recovery expected

Return a contradiction finding and summary.contradictions: 1; never present this comparison as observed_unchanged. Preserve the raw contradiction and Evidence citations.

Analysis or suspected cause

Source-derived: src/domain/changedBehavior.ts:222-252 maps every non-unchanged process dimension to either observed_change or unresolved_branch; its change predicate excludes contradiction. The summary counts these classifications. Separately, lines 137-149 pass aggregate comparison statuses to behaviorStatus at lines 344-354, whose unknown/truncated/change checks fall through to observed_unchanged for contradiction. The comparison schema has already admitted this consistent contradiction, so its inconsistency guard does not prevent either error.

Additional context

Closed #6218 and merged #6298 corrected inconsistent process-comparison aggregates in reconstruction verification. This input has a consistent contradiction aggregate, and the downstream changed-behavior failure persists; the merged PR explicitly excludes complete downstream contradiction correctness. Open #7371 concerns inconsistent filesystem facts inside capture records, and open #7294 concerns operand-order-dependent trace attribution; neither has this closure signal. No implementation repair or restoration run is claimed. Other platforms, live capture frequency and a last-known-good version were not established.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7383 ## 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 After `import_evidence_bundle` accepts a caller-authored process-comparison record with both aggregate and terminal status `contradiction`, MCP `find_changed_behavior` returns `behavior_status: "observed_unchanged"`. Its terminal finding retains raw status `contradiction` but is classified as `unresolved_branch`; `summary.contradictions` is zero. The defect reproduced in two completed stdio MCP workflows. Unchanged and changed controls returned their corresponding observed statuses. ## Reproduction Use Node 24.22.1 in a fresh checkout at the source revision below and run `npm ci && npm run build:cached`. Verification used existing locked dependencies and rebuilt the runtime; the lockfile was unchanged. Run this from the checkout root. The script constructs synthetic capture sources and caller-authored comparison Evidence inline; it starts REA's stdio server and launches no target or analysis provider. It prints actual results and exits successfully even when the bug occurs. ```sh export WORK="$PWD/work" mkdir -p "$WORK"/home "$WORK"/config "$WORK"/data "$WORK"/cache "$WORK"/state "$WORK"/tmp export HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" XDG_DATA_HOME="$WORK/data" XDG_CACHE_HOME="$WORK/cache" XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/tmp" node --input-type=module <<'JS' import assert from 'node:assert/strict'; import { mkdirSync, writeFileSync } from 'node:fs'; import { resolve, join } from 'node:path'; import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { createEvidence } from './dist/domain/evidence.js'; import { createEvidenceBundle, serializeEvidenceBundle } from './dist/domain/evidenceBundle.js'; import { PROCESS_PROVIDER } from './dist/domain/process/processEvidenceProvider.js'; import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from './dist/domain/process/processCaptureExample.js'; const work = process.env.WORK; const env = { PATH: process.env.PATH, REA_LOG_LEVEL: 'silent', NODE_DISABLE_COMPILE_CACHE: '1' }; for (const key of ['HOME', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'TMPDIR']) { env[key] = join(work, key.toLowerCase()); mkdirSync(env[key], { recursive: true }); } const sources = ['left', 'right'].map(side => createEvidence(undefined, PROCESS_PROVIDER, { predicateType: 'rea.process-capture', operation: 'capture_process_scenario', parameters: { side }, result: EMPTY_PROCESS_CAPTURE_EXAMPLE, confidence: 'observed', authority: 'controlled-replay', environment: { id: 'fixture', platform: 'fixture', architecture: 'fixture', isolation: 'process' }, })); const comparisons = ['unchanged', 'changed', 'contradiction'].map(status => createEvidence(undefined, PROCESS_PROVIDER, { predicateType: 'rea.process-comparison', operation: 'compare_process_captures', parameters: { left_evidence_id: sources[0].evidence_id, right_evidence_id: sources[1].evidence_id }, result: { status, terminal: status, interaction: 'unchanged', exit: 'unchanged', filesystem: 'unchanged', process: 'unchanged', first_divergence: status === 'unchanged' ? { status: 'none' } : { status: 'unknown', reason: 'Synthetic comparison localization unavailable.' }, limitations: [] }, confidence: 'derived', authority: 'analyst-inference', evidenceLinks: sources.map(record => record.evidence_id), })); const bundle = resolve(work, 'bundle.json'); writeFileSync(bundle, serializeEvidenceBundle(createEvidenceBundle([...sources, ...comparisons]))); const client = new Client({ name: 'comparison-check', version: '1' }); const transport = new StdioClientTransport({ command: process.execPath, args: [resolve('scripts/rea.mjs'), 'mcp'], env, stderr: 'pipe' }); transport.stderr?.on('data', () => {}); try { await client.connect(transport); const imported = await client.callTool({ name: 'import_evidence_bundle', arguments: { path: bundle } }); assert(!imported.isError, JSON.stringify(imported)); console.log('import_evidence_bundle: accepted'); for (const comparison of comparisons) { const reply = await client.callTool({ name: 'find_changed_behavior', arguments: { comparisons: [comparison] } }); assert(!reply.isError, JSON.stringify(reply)); const result = reply.structuredContent.normalized_result; console.log(JSON.stringify({ input_status: comparison.normalized_result.status, behavior_status: result.behavior_status, summary: result.summary, findings: result.findings.items.map(({ dimension, classification, status }) => ({ dimension, classification, status })) })); } } finally { await client.close(); await transport.close(); } JS ``` ## Expected behavior Preserve the terminal finding as `classification: "contradiction"`, count one contradiction, and return a non-equality aggregate. `unknown` fits the current aggregate enum. Keep the unchanged and changed controls correct. At the stated revision, `src/domain/process/processComparison.ts:26-108` admits contradiction and requires aggregate/dimension consistency; `src/domain/changedBehavior.ts:35-64` defines contradiction separately from unresolved findings and exposes its own counter. The public tool contract is `src/contracts/sessionToolContracts.ts:126-130`. ## Impact and scope P2: a bounded result-correctness defect lets callers relying on the aggregate and summary miss a retained conflict. Both executions used digest-valid imported comparisons; no native producer emission of contradiction or downstream incident was demonstrated. A safe workaround is available in the actual response: inspect each finding's raw `status` and its source comparison rather than relying on classification/counts alone. No security boundary crossing was observed. ## REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source commit: `081f2e2559db4802ffdb8783f6bc8d6803eb8c3d` (current main when verified). ## Runtime and platform Node.js `v24.22.1`; macOS `27.0.1` build `26A434`; `arm64`, measured locally. No Hopper, Ghidra, IDA, device or live external analysis service was used. ## Target and artifact context Synthetic canonical Evidence bundle containing two process-capture records based on `EMPTY_PROCESS_CAPTURE_EXAMPLE` and three analyst-inference process-comparison records. Content IDs and source citations are valid; they do not attest that a native producer emitted the comparison outcomes. No real target is involved. ## Interface MCP ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence The independent workflow printed the following after the import was accepted. The first workflow separately asserted that the contradiction must not be observed equality and failed that assertion. ```text import_evidence_bundle: accepted {"input_status":"unchanged","behavior_status":"observed_unchanged","summary":{"observed_changes":0,"static_candidates":0,"contradictions":0,"unresolved":0},"findings":[]} {"input_status":"changed","behavior_status":"observed_changed","summary":{"observed_changes":1,"static_candidates":0,"contradictions":0,"unresolved":0},"findings":[{"dimension":"terminal","classification":"observed_change","status":"changed"}]} {"input_status":"contradiction","behavior_status":"observed_unchanged","summary":{"observed_changes":0,"static_candidates":0,"contradictions":0,"unresolved":1},"findings":[{"dimension":"terminal","classification":"unresolved_branch","status":"contradiction"}]} ``` ## Typed outcome or recovery expected Return a contradiction finding and `summary.contradictions: 1`; never present this comparison as `observed_unchanged`. Preserve the raw contradiction and Evidence citations. ## Analysis or suspected cause Source-derived: `src/domain/changedBehavior.ts:222-252` maps every non-unchanged process dimension to either `observed_change` or `unresolved_branch`; its change predicate excludes contradiction. The summary counts these classifications. Separately, lines 137-149 pass aggregate comparison statuses to `behaviorStatus` at lines 344-354, whose unknown/truncated/change checks fall through to `observed_unchanged` for contradiction. The comparison schema has already admitted this consistent contradiction, so its inconsistency guard does not prevent either error. ## Additional context Closed [#6218](https://github.com/morluto/rea/issues/6218) and merged [#6298](https://github.com/morluto/rea/pull/6298) corrected inconsistent process-comparison aggregates in reconstruction verification. This input has a consistent contradiction aggregate, and the downstream changed-behavior failure persists; the merged PR explicitly excludes complete downstream contradiction correctness. Open [#7371](https://github.com/morluto/rea/issues/7371) concerns inconsistent filesystem facts inside capture records, and open [#7294](https://github.com/morluto/rea/issues/7294) concerns operand-order-dependent trace attribution; neither has this closure signal. No implementation repair or restoration run is claimed. Other platforms, live capture frequency and a last-known-good version were not established.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4389
No description provided.