fix(acp): handle terminal stdout and stderr stream errors - #507
fix(acp): handle terminal stdout and stderr stream errors#507SebTardif wants to merge 2 commits into
Conversation
ACP terminal/create attached data listeners on the child pipes with no error listeners. A broken pipe after the child exited could kill the ACP process. Ignore pipe-death codes on those streams so wait/release can still finish. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
CI type-aware oxlint rejected the emit() argument assertions. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
Codex review: needs changes before merge. Reviewed August 29, 2026, 9:59 PM ET / August 30, 2026, 01:59 UTC. ClawSweeper reviewWhat this changesThe PR adds stdout and stderr error listeners to ACP-created child terminals and tests that pipe-closure errors do not block terminal cleanup. Merge readinessThe patch correctly prevents unhandled child-pipe error events from terminating the ACP host, and the supplied compiled before/after trace is sufficient. One minor release-note update remains under repository policy. Priority: P2 Review scores
Verification
How this fits togetherACP terminal creation starts child commands for an agent session and captures their output streams. Exit state then drives terminal wait, kill, and release operations. flowchart LR
A[ACP terminal request] --> B[Terminal creation]
B --> C[Child process]
C --> D[Output streams]
D --> E[Error listener]
C --> F[Exit status]
E --> G[Wait and release]
F --> G
Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Technical reviewBest possible solution: Keep the direct child-stream listener repair and add a concise Unreleased ACP/terminal fix note before landing. Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible with high confidence: current main has the unhandled stream path, and the supplied compiled before/after harness exercises the managed child stream and cleanup lifecycle. Is this the best way to solve the issue? Yes. Attaching handlers at the child stdout/stderr ownership point is the narrowest way to prevent EventEmitter error termination without changing terminal exit semantics. Full review comments:
Overall correctness: patch is correct AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ef6b81e71756. LabelsLabel justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
History |
What Problem This Solves
Fixes an issue where users running an ACP session that calls
terminal/createwould lose the whole client process when the child command died and left a broken stdout or stderr pipe. Node treats anerrorevent on those streams as fatal when no listener is attached, soEPIPEorEIOafter the child exits could kill the ACP host instead of lettingterminal/wait_for_exitandterminal/releasefinish.Why This Change Was Made
terminal/createalready attachesdatalisteners on the child pipes and settles the terminal from the processexitevent. It did not attacherrorlisteners. This change ignores pipe-death codes on stdout and stderr (EPIPE,EIO,ECONNRESET,ERR_STREAM_DESTROYED) and never throws or exits the host from those streams. The existingexithandler still records the status, so wait and release can finish.This is not the CLI
process.stdoutpath from closed #441. That helper throws non-EPIPE and can exit. Child pipes must not. Same-file #501 timed out hung process-list helpers; it did not cover streamerrorevents. The child stdio pattern already used on queue-owner stderr ischild.stderr.on("error", () => {}).The missing listeners date to
1c94396(2026-02-19, "feat: implement full stable ACP spec coverage"), about 180 days onmain.User Impact
An agent that creates a terminal, then sees the child die with a broken pipe, keeps the ACP session alive. Operators can still wait for exit and release the terminal. The host no longer exits because a leftover child pipe raised
EPIPE.Evidence
terminal output from live
nodeagainst compiledcreateTerminal(dist-test/src/acp/terminal-manager.js).Before, on unpatched
src/acp/terminal-manager.tscompiled todist-test, the same script emits{ code: "EPIPE" }on the child stdout and the compiled path throws. Exit code 1:After, on the patched compiled
createTerminalpath, the same emit stays in-process.kill/waitForTerminalExit/releasefinish and the host exits 0:The Node contract with no listener is the same class. A raw
EventEmitterplusemit("error", { code: "EPIPE" })ends the process with exit 1:Patched bundle contains the listeners:
Real behavior proof
Behavior or issue addressed: ACP
terminal/createattacheddatalisteners on the child stdout and stderr pipes with noerrorlisteners. After the child died, a broken pipe (EPIPE/EIO) was an unhandled EventEmitter error and could kill the ACP process. Wait and release never got a chance to finish.Real environment tested: macOS Darwin 25.6.0 arm64, Node v26.7.0, full clone of
openclaw/acpxat/tmp/pr-acpx-term-err, compileddist-test/src/acp/terminal-manager.jsplus the production bundle underdist/.Exact steps or command run after this patch:
That script imports compiled
TerminalManager, callscreateTerminalwithnode -e "setInterval(() => {}, 1000)", emits{ code: "EPIPE" }on the child stdout and{ code: "EIO" }on stderr, thenkillTerminal,waitForTerminalExit, andreleaseTerminal.Evidence after fix: terminal output from the patched compiled
createTerminalpath:The same script against unpatched compiled
createTerminalprinted"code": "EPIPE"and exited 1. A raw EventEmitter with no listener also exited 1 withError: broken pipe.Observed result after fix: The ACP host process stayed alive (
hostAlive: true). Child pipeEPIPE/EIOno longer threw.waitForTerminalExitreturnedsignal: "SIGTERM"andreleaseTerminalcompleted.What was not tested: A live network ACP agent driving
terminal/createover stdio to a third-party coding agent. Windows-only pipe teardown. Writing to child stdin (stdiofor stdin isignoretoday).