[GH-ISSUE #5963] [Bug] Rizin incomplete cleanup replaces cancellation with a capability error #3632

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

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

Cancelling Rizin startup or an active command while the stop dependency reports incomplete cleanup returns AnalysisCapabilityUnavailableError instead of typed cancellation. Its projection is cleanup_incomplete with category unsupported_provider and execution_failure capability_unavailable. After a successful cleanup retry, the same retained error clears its cleanup flag but still projects capability_unavailable / unsupported_provider. The command case retains partial Evidence; the primary classification is still wrong.

Reproduction

From the pinned source checkout with locked dependencies, save this focused test as tests/boundary/providers/rizinCancellation.repro.test.ts and run:

node node_modules/vitest/vitest.mjs run --project boundary \
  tests/boundary/providers/rizinCancellation.repro.test.ts \
  --maxWorkers=1 --no-file-parallelism

The test below was executed as part of one seven-case file. That complete file passed all seven characterization assertions on Node 24.18.0 and again on Node 26.12.0. Assertions describe current faulty behavior; they do not assert a repair. Rizin is not installed on PATH. The run uses REA's injected process fixture, platform: "linux", a real host PTY for admission, and controlled stop results. No debugger or target program runs. Remove the temporary test afterwards.

import { expect, it } from "vitest";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import { AnalysisCancelledError, AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { createInjectedProcess } from "./RizinDebugSessionCoverage.fixtures.js";
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";

const reasonChainContains = (value: unknown, target: unknown): boolean => {
  const seen = new Set<unknown>();
  while (value instanceof Error && !seen.has(value)) {
    if (value === target) return true;
    seen.add(value); value = value.cause;
  }
  return value === target;
};
const input = async () => ({ path: "fixture.bin", inferior_tty: await rizinTestInferiorTty() });

for (const phase of ["startup", "command"] as const) {
  it(`${phase} cancellation becomes capability failure and stays so after cleanup retry`, async () => {
    const controller = new AbortController(); const reason = { requested: "stop", phase }; let stops = 0;
    const injected = createInjectedProcess({
      ...(phase === "startup" ? { startup: (control: any) => { control.output("startup-partial\0"); controller.abort(reason); } } : {}),
      onWrite: (_text, control) => { control.output("command-partial\0"); setImmediate(() => controller.abort(reason)); },
    });
    const manager = new RizinDebugSessionManager({}, {
      platform: "linux", spawnProcess: injected.spawnProcess,
      stopProcess: async () => { if (++stops === 1) return { status: "incomplete", reason: "injected stop uncertainty" }; injected.control.exit(); return { status: "stopped" }; },
    });
    try {
      const started = await manager.start(await input(), phase === "startup" ? controller.signal : undefined);
      const result = phase === "startup" ? started : started.ok ? await manager.execute(started.value.session_id, "slow", controller.signal) : started;
      expect(result.ok).toBe(false); if (result.ok) throw new Error("expected cancellation");
      const failure = result.error; const before = projectAnalysisError(failure);
      expect(failure._tag).toBe("AnalysisCapabilityUnavailableError"); expect(failure.cleanupIncomplete).toBe(true);
      expect(before.code).toBe("cleanup_incomplete"); expect(before.category).toBe("unsupported_provider"); expect(before.details?.execution_failure).toBe("capability_unavailable");
      expect(reasonChainContains(failure, reason)).toBe(false);
      if (phase === "command") expect(failure.partialObservation).toBeDefined();
      const retry = await manager.retryCleanupFailures(); expect(retry.ok).toBe(true); expect(failure.cleanupIncomplete).toBe(false);
      const after = projectAnalysisError(failure); expect(after.code).toBe("capability_unavailable"); expect(after.category).toBe("unsupported_provider");
      console.log(JSON.stringify({ phase: `${phase}-cleanup-incomplete`, tag: failure._tag, before: { code: before.code, category: before.category, executionFailure: before.details?.execution_failure }, after: { code: after.code, category: after.category }, retryOk: retry.ok, stopCalls: stops, partialRetained: failure.partialObservation !== undefined }));
    } finally { await manager.closeAll(); injected.control.exit(); }
  });
}

Expected behavior

The lifecycle contract requires retaining observations under typed cancellation while preserving the primary failure separately from incomplete cleanup. Owner-confirmed cleanup resolution must preserve that original failure. Cancellation should remain the primary typed outcome before and after cleanup resolves.

Impact and scope

P2: an intentional cancellation is reported as an unsupported-provider failure, including capability-oriented recovery advice. The cleanup_incomplete code and unresolved owner remain visible before retry, but execution_failure and category describe the wrong primary outcome. After retry succeeds, the retained object still misclassifies the operation. Both startup and active-command cases reproduced once in each of two focused runs. Stop uncertainty and later success were controlled dependency outcomes; no real stuck inferior, process leak, or target damage was demonstrated. No general public caller-side repair of an already returned typed error was established.

REA version

rea-agents 6.7.0; source revision 5241597594. Fetched origin/main 25e1a24a3e: relevant Rizin manager, lifecycle contract and registration lines are unchanged. An unrelated output-projection helper was added; it does not alter these classifications. The newer revision was source-compared, not executed.

Runtime and platform

macOS 27.0.1 arm64; Node.js v24.18.0 (repository pin) and v26.12.0; locked Vitest 5.0.3. Linux protocol behavior was simulated on this host. Real Linux /proc/inferior verification was not executed.

Target and artifact context

Injected process streams, object abort-reason sentinels, a real host PTY and the nominal fixture.bin launch input. The fixture path was not opened or executed. No external engine or inferior.

Interface

MCP

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

{"phase":"startup-cleanup-incomplete","tag":"AnalysisCapabilityUnavailableError","before":{"code":"cleanup_incomplete","category":"unsupported_provider","executionFailure":"capability_unavailable"},"after":{"code":"capability_unavailable","category":"unsupported_provider"},"retryOk":true,"stopCalls":2,"partialRetained":false}
{"phase":"command-cleanup-incomplete","tag":"AnalysisCapabilityUnavailableError","before":{"code":"cleanup_incomplete","category":"unsupported_provider","executionFailure":"capability_unavailable"},"after":{"code":"capability_unavailable","category":"unsupported_provider"},"retryOk":true,"stopCalls":2,"partialRetained":true}

Observed at the production manager and error-projection boundary. Public reachability is source-derived: registered Rizin handlers pass the server request signal into these same start/execute methods and deliver failed typed results. The one-shot debug-with-rizin CLI uses the same manager. No CLI subprocess or MCP cancellation transport was executed; a cancelled MCP request may have no terminal wire reply.

Typed outcome or recovery expected

Return AnalysisCancelledError with the remaining cleanup reason/resources and any partial output. While its owner remains unresolved, projection should keep code cleanup_incomplete with category cancelled and execution_failure cancelled. After confirmed owner release, it should reveal cancelled while retaining historical diagnostics and observations. This does not require relaxing detach/identity checks or forcing cleanup success.

Analysis or suspected cause

Source-derived explanation: the startup catch returns the cleanup error before testing cancellation. The command cancellation branch nests cancellation inside the cleanup error. retainCleanupFailure always creates a capability error without a cleanupPrimary cancellation; retry settlement only clears its cleanup flag. The projection consequently reports capability_unavailable as execution_failure.

Additional context

Related #5896 concerns the separate inability to complete production cleanup after protocol limits/deadlines. This report isolates cancellation classification using a stop dependency that later succeeds, so repairing protocol cleanup alone does not preserve the primary cancellation. #5090 concerns CLI exit status after successful fallback; #5759 concerns TTY preflight; #5761 concerns GDB output correlation; #5059 concerns secondary cause retention. PR #5516 still returns retainCleanupFailure for incomplete cancellation cleanup. The abort-reason omission is a separate constructor defect. NOT_ENOUGH_INFO: introducing commit, last known good release, and frequency with a real Linux Rizin engine.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5963 ### 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 Cancelling Rizin startup or an active command while the stop dependency reports incomplete cleanup returns AnalysisCapabilityUnavailableError instead of typed cancellation. Its projection is cleanup_incomplete with category unsupported_provider and execution_failure capability_unavailable. After a successful cleanup retry, the same retained error clears its cleanup flag but still projects capability_unavailable / unsupported_provider. The command case retains partial Evidence; the primary classification is still wrong. ### Reproduction From the pinned source checkout with locked dependencies, save this focused test as tests/boundary/providers/rizinCancellation.repro.test.ts and run: ```sh node node_modules/vitest/vitest.mjs run --project boundary \ tests/boundary/providers/rizinCancellation.repro.test.ts \ --maxWorkers=1 --no-file-parallelism ``` The test below was executed as part of one seven-case file. That complete file passed all seven characterization assertions on Node 24.18.0 and again on Node 26.12.0. Assertions describe current faulty behavior; they do not assert a repair. Rizin is not installed on PATH. The run uses REA's injected process fixture, platform: "linux", a real host PTY for admission, and controlled stop results. No debugger or target program runs. Remove the temporary test afterwards. ```ts import { expect, it } from "vitest"; import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js"; import { AnalysisCancelledError, AnalysisInputError } from "../../../src/domain/analysisErrorCore.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { createInjectedProcess } from "./RizinDebugSessionCoverage.fixtures.js"; import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js"; const reasonChainContains = (value: unknown, target: unknown): boolean => { const seen = new Set<unknown>(); while (value instanceof Error && !seen.has(value)) { if (value === target) return true; seen.add(value); value = value.cause; } return value === target; }; const input = async () => ({ path: "fixture.bin", inferior_tty: await rizinTestInferiorTty() }); for (const phase of ["startup", "command"] as const) { it(`${phase} cancellation becomes capability failure and stays so after cleanup retry`, async () => { const controller = new AbortController(); const reason = { requested: "stop", phase }; let stops = 0; const injected = createInjectedProcess({ ...(phase === "startup" ? { startup: (control: any) => { control.output("startup-partial\0"); controller.abort(reason); } } : {}), onWrite: (_text, control) => { control.output("command-partial\0"); setImmediate(() => controller.abort(reason)); }, }); const manager = new RizinDebugSessionManager({}, { platform: "linux", spawnProcess: injected.spawnProcess, stopProcess: async () => { if (++stops === 1) return { status: "incomplete", reason: "injected stop uncertainty" }; injected.control.exit(); return { status: "stopped" }; }, }); try { const started = await manager.start(await input(), phase === "startup" ? controller.signal : undefined); const result = phase === "startup" ? started : started.ok ? await manager.execute(started.value.session_id, "slow", controller.signal) : started; expect(result.ok).toBe(false); if (result.ok) throw new Error("expected cancellation"); const failure = result.error; const before = projectAnalysisError(failure); expect(failure._tag).toBe("AnalysisCapabilityUnavailableError"); expect(failure.cleanupIncomplete).toBe(true); expect(before.code).toBe("cleanup_incomplete"); expect(before.category).toBe("unsupported_provider"); expect(before.details?.execution_failure).toBe("capability_unavailable"); expect(reasonChainContains(failure, reason)).toBe(false); if (phase === "command") expect(failure.partialObservation).toBeDefined(); const retry = await manager.retryCleanupFailures(); expect(retry.ok).toBe(true); expect(failure.cleanupIncomplete).toBe(false); const after = projectAnalysisError(failure); expect(after.code).toBe("capability_unavailable"); expect(after.category).toBe("unsupported_provider"); console.log(JSON.stringify({ phase: `${phase}-cleanup-incomplete`, tag: failure._tag, before: { code: before.code, category: before.category, executionFailure: before.details?.execution_failure }, after: { code: after.code, category: after.category }, retryOk: retry.ok, stopCalls: stops, partialRetained: failure.partialObservation !== undefined })); } finally { await manager.closeAll(); injected.control.exit(); } }); } ``` ### Expected behavior The [lifecycle contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/tool-design.md#L101-L110) requires retaining observations under typed cancellation while preserving the primary failure separately from incomplete cleanup. [Owner-confirmed cleanup resolution](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/tool-design.md#L123-L128) must preserve that original failure. Cancellation should remain the primary typed outcome before and after cleanup resolves. ### Impact and scope P2: an intentional cancellation is reported as an unsupported-provider failure, including capability-oriented recovery advice. The cleanup_incomplete code and unresolved owner remain visible before retry, but execution_failure and category describe the wrong primary outcome. After retry succeeds, the retained object still misclassifies the operation. Both startup and active-command cases reproduced once in each of two focused runs. Stop uncertainty and later success were controlled dependency outcomes; no real stuck inferior, process leak, or target damage was demonstrated. No general public caller-side repair of an already returned typed error was established. ### REA version rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e. Fetched origin/main 25e1a24a3ed3671a0148757555f18d16c29d936a: relevant Rizin manager, lifecycle contract and registration lines are unchanged. An unrelated output-projection helper was added; it does not alter these classifications. The newer revision was source-compared, not executed. ### Runtime and platform macOS 27.0.1 arm64; Node.js v24.18.0 (repository pin) and v26.12.0; locked Vitest 5.0.3. Linux protocol behavior was simulated on this host. Real Linux /proc/inferior verification was not executed. ### Target and artifact context Injected process streams, object abort-reason sentinels, a real host PTY and the nominal fixture.bin launch input. The fixture path was not opened or executed. No external engine or inferior. ### Interface MCP ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence ```json {"phase":"startup-cleanup-incomplete","tag":"AnalysisCapabilityUnavailableError","before":{"code":"cleanup_incomplete","category":"unsupported_provider","executionFailure":"capability_unavailable"},"after":{"code":"capability_unavailable","category":"unsupported_provider"},"retryOk":true,"stopCalls":2,"partialRetained":false} {"phase":"command-cleanup-incomplete","tag":"AnalysisCapabilityUnavailableError","before":{"code":"cleanup_incomplete","category":"unsupported_provider","executionFailure":"capability_unavailable"},"after":{"code":"capability_unavailable","category":"unsupported_provider"},"retryOk":true,"stopCalls":2,"partialRetained":true} ``` Observed at the production manager and error-projection boundary. Public reachability is source-derived: [registered Rizin handlers](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/server/registerRizinDebugTools.ts#L32-L109) pass the server request signal into these same start/execute methods and deliver failed typed results. The one-shot debug-with-rizin CLI uses the same manager. No CLI subprocess or MCP cancellation transport was executed; a cancelled MCP request may have no terminal wire reply. ### Typed outcome or recovery expected Return AnalysisCancelledError with the remaining cleanup reason/resources and any partial output. While its owner remains unresolved, projection should keep code cleanup_incomplete with category cancelled and execution_failure cancelled. After confirmed owner release, it should reveal cancelled while retaining historical diagnostics and observations. This does not require relaxing detach/identity checks or forcing cleanup success. ### Analysis or suspected cause Source-derived explanation: the [startup catch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L493-L512) returns the cleanup error before testing cancellation. The [command cancellation branch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L744-L779) nests cancellation inside the cleanup error. [retainCleanupFailure](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L1284-L1298) always creates a capability error without a cleanupPrimary cancellation; [retry settlement](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/rizin/RizinDebugSessionManager.ts#L1115-L1128) only clears its cleanup flag. The [projection](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorProjection.ts#L69-L88) consequently reports capability_unavailable as execution_failure. ### Additional context Related #5896 concerns the separate inability to complete production cleanup after protocol limits/deadlines. This report isolates cancellation classification using a stop dependency that later succeeds, so repairing protocol cleanup alone does not preserve the primary cancellation. #5090 concerns CLI exit status after successful fallback; #5759 concerns TTY preflight; #5761 concerns GDB output correlation; #5059 concerns secondary cause retention. PR #5516 still returns retainCleanupFailure for incomplete cancellation cleanup. The abort-reason omission is a separate constructor defect. NOT_ENOUGH_INFO: introducing commit, last known good release, and frequency with a real Linux Rizin engine.
Author
Owner

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

Checked the reported fix on main at d87e73f2db.

Startup computes cancellation before stop finalization and passes a typed primary error through the common finalizer. Command cancellation likewise creates AnalysisCancelledError with partial output/Evidence before cleanup. retainCleanupFailure now creates a cancellation error when its cause is cancellation and retains caller reason and partial observations; owner retry clears its cleanup flags without changing its class.

Implementation: RizinDebugSessionManager.ts, RizinDebugSessionManager.ts.

The relevant regression checks in RizinDebugSessionCoverage.part3.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380745244 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Startup computes cancellation before stop finalization and passes a typed primary error through the common finalizer. Command cancellation likewise creates AnalysisCancelledError with partial output/Evidence before cleanup. retainCleanupFailure now creates a cancellation error when its cause is cancellation and retains caller reason and partial observations; owner retry clears its cleanup flags without changing its class. Implementation: [RizinDebugSessionManager.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/rizin/RizinDebugSessionManager.ts#L546), [RizinDebugSessionManager.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/rizin/RizinDebugSessionManager.ts#L1491). The relevant regression checks in [RizinDebugSessionCoverage.part3.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/RizinDebugSessionCoverage.part3.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. 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#3632
No description provided.