Skip to content

fix: stop losing a fast-exiting direct agent's output and close event (#5791) - #5988

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-5791
Sep 3, 2026
Merged

fix: stop losing a fast-exiting direct agent's output and close event (#5791)#5988
atomantic merged 1 commit into
mainfrom
claim/issue-5791

Conversation

@atomantic

@atomantic atomantic commented Sep 3, 2026

Copy link
Copy Markdown
Owner

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's error listener was already attached synchronously after spawn(), but stdout, stderr and close were registered far below, past await 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 its close event land on nothing: the run record, the execution lane and the activeAgents entry 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:

  • output first, so the transcript is complete — and enqueued onto transcriptWriteTail — before a buffered terminal event finalizes the run;
  • exactly one terminal event, since a failed spawn emits error and then close and the error path already finalizes;
  • one ordered queue for both streams, so interleaved stdout/stderr chunks keep their emission order.

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 on handleClose instead of the claudeProcess.on('close') registration, which is a shim now.

Test plan

  • New regression test in server/services/agentCliSpawning.test.js: the fake child emits a stdout chunk and then close from inside the updateAgent(pid) yield, and the test asserts finalizeAgent ran with the right exit code and that the pre-exit output survived in outputBuffer. 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

…#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
@atomantic
atomantic merged commit 2926bc7 into main Sep 3, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-5791 branch September 3, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Direct agent spawn can lose a fast child's close event: stdout/stderr/close listeners are attached after an await

1 participant