[GH-ISSUE #5998] [Bug] inspect_asset_catalog loses the caller reason when translating process cancellation #3665

Closed
opened 2026-10-31 00:42:51 +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/5998

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

An executed artifact-provider test cancelled inspect_asset_catalog at its subprocess boundary with an opaque object reason. The subprocess rejection was ExecFileOutputCancelledError carrying that exact object. The returned ArtifactOperationError retained reason cancelled but had cause === undefined.

The ordinary [] utility-output control succeeded. Real directory enumeration and catalog digest reads ran against an inert fixture; utility output/cancellation was simulated. This does not establish real assetutil behavior or a public CLI/MCP transport result.

Reproduction

On macOS, from the reported revision with locked dependencies installed, save these executed cases as src/ida/assetCancellationRepro.test.ts and run:

node node_modules/vitest/vitest.mjs run src/ida/assetCancellationRepro.test.ts --maxWorkers=1

These cases were executed together at the production artifact-provider boundary. The cancellation cause assertion failed; the success control and both close assertions passed.

import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import * as processOutput from "../process/ExecFileOutput.js";
import { ArtifactProvider } from "../artifacts/ArtifactProvider.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";

it.each([false, true])("asset catalog cancellation=%s", async (cancel) => {
  const bundle = await mkdtemp(join(tmpdir(), "rea-catalog-test-"));
  const controller = new AbortController();
  const reason = { requested: "stop", marker: 8 };
  await mkdir(join(bundle, "Contents", "Resources"), { recursive: true });
  await writeFile(join(bundle, "Contents", "Resources", "Assets.car"), "inert catalog fixture");
  let calls = 0;
  const spy = vi.spyOn(processOutput, "execFileOutput").mockImplementation(async () => {
    calls++;
    if (cancel) { controller.abort(reason); throw new processOutput.ExecFileOutputCancelledError(reason); }
    return { stdout: "[]", stderr: "", stdoutBytes: 2, stderrBytes: 0 };
  });
  const client = new ArtifactProvider({}, "darwin").createBundleResourceClient({ callerPath: bundle, canonicalPath: bundle, infoPlistPath: join(bundle, "Contents", "Info.plist"), layout: "contents", executableSha256: null });
  try {
    const result = await client.execute("inspect_asset_catalog", {}, { signal: controller.signal });
    console.log(JSON.stringify({ cancel, calls, ok: result.ok, ...(!result.ok ? { tag: result.error._tag, sameReason: result.error.cause === reason, projection: projectAnalysisError(result.error) } : {}) }));
    expect(calls).toBe(1);
    if (!cancel) { expect(result.ok).toBe(true); return; }
    if (result.ok) throw new Error("expected cancellation");
    expect(result.error.cause).toBe(reason);
  } finally { expect((await client.close()).ok).toBe(true); spy.mockRestore(); await rm(bundle, { recursive: true, force: true }); }
});

Expected behavior

Preserve the caller’s exact reason as the typed cancellation cause through artifact failure wrapping. The lifecycle contract requires this. ExecFileOutputCancelledError already retains the observed reason, and the artifact translator already consumes the reader cancellation record.

Impact and scope

P2: cancellation remains correctly classified, but the typed result loses the explicit caller-reason provenance required by the lifecycle contract. Consumers inspecting the typed cause cannot recover the opaque reason from this result. The exercised utility-cancellation branch lost the reason in the initial application test and in both production-provider runs; the non-aborted provider control succeeded. Real assetutil cancellation frequency and client dependence on this internal cause are unknown. No public cause field, target mutation, or data-loss claim is made.

REA version

rea-agents 6.7.0, source revision 52415975942a4b9c53a817915b313ccc404f280e. Fetched origin/main at c538a843cb49f7156f86beb3b21501e03255af9a leaves the producing code and cited lifecycle contract unchanged; that newer revision was source-compared, not executed.

Runtime and platform

macOS 27.0.1 (26A434), arm64; Node.js v26.12.0; locked Vitest 5.0.3. No native utility or deep-analysis engine was launched.

Target and artifact context

A synthetic macOS-layout bundle-resource directory containing one regular Contents/Resources/Assets.car with inert bytes. Successful utility output is supplied at the subprocess boundary; native catalog validity is not established.

Interface

Other

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Observed provider results:

control: utility_calls=1; ok=true
cancelled: utility_calls=1; ok=false
  _tag=ArtifactOperationError
  cause === callerReason: false
  actual cause: undefined
  projection.code=cancelled
  projection.details.operation=inspect_asset_catalog
  projection.details.reason=cancelled
  projection.details.detail="Asset catalog inspection cancelled"

AssertionError: expected undefined to be { requested: 'stop', marker: 8 }

A separate application-boundary execution also observed ArtifactReaderFailure.cancellation.reason === undefined before translation.

Typed outcome or recovery expected

Keep ArtifactReaderFailure.cancellation.reason equal to the exact reason observed by the subprocess owner and translate it to ArtifactOperationError.cause without changing its identity. Preserve cancellation classification and actual cleanup facts.

Analysis or suspected cause

Source-derived: runAssetUtil’s catch checks the signal but constructs a new reader failure without cause or cancellation. ArtifactReaderFailure consequently records an undefined cancellation reason, which the accessor passes onward. The digest loop has the same bare cancellation constructor by source inspection; that loop-abort case was not separately executed. Preserve the reason at those producing checkpoints instead of trying to infer it after wrapping.

Additional context

Related cancellation reports: #5822, #5935, #5944, #5959, #5960, #5964, #5968, #5985, #5987. They concern different producers or operation attribution. #4858 covers ArtifactAnalysisResources/sigrok/tshark and #4638 covers OwnedCommand; neither repairs this local asset-catalog constructor. #5976 covers assetutil timeout classification; #5977 covers malformed successful utility output; #5698 concerns nonzero exit diagnostics for damaged catalogs. Those require different restoration checks. PR #3285 overlaps bundle walks and cancellation of the sibling Interface Builder task; it does not preserve this utility abort reason. #5992 concerns a directory-close failure replacing an existing catalog failure; the present case has successful cleanup and loses the reason inside the utility-cancellation constructor. No software fix or regression range was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5998 ### 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 An executed artifact-provider test cancelled `inspect_asset_catalog` at its subprocess boundary with an opaque object reason. The subprocess rejection was `ExecFileOutputCancelledError` carrying that exact object. The returned `ArtifactOperationError` retained reason `cancelled` but had `cause === undefined`. The ordinary `[]` utility-output control succeeded. Real directory enumeration and catalog digest reads ran against an inert fixture; utility output/cancellation was simulated. This does not establish real assetutil behavior or a public CLI/MCP transport result. ### Reproduction On macOS, from the reported revision with locked dependencies installed, save these executed cases as `src/ida/assetCancellationRepro.test.ts` and run: ```sh node node_modules/vitest/vitest.mjs run src/ida/assetCancellationRepro.test.ts --maxWorkers=1 ``` These cases were executed together at the production artifact-provider boundary. The cancellation cause assertion failed; the success control and both close assertions passed. ```ts import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, it, vi } from "vitest"; import * as processOutput from "../process/ExecFileOutput.js"; import { ArtifactProvider } from "../artifacts/ArtifactProvider.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; it.each([false, true])("asset catalog cancellation=%s", async (cancel) => { const bundle = await mkdtemp(join(tmpdir(), "rea-catalog-test-")); const controller = new AbortController(); const reason = { requested: "stop", marker: 8 }; await mkdir(join(bundle, "Contents", "Resources"), { recursive: true }); await writeFile(join(bundle, "Contents", "Resources", "Assets.car"), "inert catalog fixture"); let calls = 0; const spy = vi.spyOn(processOutput, "execFileOutput").mockImplementation(async () => { calls++; if (cancel) { controller.abort(reason); throw new processOutput.ExecFileOutputCancelledError(reason); } return { stdout: "[]", stderr: "", stdoutBytes: 2, stderrBytes: 0 }; }); const client = new ArtifactProvider({}, "darwin").createBundleResourceClient({ callerPath: bundle, canonicalPath: bundle, infoPlistPath: join(bundle, "Contents", "Info.plist"), layout: "contents", executableSha256: null }); try { const result = await client.execute("inspect_asset_catalog", {}, { signal: controller.signal }); console.log(JSON.stringify({ cancel, calls, ok: result.ok, ...(!result.ok ? { tag: result.error._tag, sameReason: result.error.cause === reason, projection: projectAnalysisError(result.error) } : {}) })); expect(calls).toBe(1); if (!cancel) { expect(result.ok).toBe(true); return; } if (result.ok) throw new Error("expected cancellation"); expect(result.error.cause).toBe(reason); } finally { expect((await client.close()).ok).toBe(true); spy.mockRestore(); await rm(bundle, { recursive: true, force: true }); } }); ``` ### Expected behavior Preserve the caller’s exact reason as the typed cancellation cause through artifact failure wrapping. [The lifecycle contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/tool-design.md#L101-L110) requires this. [ExecFileOutputCancelledError](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/process/ExecFileOutput.ts#L26-L35) already retains the observed reason, and [the artifact translator](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/ArtifactOperationFailure.ts#L16-L38) already consumes the reader cancellation record. ### Impact and scope P2: cancellation remains correctly classified, but the typed result loses the explicit caller-reason provenance required by the lifecycle contract. Consumers inspecting the typed cause cannot recover the opaque reason from this result. The exercised utility-cancellation branch lost the reason in the initial application test and in both production-provider runs; the non-aborted provider control succeeded. Real assetutil cancellation frequency and client dependence on this internal cause are unknown. No public cause field, target mutation, or data-loss claim is made. ### REA version `rea-agents` 6.7.0, source revision `52415975942a4b9c53a817915b313ccc404f280e`. Fetched `origin/main` at `c538a843cb49f7156f86beb3b21501e03255af9a` leaves the producing code and cited lifecycle contract unchanged; that newer revision was source-compared, not executed. ### Runtime and platform macOS 27.0.1 (26A434), arm64; Node.js v26.12.0; locked Vitest 5.0.3. No native utility or deep-analysis engine was launched. ### Target and artifact context A synthetic macOS-layout bundle-resource directory containing one regular `Contents/Resources/Assets.car` with inert bytes. Successful utility output is supplied at the subprocess boundary; native catalog validity is not established. ### Interface Other ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Observed provider results: ```text control: utility_calls=1; ok=true cancelled: utility_calls=1; ok=false _tag=ArtifactOperationError cause === callerReason: false actual cause: undefined projection.code=cancelled projection.details.operation=inspect_asset_catalog projection.details.reason=cancelled projection.details.detail="Asset catalog inspection cancelled" AssertionError: expected undefined to be { requested: 'stop', marker: 8 } ``` A separate application-boundary execution also observed `ArtifactReaderFailure.cancellation.reason === undefined` before translation. ### Typed outcome or recovery expected Keep `ArtifactReaderFailure.cancellation.reason` equal to the exact reason observed by the subprocess owner and translate it to `ArtifactOperationError.cause` without changing its identity. Preserve cancellation classification and actual cleanup facts. ### Analysis or suspected cause Source-derived: [runAssetUtil’s catch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/apple/AppleAssetCatalogAnalysis.ts#L260-L292) checks the signal but constructs a new reader failure without `cause` or `cancellation`. [ArtifactReaderFailure](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/ArtifactReader.ts#L188-L220) consequently records an undefined cancellation reason, which [the accessor](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/ArtifactFileOperation.ts#L155-L162) passes onward. The [digest loop](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/artifacts/apple/AppleAssetCatalogAnalysis.ts#L295-L324) has the same bare cancellation constructor by source inspection; that loop-abort case was not separately executed. Preserve the reason at those producing checkpoints instead of trying to infer it after wrapping. ### Additional context Related cancellation reports: [#5822](https://github.com/morluto/rea/issues/5822), [#5935](https://github.com/morluto/rea/issues/5935), [#5944](https://github.com/morluto/rea/issues/5944), [#5959](https://github.com/morluto/rea/issues/5959), [#5960](https://github.com/morluto/rea/issues/5960), [#5964](https://github.com/morluto/rea/issues/5964), [#5968](https://github.com/morluto/rea/issues/5968), [#5985](https://github.com/morluto/rea/issues/5985), [#5987](https://github.com/morluto/rea/issues/5987). They concern different producers or operation attribution. #4858 covers ArtifactAnalysisResources/sigrok/tshark and #4638 covers OwnedCommand; neither repairs this local asset-catalog constructor. [#5976](https://github.com/morluto/rea/issues/5976) covers assetutil timeout classification; [#5977](https://github.com/morluto/rea/issues/5977) covers malformed successful utility output; #5698 concerns nonzero exit diagnostics for damaged catalogs. Those require different restoration checks. PR #3285 overlaps bundle walks and cancellation of the sibling Interface Builder task; it does not preserve this utility abort reason. [#5992](https://github.com/morluto/rea/issues/5992) concerns a directory-close failure replacing an existing catalog failure; the present case has successful cleanup and loses the reason inside the utility-cancellation constructor. No software fix or regression range 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#3665
No description provided.