[GH-ISSUE #6445] [Bug] capture-process records exit code zero for a signal-terminated target #3861

Closed
opened 2026-10-31 00:44:43 +00:00 by bsakaguchi · 0 comments
Owner

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

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

node scripts/rea.mjs capture-process scenario.json --format json returns successful capture Evidence whose normalized_result.exit contains code: 0 when a scenario event terminates the target with SIGKILL or SIGTERM. The nonzero terminating signal is preserved. The problem is the captured target's numeric exit code; REA's own CLI exiting 0 after a completed capture is not the reported defect.

Reproduction

From a fresh checkout at 9eb829aca34be36ff112826337996f6139f82eff, prepare the locked runtime with npm ci && npm run build:cached. Then run this block from the checkout root on a supported macOS/Linux capture host:

WORK="$(mktemp -d)"
export WORK
mkdir -p "$WORK/home"
node <<'JS'
const fs = require('node:fs');
const cases = {
  kill: {events: [{type: 'signal', at_ms: 200, signal: 'SIGKILL'}]},
  term: {events: [{type: 'signal', at_ms: 200, signal: 'SIGTERM'}]},
  timeout: {timeout_ms: 300},
  normal: {arguments: ['-e', 'process.exit(3)']}
};
for (const [name, extra] of Object.entries(cases)) {
  fs.writeFileSync(`${process.env.WORK}/${name}.json`, JSON.stringify({
    executable: 'node', arguments: ['-e', 'setTimeout(() => {}, 5000)'],
    timeout_ms: 5000, settle_ms: 300, ...extra
  }));
}
JS
for case_name in kill term timeout normal; do
  HOME="$WORK/home" node scripts/rea.mjs capture-process "$WORK/$case_name.json" --format json > "$WORK/$case_name.repeat.json"
done
node <<'JS'
const fs = require('node:fs');
for (const name of ['kill', 'term', 'timeout', 'normal']) {
  const capture = JSON.parse(fs.readFileSync(`${process.env.WORK}/${name}.repeat.json`, 'utf8'));
  console.log(name, JSON.stringify(capture.normalized_result.exit));
}
JS

The capture cases were executed twice each against the already built baseline. Both signal cases reproduced on both runs; the timeout and normal-exit controls also matched on both runs. Dependency installation and rebuilding were not repeated during these observations.

Expected behavior

Represent the absence of a normal target exit code as exit.code: null when a nonzero exit.signal identifies signal termination, preserving the observed signal. Keep numeric codes for normal exits, including exit 3.

The evidence ownership contract calls for preserving omission and uncertainty. The existing finalization semantics explicitly distinguish normal exit codes from signal termination and use null for the latter. That passage documents finalization.exit_code; applying the same distinction to ordinary exit.code is the expected normalization here, rather than a quotation of a universal rule already stated in that passage. Deadline reasons and their existing null-code behavior should stay intact.

Impact and scope

P2: bounded misleading process-exit evidence. A consumer that treats numeric code 0 as a normal successful target exit can misinterpret a killed target. Checking the preserved nonzero signal provides a practical workaround. No downstream consumer malfunction was tested.

Observed on macOS with native node-pty, 2/2 trials per signal. All eight captures settled as quiesced with cleanup_outcome: not_required. Linux and MCP were not executed; no cleanup failure or security boundary crossing was observed.

REA version

6.7.0 (source 9eb829aca3)

Runtime and platform

Node.js v26.12.1; macOS 27.0.1; arm64; native node-pty capture backend.

Target and artifact context

Inline finite Node timer, terminated by a caller-selected scenario signal. Controls use the same timer with a capture deadline and a Node process that exits with code 3. No external service or private target is required.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Selected fields printed from the actual CLI JSON results:

kill {"code":0,"signal":9,"reason":"exited"}
term {"code":0,"signal":15,"reason":"exited"}
timeout {"code":null,"signal":9,"reason":"timeout"}
normal {"code":3,"signal":0,"reason":"exited"}

All CLI invocations exited 0. The signal events were recorded as dispatched. These are observed public-CLI results, not synthetic test output.

Typed outcome or recovery expected

For the signal cases, retain reason: "exited" and the observed signal while returning a null normal exit code. A proposed restoration check is to rerun all four cases and verify that only the two signal cases' numeric code changes to null. No fix was tested.

Analysis or suspected cause

Source-derived explanation: buildCaptureResult accepts any nonnegative PTY exitCode for reason exited without consulting the signal. Retained observations repeat that conversion. In contrast, finalization conversion checks that the signal is zero before retaining a numeric exit code. The capture validator checks code consistency only for deadline termination.

Additional context

Searched open and closed issues. #1253 and merged #1583 concern bounded deadline finalization; #1232 concerns CLI interruption leaving a target alive; #5756 concerns browser termination diagnostics. None reports this ordinary signal-exit code conversion. NOT_ENOUGH_INFO: no last-known-good version or regression range was established.

Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6445 ### 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 `node scripts/rea.mjs capture-process scenario.json --format json` returns successful capture Evidence whose `normalized_result.exit` contains `code: 0` when a scenario event terminates the target with SIGKILL or SIGTERM. The nonzero terminating signal is preserved. The problem is the captured target's numeric exit code; REA's own CLI exiting 0 after a completed capture is not the reported defect. ### Reproduction From a fresh checkout at `9eb829aca34be36ff112826337996f6139f82eff`, prepare the locked runtime with `npm ci && npm run build:cached`. Then run this block from the checkout root on a supported macOS/Linux capture host: ```sh WORK="$(mktemp -d)" export WORK mkdir -p "$WORK/home" node <<'JS' const fs = require('node:fs'); const cases = { kill: {events: [{type: 'signal', at_ms: 200, signal: 'SIGKILL'}]}, term: {events: [{type: 'signal', at_ms: 200, signal: 'SIGTERM'}]}, timeout: {timeout_ms: 300}, normal: {arguments: ['-e', 'process.exit(3)']} }; for (const [name, extra] of Object.entries(cases)) { fs.writeFileSync(`${process.env.WORK}/${name}.json`, JSON.stringify({ executable: 'node', arguments: ['-e', 'setTimeout(() => {}, 5000)'], timeout_ms: 5000, settle_ms: 300, ...extra })); } JS for case_name in kill term timeout normal; do HOME="$WORK/home" node scripts/rea.mjs capture-process "$WORK/$case_name.json" --format json > "$WORK/$case_name.repeat.json" done node <<'JS' const fs = require('node:fs'); for (const name of ['kill', 'term', 'timeout', 'normal']) { const capture = JSON.parse(fs.readFileSync(`${process.env.WORK}/${name}.repeat.json`, 'utf8')); console.log(name, JSON.stringify(capture.normalized_result.exit)); } JS ``` The capture cases were executed twice each against the already built baseline. Both signal cases reproduced on both runs; the timeout and normal-exit controls also matched on both runs. Dependency installation and rebuilding were not repeated during these observations. ### Expected behavior Represent the absence of a normal target exit code as `exit.code: null` when a nonzero `exit.signal` identifies signal termination, preserving the observed signal. Keep numeric codes for normal exits, including exit 3. The [evidence ownership contract](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/AGENTS.md#L19) calls for preserving omission and uncertainty. The existing [finalization semantics](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/docs/process-capture.md#L376) explicitly distinguish normal exit codes from signal termination and use null for the latter. That passage documents `finalization.exit_code`; applying the same distinction to ordinary `exit.code` is the expected normalization here, rather than a quotation of a universal rule already stated in that passage. Deadline reasons and their existing null-code behavior should stay intact. ### Impact and scope P2: bounded misleading process-exit evidence. A consumer that treats numeric code 0 as a normal successful target exit can misinterpret a killed target. Checking the preserved nonzero signal provides a practical workaround. No downstream consumer malfunction was tested. Observed on macOS with native node-pty, 2/2 trials per signal. All eight captures settled as `quiesced` with `cleanup_outcome: not_required`. Linux and MCP were not executed; no cleanup failure or security boundary crossing was observed. ### REA version 6.7.0 (source 9eb829aca34be36ff112826337996f6139f82eff) ### Runtime and platform Node.js v26.12.1; macOS 27.0.1; arm64; native node-pty capture backend. ### Target and artifact context Inline finite Node timer, terminated by a caller-selected scenario signal. Controls use the same timer with a capture deadline and a Node process that exits with code 3. No external service or private target is required. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Selected fields printed from the actual CLI JSON results: ```text kill {"code":0,"signal":9,"reason":"exited"} term {"code":0,"signal":15,"reason":"exited"} timeout {"code":null,"signal":9,"reason":"timeout"} normal {"code":3,"signal":0,"reason":"exited"} ``` All CLI invocations exited 0. The signal events were recorded as dispatched. These are observed public-CLI results, not synthetic test output. ### Typed outcome or recovery expected For the signal cases, retain `reason: "exited"` and the observed signal while returning a null normal exit code. A proposed restoration check is to rerun all four cases and verify that only the two signal cases' numeric code changes to null. No fix was tested. ### Analysis or suspected cause Source-derived explanation: [buildCaptureResult](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/ProcessCaptureLifecycle.ts#L333) accepts any nonnegative PTY exitCode for reason `exited` without consulting the signal. [Retained observations](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/ProcessHarness.ts#L802) repeat that conversion. In contrast, [finalization conversion](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/process/capture/ProcessCaptureLifecycle.ts#L988) checks that the signal is zero before retaining a numeric exit code. The [capture validator](https://github.com/morluto/rea/blob/9eb829aca34be36ff112826337996f6139f82eff/src/domain/process/processCaptureValidation.ts#L257) checks code consistency only for deadline termination. ### Additional context Searched open and closed issues. #1253 and merged #1583 concern bounded deadline finalization; #1232 concerns CLI interruption leaving a target alive; #5756 concerns browser termination diagnostics. None reports this ordinary signal-exit code conversion. NOT_ENOUGH_INFO: no last-known-good version 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#3861
No description provided.