[GH-ISSUE #5084] [Bug] Inspector target listing continues filesystem work after cancellation #3122

Closed
opened 2026-10-31 00:36:52 +00:00 by bsakaguchi · 1 comment
Owner

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

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

A source-boundary test of listJavaScriptRuntimeTargets with the production V8InspectorProvider observed two additional file stat calls after the caller's signal was already aborted during the first target's stat.

The application eventually returns AnalysisCancelledError with partial Evidence. The defect is continuing per-target filesystem work before that return. Discovery uses a synthetic loopback Inspector; the abort is injected at a mocked filesystem boundary that forwards all reads to the real filesystem. No real Node/Electron target or MCP client was run.

Reproduction

From the source checkout at the revision below, with locked dependencies available, place the following source test under tests/process-global/ and assign its relative test-file path to PROBE. It uses the repository's existing fake Inspector fixture, one inert file and three discovery targets.

import { basename, join } from "node:path";
import { pathToFileURL } from "node:url";
import { writeFile, rm } from "node:fs/promises";
import { expect, test, vi } from "vitest";
import { V8InspectorProvider } from "../../src/inspector/V8InspectorProvider.js";
import { listJavaScriptRuntimeTargets } from "../../src/application/javascript/JavaScriptRuntimeObservationService.js";
import { startFakeV8Inspector } from "../fixtures/inspector/fakeV8Inspector.js";

const seam = vi.hoisted(() => ({
  controller: undefined as AbortController | undefined,
  calls: [] as { file: string; aborted: boolean }[],
}));
vi.mock("node:fs/promises", async (importOriginal) => {
  const actual = await importOriginal<typeof import("node:fs/promises")>();
  return {
    ...actual,
    stat: async (...args: Parameters<typeof actual.stat>) => {
      if (String(args[0]).includes("cancel-fixture")) {
        seam.calls.push({file: String(args[0]), aborted: seam.controller?.signal.aborted ?? false});
        if (seam.calls.length === 1) seam.controller?.abort("fixture cancellation during first stat");
      }
      return actual.stat(...args);
    },
  };
});

test("characterizes authorization work after cancellation and public completion guard", async () => {
  const root = process.env.VERIFY_ROOT!;
  const entry = join(root, "cancel-fixture.js");
  await writeFile(entry, "// inert fixture\n");
  const fake = await startFakeV8Inspector({
    targetUrl: pathToFileURL(entry).href,
    targetType: "page",
    runtimeProduct: "Chrome/fixture",
    additionalTargetCount: 2,
  });
  try {
    const provider = new V8InspectorProvider();
    const control = await listJavaScriptRuntimeTargets(provider, {inspector_endpoint: fake.endpoint});
    expect(control.ok).toBe(true);
    expect(seam.calls).toHaveLength(3);
    seam.calls.length = 0;
    const controller = new AbortController();
    seam.controller = controller;
    const result = await listJavaScriptRuntimeTargets(provider, {inspector_endpoint: fake.endpoint}, {signal: controller.signal});
    expect(seam.calls.map(c => c.aborted)).toEqual([false, true, true]);
    expect(result).toMatchObject({ok: false, error: {_tag: "AnalysisCancelledError", cause: "fixture cancellation during first stat"}});
    if (result.ok) throw new Error("expected typed cancellation");
    expect(result.error.partialObservation).toBeDefined();
    console.log(JSON.stringify({controlOk: control.ok, statCalls: seam.calls.map(c => ({file: basename(c.file), aborted: c.aborted})), resultTag: result.error._tag, partialEvidenceRetained: result.error.partialObservation !== undefined}));
  } finally {
    seam.controller = undefined;
    await fake.close();
    await rm(entry);
  }
});

Run with isolated fixture/configuration directories:

WORK=$(mktemp -d)
mkdir -p "$WORK/home" "$WORK/tmp"
HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/home/config" \
XDG_CACHE_HOME="$WORK/home/cache" TMPDIR="$WORK/tmp" VERIFY_ROOT="$WORK" \
node node_modules/vitest/vitest.mjs run --project process-global "$PROBE" \
  --maxWorkers=1 --no-file-parallelism

Observed once with the successful control and cancelled case in the same test: one test passed; the cancelled case made three target-file stats with aborted states [false, true, true].

Expected behavior

After observing cancellation at an awaited target-authorization boundary, stop admitting filesystem work for later targets and return typed cancellation. An already-started filesystem call may need to settle first.

The lifecycle contract requires cancellation through result consumption. The regression guidance explicitly states that a downstream guard does not repair a provider's completion contract.

Impact and scope

Cancelled file-backed target inventories continue filesystem authorization for later targets, delaying provider completion and doing unnecessary work. The test demonstrates two such calls; latency on slow filesystems and real-world frequency are NOT_ENOUGH_INFO. No data mutation occurred. Final application cancellation and retained Evidence already work.

REA version

Source SHA 3537e58c1ab29309f06eabf50bc52adba9817a0c, package version 6.7.0. Relevant provider, scope, discovery, file-resolution and application-completion source is unchanged from stable tag rea-agents-6.7.0; that released package was not executed.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64; locked Vitest 5.0.3. The run used a package-supported Node version, rather than the repository's Node 24.18.0 pin.

Target and artifact context

Synthetic loopback Inspector discovery with three page targets referencing one source-owned inert regular JavaScript file. No target application, external service, or paid API was used.

Interface

Other

Shared application/provider source boundary; the MCP registration calls the same workflow.

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

{"controlOk":true,"statCalls":[{"file":"cancel-fixture.js","aborted":false},{"file":"cancel-fixture.js","aborted":true},{"file":"cancel-fixture.js","aborted":true}],"resultTag":"AnalysisCancelledError","partialEvidenceRetained":true}

Typed outcome or recovery expected

AnalysisCancelledError("list_javascript_runtime_targets") at the provider boundary without admitting remaining target-file authorization after abort. Preserve completed facts as supported by the existing Evidence contract.

Analysis or suspected cause

Source-derived: the signal reaches Inspector discovery, while authorizeRuntimeTargetLocation accepts no signal and listTargets checks neither between targets nor before its successful provider result. The shared application guard correctly classifies cancellation later, after all target authorizations.

Additional context

Related closed issue #4261 covers retaining completed Evidence on late cancellation. That completion behavior works in this test; stopping additional target authorization is a distinct remaining producer behavior. ASAR reads, real-provider timing and released-package behavior were not exercised.

Originally created by @coygeek on GitHub (Oct 18, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5084 ## 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 Process capture or runtime observation ## Observed behavior A source-boundary test of `listJavaScriptRuntimeTargets` with the production `V8InspectorProvider` observed two additional file `stat` calls after the caller's signal was already aborted during the first target's `stat`. The application eventually returns `AnalysisCancelledError` with partial Evidence. The defect is continuing per-target filesystem work before that return. Discovery uses a synthetic loopback Inspector; the abort is injected at a mocked filesystem boundary that forwards all reads to the real filesystem. No real Node/Electron target or MCP client was run. ## Reproduction From the source checkout at the revision below, with locked dependencies available, place the following source test under `tests/process-global/` and assign its relative test-file path to `PROBE`. It uses the repository's existing fake Inspector fixture, one inert file and three discovery targets. ```ts import { basename, join } from "node:path"; import { pathToFileURL } from "node:url"; import { writeFile, rm } from "node:fs/promises"; import { expect, test, vi } from "vitest"; import { V8InspectorProvider } from "../../src/inspector/V8InspectorProvider.js"; import { listJavaScriptRuntimeTargets } from "../../src/application/javascript/JavaScriptRuntimeObservationService.js"; import { startFakeV8Inspector } from "../fixtures/inspector/fakeV8Inspector.js"; const seam = vi.hoisted(() => ({ controller: undefined as AbortController | undefined, calls: [] as { file: string; aborted: boolean }[], })); vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal<typeof import("node:fs/promises")>(); return { ...actual, stat: async (...args: Parameters<typeof actual.stat>) => { if (String(args[0]).includes("cancel-fixture")) { seam.calls.push({file: String(args[0]), aborted: seam.controller?.signal.aborted ?? false}); if (seam.calls.length === 1) seam.controller?.abort("fixture cancellation during first stat"); } return actual.stat(...args); }, }; }); test("characterizes authorization work after cancellation and public completion guard", async () => { const root = process.env.VERIFY_ROOT!; const entry = join(root, "cancel-fixture.js"); await writeFile(entry, "// inert fixture\n"); const fake = await startFakeV8Inspector({ targetUrl: pathToFileURL(entry).href, targetType: "page", runtimeProduct: "Chrome/fixture", additionalTargetCount: 2, }); try { const provider = new V8InspectorProvider(); const control = await listJavaScriptRuntimeTargets(provider, {inspector_endpoint: fake.endpoint}); expect(control.ok).toBe(true); expect(seam.calls).toHaveLength(3); seam.calls.length = 0; const controller = new AbortController(); seam.controller = controller; const result = await listJavaScriptRuntimeTargets(provider, {inspector_endpoint: fake.endpoint}, {signal: controller.signal}); expect(seam.calls.map(c => c.aborted)).toEqual([false, true, true]); expect(result).toMatchObject({ok: false, error: {_tag: "AnalysisCancelledError", cause: "fixture cancellation during first stat"}}); if (result.ok) throw new Error("expected typed cancellation"); expect(result.error.partialObservation).toBeDefined(); console.log(JSON.stringify({controlOk: control.ok, statCalls: seam.calls.map(c => ({file: basename(c.file), aborted: c.aborted})), resultTag: result.error._tag, partialEvidenceRetained: result.error.partialObservation !== undefined})); } finally { seam.controller = undefined; await fake.close(); await rm(entry); } }); ``` Run with isolated fixture/configuration directories: ```sh WORK=$(mktemp -d) mkdir -p "$WORK/home" "$WORK/tmp" HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/home/config" \ XDG_CACHE_HOME="$WORK/home/cache" TMPDIR="$WORK/tmp" VERIFY_ROOT="$WORK" \ node node_modules/vitest/vitest.mjs run --project process-global "$PROBE" \ --maxWorkers=1 --no-file-parallelism ``` Observed once with the successful control and cancelled case in the same test: one test passed; the cancelled case made three target-file stats with aborted states `[false, true, true]`. ## Expected behavior After observing cancellation at an awaited target-authorization boundary, stop admitting filesystem work for later targets and return typed cancellation. An already-started filesystem call may need to settle first. [The lifecycle contract](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/docs/tool-design.md#L101-L109) requires cancellation through result consumption. [The regression guidance](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/docs/testing.md#L258-L264) explicitly states that a downstream guard does not repair a provider's completion contract. ## Impact and scope Cancelled file-backed target inventories continue filesystem authorization for later targets, delaying provider completion and doing unnecessary work. The test demonstrates two such calls; latency on slow filesystems and real-world frequency are NOT_ENOUGH_INFO. No data mutation occurred. Final application cancellation and retained Evidence already work. ## REA version Source SHA `3537e58c1ab29309f06eabf50bc52adba9817a0c`, package version 6.7.0. Relevant provider, scope, discovery, file-resolution and application-completion source is unchanged from stable tag `rea-agents-6.7.0`; that released package was not executed. ## Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64; locked Vitest 5.0.3. The run used a package-supported Node version, rather than the repository's Node 24.18.0 pin. ## Target and artifact context Synthetic loopback Inspector discovery with three page targets referencing one source-owned inert regular JavaScript file. No target application, external service, or paid API was used. ## Interface Other Shared application/provider source boundary; the MCP registration calls the same workflow. ## Earliest failure stage Cleanup, cancellation, or lifecycle ## Relevant output or evidence ```json {"controlOk":true,"statCalls":[{"file":"cancel-fixture.js","aborted":false},{"file":"cancel-fixture.js","aborted":true},{"file":"cancel-fixture.js","aborted":true}],"resultTag":"AnalysisCancelledError","partialEvidenceRetained":true} ``` - [Provider target loop](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/inspector/V8InspectorProvider.ts#L91-L139): discovery receives the signal; the authorization loop does not check or forward it. - [Target-location resolution](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/inspector/JavaScriptRuntimeScope.ts#L93-L119) reaches [filesystem resolution](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/browser/ElectronFileScope.ts#L40-L47). - [Application completion](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/javascript/JavaScriptRuntimeObservationService.ts#L122-L137) calls [the late-cancellation guard](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/application/ProviderOperationExecution.ts#L67-L81). This preserves typed cancellation after the unnecessary work completes. - [MCP registration](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/server/registerJavaScriptRuntimeObservationTools.ts#L44-L54) forwards the request signal to that workflow. MCP client cancellation delivery was not exercised. ## Typed outcome or recovery expected `AnalysisCancelledError("list_javascript_runtime_targets")` at the provider boundary without admitting remaining target-file authorization after abort. Preserve completed facts as supported by the existing Evidence contract. ## Analysis or suspected cause Source-derived: the signal reaches Inspector discovery, while `authorizeRuntimeTargetLocation` accepts no signal and `listTargets` checks neither between targets nor before its successful provider result. The shared application guard correctly classifies cancellation later, after all target authorizations. ## Additional context Related closed issue [#4261](https://github.com/morluto/rea/issues/4261) covers retaining completed Evidence on late cancellation. That completion behavior works in this test; stopping additional target authorization is a distinct remaining producer behavior. ASAR reads, real-provider timing and released-package behavior were not exercised.
Author
Owner

@morluto commented on GitHub (Oct 19, 2026):

Fixed on main in 87ab054ea3.

Cancellation reaches the file-authorization producer and is checked before/after awaited filesystem work, between targets, and before completion. The same rule now covers Electron frame/resource/DOM/worker/script consumers. A running filesystem call may settle, but subsequent work is not admitted. Completed target entries remain in typed prefix diagnostics with examined/discovered counts and an explicitly unknown suffix; the application guard is no longer the cancellation owner.

Production-provider tests against loopback discovery and real inert files abort during stat and prove no later realpath/target work. A separate case completes one Inspector target before aborting on the second, retaining 1/3 examined coverage. Post-authorization accounting and shutdown during held discovery are also covered. The related ASAR descriptor issue is tracked and fixed as #5184.

Validation on pinned Node 24.18.0: 696 passed, 5 skipped across 67 selected files; full npm run check, npm run docs:check, and test discovery passed (1,632 files discovered exactly once). Controlled filesystem/final-close seams and loopback HTTP/WebSocket peers establish the described boundaries; no real Chrome/Electron target, natural OS close failure, Windows, or Bun qualification is claimed.

<!-- gh-comment-id:6260832687 --> @morluto commented on GitHub (Oct 19, 2026): Fixed on main in 87ab054ea384b3fcbae2998b1f94685edcd5f624. Cancellation reaches the file-authorization producer and is checked before/after awaited filesystem work, between targets, and before completion. The same rule now covers Electron frame/resource/DOM/worker/script consumers. A running filesystem call may settle, but subsequent work is not admitted. Completed target entries remain in typed prefix diagnostics with examined/discovered counts and an explicitly unknown suffix; the application guard is no longer the cancellation owner. Production-provider tests against loopback discovery and real inert files abort during stat and prove no later realpath/target work. A separate case completes one Inspector target before aborting on the second, retaining 1/3 examined coverage. Post-authorization accounting and shutdown during held discovery are also covered. The related ASAR descriptor issue is tracked and fixed as #5184. Validation on pinned Node 24.18.0: 696 passed, 5 skipped across 67 selected files; full `npm run check`, `npm run docs:check`, and test discovery passed (1,632 files discovered exactly once). Controlled filesystem/final-close seams and loopback HTTP/WebSocket peers establish the described boundaries; no real Chrome/Electron target, natural OS close failure, Windows, or Bun qualification is claimed.
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#3122
No description provided.