[GH-ISSUE #1232] fix(process): interrupted capture CLI leaves its owned target running #430

Closed
opened 2026-10-15 18:25:00 +00:00 by bsakaguchi · 0 comments
Owner

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

Summary

Interrupting capture-process with SIGINT ends the CLI process without stopping the target that it launched in its PTY. A finite owned Node fixture that handles SIGHUP remains alive after the CLI exits and subsequently writes a file. This report concerns capture cleanup; it does not require a cancellation JSON document from a CLI command whose output contract permits silence after interruption.

Steps to reproduce

Use the package baseline below. Save the following as repro.mjs in a disposable directory and run node repro.mjs. The script starts only its own finite fixture, sends SIGINT to the owned npx launch group after the target has announced readiness, observes the later file write, waits for the target's self-termination, and removes its fixture directory.

import { spawn } from 'node:child_process';
import { mkdtempSync, writeFileSync, existsSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const root = mkdtempSync(join(tmpdir(), 'rea-interrupt-repro-'));
const pidPath = join(root, 'target.pid');
const donePath = join(root, 'later.txt');
const targetPath = join(root, 'target.mjs');
writeFileSync(targetPath, `import { writeFileSync } from 'node:fs';
process.on('SIGHUP', () => {});
writeFileSync(process.argv[2], String(process.pid));
setTimeout(() => writeFileSync(process.argv[3], 'ran after interrupt'), 1500);
setTimeout(() => process.exit(0), 3500);
`);
const scenarioPath = join(root, 'scenario.json');
writeFileSync(scenarioPath, JSON.stringify({ executable: process.execPath, arguments: [targetPath, pidPath, donePath], working_directory: root, timeout_ms: 10000 }));
const cli = spawn('npx', ['-y', 'rea-agents@6.1.0', 'capture-process', scenarioPath, '--json'], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
const exited = new Promise(resolve => cli.on('exit', (code, signal) => resolve({ code, signal })));
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const deadline = Date.now() + 15000;
while (!existsSync(pidPath)) { if (Date.now() > deadline) throw new Error('target did not start'); await delay(10); }
const targetPid = Number(readFileSync(pidPath, 'utf8'));
process.kill(-cli.pid, 'SIGINT');
console.log('CLI exit', await exited);
await delay(300);
let alive = true;
try { process.kill(targetPid, 0); } catch { alive = false; }
console.log('target alive after CLI exit', alive);
await delay(1400);
console.log('later file written', existsSync(donePath));
await delay(2200);
rmSync(root, { recursive: true, force: true });

Expected behavior

An interrupted process capture should reach the existing owned-process cleanup path and stop its launched target. docs/process-capture.md states that cancellation and timeout use the same owned-process cleanup path. docs/mcp-contracts.md, under Progress and cancellation, states that CLI SIGINT is translated into the same AbortSignal used by providers and that controlled-process cleanup still applies. The fixture's SIGHUP handler should not bypass the capture's owned cleanup.

Actual behavior

The CLI exits with signal: "SIGINT"; stdout and stderr are empty in the observed trials. The target is still alive 300 ms after CLI exit, and later.txt is written after interruption. This reproduced with the published npx runner, the release source build, and the main source build. All fixtures self-terminated. As a discriminating control, cancelling an active capture_process_scenario request over MCP stopped the same SIGHUP-handling fixture before the first 503 ms observation and prevented its delayed file write.

Evidence

  • Expected source: docs/process-capture.md, Capture a command, cancellation/timeout cleanup paragraph; docs/mcp-contracts.md, Progress and cancellation, CLI SIGINT paragraph.
  • Failure source: Executed npx launch-group SIGINT reproduction above and the direct release/main CLI launch reproductions, each showing a surviving target and a later file write.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Runtime or environment

REA npm package 6.1.0; release source revision ae9aaee16b9c738d761d3938a3b34fdf37e581b5; main source revision f0da05f67fff1411cf2f7a48a752ee7b348c71ff; Node.js v26.10.0; macOS 27.0.1 arm64. The workload is an owned, finite Node process with a SIGHUP handler; no deep analysis provider is involved.

Restoration check

Repeat the fixture and SIGINT sequence. After the interrupted CLI has finished its owned cleanup, the target should be gone and the delayed file should not be written. Preserve the successful active-MCP cancellation control and normal non-interrupted capture behavior. No particular cancellation output representation is required by this issue.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Interruption of a supported process-capture CLI leaves the selected workload executing after its supervising CLI has ended; the bounded reproduction demonstrates a subsequent filesystem side effect. Longer-lived user workloads can keep running, but this report did not execute such workloads.
  • Workaround status: available
  • Workaround basis: Use capture_process_scenario over MCP and cancel while the root target is active; the same fixture was stopped and its delayed file write was prevented in the observed control.
Originally created by @coygeek on GitHub (Oct 9, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1232 ## Summary Interrupting `capture-process` with SIGINT ends the CLI process without stopping the target that it launched in its PTY. A finite owned Node fixture that handles SIGHUP remains alive after the CLI exits and subsequently writes a file. This report concerns capture cleanup; it does not require a cancellation JSON document from a CLI command whose output contract permits silence after interruption. ## Steps to reproduce Use the package baseline below. Save the following as `repro.mjs` in a disposable directory and run `node repro.mjs`. The script starts only its own finite fixture, sends SIGINT to the owned npx launch group after the target has announced readiness, observes the later file write, waits for the target's self-termination, and removes its fixture directory. ```js import { spawn } from 'node:child_process'; import { mkdtempSync, writeFileSync, existsSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const root = mkdtempSync(join(tmpdir(), 'rea-interrupt-repro-')); const pidPath = join(root, 'target.pid'); const donePath = join(root, 'later.txt'); const targetPath = join(root, 'target.mjs'); writeFileSync(targetPath, `import { writeFileSync } from 'node:fs'; process.on('SIGHUP', () => {}); writeFileSync(process.argv[2], String(process.pid)); setTimeout(() => writeFileSync(process.argv[3], 'ran after interrupt'), 1500); setTimeout(() => process.exit(0), 3500); `); const scenarioPath = join(root, 'scenario.json'); writeFileSync(scenarioPath, JSON.stringify({ executable: process.execPath, arguments: [targetPath, pidPath, donePath], working_directory: root, timeout_ms: 10000 })); const cli = spawn('npx', ['-y', 'rea-agents@6.1.0', 'capture-process', scenarioPath, '--json'], { detached: true, stdio: ['ignore', 'pipe', 'pipe'] }); const exited = new Promise(resolve => cli.on('exit', (code, signal) => resolve({ code, signal }))); const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); const deadline = Date.now() + 15000; while (!existsSync(pidPath)) { if (Date.now() > deadline) throw new Error('target did not start'); await delay(10); } const targetPid = Number(readFileSync(pidPath, 'utf8')); process.kill(-cli.pid, 'SIGINT'); console.log('CLI exit', await exited); await delay(300); let alive = true; try { process.kill(targetPid, 0); } catch { alive = false; } console.log('target alive after CLI exit', alive); await delay(1400); console.log('later file written', existsSync(donePath)); await delay(2200); rmSync(root, { recursive: true, force: true }); ``` ## Expected behavior An interrupted process capture should reach the existing owned-process cleanup path and stop its launched target. `docs/process-capture.md` states that cancellation and timeout use the same owned-process cleanup path. `docs/mcp-contracts.md`, under Progress and cancellation, states that CLI SIGINT is translated into the same AbortSignal used by providers and that controlled-process cleanup still applies. The fixture's SIGHUP handler should not bypass the capture's owned cleanup. ## Actual behavior The CLI exits with `signal: "SIGINT"`; stdout and stderr are empty in the observed trials. The target is still alive 300 ms after CLI exit, and `later.txt` is written after interruption. This reproduced with the published npx runner, the release source build, and the main source build. All fixtures self-terminated. As a discriminating control, cancelling an active `capture_process_scenario` request over MCP stopped the same SIGHUP-handling fixture before the first 503 ms observation and prevented its delayed file write. ## Evidence - Expected source: `docs/process-capture.md`, Capture a command, cancellation/timeout cleanup paragraph; `docs/mcp-contracts.md`, Progress and cancellation, CLI SIGINT paragraph. - Failure source: Executed npx launch-group SIGINT reproduction above and the direct release/main CLI launch reproductions, each showing a surviving target and a later file write. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ## Runtime or environment REA npm package 6.1.0; release source revision `ae9aaee16b9c738d761d3938a3b34fdf37e581b5`; main source revision `f0da05f67fff1411cf2f7a48a752ee7b348c71ff`; Node.js v26.10.0; macOS 27.0.1 arm64. The workload is an owned, finite Node process with a SIGHUP handler; no deep analysis provider is involved. ## Restoration check Repeat the fixture and SIGINT sequence. After the interrupted CLI has finished its owned cleanup, the target should be gone and the delayed file should not be written. Preserve the successful active-MCP cancellation control and normal non-interrupted capture behavior. No particular cancellation output representation is required by this issue. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Interruption of a supported process-capture CLI leaves the selected workload executing after its supervising CLI has ended; the bounded reproduction demonstrates a subsequent filesystem side effect. Longer-lived user workloads can keep running, but this report did not execute such workloads. - Workaround status: available - Workaround basis: Use `capture_process_scenario` over MCP and cancel while the root target is active; the same fixture was stopped and its delayed file write was prevented in the observed control.
bsakaguchi 2026-10-15 18:25:00 +00:00
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#430
No description provided.