fix: stop losing a fast-exiting direct agent's output and close event (#5791) - #5988
Merged
Conversation
…#5791) A CLI agent spawned directly (no runner) could vanish into a permanently "running" state when it died instantly — a bad flag, an immediate auth refusal. Between spawn() and the point where spawnDirectly registered its stdout/stderr/close handlers sat `await updateAgent(agentId, { pid })`, real state-file I/O that yields for a macrotask. A child that ran its whole life cycle inside that window had its close event land on nothing, leaving the run record, the execution lane and the activeAgents entry non-terminal until the orphan reaper eventually noticed — and the stdout/stderr that would have explained the failure was dropped too. Every child listener is now attached in the same tick as spawn(), into forwarding shims that buffer into bindings declared above it. The real handler bodies (which close over state the async setup builds) are assigned as before, then the buffered events are replayed: output first, so the transcript is complete before a buffered terminal event finalizes the run, and exactly one terminal event, since a failed spawn emits both 'error' and 'close'. stdout/stderr share one ordered queue so interleaved chunks keep their emission order. Claude-Session: https://claude.ai/code/session_01Uz59AsJawa1Djqm2T9Fb8t
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A CLI agent spawned directly (no runner) could vanish into a permanently "running" state when it died instantly — a bad flag, an immediate auth refusal.
In
spawnDirectly(server/services/agentCliSpawning.js) the child'serrorlistener was already attached synchronously afterspawn(), butstdout,stderrandclosewere registered far below, pastawait updateAgent(agentId, { pid })— real state-file I/O, so it yields for a macrotask, not just a microtask. A child that ran its whole life cycle inside that window had itscloseevent land on nothing: the run record, the execution lane and theactiveAgentsentry all stayed non-terminal until the orphan reaper eventually noticed. Any stdout/stderr the child wrote before exiting — exactly the output that would have explained the failure — was dropped too.Every child listener is now attached in the same tick as
spawn(), into small forwarding shims that buffer into bindings declared just above. The real handler bodies (which close over a lot of state the async setup builds) are assigned in place as before, then the buffered events are replayed:transcriptWriteTail— before a buffered terminal event finalizes the run;errorand thencloseand the error path already finalizes;Scoped to the direct-spawn path. The runner path (
server/cos-runner/index.js) attaches its listeners synchronously already and is unaffected.server/services/agentManagement.test.js's close-handler source contract now anchors onhandleCloseinstead of theclaudeProcess.on('close')registration, which is a shim now.Test plan
server/services/agentCliSpawning.test.js: the fake child emits a stdout chunk and thenclosefrom inside theupdateAgent(pid)yield, and the test assertsfinalizeAgentran with the right exit code and that the pre-exit output survived inoutputBuffer. Verified it fails on pre-fix code (finalizeAgent: 0 calls) and passes after.cd server && npm test— 1888 files / 38070 tests passing.Closes #5791
https://claude.ai/code/session_01Uz59AsJawa1Djqm2T9Fb8t