[GH-ISSUE #6132] [Bug] Pre-aborted WASM inspection drops the exact caller cancellation reason #3760

Closed
opened 2026-10-31 00:43:46 +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/6132

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

WasmArtifactService.inspect returns AnalysisCancelledError with cause === undefined when the caller supplies an already-aborted signal. The exact caller reason is lost from the typed error for null, string and object values, although it remains available on the original signal. No provider work starts.

This was executed at the application-service boundary using the repository's existing WASM fixtures and a recording simulated provider. The non-aborted control returned valid observed Evidence. No real WABT executable, CLI signal handling or MCP cancellation transport was executed.

Reproduction

  1. Check out the source revision below, use the repository-pinned Node.js and npm versions, then run npm ci and npm run build:cached.
  2. Save the following temporary test as src/application/wasm/WasmPreadmissionReason.test.ts.
  3. Run node_modules/.bin/vitest run src/application/wasm/WasmPreadmissionReason.test.ts --maxWorkers=1 --no-file-parallelism.
  4. Observe three failures at the final cause-identity assertion and one passing non-aborted control. All preceding assertions passed, including the exact reason on the original signal, cancellation classification and zero provider calls. Remove the temporary test afterwards.
import { expect, it } from "vitest";
import { WasmArtifactService } from "./WasmArtifactService.js";
import { wasmArtifactFixture, wasmArtifactExecution } from "../../../tests/fixtures/wasm/artifact.js";
import { ok } from "../../domain/result.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";

it.each([
  ["null", null],
  ["string", "stop WASM"],
  ["object", { requested: "stop WASM" }],
])("preserves a pre-aborted %s reason", async (kind, reason) => {
  const value = wasmArtifactFixture();
  let calls = 0;
  const service = new WasmArtifactService({
    close: async () => {},
    inspect: async () => { calls++; return ok(wasmArtifactExecution(value)); },
  });
  const controller = new AbortController();
  controller.abort(reason);
  try {
    const result = await service.inspect({ path: value.artifact.path }, { signal: controller.signal });
    expect(controller.signal.reason).toBe(reason);
    expect(result.ok).toBe(false);
    if (result.ok) throw new Error("Expected cancellation");
    const projection = projectAnalysisError(result.error);
    console.log(JSON.stringify({ kind, tag: result.error._tag, operation: result.error.operation,
      code: projection.code, causeType: typeof result.error.cause,
      exactReason: Object.is(result.error.cause, reason), calls,
      cleanupIncomplete: result.error.cleanupIncomplete }));
    expect(result.error._tag).toBe("AnalysisCancelledError");
    expect(result.error.operation).toBe("inspect_wasm_artifact");
    expect(projection.code).toBe("cancelled");
    expect(result.error.cleanupIncomplete).toBe(false);
    expect(calls).toBe(0);
    expect(result.error.cause).toBe(reason);
  } finally { await service.close(); }
});

it("delivers observed Evidence for a non-aborted control", async () => {
  const value = wasmArtifactFixture();
  let calls = 0;
  const service = new WasmArtifactService({
    close: async () => {},
    inspect: async () => { calls++; return ok(wasmArtifactExecution(value)); },
  });
  const controller = new AbortController();
  try {
    const result = await service.inspect({ path: value.artifact.path }, { signal: controller.signal });
    expect(result.ok).toBe(true);
    if (!result.ok) throw result.error;
    expect(result.value.confidence).toBe("observed");
    expect(result.value.normalized_result).toEqual(value);
    expect(calls).toBe(1);
    console.log(JSON.stringify({ kind: "non-aborted control", ok: result.ok, confidence: result.value.confidence, calls }));
  } finally { await service.close(); }
});

Expected behavior

Return AnalysisCancelledError for inspect_wasm_artifact with its direct cause equal to the exact caller abort reason, including explicit null and object identity, before any provider work.

The lifecycle contract requires the caller's abort reason as the typed cancellation cause. The testing contract explicitly requires reason identity checks at admission.

Impact and scope

P2: bounded typed-error diagnostic loss for requests whose signal is already aborted at service entry. All three reason values tested lost their direct cause. Cancellation still prevents provider work and reports code cancelled; the control succeeds. Consumers retaining the original signal can read signal.reason, as the test confirms. No data loss, resource leak or real-client recovery failure was established. Natural occurrence frequency and other platforms were not measured. This report does not require arbitrary causes to be exposed in public JSON.

REA version

01788bac86

Runtime and platform

Node.js v24.18.0; npm 11.16.0; repository-pinned Vitest 5.0.3; macOS 27.0.1 arm64. Package metadata reports REA 6.7.0.

Target and artifact context

The repository's existing minimal WASM artifact fixture and wasmArtifactExecution supply the simulated successful provider result. The pre-aborted cases never read artifact bytes or enter the provider. No WASM code was executed.

Interface

Other

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Executed one focused file on the revision above:

reason kind  typed error             public code  cause       exact reason  provider calls  cleanupIncomplete
null         AnalysisCancelledError  cancelled    undefined   false         0               false
string       AnalysisCancelledError  cancelled    undefined   false         0               false
object       AnalysisCancelledError  cancelled    undefined   false         0               false
non-aborted control: ok=true, confidence=observed, provider calls=1
Tests: 3 failed | 1 passed (4)

Each failure was expect(result.error.cause).toBe(reason): received undefined instead of the supplied value. Dependency installation and the runtime build completed successfully before execution.

Source-derived public reachability: the supported CLI command passes its signal into this service, and the MCP registration forwards execution options into the same service. These adapters were inspected, not executed in this reproduction.

Typed outcome or recovery expected

Keep cancellation classification and admission precedence, with result.error.cause === options.signal.reason. Preserve opaque reason values without coercion or replacing them with a default error.

Analysis or suspected cause

Source-derived: the pre-admission branch constructs new AnalysisCancelledError(OPERATION) without passing the signal's reason. It returns before the provider delegation, so changing WABT command-failure translation cannot repair this checkpoint.

Additional context

Related cancellation-reason reports: #6002, #5935, #5944, #5985, #5987, #6100 and #6110 cover other producers or checkpoints.

The diff of open PR #6018 was inspected: it fixes #6002 by selecting the command-owned reason in WabtFailures.ts, but does not change the service's pre-admission constructor. Open PR #5274 refactors this service's validation and summary admission; its diff retains the same bare constructor in inspect and adds another in inspectValidated, so it does not resolve this report either. Searches covered open and closed issues and PRs, including the service filename and function name in open PRs.

NOT_ENOUGH_INFO: introducing commit, last known good release, real-client frequency, or qualification on another operating system.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6132 ### 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 `WasmArtifactService.inspect` returns `AnalysisCancelledError` with `cause === undefined` when the caller supplies an already-aborted signal. The exact caller reason is lost from the typed error for `null`, string and object values, although it remains available on the original signal. No provider work starts. This was executed at the application-service boundary using the repository's existing WASM fixtures and a recording simulated provider. The non-aborted control returned valid observed Evidence. No real WABT executable, CLI signal handling or MCP cancellation transport was executed. ### Reproduction 1. Check out the source revision below, use the repository-pinned Node.js and npm versions, then run `npm ci` and `npm run build:cached`. 2. Save the following temporary test as `src/application/wasm/WasmPreadmissionReason.test.ts`. 3. Run `node_modules/.bin/vitest run src/application/wasm/WasmPreadmissionReason.test.ts --maxWorkers=1 --no-file-parallelism`. 4. Observe three failures at the final cause-identity assertion and one passing non-aborted control. All preceding assertions passed, including the exact reason on the original signal, cancellation classification and zero provider calls. Remove the temporary test afterwards. ```ts import { expect, it } from "vitest"; import { WasmArtifactService } from "./WasmArtifactService.js"; import { wasmArtifactFixture, wasmArtifactExecution } from "../../../tests/fixtures/wasm/artifact.js"; import { ok } from "../../domain/result.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; it.each([ ["null", null], ["string", "stop WASM"], ["object", { requested: "stop WASM" }], ])("preserves a pre-aborted %s reason", async (kind, reason) => { const value = wasmArtifactFixture(); let calls = 0; const service = new WasmArtifactService({ close: async () => {}, inspect: async () => { calls++; return ok(wasmArtifactExecution(value)); }, }); const controller = new AbortController(); controller.abort(reason); try { const result = await service.inspect({ path: value.artifact.path }, { signal: controller.signal }); expect(controller.signal.reason).toBe(reason); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected cancellation"); const projection = projectAnalysisError(result.error); console.log(JSON.stringify({ kind, tag: result.error._tag, operation: result.error.operation, code: projection.code, causeType: typeof result.error.cause, exactReason: Object.is(result.error.cause, reason), calls, cleanupIncomplete: result.error.cleanupIncomplete })); expect(result.error._tag).toBe("AnalysisCancelledError"); expect(result.error.operation).toBe("inspect_wasm_artifact"); expect(projection.code).toBe("cancelled"); expect(result.error.cleanupIncomplete).toBe(false); expect(calls).toBe(0); expect(result.error.cause).toBe(reason); } finally { await service.close(); } }); it("delivers observed Evidence for a non-aborted control", async () => { const value = wasmArtifactFixture(); let calls = 0; const service = new WasmArtifactService({ close: async () => {}, inspect: async () => { calls++; return ok(wasmArtifactExecution(value)); }, }); const controller = new AbortController(); try { const result = await service.inspect({ path: value.artifact.path }, { signal: controller.signal }); expect(result.ok).toBe(true); if (!result.ok) throw result.error; expect(result.value.confidence).toBe("observed"); expect(result.value.normalized_result).toEqual(value); expect(calls).toBe(1); console.log(JSON.stringify({ kind: "non-aborted control", ok: result.ok, confidence: result.value.confidence, calls })); } finally { await service.close(); } }); ``` ### Expected behavior Return `AnalysisCancelledError` for `inspect_wasm_artifact` with its direct cause equal to the exact caller abort reason, including explicit `null` and object identity, before any provider work. The [lifecycle contract](https://github.com/morluto/rea/blob/01788bac863ac786bfbb648cad69b11e23b40a89/docs/tool-design.md#L101-L110) requires the caller's abort reason as the typed cancellation cause. The [testing contract](https://github.com/morluto/rea/blob/01788bac863ac786bfbb648cad69b11e23b40a89/docs/testing.md#L309-L314) explicitly requires reason identity checks at admission. ### Impact and scope P2: bounded typed-error diagnostic loss for requests whose signal is already aborted at service entry. All three reason values tested lost their direct cause. Cancellation still prevents provider work and reports code `cancelled`; the control succeeds. Consumers retaining the original signal can read `signal.reason`, as the test confirms. No data loss, resource leak or real-client recovery failure was established. Natural occurrence frequency and other platforms were not measured. This report does not require arbitrary causes to be exposed in public JSON. ### REA version 01788bac863ac786bfbb648cad69b11e23b40a89 ### Runtime and platform Node.js v24.18.0; npm 11.16.0; repository-pinned Vitest 5.0.3; macOS 27.0.1 arm64. Package metadata reports REA 6.7.0. ### Target and artifact context The repository's existing minimal WASM artifact fixture and `wasmArtifactExecution` supply the simulated successful provider result. The pre-aborted cases never read artifact bytes or enter the provider. No WASM code was executed. ### Interface Other ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Executed one focused file on the revision above: ```text reason kind typed error public code cause exact reason provider calls cleanupIncomplete null AnalysisCancelledError cancelled undefined false 0 false string AnalysisCancelledError cancelled undefined false 0 false object AnalysisCancelledError cancelled undefined false 0 false non-aborted control: ok=true, confidence=observed, provider calls=1 Tests: 3 failed | 1 passed (4) ``` Each failure was `expect(result.error.cause).toBe(reason)`: received `undefined` instead of the supplied value. Dependency installation and the runtime build completed successfully before execution. Source-derived public reachability: the supported [CLI command](https://github.com/morluto/rea/blob/01788bac863ac786bfbb648cad69b11e23b40a89/src/cli/wasmCommands.ts#L61-L84) passes its signal into this service, and the [MCP registration](https://github.com/morluto/rea/blob/01788bac863ac786bfbb648cad69b11e23b40a89/src/server/registerWasmTools.ts#L60-L81) forwards execution options into the same service. These adapters were inspected, not executed in this reproduction. ### Typed outcome or recovery expected Keep cancellation classification and admission precedence, with `result.error.cause === options.signal.reason`. Preserve opaque reason values without coercion or replacing them with a default error. ### Analysis or suspected cause Source-derived: the [pre-admission branch](https://github.com/morluto/rea/blob/01788bac863ac786bfbb648cad69b11e23b40a89/src/application/wasm/WasmArtifactService.ts#L33-L40) constructs `new AnalysisCancelledError(OPERATION)` without passing the signal's reason. It returns before the [provider delegation](https://github.com/morluto/rea/blob/01788bac863ac786bfbb648cad69b11e23b40a89/src/application/wasm/WasmArtifactService.ts#L68-L71), so changing WABT command-failure translation cannot repair this checkpoint. ### Additional context Related cancellation-reason reports: [#6002](https://github.com/morluto/rea/issues/6002), [#5935](https://github.com/morluto/rea/issues/5935), [#5944](https://github.com/morluto/rea/issues/5944), [#5985](https://github.com/morluto/rea/issues/5985), [#5987](https://github.com/morluto/rea/issues/5987), [#6100](https://github.com/morluto/rea/issues/6100) and [#6110](https://github.com/morluto/rea/issues/6110) cover other producers or checkpoints. The diff of open [PR #6018](https://github.com/morluto/rea/pull/6018) was inspected: it fixes #6002 by selecting the command-owned reason in `WabtFailures.ts`, but does not change the service's pre-admission constructor. Open [PR #5274](https://github.com/morluto/rea/pull/5274) refactors this service's validation and summary admission; its diff retains the same bare constructor in `inspect` and adds another in `inspectValidated`, so it does not resolve this report either. Searches covered open and closed issues and PRs, including the service filename and function name in open PRs. NOT_ENOUGH_INFO: introducing commit, last known good release, real-client frequency, or qualification on another operating system.
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#3760
No description provided.