[GH-ISSUE #5913] [Bug] Late cancellation at LLDB output closure drops a completed trace #3591

Closed
opened 2026-10-31 00:42:10 +00:00 by bsakaguchi · 2 comments
Owner

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

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

A completed native call trace loses its final facts if caller cancellation arrives when the production LLDB runner finishes waiting for output closure. The caller correctly receives AnalysisCancelledError, but its partial observation contains only journal events and output prefixes, omitting the completed run's outcome, exit status, termination, elapsed time, launch identity and breakpoint results. The debugger version becomes null and fully captured stdout is reported incomplete/truncated.

This was executed with the production default process runner and readback path, using an executable Node peer in place of LLDB. An otherwise identical run without cancellation retained the complete result. No real LLDB session or public CLI/MCP request was executed.

Reproduction

Place the following focused test alongside tests/boundary/providers/native/lldbCallTracerFailures.test.ts, for example as completedTraceOutputClose.test.ts, and run:

node node_modules/vitest/vitest.mjs run tests/boundary/providers/native/completedTraceOutputClose.test.ts --maxWorkers 1

The peer writes a valid completed result and exits 0. The spy first awaits the real waitForOutputClose; it then aborts only when output closure and exit code 0 have been observed. The inert Node child supplies a simulated target PID that is already reaped, so both runs also verify successful cleanup.

import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { expect, it, vi } from 'vitest';
import { LldbCallTracer } from '../../../../src/native/LldbCallTracer.js';
import { nativeCallObservationInputSchema } from '../../../../src/domain/native/nativeCallObservation.js';
import { nativeCallPartialObservationSchema } from '../../../../src/domain/native/nativeCallPartialObservation.js';
import { AnalysisCancelledError } from '../../../../src/domain/analysisErrorCore.js';
import { ProviderProcessSupervisor } from '../../../../src/process/ProviderProcess.js';
import { ok } from '../../../../src/domain/result.js';

it.each([false, true])('retains completed facts with output-close cancellation=%s', async (abort) => {
  const root = await mkdtemp(join(tmpdir(), 'rea-output-close-'));
  const executable = join(root, 'peer');
  const target = join(root, 'target');
  const event = {
    sequence: 0, elapsed_ms: 1, thread_id: 2, breakpoint_index: 0,
    load_address: '0x1000', file_address: '0x1000', module: 'fixture',
    module_path: target, symbol: 'main', receiver_class: null,
    selector: null, registers: [], backtrace: [],
  };
  const output = {
    status: 'traced', version: 'lldb-fixture', outcome: 'exited',
    resource_limit_reached: false, exit_status: 0, exit_description: null,
    killed: false, terminated: true, elapsed_ms: 1,
    target_identity: {
      loaded_image_sha256: null, selected_file_sha256: 'a'.repeat(64),
      file_device: '1', file_inode: '2', module_path: target,
      module_uuid: 'fixture', stable: true,
    },
    target_output: { stdout_bytes: 2, stderr_bytes: 0,
      stdout_truncated: false, stderr_truncated: false,
      stdout_complete: true, stderr_complete: true },
    breakpoints: [{ index: 0, location_count: 1, locations: [] }],
    events: [event], other_stops: [],
  };
  await writeFile(executable, `#!${process.execPath}\nconst fs = require('node:fs');
const {spawnSync} = require('node:child_process');
const command = process.argv.at(-1);
if (!command.startsWith('rea_trace ')) process.exit(91);
const c = JSON.parse(fs.readFileSync(command.slice(10), 'utf8'));
const child = spawnSync(process.execPath, ['-e', '']);
const result = ${JSON.stringify(output)};
result.pid = child.pid;
fs.writeFileSync(c.pid_path, String(child.pid));
fs.writeFileSync(c.stdout_capture_path, 'ok');
fs.writeFileSync(c.stderr_capture_path, '');
fs.writeFileSync(c.observation_path, JSON.stringify({kind:'event',event:result.events[0]})+'\\n');
fs.writeFileSync(c.result_path, JSON.stringify(result));
process.exit(0);
`, {mode: 0o700});
  const controller = new AbortController();
  let interrupted = false;
  const originalWait = ProviderProcessSupervisor.prototype.waitForOutputClose;
  const spy = vi.spyOn(ProviderProcessSupervisor.prototype, 'waitForOutputClose')
    .mockImplementation(async function(this: ProviderProcessSupervisor, timeoutMs?: number, signal?: AbortSignal) {
      const closed = await originalWait.call(this, timeoutMs, signal);
      if (abort && !interrupted && closed && this.observedExit()?.code === 0) {
        interrupted = true;
        controller.abort(new Error('abort at output-close loop exit'));
      }
      return closed;
    });
  const tracer = new LldbCallTracer({}, undefined, async () => ok({path: executable, sha256: 'f'.repeat(64)}));
  try {
    const result = await tracer.trace({
      executable: target, architecture: 'arm64', expectedSha256: 'a'.repeat(64),
      input: nativeCallObservationInputSchema.parse({breakpoints:[{kind:'function', name:'main'}]}),
    }, controller.signal);
    expect(interrupted).toBe(abort);
    if (!abort) {
      expect(result.ok).toBe(true);
      if (!result.ok) throw result.error;
      expect(result.value.run).toMatchObject(output);
      expect(result.value.stdout).toMatchObject({text:'ok', bytes:2, complete:true});
      return;
    }
    expect(result.ok).toBe(false);
    if (result.ok) throw new Error('Expected cancellation');
    expect(result.error).toBeInstanceOf(AnalysisCancelledError);
    expect(result.error.cleanupIncomplete).toBe(false);
    const partial = nativeCallPartialObservationSchema.parse(result.error.partialObservation);
    console.log('CANCELLED_PARTIAL', JSON.stringify(partial));
    expect(partial.events).toEqual([event]);
    expect(partial).toMatchObject({
      coverage: {status:'partial', reason:'cancelled'},
      process: {outcome:'exited', exit_status:0, terminated:true, elapsed_ms:1,
        stdout:{text:'ok', bytes:2, complete:true}},
      debugger:{version:'lldb-fixture'},
      target:{launch_identity:output.target_identity},
      breakpoints:[{index:0, location_count:1}],
    });
  } finally {
    spy.mockRestore();
    expect((await tracer.close()).ok).toBe(true);
    await rm(root, {recursive:true, force:true});
  }
});

Executed result: the non-aborted control passed; the cancellation case failed its completed-facts assertion. The cancellation type, journal event and successful cleanup assertions passed before that failure. This is one deterministic fault/control pair, not a measurement of real debugger race frequency.

Expected behavior

Keep typed cancellation and retain the completed result's available facts in its partial observation, with cleanup reported separately. The native-call retention contract explicitly requires retention of completed exit, termination, launch identity, breakpoint and final stream-counter facts after cancellation. The lifecycle regression contract also requires typed cancellation with available partial observations after workload exit.

Impact and scope

P2: a bounded finalization race removes completed observations and misstates capture completeness. In the executed case, the event and stdout text survived, but callers could no longer determine the completed run outcome or breakpoint resolution from the returned partial. Non-aborted runs were unaffected. Real LLDB incidence, signal timing and deadline-only behavior were not measured. There is no verified recovery path for the discarded final fields in the returned error; the test's completed runtime was cleaned up normally.

REA version

6.7.0 source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64, Node v26.12.0, repository-pinned Vitest 5.0.3. The test used the current PATH runtime; it did not use the repository's Node 24.18.0 pin.

Target and artifact context

Generated executable Node peer standing in for LLDB, an inert synchronously reaped Node child standing in for the target, a valid status: "traced" JSON result, one event journal row and stdout ok. Real Mach-O execution and debugger permission handling were not tested.

Interface

Other

Earliest failure stage

Cleanup, cancellation, or lifecycle

Relevant output or evidence

Reduced fields from the executed cancellation result:

{
  "error": "AnalysisCancelledError",
  "cleanupIncomplete": false,
  "partialObservation": {
    "coverage": { "status": "partial", "reason": "cancelled" },
    "debugger": { "version": null },
    "process": {
      "stdout": { "text": "ok", "bytes": 2, "truncated": true, "complete": false },
      "stderr": { "text": "", "bytes": 0, "truncated": true, "complete": false },
      "other_stops": []
    }
  }
}

The full partial retained the supplied event but had no process.outcome, process.exit_status, process.terminated, process.elapsed_ms, target.launch_identity or breakpoints. Its limitations claimed that no complete result or final counters had been produced, although the passing control consumed those same completed producer fields. Both tracer cleanup calls returned success.

The real interface path is source-derived: the CLI command dispatches observe_native_calls, which the native provider routes to the shared observation/tracer workflow. The test exercises that tracer's production process boundary directly.

Typed outcome or recovery expected

AnalysisCancelledError with partial coverage reason cancelled, the completed trace facts retained, and actual cleanup state reported independently. Use journal-only fallback when a complete valid result is unavailable.

Analysis or suspected cause

Source-derived explanation: result readback runs only for a launch classified as exited. The process failure latch covers nonzero exits and signals. Therefore cancellation observed at output-close completion after exit 0 selects an interrupted outcome and skips an already-written result.

The cancellation branch uses the journal partial builder. Its projection lacks completed-run fields and adds incomplete-result/lower-bound limitations. The readback and partial projection boundary needs to preserve valid completed output while maintaining typed cancellation.

Additional context

The affected tracer and retained-observation source files are unchanged in fetched upstream main 647717c050ff4dbaef83fb1c5555ef5c33979cd4 relative to the tested revision. That upstream revision was source-compared, not separately executed. Internal-deadline behavior follows related source branches but was not part of this reproduction.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5913 ### 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 A completed native call trace loses its final facts if caller cancellation arrives when the production LLDB runner finishes waiting for output closure. The caller correctly receives `AnalysisCancelledError`, but its partial observation contains only journal events and output prefixes, omitting the completed run's outcome, exit status, termination, elapsed time, launch identity and breakpoint results. The debugger version becomes null and fully captured stdout is reported incomplete/truncated. This was executed with the production default process runner and readback path, using an executable Node peer in place of LLDB. An otherwise identical run without cancellation retained the complete result. No real LLDB session or public CLI/MCP request was executed. ### Reproduction Place the following focused test alongside `tests/boundary/providers/native/lldbCallTracerFailures.test.ts`, for example as `completedTraceOutputClose.test.ts`, and run: ```sh node node_modules/vitest/vitest.mjs run tests/boundary/providers/native/completedTraceOutputClose.test.ts --maxWorkers 1 ``` The peer writes a valid completed result and exits 0. The spy first awaits the real `waitForOutputClose`; it then aborts only when output closure and exit code 0 have been observed. The inert Node child supplies a simulated target PID that is already reaped, so both runs also verify successful cleanup. ```ts import { mkdtemp, writeFile, rm } from 'node:fs/promises'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { expect, it, vi } from 'vitest'; import { LldbCallTracer } from '../../../../src/native/LldbCallTracer.js'; import { nativeCallObservationInputSchema } from '../../../../src/domain/native/nativeCallObservation.js'; import { nativeCallPartialObservationSchema } from '../../../../src/domain/native/nativeCallPartialObservation.js'; import { AnalysisCancelledError } from '../../../../src/domain/analysisErrorCore.js'; import { ProviderProcessSupervisor } from '../../../../src/process/ProviderProcess.js'; import { ok } from '../../../../src/domain/result.js'; it.each([false, true])('retains completed facts with output-close cancellation=%s', async (abort) => { const root = await mkdtemp(join(tmpdir(), 'rea-output-close-')); const executable = join(root, 'peer'); const target = join(root, 'target'); const event = { sequence: 0, elapsed_ms: 1, thread_id: 2, breakpoint_index: 0, load_address: '0x1000', file_address: '0x1000', module: 'fixture', module_path: target, symbol: 'main', receiver_class: null, selector: null, registers: [], backtrace: [], }; const output = { status: 'traced', version: 'lldb-fixture', outcome: 'exited', resource_limit_reached: false, exit_status: 0, exit_description: null, killed: false, terminated: true, elapsed_ms: 1, target_identity: { loaded_image_sha256: null, selected_file_sha256: 'a'.repeat(64), file_device: '1', file_inode: '2', module_path: target, module_uuid: 'fixture', stable: true, }, target_output: { stdout_bytes: 2, stderr_bytes: 0, stdout_truncated: false, stderr_truncated: false, stdout_complete: true, stderr_complete: true }, breakpoints: [{ index: 0, location_count: 1, locations: [] }], events: [event], other_stops: [], }; await writeFile(executable, `#!${process.execPath}\nconst fs = require('node:fs'); const {spawnSync} = require('node:child_process'); const command = process.argv.at(-1); if (!command.startsWith('rea_trace ')) process.exit(91); const c = JSON.parse(fs.readFileSync(command.slice(10), 'utf8')); const child = spawnSync(process.execPath, ['-e', '']); const result = ${JSON.stringify(output)}; result.pid = child.pid; fs.writeFileSync(c.pid_path, String(child.pid)); fs.writeFileSync(c.stdout_capture_path, 'ok'); fs.writeFileSync(c.stderr_capture_path, ''); fs.writeFileSync(c.observation_path, JSON.stringify({kind:'event',event:result.events[0]})+'\\n'); fs.writeFileSync(c.result_path, JSON.stringify(result)); process.exit(0); `, {mode: 0o700}); const controller = new AbortController(); let interrupted = false; const originalWait = ProviderProcessSupervisor.prototype.waitForOutputClose; const spy = vi.spyOn(ProviderProcessSupervisor.prototype, 'waitForOutputClose') .mockImplementation(async function(this: ProviderProcessSupervisor, timeoutMs?: number, signal?: AbortSignal) { const closed = await originalWait.call(this, timeoutMs, signal); if (abort && !interrupted && closed && this.observedExit()?.code === 0) { interrupted = true; controller.abort(new Error('abort at output-close loop exit')); } return closed; }); const tracer = new LldbCallTracer({}, undefined, async () => ok({path: executable, sha256: 'f'.repeat(64)})); try { const result = await tracer.trace({ executable: target, architecture: 'arm64', expectedSha256: 'a'.repeat(64), input: nativeCallObservationInputSchema.parse({breakpoints:[{kind:'function', name:'main'}]}), }, controller.signal); expect(interrupted).toBe(abort); if (!abort) { expect(result.ok).toBe(true); if (!result.ok) throw result.error; expect(result.value.run).toMatchObject(output); expect(result.value.stdout).toMatchObject({text:'ok', bytes:2, complete:true}); return; } expect(result.ok).toBe(false); if (result.ok) throw new Error('Expected cancellation'); expect(result.error).toBeInstanceOf(AnalysisCancelledError); expect(result.error.cleanupIncomplete).toBe(false); const partial = nativeCallPartialObservationSchema.parse(result.error.partialObservation); console.log('CANCELLED_PARTIAL', JSON.stringify(partial)); expect(partial.events).toEqual([event]); expect(partial).toMatchObject({ coverage: {status:'partial', reason:'cancelled'}, process: {outcome:'exited', exit_status:0, terminated:true, elapsed_ms:1, stdout:{text:'ok', bytes:2, complete:true}}, debugger:{version:'lldb-fixture'}, target:{launch_identity:output.target_identity}, breakpoints:[{index:0, location_count:1}], }); } finally { spy.mockRestore(); expect((await tracer.close()).ok).toBe(true); await rm(root, {recursive:true, force:true}); } }); ``` Executed result: the non-aborted control passed; the cancellation case failed its completed-facts assertion. The cancellation type, journal event and successful cleanup assertions passed before that failure. This is one deterministic fault/control pair, not a measurement of real debugger race frequency. ### Expected behavior Keep typed cancellation and retain the completed result's available facts in its partial observation, with cleanup reported separately. The [native-call retention contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/native-investigation.md#L599-L608) explicitly requires retention of completed exit, termination, launch identity, breakpoint and final stream-counter facts after cancellation. The [lifecycle regression contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/testing.md#L247-L259) also requires typed cancellation with available partial observations after workload exit. ### Impact and scope P2: a bounded finalization race removes completed observations and misstates capture completeness. In the executed case, the event and stdout text survived, but callers could no longer determine the completed run outcome or breakpoint resolution from the returned partial. Non-aborted runs were unaffected. Real LLDB incidence, signal timing and deadline-only behavior were not measured. There is no verified recovery path for the discarded final fields in the returned error; the test's completed runtime was cleaned up normally. ### REA version 6.7.0 source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64, Node v26.12.0, repository-pinned Vitest 5.0.3. The test used the current PATH runtime; it did not use the repository's Node 24.18.0 pin. ### Target and artifact context Generated executable Node peer standing in for LLDB, an inert synchronously reaped Node child standing in for the target, a valid `status: "traced"` JSON result, one event journal row and stdout `ok`. Real Mach-O execution and debugger permission handling were not tested. ### Interface Other ### Earliest failure stage Cleanup, cancellation, or lifecycle ### Relevant output or evidence Reduced fields from the executed cancellation result: ```json { "error": "AnalysisCancelledError", "cleanupIncomplete": false, "partialObservation": { "coverage": { "status": "partial", "reason": "cancelled" }, "debugger": { "version": null }, "process": { "stdout": { "text": "ok", "bytes": 2, "truncated": true, "complete": false }, "stderr": { "text": "", "bytes": 0, "truncated": true, "complete": false }, "other_stops": [] } } } ``` The full partial retained the supplied event but had no `process.outcome`, `process.exit_status`, `process.terminated`, `process.elapsed_ms`, `target.launch_identity` or `breakpoints`. Its limitations claimed that no complete result or final counters had been produced, although the passing control consumed those same completed producer fields. Both tracer cleanup calls returned success. The real interface path is source-derived: the [CLI command](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/nativeCallCommands.ts#L16-L70) dispatches `observe_native_calls`, which the [native provider](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/NativeMacOSProvider.ts#L316-L333) routes to the shared observation/tracer workflow. The test exercises that tracer's production process boundary directly. ### Typed outcome or recovery expected `AnalysisCancelledError` with partial coverage reason `cancelled`, the completed trace facts retained, and actual cleanup state reported independently. Use journal-only fallback when a complete valid result is unavailable. ### Analysis or suspected cause Source-derived explanation: [result readback](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/LldbCallTracer.ts#L507-L511) runs only for a launch classified as `exited`. The [process failure latch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/LldbCallTracer.ts#L1165-L1179) covers nonzero exits and signals. Therefore cancellation observed at output-close completion after exit 0 selects an interrupted outcome and skips an already-written result. The [cancellation branch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/LldbCallTracer.ts#L558-L593) uses the journal partial builder. Its [projection](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/LldbRetainedObservations.ts#L62-L96) lacks completed-run fields and adds incomplete-result/lower-bound limitations. The readback and partial projection boundary needs to preserve valid completed output while maintaining typed cancellation. ### Additional context The affected tracer and retained-observation source files are unchanged in fetched upstream main `647717c050ff4dbaef83fb1c5555ef5c33979cd4` relative to the tested revision. That upstream revision was source-compared, not separately executed. Internal-deadline behavior follows related source branches but was not part of this reproduction.
Author
Owner

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

Two additional verified cleanup-failure cases

The same completed-facts loss also occurs after a valid status: "traced" result has already been read and schema-validated:

  • Process-group cleanup failure: an exited launcher reports cleanupFailure; the process-group cleanup branch returns ProviderCleanupError with cleanupIncomplete: true.
  • Target-termination uncertainty: the launcher reports targetLaunched: true, but no target PID/start identity is available; the target cleanup branch returns the same cleanup error. This target-only case also labels coverage tracer-failure, whereas the process-group case uses cleanup-failure (reason selection).

Both partial observations retain the journal event but omit completed outcome, exit status, elapsed time, breakpoint results and launch identity. They replace the final stdout count 4096 with the captured prefix length 3, mark output incomplete, and claim that no complete result/final counters were produced. Correction for these two cases: debugger.version survives as lldb-fixture; it is not lost as in the output-close cancellation case.

Executed at baseline 52415975942a4b9c53a817915b313ccc404f280e through the production LldbCallTracer runner with an injected simulated launcher/resolver, using repository-pinned Vitest 5.0.3. No real LLDB or target process was launched. The simulated launcher wrote a valid completed result, one journal event and stdout prefix abc, then selected either cleanup branch. All three cases passed assertions of current behavior: the clean control retained outcome exited, one breakpoint and stdout bytes 4096; both cleanup cases demonstrated the loss. Key assertions for each cleanup case:

expect(result.error).toBeInstanceOf(ProviderCleanupError);
expect(result.error.cleanupIncomplete).toBe(true);
const partial = nativeCallPartialObservationSchema.parse(result.error.partialObservation);
expect(partial.events).toEqual([EVENT]);
expect(partial.debugger.version).toBe("lldb-fixture");
expect(partial.process.outcome).toBeUndefined();
expect(partial.process.exit_status).toBeUndefined();
expect(partial.process.elapsed_ms).toBeUndefined();
expect(partial.target.launch_identity).toBeUndefined();
expect(partial.breakpoints).toBeUndefined();
expect(partial.process.stdout?.bytes).toBe(3);
expect(partial.coverage.reason).toBe(
  phase === "group-cleanup" ? "cleanup-failure" : "tracer-failure",
);

Completed producer facts should survive while host cleanup uncertainty remains explicit; the producer's termination report does not independently prove host cleanup.

<!-- gh-comment-id:6314562202 --> @coygeek commented on GitHub (Oct 21, 2026): ### Two additional verified cleanup-failure cases The same completed-facts loss also occurs after a valid `status: "traced"` result has already been read and schema-validated: - **Process-group cleanup failure:** an exited launcher reports `cleanupFailure`; the [process-group cleanup branch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/LldbCallTracer.ts#L610-L630) returns `ProviderCleanupError` with `cleanupIncomplete: true`. - **Target-termination uncertainty:** the launcher reports `targetLaunched: true`, but no target PID/start identity is available; the [target cleanup branch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/LldbCallTracer.ts#L631-L649) returns the same cleanup error. This target-only case also labels coverage `tracer-failure`, whereas the process-group case uses `cleanup-failure` ([reason selection](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/native/LldbCallTracer.ts#L549-L556)). Both partial observations retain the journal event but omit completed outcome, exit status, elapsed time, breakpoint results and launch identity. They replace the final stdout count `4096` with the captured prefix length `3`, mark output incomplete, and claim that no complete result/final counters were produced. Correction for these two cases: `debugger.version` **survives** as `lldb-fixture`; it is not lost as in the output-close cancellation case. Executed at baseline `52415975942a4b9c53a817915b313ccc404f280e` through the production `LldbCallTracer` runner with an injected simulated launcher/resolver, using repository-pinned Vitest 5.0.3. No real LLDB or target process was launched. The simulated launcher wrote a valid completed result, one journal event and stdout prefix `abc`, then selected either cleanup branch. All three cases passed assertions of current behavior: the clean control retained outcome `exited`, one breakpoint and stdout bytes `4096`; both cleanup cases demonstrated the loss. Key assertions for each cleanup case: ```ts expect(result.error).toBeInstanceOf(ProviderCleanupError); expect(result.error.cleanupIncomplete).toBe(true); const partial = nativeCallPartialObservationSchema.parse(result.error.partialObservation); expect(partial.events).toEqual([EVENT]); expect(partial.debugger.version).toBe("lldb-fixture"); expect(partial.process.outcome).toBeUndefined(); expect(partial.process.exit_status).toBeUndefined(); expect(partial.process.elapsed_ms).toBeUndefined(); expect(partial.target.launch_identity).toBeUndefined(); expect(partial.breakpoints).toBeUndefined(); expect(partial.process.stdout?.bytes).toBe(3); expect(partial.coverage.reason).toBe( phase === "group-cleanup" ? "cleanup-failure" : "tracer-failure", ); ``` Completed producer facts should survive while host cleanup uncertainty remains explicit; the producer's termination report does not independently prove host cleanup.
Author
Owner

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

The original output-close cancellation case now has direct regression assertions for typed cancellation retaining completed outcome, exit status, launch identity, breakpoints and complete stdout. Source also treats closed output with an observed exit as an exited launch before readback. I have not executed these tests or a real LLDB session.

The later cleanup cases remain distinct. After completed output is read and validated, the cleanup branches still route process-group cleanup failure and uncertain target termination through failureWithPartial. That helper reads the journal/prefixes, forwarding only the completed debugger version. Its partial projector omits completed outcome, exit status, elapsed time, launch identity and breakpoints, and claims no complete result/final counters existed. Target-only uncertainty also selects tracer-failure rather than cleanup-failure. This is source-derived confirmation of a remaining path, not a fresh runtime reproduction of the author's simulated cases.

The retention contract separates available completed observations from cleanup uncertainty. Closure should cover both cleanup branches with an already validated completed result, a clean control, and a final stream count larger than the retained prefix, as in the reported 4096-byte/abc case. Assert completed fields/counters survive, cleanup remains incomplete and retry-owned, and the coverage reason identifies cleanup failure. Preserve producer facts without treating its termination report as independent host cleanup proof; journal-only fallback remains appropriate when no valid completed result exists.

<!-- gh-comment-id:6380896456 --> @coygeek commented on GitHub (Oct 24, 2026): The original output-close cancellation case now has direct [regression assertions](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/providers/native/lldbCallTracerFailures.test.ts#L656-L724) for typed cancellation retaining completed outcome, exit status, launch identity, breakpoints and complete stdout. Source also treats closed output with an observed exit as an exited launch before readback. I have not executed these tests or a real LLDB session. The later cleanup cases remain distinct. After completed output is read and validated, the [cleanup branches](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/native/LldbCallTracer.ts#L647-L789) still route process-group cleanup failure and uncertain target termination through `failureWithPartial`. That helper reads the journal/prefixes, forwarding only the completed debugger version. Its [partial projector](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/native/LldbRetainedObservations.ts#L59-L96) omits completed outcome, exit status, elapsed time, launch identity and breakpoints, and claims no complete result/final counters existed. Target-only uncertainty also selects `tracer-failure` rather than `cleanup-failure`. This is source-derived confirmation of a remaining path, not a fresh runtime reproduction of the author's simulated cases. The [retention contract](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/docs/native-investigation.md#L954-L965) separates available completed observations from cleanup uncertainty. Closure should cover both cleanup branches with an already validated completed result, a clean control, and a final stream count larger than the retained prefix, as in the reported 4096-byte/`abc` case. Assert completed fields/counters survive, cleanup remains incomplete and retry-owned, and the coverage reason identifies cleanup failure. Preserve producer facts without treating its termination report as independent host cleanup proof; journal-only fallback remains appropriate when no valid completed result exists.
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#3591
No description provided.