[GH-ISSUE #5987] [Bug] Static JavaScript application checkpoints lose the caller cancellation reason #3655

Closed
opened 2026-10-31 00:42:46 +00:00 by bsakaguchi · 1 comment
Owner

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

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

Static JavaScript application analysis loses the exact caller cancellation reason at application-owned checkpoints. Four executed service calls reproduced this: pre-aborted admission, immediately before result validation, after source analysis during graph construction, and terminal publication.

Admission, validation and terminal publication returned ArtifactOperationError wrapping ArtifactReaderFailure whose cancellation.reason was undefined. The completed-analysis graph checkpoint returned ArtifactOperationError with an undefined direct cause. Each signal still retained the original object reason. All outcomes remained cancelled; the three late cases retained partial observations and reported cleanupIncomplete: false. An ordinary non-aborted control succeeded.

This is static analyze_javascript_application behavior. Source recovery and the artifact reader's own cancelled-read checkpoint were not the cancellation producers exercised here.

Reproduction

In a prepared source checkout, save the following reduced subset of the executed focused test as tests/boundary/providers/kuna/javascriptAnalysisReason.repro.test.ts, then run:

node node_modules/vitest/vitest.mjs run tests/boundary/providers/kuna/javascriptAnalysisReason.repro.test.ts --maxWorkers=1

The executed run had four failing JavaScript reason-identity assertions and one passing non-aborted JavaScript control. It used the production static-analysis service, the repository’s resource-scoped adapter, real files, and the actual analysis worker. No filesystem reader, worker, or returned error was mocked. The progress callback synchronizes each late cancellation with the named application phase; the input JavaScript is parsed as data, never executed.

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js";
import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js";

it.each(["pre", "validate_javascript_application_result", "build_javascript_application_graph", "terminal", "control"] as const)(
  "JavaScript static analysis preserves reason at %s", async phase => {
    const root = await createTestTempDirectory("rea-js-reason-");
    await writeFile(join(root, "main.js"), "module.exports = 42;\n");
    const controller = new AbortController();
    const reason = Object.freeze({ requested: "analysis-stop", phase });
    let intercepted = phase === "pre";
    if (intercepted) controller.abort(reason);
    const result = await analyzeJavaScriptApplication({ input_path: root }, {
      signal: controller.signal,
      progress: { async report(update) {
        if (update.phase === phase || (phase === "terminal" && update.terminal)) {
          intercepted = true;
          controller.abort(reason);
        }
      } },
    });
    if (phase === "control") { expect(result.ok).toBe(true); console.log(JSON.stringify({ producer: "javascript-analysis", phase, controlOk: result.ok })); return; }
    expect(intercepted).toBe(true);
    if (result.ok) throw new Error("Expected cancellation");
    const nested = result.error.cause;
    console.log(JSON.stringify({ producer: "javascript-analysis", phase, tag: result.error._tag, reason: "reason" in result.error ? result.error.reason : null,
      causeAbsent: nested === undefined, nestedReader: nested instanceof ArtifactReaderFailure,
      nestedReasonAbsent: nested instanceof ArtifactReaderFailure ? nested.cancellation?.reason === undefined : null,
      signalRetainsReason: controller.signal.reason === reason, cleanupIncomplete: result.error.cleanupIncomplete,
      hasPartialObservation: result.error.partialObservation !== undefined }));
    expect(result.error.cause).toBe(reason);
});

Expected behavior

Application checkpoints should preserve signal.reason as the typed cancellation cause, including after source analysis completes. The lifecycle contract requires retaining that exact fact through failure wrapping. The canonical artifact checkpoint and cancellation accessor show the existing representation for retaining/extracting it.

Impact and scope

P2: downstream typed-error consumers lose required cancellation provenance. Cancellation classification and partial Evidence retention still work in the measured cases. The original signal retains its reason and provides a practical diagnostic workaround for callers that keep it.

Each listed phase reproduced once. Public wire diagnostics, natural race frequency, ASAR inputs, and other platforms were not measured. No corrupted artifact, wrong successful analysis, failed cleanup, or missing public JSON cause field is claimed.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0; Vitest 5.0.3. Production static JavaScript analysis worker; no Wakaru or native decompiler.

Target and artifact context

A synthetic directory with main.js containing module.exports = 42;; real inventory, file reading and static parsing.

Interface

Other

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Observed diagnostics:

{"phase":"pre","tag":"ArtifactOperationError","reason":"cancelled","causeAbsent":false,"nestedReader":true,"nestedReasonAbsent":true,"signalRetainsReason":true,"cleanupIncomplete":false,"hasPartialObservation":false}
{"phase":"validate_javascript_application_result","tag":"ArtifactOperationError","reason":"cancelled","causeAbsent":false,"nestedReader":true,"nestedReasonAbsent":true,"signalRetainsReason":true,"cleanupIncomplete":false,"hasPartialObservation":true}
{"phase":"build_javascript_application_graph","tag":"ArtifactOperationError","reason":"cancelled","causeAbsent":true,"nestedReader":false,"nestedReasonAbsent":null,"signalRetainsReason":true,"cleanupIncomplete":false,"hasPartialObservation":true}
{"phase":"terminal","tag":"ArtifactOperationError","reason":"cancelled","causeAbsent":false,"nestedReader":true,"nestedReasonAbsent":true,"signalRetainsReason":true,"cleanupIncomplete":false,"hasPartialObservation":true}
{"phase":"control","controlOk":true}

Reason-identity assertions received either the reasonless ArtifactReaderFailure wrapper or undefined instead of the caller object. Source-derived public reachability: MCP registration forwards signal/progress to this service; the CLI adapter invokes the same service. No public transport cancellation request was executed.

Typed outcome or recovery expected

Retain the exact observed abort reason, existing cancelled classification, partial observations and actual cleanup state. Consume the producer-owned cancellation reason when converting a reader failure to the operation error, so the caller can inspect it without guessing through a wrapper chain. Arbitrary causes need not be serialized into public JSON.

Analysis or suspected cause

The application checkpoint helper supplies no cause, and the reader failure constructor consequently records an undefined cancellation reason. The service conversion wraps the failure rather than extracting its cancellation fact. Separately, the completed-analysis checkpoint constructs a cancelled operation error without options. These source paths explain the four observations.

Additional bare constructors exist in source adoption, its catch path, and the reconstruction pre-check. Those sites were inspected only, not separately synchronized by this test. Worker-produced cancellation can already preserve the reason, so reason loss depends on the producer checkpoint.

Additional context

Related #5935 covers the distinct helper in JavaScriptArtifactFiles, while #5964 covers JavaScriptRecoveryService and Wakaru recovery checkpoints. This report excludes those already reported producers. #5944 concerns runDerivedOperation, #5959 Rizin, and #5960 call-path traversal; their fixes do not change the static application checkpoints above. #5985 covers the separate ManagedStaticClient entry checkpoint. PR #1934 retains completed JavaScript facts, and #1968 / PR #1975 concern attribution of unselected files; neither addresses exact cancellation reasons.

Fetched upstream main 25e1a24a3ed3671a0148757555f18d16c29d936a and compared the relevant source. Worker creation/resource ownership changed in JavaScriptArtifactReconstruction, but these cancellation constructors and service conversion remain unchanged. That newer revision was not executed. No fix or regression range was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5987 ### 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 Static JavaScript application analysis loses the exact caller cancellation reason at application-owned checkpoints. Four executed service calls reproduced this: pre-aborted admission, immediately before result validation, after source analysis during graph construction, and terminal publication. Admission, validation and terminal publication returned `ArtifactOperationError` wrapping `ArtifactReaderFailure` whose `cancellation.reason` was undefined. The completed-analysis graph checkpoint returned `ArtifactOperationError` with an undefined direct cause. Each signal still retained the original object reason. All outcomes remained `cancelled`; the three late cases retained partial observations and reported `cleanupIncomplete: false`. An ordinary non-aborted control succeeded. This is static `analyze_javascript_application` behavior. Source recovery and the artifact reader's own cancelled-read checkpoint were not the cancellation producers exercised here. ### Reproduction In a prepared source checkout, save the following reduced subset of the executed focused test as `tests/boundary/providers/kuna/javascriptAnalysisReason.repro.test.ts`, then run: ```sh node node_modules/vitest/vitest.mjs run tests/boundary/providers/kuna/javascriptAnalysisReason.repro.test.ts --maxWorkers=1 ``` The executed run had four failing JavaScript reason-identity assertions and one passing non-aborted JavaScript control. It used the production static-analysis service, the repository’s resource-scoped adapter, real files, and the actual analysis worker. No filesystem reader, worker, or returned error was mocked. The progress callback synchronizes each late cancellation with the named application phase; the input JavaScript is parsed as data, never executed. ```ts import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import { analyzeJavaScriptApplication } from "../../../support/javascriptApplicationScope.js"; import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js"; it.each(["pre", "validate_javascript_application_result", "build_javascript_application_graph", "terminal", "control"] as const)( "JavaScript static analysis preserves reason at %s", async phase => { const root = await createTestTempDirectory("rea-js-reason-"); await writeFile(join(root, "main.js"), "module.exports = 42;\n"); const controller = new AbortController(); const reason = Object.freeze({ requested: "analysis-stop", phase }); let intercepted = phase === "pre"; if (intercepted) controller.abort(reason); const result = await analyzeJavaScriptApplication({ input_path: root }, { signal: controller.signal, progress: { async report(update) { if (update.phase === phase || (phase === "terminal" && update.terminal)) { intercepted = true; controller.abort(reason); } } }, }); if (phase === "control") { expect(result.ok).toBe(true); console.log(JSON.stringify({ producer: "javascript-analysis", phase, controlOk: result.ok })); return; } expect(intercepted).toBe(true); if (result.ok) throw new Error("Expected cancellation"); const nested = result.error.cause; console.log(JSON.stringify({ producer: "javascript-analysis", phase, tag: result.error._tag, reason: "reason" in result.error ? result.error.reason : null, causeAbsent: nested === undefined, nestedReader: nested instanceof ArtifactReaderFailure, nestedReasonAbsent: nested instanceof ArtifactReaderFailure ? nested.cancellation?.reason === undefined : null, signalRetainsReason: controller.signal.reason === reason, cleanupIncomplete: result.error.cleanupIncomplete, hasPartialObservation: result.error.partialObservation !== undefined })); expect(result.error.cause).toBe(reason); }); ``` ### Expected behavior Application checkpoints should preserve `signal.reason` as the typed cancellation cause, including after source analysis completes. The [lifecycle contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/tool-design.md#L101-L110) requires retaining that exact fact through failure wrapping. The [canonical artifact checkpoint and cancellation accessor](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/ArtifactFileOperation.ts#L155-L172) show the existing representation for retaining/extracting it. ### Impact and scope P2: downstream typed-error consumers lose required cancellation provenance. Cancellation classification and partial Evidence retention still work in the measured cases. The original signal retains its reason and provides a practical diagnostic workaround for callers that keep it. Each listed phase reproduced once. Public wire diagnostics, natural race frequency, ASAR inputs, and other platforms were not measured. No corrupted artifact, wrong successful analysis, failed cleanup, or missing public JSON cause field is claimed. ### REA version rea-agents 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0; Vitest 5.0.3. Production static JavaScript analysis worker; no Wakaru or native decompiler. ### Target and artifact context A synthetic directory with main.js containing `module.exports = 42;`; real inventory, file reading and static parsing. ### Interface Other ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Observed diagnostics: ```json {"phase":"pre","tag":"ArtifactOperationError","reason":"cancelled","causeAbsent":false,"nestedReader":true,"nestedReasonAbsent":true,"signalRetainsReason":true,"cleanupIncomplete":false,"hasPartialObservation":false} {"phase":"validate_javascript_application_result","tag":"ArtifactOperationError","reason":"cancelled","causeAbsent":false,"nestedReader":true,"nestedReasonAbsent":true,"signalRetainsReason":true,"cleanupIncomplete":false,"hasPartialObservation":true} {"phase":"build_javascript_application_graph","tag":"ArtifactOperationError","reason":"cancelled","causeAbsent":true,"nestedReader":false,"nestedReasonAbsent":null,"signalRetainsReason":true,"cleanupIncomplete":false,"hasPartialObservation":true} {"phase":"terminal","tag":"ArtifactOperationError","reason":"cancelled","causeAbsent":false,"nestedReader":true,"nestedReasonAbsent":true,"signalRetainsReason":true,"cleanupIncomplete":false,"hasPartialObservation":true} {"phase":"control","controlOk":true} ``` Reason-identity assertions received either the reasonless `ArtifactReaderFailure` wrapper or `undefined` instead of the caller object. Source-derived public reachability: [MCP registration](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/server/registerElectronTools.ts#L100-L119) forwards signal/progress to this service; the [CLI adapter](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/javascriptApplicationAnalysis.ts#L11-L27) invokes the same service. No public transport cancellation request was executed. ### Typed outcome or recovery expected Retain the exact observed abort reason, existing cancelled classification, partial observations and actual cleanup state. Consume the producer-owned cancellation reason when converting a reader failure to the operation error, so the caller can inspect it without guessing through a wrapper chain. Arbitrary causes need not be serialized into public JSON. ### Analysis or suspected cause The [application checkpoint helper](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/javascript/JavaScriptAnalysisControl.ts#L5-L20) supplies no cause, and the [reader failure constructor](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/ArtifactReader.ts#L198-L214) consequently records an undefined cancellation reason. The [service conversion](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/javascript/JavaScriptApplicationService.ts#L177-L192) wraps the failure rather than extracting its cancellation fact. Separately, the [completed-analysis checkpoint](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/javascript/JavaScriptArtifactReconstruction.ts#L212-L225) constructs a cancelled operation error without options. These source paths explain the four observations. Additional bare constructors exist in [source adoption](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/javascript/JavaScriptArtifactAnalysis.ts#L150-L162), its [catch path](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/javascript/JavaScriptArtifactAnalysis.ts#L245-L255), and the [reconstruction pre-check](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/application/javascript/JavaScriptArtifactReconstruction.ts#L423-L429). Those sites were inspected only, not separately synchronized by this test. Worker-produced cancellation can already preserve the reason, so reason loss depends on the producer checkpoint. ### Additional context Related [#5935](https://github.com/morluto/rea/issues/5935) covers the distinct helper in JavaScriptArtifactFiles, while [#5964](https://github.com/morluto/rea/issues/5964) covers JavaScriptRecoveryService and Wakaru recovery checkpoints. This report excludes those already reported producers. [#5944](https://github.com/morluto/rea/issues/5944) concerns runDerivedOperation, [#5959](https://github.com/morluto/rea/issues/5959) Rizin, and [#5960](https://github.com/morluto/rea/issues/5960) call-path traversal; their fixes do not change the static application checkpoints above. [#5985](https://github.com/morluto/rea/issues/5985) covers the separate ManagedStaticClient entry checkpoint. PR [#1934](https://github.com/morluto/rea/pull/1934) retains completed JavaScript facts, and [#1968](https://github.com/morluto/rea/issues/1968) / PR [#1975](https://github.com/morluto/rea/pull/1975) concern attribution of unselected files; neither addresses exact cancellation reasons. Fetched upstream main `25e1a24a3ed3671a0148757555f18d16c29d936a` and compared the relevant source. Worker creation/resource ownership changed in JavaScriptArtifactReconstruction, but these cancellation constructors and service conversion remain unchanged. That newer revision was not executed. No fix or regression range was established.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked on main at d87e73f2db50be72d33a4b3c7199af35518b441c: Application and reconstruction checkpoints now preserve signal.reason, and shared artifact translation exposes that value as direct operation cause. Regressions assert exact identity during graph construction, validation and terminal publication while retaining completed Evidence. JavaScriptAnalysisControl.ts JavaScriptArtifactReconstruction.ts JavaScriptArtifactReconstruction.ts ArtifactOperationFailure.ts javascriptApplicationCancellation.test.ts javascriptApplicationCancellation.test.ts

I ran the cited regression file on this revision; all passed as part of the focused 28-file, 603-test run. Provider-facing checks here use controlled fixtures; this does not claim a run with the real analysis engine. Closing as completed on main.

<!-- gh-comment-id:6380639394 --> @coygeek commented on GitHub (Oct 24, 2026): Checked on main at `d87e73f2db50be72d33a4b3c7199af35518b441c`: Application and reconstruction checkpoints now preserve signal.reason, and shared artifact translation exposes that value as direct operation cause. Regressions assert exact identity during graph construction, validation and terminal publication while retaining completed Evidence. [JavaScriptAnalysisControl.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/javascript/JavaScriptAnalysisControl.ts#L5) [JavaScriptArtifactReconstruction.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/javascript/JavaScriptArtifactReconstruction.ts#L241) [JavaScriptArtifactReconstruction.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/javascript/JavaScriptArtifactReconstruction.ts#L519) [ArtifactOperationFailure.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/artifacts/ArtifactOperationFailure.ts#L21) [javascriptApplicationCancellation.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/javascript/javascriptApplicationCancellation.test.ts#L420) [javascriptApplicationCancellation.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/javascript/javascriptApplicationCancellation.test.ts#L481) I ran the cited regression file on this revision; all passed as part of the focused 28-file, 603-test run. Provider-facing checks here use controlled fixtures; this does not claim a run with the real analysis engine. Closing as completed on main.
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#3655
No description provided.