fix(agent-core): handle stdout/stderr stream errors in harness exec#101370
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed July 7, 2026, 2:32 AM ET / 06:32 UTC. Summary PR surface: Source +32, Tests +116. Total +148 across 2 files. Reproducibility: yes. for source-level reproduction: current main lacks stdout/stderr stream-error listeners in Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Copy recommended automerge instructionNext step before merge
Security Review findings
Review detailsBest possible solution: Keep the focused runtime guard, but mock process-tree termination in the tests and add redacted after-fix terminal or log proof for the stream-error path before merge. Do we have a high-confidence way to reproduce the issue? Yes for source-level reproduction: current main lacks stdout/stderr stream-error listeners in Is this the best way to solve the issue? No as submitted: the runtime fix is the right owner-boundary mitigation, but the tests should not call the real process-tree killer with a hard-coded PID. Mocking Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 33163d427391. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +32, Tests +116. Total +148 across 2 files. View PR surface stats
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (3 earlier review cycles)
|
|
@clawsweeper re-review — fixed test mock pattern (vi.hoisted), lint (curly braces), and added stream-error proof showing error listeners prevent uncaught exception. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Register error handlers on stdout and stderr streams in NodeExecutionEnv.exec() to prevent uncaught exceptions when a pipe breaks (e.g. EPIPE after child process exit). Without these listeners, Node.js throws an uncaught exception that crashes the entire agent-core process. Co-Authored-By: Claude <[email protected]>
- Use vi.hoisted pattern for spawnMock to fix ReferenceError in CI - Wrap one-line if guard in braces per repo curly rule Co-Authored-By: Claude <[email protected]>
7d6df6a to
74e7e51
Compare
|
Land-ready proof
Release-note context: output-stream failures now fail the command or shell probe normally instead of escaping as uncaught EventEmitter errors. The native normal-PR gate keeps root changelog edits release-owned. |
|
Merged via squash.
|
Refactor the inline stream error handling into a reusable listenForChildOutputErrors helper, matching the pattern established in the merged openclaw#101370 (agent-core). The helper registers error listeners on stdout/stderr via a shared loop, keeping the pattern consistent across the codebase. Co-Authored-By: Claude <[email protected]>
…mand (#101402) * fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand Register error handlers on stdout and stderr streams in runCliCommand() to prevent uncaught exceptions when a pipe breaks (e.g. EPIPE after child process exit). Without these listeners, Node.js throws an uncaught exception that crashes the process. Co-Authored-By: Claude <[email protected]> * fix(memory-host-sdk): extract listenForChildOutputErrors helper Refactor the inline stream error handling into a reusable listenForChildOutputErrors helper, matching the pattern established in the merged #101370 (agent-core). The helper registers error listeners on stdout/stderr via a shared loop, keeping the pattern consistent across the codebase. Co-Authored-By: Claude <[email protected]> * fix(memory-host-sdk): harden qmd stream failures * fix(memory-host-sdk): harden qmd stream failures * docs(changelog): note qmd stream hardening * chore: keep release changelog out of contributor PR --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…penclaw#101370) * fix(agent-core): handle stdout/stderr stream errors in harness exec Register error handlers on stdout and stderr streams in NodeExecutionEnv.exec() to prevent uncaught exceptions when a pipe breaks (e.g. EPIPE after child process exit). Without these listeners, Node.js throws an uncaught exception that crashes the entire agent-core process. Co-Authored-By: Claude <[email protected]> * fix(agent-core): address review — hoist-safe mock and curly lint - Use vi.hoisted pattern for spawnMock to fix ReferenceError in CI - Wrap one-line if guard in braces per repo curly rule Co-Authored-By: Claude <[email protected]> * chore: keep release notes in PR context --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…mand (openclaw#101402) * fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand Register error handlers on stdout and stderr streams in runCliCommand() to prevent uncaught exceptions when a pipe breaks (e.g. EPIPE after child process exit). Without these listeners, Node.js throws an uncaught exception that crashes the process. Co-Authored-By: Claude <[email protected]> * fix(memory-host-sdk): extract listenForChildOutputErrors helper Refactor the inline stream error handling into a reusable listenForChildOutputErrors helper, matching the pattern established in the merged openclaw#101370 (agent-core). The helper registers error listeners on stdout/stderr via a shared loop, keeping the pattern consistent across the codebase. Co-Authored-By: Claude <[email protected]> * fix(memory-host-sdk): harden qmd stream failures * fix(memory-host-sdk): harden qmd stream failures * docs(changelog): note qmd stream hardening * chore: keep release changelog out of contributor PR --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…penclaw#101370) * fix(agent-core): handle stdout/stderr stream errors in harness exec Register error handlers on stdout and stderr streams in NodeExecutionEnv.exec() to prevent uncaught exceptions when a pipe breaks (e.g. EPIPE after child process exit). Without these listeners, Node.js throws an uncaught exception that crashes the entire agent-core process. Co-Authored-By: Claude <[email protected]> * fix(agent-core): address review — hoist-safe mock and curly lint - Use vi.hoisted pattern for spawnMock to fix ReferenceError in CI - Wrap one-line if guard in braces per repo curly rule Co-Authored-By: Claude <[email protected]> * chore: keep release notes in PR context --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…mand (openclaw#101402) * fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand Register error handlers on stdout and stderr streams in runCliCommand() to prevent uncaught exceptions when a pipe breaks (e.g. EPIPE after child process exit). Without these listeners, Node.js throws an uncaught exception that crashes the process. Co-Authored-By: Claude <[email protected]> * fix(memory-host-sdk): extract listenForChildOutputErrors helper Refactor the inline stream error handling into a reusable listenForChildOutputErrors helper, matching the pattern established in the merged openclaw#101370 (agent-core). The helper registers error listeners on stdout/stderr via a shared loop, keeping the pattern consistent across the codebase. Co-Authored-By: Claude <[email protected]> * fix(memory-host-sdk): harden qmd stream failures * fix(memory-host-sdk): harden qmd stream failures * docs(changelog): note qmd stream hardening * chore: keep release changelog out of contributor PR --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…penclaw#101370) * fix(agent-core): handle stdout/stderr stream errors in harness exec Register error handlers on stdout and stderr streams in NodeExecutionEnv.exec() to prevent uncaught exceptions when a pipe breaks (e.g. EPIPE after child process exit). Without these listeners, Node.js throws an uncaught exception that crashes the entire agent-core process. Co-Authored-By: Claude <[email protected]> * fix(agent-core): address review — hoist-safe mock and curly lint - Use vi.hoisted pattern for spawnMock to fix ReferenceError in CI - Wrap one-line if guard in braces per repo curly rule Co-Authored-By: Claude <[email protected]> * chore: keep release notes in PR context --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…mand (openclaw#101402) * fix(memory-host-sdk): handle stdout/stderr stream errors in runCliCommand Register error handlers on stdout and stderr streams in runCliCommand() to prevent uncaught exceptions when a pipe breaks (e.g. EPIPE after child process exit). Without these listeners, Node.js throws an uncaught exception that crashes the process. Co-Authored-By: Claude <[email protected]> * fix(memory-host-sdk): extract listenForChildOutputErrors helper Refactor the inline stream error handling into a reusable listenForChildOutputErrors helper, matching the pattern established in the merged openclaw#101370 (agent-core). The helper registers error listeners on stdout/stderr via a shared loop, keeping the pattern consistent across the codebase. Co-Authored-By: Claude <[email protected]> * fix(memory-host-sdk): harden qmd stream failures * fix(memory-host-sdk): harden qmd stream failures * docs(changelog): note qmd stream hardening * chore: keep release changelog out of contributor PR --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
Summary
Problem:
NodeExecutionEnv.exec()— the core execution engine for all agent shell commands — spawns child processes with piped stdout/stderr but never registers error listeners on those streams. Per Node.js docs, a Readable stream that emitserrorwith no listener throws an uncaught exception that crashes the entire gateway process.Solution: Add stream error handlers on
child.stdoutandchild.stderrthat kill the child process and settle the exec promise with aspawn_error. Follows the identical pattern already merged in docker.ts (#101031), grep.ts, and other stream-error fixes.What changed:
packages/agent-core/src/harness/env/nodejs.ts—exec()method (+17 lines): stream error guard on stdout/stderr.packages/agent-core/src/harness/env/nodejs.test.ts(+98 lines): three new test cases covering stdout error, stderr error, and normal completion paths.What did NOT change:
runCommand()(line 143) — used only forwhich bashshell detection during startup, low risk of pipe failure. No public API, config, or CLI changes.Design decision: Stream errors kill the child (
onAbort()) and reject the promise rather than silently ignoring. A broken pipe means captured output is incomplete and the exit code may not reflect the true outcome.What Problem This Solves
Problem: When a child process spawned by
exec()exits or is killed while stdout/stderr pipes still have buffered data, the OS closes the pipe and the stream emitserror: EPIPE. With no error listener, Node.js throws an uncaught exception that crashes the gateway. This can happen during rapid abort (user Ctrl+C), process group kill (timeout), or kernel pipe exhaustion.Why This Change Was Made: The fix follows the same defensive pattern already applied in docker.ts (#101031), grep.ts, tool-search.ts (#101022), and codex-supervisor (#100785).
bash.tswas not affected because it delegates towaitForChildProcesswhich already registers stream error handlers.User Impact: Normal operation unchanged. Prevents rare but fatal gateway crashes during agent command execution.
Root Cause
Trigger condition: Child process spawned by
exec()is killed (abort/timeout) while pipe data is still being drained. OS closes pipe → stream emitserror: EPIPE→ no listener → uncaught exception.Root cause analysis:
errorlistener registered on the streamexec()only registereddataand process-levelerror/closeeventsbash.tsuseswaitForChildProcess(which adds error handlers internally), butexec()manages child processes directlyAffected scope:
packages/agent-core/src/harness/env/nodejs.ts—exec()method. All agent shell commands through agent-core.Linked context
exec()— stream error handlers on stdout + stderrrunCommand()— shell detection only, low riskReal behavior proof
Behavior addressed: Uncaught EPIPE on child stdout/stderr crashes the gateway; fix registers error listeners that reject the exec promise.
Real environment tested: Linux x86_64, Node.js 22,
/bin/bashExact steps or command run after this patch:
Source verification:
Real execution — stdout:
Real execution — stderr:
After-fix evidence:
Observed result after the fix: Three references to
onStreamErrorin source confirm the guard is present. Real execution vianode --import tsxsucceeds with correct stdout/stderr capture. Pre-commit hooks pass.What was not tested: Real EPIPE trigger on a live child stream — requires precise mid-output kill timing, not reproducible deterministically across environments. The fix follows the identical pattern as already-merged docker.ts (#101031) and grep.ts, both validated in production. Unit tests cover mock-based stream error scenarios.
Tests and validation
Unit tests
Three new test cases in
packages/agent-core/src/harness/env/nodejs.test.ts:rejects with spawn_error when stdout/stderr stream emits an error— param test for both streamskeeps the other stream guarded after a stdout error— late stderr error must not throwcompletes normally when no stream errors occur— positive controlMock pattern uses
vi.mock("node:child_process")withPassThroughstreams, identical togrep.stream-errors.test.tsanddocker.test.ts.Integration
Real execution verified via
node --import tsxas shown in Real behavior proof above.Risk checklist
/bin/bashshell, LinuxMerge-risk: Low. The change is exclusively additive (adds error listeners) and scoped to
exec(). TherunCommand()helper used for shell detection is not modified. Stream errors that previously crashed the process are now caught and reported asspawn_error, which is the same error code already used for child process spawn failures — existing error handlers in callers will treat it correctly.Current review state
AI-assisted.