[GH-ISSUE #6037] [Bug] LLDB discovery cancellation drops the exact caller reason #3682

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

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

Cancellation during LLDB discovery returns AnalysisCancelledError with cause === undefined, although the resolver failure retains the exact caller reason in cancellation.reason. Executed provider-client cases reproduced the loss for null, a string and an object. The public classification remains cancelled.

This is a controlled production-provider reproduction with a simulated resolver failure. It does not establish a missing MCP wire field: the ordinary public error projection does not serialize opaque causes.

Reproduction

The following cases are extracted from an executed eight-case focused test. Save the code as tests/boundary/providers/native/lldbDiscovery.repro.test.ts in the prepared source checkout and run with the repository-pinned Node:

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

The assertions demonstrate current behavior. The test exercises the production provider client and target digest preflight against the existing macOS /usr/bin/true, while injecting only the tool resolver result. It never launches LLDB or the target. Client closure succeeds for every case.

import { expect, it } from 'vitest';
import { AnalysisCancelledError, AnalysisTimeoutError } from '../../../../src/domain/analysisErrorCore.js';
import { projectAnalysisError } from '../../../../src/domain/analysisErrorProjection.js';
import { err } from '../../../../src/domain/result.js';
import { LldbCallTracer } from '../../../../src/native/LldbCallTracer.js';
import { NativeCommandFailure, NATIVE_COMMAND_TIMEOUT_MS } from '../../../../src/native/CommandRunner.js';
import { NativeMacOSProvider } from '../../../../src/native/NativeMacOSProvider.js';
import { NativeFixtureRunner, nativeMachoTargetForFile } from '../../../fixtures/nativeCommands.js';

const input = { breakpoints: [{ kind: 'function', name: 'main' }] };
async function run(reason: 'cancelled' | 'timeout' | 'output-limit' | 'io' | 'unavailable', callerReason?: unknown) {
  const controller = new AbortController();
  let launches = 0;
  let resolutions = 0;
  const failure = new NativeCommandFailure('lldb', reason, null, {
    cause: new Error('controlled resolver failure'),
    ...(reason === 'cancelled' ? { cancellation: { reason: callerReason } } : {}),
  });
  const tracer = new LldbCallTracer({}, async () => {
    launches++;
    throw new Error('LLDB must not launch');
  }, async (tool, signal) => {
    expect(tool).toBe('lldb');
    expect(signal).toBe(controller.signal);
    resolutions++;
    if (reason === 'cancelled') controller.abort(callerReason);
    return err(failure);
  });
  const client = new NativeMacOSProvider({}, new NativeFixtureRunner(), 'darwin', () => tracer)
    .createClient(await nativeMachoTargetForFile('/usr/bin/true'));
  try {
    const result = await client.execute('observe_native_calls', input, { signal: controller.signal });
    expect(resolutions).toBe(1);
    expect(launches).toBe(0);
    expect(result.ok).toBe(false);
    if (result.ok) throw new Error('expected discovery failure');
    return { error: result.error, projection: projectAnalysisError(result.error), failure };
  } finally {
    expect((await client.close()).ok).toBe(true);
  }
}
it.each([null, 'caller stop', { source: 'caller' }])('drops exact discovery cancellation reason %j', async (reason) => {
  const result = await run('cancelled', reason);
  expect(result.failure.cancellation?.reason).toBe(reason);
  expect(result.error).toBeInstanceOf(AnalysisCancelledError);
  expect(result.error.cause).toBeUndefined();
  expect(result.projection.code).toBe('cancelled');
  console.log('cancellation', { retainedByResolver: true, lostByTracer: result.error.cause === undefined, code: result.projection.code });
});

All three cancellation cases passed assertions of the current incorrect behavior. In a second run, replacing expect(result.error.cause).toBeUndefined() in the cancellation case with expect(result.error.cause).toBe(reason) failed for all three values. The resolver record identity and error type assertions passed before each failure.

Expected behavior

Preserve the exact retained NativeCommandFailure.cancellation.reason as AnalysisCancelledError.cause. The lifecycle contract requires this fact to survive wrapping independently of physical command failures and cleanup ownership. Forward the opaque value, including null, without interpreting it or sampling the signal later.

Impact and scope

P2: a bounded provider-boundary provenance defect removes the reason required by the cancellation contract. Consumers of the typed error cannot recover it there; a caller retaining the original signal can still inspect signal.reason. Cancellation classification and client closure remained correct in these cases. No data loss, cleanup failure or session hang was observed.

Natural frequency with real Xcode discovery, stdio MCP timing and other platforms were not measured.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64; Node.js v24.18.0; repository-locked Vitest 5.0.3. No external debugger was started.

Target and artifact context

Existing macOS /usr/bin/true, with real file digest validation and fixture Mach-O metadata. No executable or target process was launched and no binary was modified.

Interface

Other

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

resolver cancellation.reason: exact null / string / object supplied by caller
returned error: AnalysisCancelledError
returned cause: undefined
projected code: cancelled
LLDB launches: 0
client.close(): success

The registered CLI command calls observe_native_calls; the native provider dispatches to the shared observation workflow. That public reachability is source-derived; the executed test enters at the production provider client, without CLI or MCP transport.

Typed outcome or recovery expected

Keep typed cancellation and set its cause from the resolver's retained cancellation record, preserving identity. If no cancellation record exists, do not invent a caller cause. Exposing arbitrary opaque causes in the public MCP JSON is outside this report.

Analysis or suspected cause

The discovery failure branch constructs new AnalysisCancelledError(OPERATION) without options. The resolver wrapper already preserves the independent cancellation record. The native command translator demonstrates the expected mapping elsewhere in the same provider.

Additional context

Related reports #4638 and #5287 repaired the shared command owner and native command/resolver translators. The current resolver already retains the exact reason; neither changes this LLDB discovery consumer. #5913 and its cleanup comment concern completed observations after launch, and #5385 concerns cancellation during cleanup after a physical LLDB exit failure. Those are different stages and fixes.

Related per-producer cancellation reports from the same investigation: JavaScript artifact reads (#5935), derived MCP cancellation (#5944), Rizin (#5959), call-path traversal (#5960), Wakaru (#5964), managed static analysis (#5985), static JavaScript checkpoints (#5987), assetutil (#5998), ZIP extraction (#5999), WABT (#6002), and DMG commands (#6032). They concern different producers; this report concerns the LLDB discovery consumer.

Fetched upstream main 6d356b84344659085f2b912857b5e27fe0160c9b has identical tracer, command resolver, observation workflow and cancellation constructor. That later revision was source-compared, not executed.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6037 ### 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 Cancellation during LLDB discovery returns `AnalysisCancelledError` with `cause === undefined`, although the resolver failure retains the exact caller reason in `cancellation.reason`. Executed provider-client cases reproduced the loss for `null`, a string and an object. The public classification remains `cancelled`. This is a controlled production-provider reproduction with a simulated resolver failure. It does not establish a missing MCP wire field: the ordinary public error projection does not serialize opaque causes. ### Reproduction The following cases are extracted from an executed eight-case focused test. Save the code as `tests/boundary/providers/native/lldbDiscovery.repro.test.ts` in the prepared source checkout and run with the repository-pinned Node: ```sh node node_modules/vitest/vitest.mjs run --project boundary tests/boundary/providers/native/lldbDiscovery.repro.test.ts --maxWorkers=1 --no-file-parallelism ``` The assertions demonstrate current behavior. The test exercises the production provider client and target digest preflight against the existing macOS `/usr/bin/true`, while injecting only the tool resolver result. It never launches LLDB or the target. Client closure succeeds for every case. ```ts import { expect, it } from 'vitest'; import { AnalysisCancelledError, AnalysisTimeoutError } from '../../../../src/domain/analysisErrorCore.js'; import { projectAnalysisError } from '../../../../src/domain/analysisErrorProjection.js'; import { err } from '../../../../src/domain/result.js'; import { LldbCallTracer } from '../../../../src/native/LldbCallTracer.js'; import { NativeCommandFailure, NATIVE_COMMAND_TIMEOUT_MS } from '../../../../src/native/CommandRunner.js'; import { NativeMacOSProvider } from '../../../../src/native/NativeMacOSProvider.js'; import { NativeFixtureRunner, nativeMachoTargetForFile } from '../../../fixtures/nativeCommands.js'; const input = { breakpoints: [{ kind: 'function', name: 'main' }] }; async function run(reason: 'cancelled' | 'timeout' | 'output-limit' | 'io' | 'unavailable', callerReason?: unknown) { const controller = new AbortController(); let launches = 0; let resolutions = 0; const failure = new NativeCommandFailure('lldb', reason, null, { cause: new Error('controlled resolver failure'), ...(reason === 'cancelled' ? { cancellation: { reason: callerReason } } : {}), }); const tracer = new LldbCallTracer({}, async () => { launches++; throw new Error('LLDB must not launch'); }, async (tool, signal) => { expect(tool).toBe('lldb'); expect(signal).toBe(controller.signal); resolutions++; if (reason === 'cancelled') controller.abort(callerReason); return err(failure); }); const client = new NativeMacOSProvider({}, new NativeFixtureRunner(), 'darwin', () => tracer) .createClient(await nativeMachoTargetForFile('/usr/bin/true')); try { const result = await client.execute('observe_native_calls', input, { signal: controller.signal }); expect(resolutions).toBe(1); expect(launches).toBe(0); expect(result.ok).toBe(false); if (result.ok) throw new Error('expected discovery failure'); return { error: result.error, projection: projectAnalysisError(result.error), failure }; } finally { expect((await client.close()).ok).toBe(true); } } it.each([null, 'caller stop', { source: 'caller' }])('drops exact discovery cancellation reason %j', async (reason) => { const result = await run('cancelled', reason); expect(result.failure.cancellation?.reason).toBe(reason); expect(result.error).toBeInstanceOf(AnalysisCancelledError); expect(result.error.cause).toBeUndefined(); expect(result.projection.code).toBe('cancelled'); console.log('cancellation', { retainedByResolver: true, lostByTracer: result.error.cause === undefined, code: result.projection.code }); }); ``` All three cancellation cases passed assertions of the current incorrect behavior. In a second run, replacing `expect(result.error.cause).toBeUndefined()` in the cancellation case with `expect(result.error.cause).toBe(reason)` failed for all three values. The resolver record identity and error type assertions passed before each failure. ### Expected behavior Preserve the exact retained `NativeCommandFailure.cancellation.reason` as `AnalysisCancelledError.cause`. The [lifecycle contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/tool-design.md#L101-L110) requires this fact to survive wrapping independently of physical command failures and cleanup ownership. Forward the opaque value, including `null`, without interpreting it or sampling the signal later. ### Impact and scope P2: a bounded provider-boundary provenance defect removes the reason required by the cancellation contract. Consumers of the typed error cannot recover it there; a caller retaining the original signal can still inspect `signal.reason`. Cancellation classification and client closure remained correct in these cases. No data loss, cleanup failure or session hang was observed. Natural frequency with real Xcode discovery, stdio MCP timing and other platforms were not measured. ### REA version rea-agents 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64; Node.js v24.18.0; repository-locked Vitest 5.0.3. No external debugger was started. ### Target and artifact context Existing macOS `/usr/bin/true`, with real file digest validation and fixture Mach-O metadata. No executable or target process was launched and no binary was modified. ### Interface Other ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence ```text resolver cancellation.reason: exact null / string / object supplied by caller returned error: AnalysisCancelledError returned cause: undefined projected code: cancelled LLDB launches: 0 client.close(): success ``` The [registered CLI command](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/nativeCallCommands.ts#L16-L70) calls `observe_native_calls`; the [native provider](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/NativeMacOSProvider.ts#L316-L333) dispatches to the [shared observation workflow](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/NativeCallObservation.ts#L54-L88). That public reachability is source-derived; the executed test enters at the production provider client, without CLI or MCP transport. ### Typed outcome or recovery expected Keep typed cancellation and set its cause from the resolver's retained cancellation record, preserving identity. If no cancellation record exists, do not invent a caller cause. Exposing arbitrary opaque causes in the public MCP JSON is outside this report. ### Analysis or suspected cause The [discovery failure branch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/LldbCallTracer.ts#L213-L224) constructs `new AnalysisCancelledError(OPERATION)` without options. The [resolver wrapper](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/CommandRunner.ts#L375-L406) already preserves the independent cancellation record. The [native command translator](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/NativeMacOSProvider.ts#L1318-L1325) demonstrates the expected mapping elsewhere in the same provider. ### Additional context Related reports #4638 and #5287 repaired the shared command owner and native command/resolver translators. The current resolver already retains the exact reason; neither changes this LLDB discovery consumer. #5913 and its cleanup comment concern completed observations after launch, and #5385 concerns cancellation during cleanup after a physical LLDB exit failure. Those are different stages and fixes. Related per-producer cancellation reports from the same investigation: [JavaScript artifact reads (#5935)](https://github.com/morluto/rea/issues/5935), [derived MCP cancellation (#5944)](https://github.com/morluto/rea/issues/5944), [Rizin (#5959)](https://github.com/morluto/rea/issues/5959), [call-path traversal (#5960)](https://github.com/morluto/rea/issues/5960), [Wakaru (#5964)](https://github.com/morluto/rea/issues/5964), [managed static analysis (#5985)](https://github.com/morluto/rea/issues/5985), [static JavaScript checkpoints (#5987)](https://github.com/morluto/rea/issues/5987), [assetutil (#5998)](https://github.com/morluto/rea/issues/5998), [ZIP extraction (#5999)](https://github.com/morluto/rea/issues/5999), [WABT (#6002)](https://github.com/morluto/rea/issues/6002), and [DMG commands (#6032)](https://github.com/morluto/rea/issues/6032). They concern different producers; this report concerns the LLDB discovery consumer. Fetched upstream main `6d356b84344659085f2b912857b5e27fe0160c9b` has identical tracer, command resolver, observation workflow and cancellation constructor. That later revision was source-compared, not executed.
Author
Owner

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

At d87e73f2db50be72d33a4b3c7199af35518b441c, LLDB discovery uses the shared native-command translator (discovery), which forwards the retained cancellation reason directly. The provider-client regression asserts exact cancellation-object identity, cancelled classification and zero LLDB launches.

I also replayed the original cancellation cases with restoration assertions for null, string, and object reasons. All three preserve exact cause identity, return cancelled, launch no LLDB process, and close successfully.

I ran the cited regression files 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:6380644212 --> @coygeek commented on GitHub (Oct 24, 2026): At `d87e73f2db50be72d33a4b3c7199af35518b441c`, LLDB discovery uses the shared native-command translator ([discovery](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/native/LldbCallTracer.ts#L503)), which [forwards the retained cancellation reason directly](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/native/NativeCommandAnalysisError.ts#L156). The [provider-client regression](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/native/lldbDiscovery.test.ts#L25) asserts exact cancellation-object identity, cancelled classification and zero LLDB launches. I also replayed the original cancellation cases with restoration assertions for null, string, and object reasons. All three preserve exact cause identity, return `cancelled`, launch no LLDB process, and close successfully. I ran the cited regression files 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#3682
No description provided.