[GH-ISSUE #6077] [Bug] Evidence-file analysis views return UNKNOWN when the input descriptor cannot close #3717

Closed
opened 2026-10-31 00:43:19 +00:00 by bsakaguchi · 0 comments
Owner

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

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

A completed Evidence JSON read followed by a denied descriptor close rejects the shared analysis-view service outside its declared Result channel. The registered inspect-analysis-view CLI handler returns code: "UNKNOWN" with the regular-file cleanup exception message, without typed cleanup status or remaining-resource fields. The handle remains open until explicitly reclaimed.

This was executed with a real valid Evidence file and controlled close rejection. It was not a naturally occurring filesystem fault. With an explicit resource scope, a transient close refusal recovers and the same view succeeds; a persistent refusal still rejects the application service.

Reproduction

Using the source checkout and locked dependencies, save the following test as tests/process-global/evidenceClose.repro.test.ts and run node node_modules/vitest/vitest.mjs run tests/process-global/evidenceClose.repro.test.ts --maxWorkers=1. The test exercises the production file owner and application service, plus the registered CLI parser/handler in process. Its assertions describe the current failure and the transient-retry control. Restore the close method before releasing the exact retained handles.

import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { afterEach, expect, it, vi } from 'vitest';
import { createCli } from '../../src/cli.js';
import { createTestTempDirectory } from '../fixtures/temporaryDirectory.js';
import { analysisViewNativeEvidence } from '../fixtures/analysisView.js';
import { inspectAnalysisViewWithFiles } from '../../src/application/analysisView/AnalysisViewService.js';
import { OwnedFileHandle } from '../../src/filesystem/OwnedFileHandle.js';
import { RegularFileCleanupFailure } from '../../src/application/RegularFileRead.js';
import { ArtifactResourceScope } from '../../src/artifacts/ArtifactResourceScope.js';
afterEach(() => vi.restoreAllMocks());
it.each(['none', 'persistent', 'transient'] as const)('Evidence source final close: %s', async mode => {
  const root = await createTestTempDirectory('rea-view-close-');
  const path = join(root, 'evidence.json');
  await writeFile(path, JSON.stringify(analysisViewNativeEvidence()));
  const resources = mode === 'none' ? undefined : new ArtifactResourceScope();
  const original = OwnedFileHandle.prototype.close;
  let attempts = 0;
  const spy = vi.spyOn(OwnedFileHandle.prototype, 'close').mockImplementation(async function(this: OwnedFileHandle) {
    attempts++;
    if (mode !== 'transient' || attempts === 1) throw new Error('controlled close EIO');
    return original.call(this);
  });
  let failure: unknown;
  try {
    const request = {source: {kind: 'evidence-file', path}, view: {kind: 'native', facet: 'callees', offset: 0, limit: 1}};
    if (mode === 'transient') {
      const result = await inspectAnalysisViewWithFiles(request, undefined, {resources});
      expect(result.ok).toBe(true);
      expect(attempts).toBe(2);
    } else {
      try { await inspectAnalysisViewWithFiles(request, undefined, {resources}); } catch (cause) { failure = cause; }
      expect(failure).toBeInstanceOf(RegularFileCleanupFailure);
      expect((failure as RegularFileCleanupFailure).outcome.kind).toBe('completed');
      expect(attempts).toBe(mode === 'none' ? 1 : 2);
    }
    console.log('VIEW_CLOSE', JSON.stringify({mode, attempts, rejected: failure instanceof RegularFileCleanupFailure}));
  } finally {
    spy.mockRestore();
    if (resources) await resources.close();
    if (failure instanceof RegularFileCleanupFailure) await failure.owner.close();
  }
});

it('registered CLI projects the evidence close refusal as generic UNKNOWN', async () => {
  const root = await createTestTempDirectory('rea-view-cli-close-');
  const path = join(root, 'evidence.json');
  await writeFile(path, JSON.stringify(analysisViewNativeEvidence()));
  const original = OwnedFileHandle.prototype.close;
  const owners = new Set<OwnedFileHandle>();
  const spy = vi.spyOn(OwnedFileHandle.prototype, 'close').mockImplementation(async function(this: OwnedFileHandle) {
    owners.add(this); throw new Error('controlled evidence close EIO');
  });
  let stdout = ''; let failure: unknown;
  try {
    const input = JSON.stringify({source:{kind:'evidence-file',path},view:{kind:'native',facet:'callees',offset:0,limit:1}});
    try { await createCli({}).serve(['inspect-analysis-view', input, '--json'], {env:{},exit:()=>{},stdout:s=>{stdout+=s;}}); } catch(cause) { failure=cause; }
    console.log('CLI_CLOSE', JSON.stringify({stdout, rejected: failure !== undefined, owners:owners.size}));
    expect(owners.size).toBe(1);
    expect(JSON.parse(stdout)).toMatchObject({code:'UNKNOWN'});
    expect(stdout).not.toContain('cleanup_incomplete');
  } finally { spy.mockRestore(); process.exitCode=0; for (const owner of owners) await original.call(owner); }
});

Expected behavior

Retain the exact descriptor owner and return the expected typed outcome. A transient refusal should recover through the resource scope and deliver the requested view; an unresolved close should produce cleanup_incomplete with the retained resource and recovery information. The lifecycle contract requires typed owner-confirmed cleanup. The file-source service declares a Result of Evidence or AnalysisError.

Impact and scope

P2: a bounded descriptor-close failure prevents a read-only view and removes the typed cleanup information used by automation. Normal file reads work; the explicit-scope transient-retry control also works. No corruption of the selected input or native-target execution occurred. Natural OS fault incidence and public MCP transport behavior were not measured.

REA version

rea-agents 6.7.0; source revision 5241597594.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; locked Vitest 5.0.3. Source application and registered CLI handler, real file handles, controlled close rejection. The pinned Node 24.18.0 runtime was not exercised.

Target and artifact context

A valid native-analysis Evidence fixture stored as JSON, projected with the supported native callees view. No native provider is started.

Interface

CLI

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

The final focused run passed eight controls across the related lifecycle checks. Four concern this path:

Case Close attempts Result
Application, no scope, denied close 1 Rejects RegularFileCleanupFailure after completed JSON read
Application, scoped persistent denial 2 Same rejection
Application, scoped transient denial 2 Successful view Evidence
Registered CLI, denied close 1 UNKNOWN; no cleanup_incomplete fields

The CLI response shape was { "code": "UNKNOWN", "message": "Regular file cleanup failed for <selected evidence file>" }. The path is replaced here for portability. The CLI framework catches the untyped application rejection; the CLI itself does not expose a rejected Promise to its caller.

Typed outcome or recovery expected

A successful view after owner-confirmed close retry, or a typed cleanup_incomplete outcome with exact owner/resources retained for retry. Do not imply that JSON authentication or view projection completed before the close failure.

Analysis or suspected cause

Source-derived: readEvidenceFileSource awaits a reader that deliberately rethrows RegularFileCleanupFailure, but does not map it. The CLI registration supplies no resource scope; CLI logging projects only AnalysisError. The shared reader already retains the failed owner, so this consumer needs to adapt the lifecycle outcome and the one-shot CLI needs an owner.

Additional context

Fetched upstream main f86206535f; these file-source and CLI lines are unchanged. The newer runtime was not rebuilt. Related reports cover different producers: #5686 concerns late cancellation replacing a JSON physical read failure; #6047, #6030 and #6052 concern Android, Hopper and Kuna raw descriptor readers. The implemented evidence-file feature in PR #5293 does not handle this rejection. No introducing revision or naturally occurring failure rate was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6077 ### 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 A completed Evidence JSON read followed by a denied descriptor close rejects the shared analysis-view service outside its declared Result channel. The registered `inspect-analysis-view` CLI handler returns `code: "UNKNOWN"` with the regular-file cleanup exception message, without typed cleanup status or remaining-resource fields. The handle remains open until explicitly reclaimed. This was executed with a real valid Evidence file and controlled close rejection. It was not a naturally occurring filesystem fault. With an explicit resource scope, a transient close refusal recovers and the same view succeeds; a persistent refusal still rejects the application service. ### Reproduction Using the source checkout and locked dependencies, save the following test as `tests/process-global/evidenceClose.repro.test.ts` and run `node node_modules/vitest/vitest.mjs run tests/process-global/evidenceClose.repro.test.ts --maxWorkers=1`. The test exercises the production file owner and application service, plus the registered CLI parser/handler in process. Its assertions describe the current failure and the transient-retry control. Restore the close method before releasing the exact retained handles. ```ts import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { afterEach, expect, it, vi } from 'vitest'; import { createCli } from '../../src/cli.js'; import { createTestTempDirectory } from '../fixtures/temporaryDirectory.js'; import { analysisViewNativeEvidence } from '../fixtures/analysisView.js'; import { inspectAnalysisViewWithFiles } from '../../src/application/analysisView/AnalysisViewService.js'; import { OwnedFileHandle } from '../../src/filesystem/OwnedFileHandle.js'; import { RegularFileCleanupFailure } from '../../src/application/RegularFileRead.js'; import { ArtifactResourceScope } from '../../src/artifacts/ArtifactResourceScope.js'; afterEach(() => vi.restoreAllMocks()); it.each(['none', 'persistent', 'transient'] as const)('Evidence source final close: %s', async mode => { const root = await createTestTempDirectory('rea-view-close-'); const path = join(root, 'evidence.json'); await writeFile(path, JSON.stringify(analysisViewNativeEvidence())); const resources = mode === 'none' ? undefined : new ArtifactResourceScope(); const original = OwnedFileHandle.prototype.close; let attempts = 0; const spy = vi.spyOn(OwnedFileHandle.prototype, 'close').mockImplementation(async function(this: OwnedFileHandle) { attempts++; if (mode !== 'transient' || attempts === 1) throw new Error('controlled close EIO'); return original.call(this); }); let failure: unknown; try { const request = {source: {kind: 'evidence-file', path}, view: {kind: 'native', facet: 'callees', offset: 0, limit: 1}}; if (mode === 'transient') { const result = await inspectAnalysisViewWithFiles(request, undefined, {resources}); expect(result.ok).toBe(true); expect(attempts).toBe(2); } else { try { await inspectAnalysisViewWithFiles(request, undefined, {resources}); } catch (cause) { failure = cause; } expect(failure).toBeInstanceOf(RegularFileCleanupFailure); expect((failure as RegularFileCleanupFailure).outcome.kind).toBe('completed'); expect(attempts).toBe(mode === 'none' ? 1 : 2); } console.log('VIEW_CLOSE', JSON.stringify({mode, attempts, rejected: failure instanceof RegularFileCleanupFailure})); } finally { spy.mockRestore(); if (resources) await resources.close(); if (failure instanceof RegularFileCleanupFailure) await failure.owner.close(); } }); it('registered CLI projects the evidence close refusal as generic UNKNOWN', async () => { const root = await createTestTempDirectory('rea-view-cli-close-'); const path = join(root, 'evidence.json'); await writeFile(path, JSON.stringify(analysisViewNativeEvidence())); const original = OwnedFileHandle.prototype.close; const owners = new Set<OwnedFileHandle>(); const spy = vi.spyOn(OwnedFileHandle.prototype, 'close').mockImplementation(async function(this: OwnedFileHandle) { owners.add(this); throw new Error('controlled evidence close EIO'); }); let stdout = ''; let failure: unknown; try { const input = JSON.stringify({source:{kind:'evidence-file',path},view:{kind:'native',facet:'callees',offset:0,limit:1}}); try { await createCli({}).serve(['inspect-analysis-view', input, '--json'], {env:{},exit:()=>{},stdout:s=>{stdout+=s;}}); } catch(cause) { failure=cause; } console.log('CLI_CLOSE', JSON.stringify({stdout, rejected: failure !== undefined, owners:owners.size})); expect(owners.size).toBe(1); expect(JSON.parse(stdout)).toMatchObject({code:'UNKNOWN'}); expect(stdout).not.toContain('cleanup_incomplete'); } finally { spy.mockRestore(); process.exitCode=0; for (const owner of owners) await original.call(owner); } }); ``` ### Expected behavior Retain the exact descriptor owner and return the expected typed outcome. A transient refusal should recover through the resource scope and deliver the requested view; an unresolved close should produce `cleanup_incomplete` with the retained resource and recovery information. The [lifecycle contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/tool-design.md#L132-L147) requires typed owner-confirmed cleanup. The [file-source service](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/analysisView/evidenceFileSource.ts#L74-L101) declares a Result of Evidence or AnalysisError. ### Impact and scope P2: a bounded descriptor-close failure prevents a read-only view and removes the typed cleanup information used by automation. Normal file reads work; the explicit-scope transient-retry control also works. No corruption of the selected input or native-target execution occurred. Natural OS fault incidence and public MCP transport behavior were not measured. ### REA version rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; locked Vitest 5.0.3. Source application and registered CLI handler, real file handles, controlled close rejection. The pinned Node 24.18.0 runtime was not exercised. ### Target and artifact context A valid native-analysis Evidence fixture stored as JSON, projected with the supported native callees view. No native provider is started. ### Interface CLI ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence The final focused run passed eight controls across the related lifecycle checks. Four concern this path: | Case | Close attempts | Result | | --- | --- | --- | | Application, no scope, denied close | 1 | Rejects RegularFileCleanupFailure after completed JSON read | | Application, scoped persistent denial | 2 | Same rejection | | Application, scoped transient denial | 2 | Successful view Evidence | | Registered CLI, denied close | 1 | UNKNOWN; no cleanup_incomplete fields | The CLI response shape was `{ "code": "UNKNOWN", "message": "Regular file cleanup failed for <selected evidence file>" }`. The path is replaced here for portability. The CLI framework catches the untyped application rejection; the CLI itself does not expose a rejected Promise to its caller. ### Typed outcome or recovery expected A successful view after owner-confirmed close retry, or a typed `cleanup_incomplete` outcome with exact owner/resources retained for retry. Do not imply that JSON authentication or view projection completed before the close failure. ### Analysis or suspected cause Source-derived: [readEvidenceFileSource](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/analysisView/evidenceFileSource.ts#L74-L101) awaits a reader that [deliberately rethrows RegularFileCleanupFailure](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/JsonFiles.ts#L116-L132), but does not map it. [The CLI registration](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/analysisViewCommands.ts#L72-L106) supplies no resource scope; [CLI logging](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cliLogging.ts#L130-L146) projects only AnalysisError. The shared reader already retains the failed owner, so this consumer needs to adapt the lifecycle outcome and the one-shot CLI needs an owner. ### Additional context Fetched upstream main f86206535f8ec71014781f9d432a84b01e183e5d; these file-source and CLI lines are unchanged. The newer runtime was not rebuilt. Related reports cover different producers: #5686 concerns late cancellation replacing a JSON physical read failure; #6047, #6030 and #6052 concern Android, Hopper and Kuna raw descriptor readers. The implemented evidence-file feature in PR #5293 does not handle this rejection. No introducing revision or naturally occurring failure rate was 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#3717
No description provided.