query() closes stdin while background subagents are still running, causing "Stream closed" transport death
Summary
When using query(), the SDK closes stdin to the CLI process after the first result message. If background subagents (run_in_background: true) are still in-flight inside the CLI, their subsequent control requests fail with "Stream closed". This kills all tool calls for the remainder of the session.
Root cause
query() sets isSingleUserTurn=true. When a result message arrives in Query.readMessages(), firstResultReceivedResolve fires, which unblocks Query.streamInput(), which calls this.transport.endInput() - closing stdin to the CLI.
Inside the CLI, the stdin reader loop completes when stdin closes, setting this.inputClosed = true. All subsequent sendRequest() calls throw Error("Stream closed").
Background subagents spawned during the turn are still running inside the CLI process. Any control request they trigger (permission checks, tool calls) hits inputClosed=true and fails. This is a race on the Node event loop between the stdin-close event propagating and the next control request firing.
Trace evidence
Instrumented both sdk.mjs and cli.js with TRACE logging:
21:07:09 [CLI createCanUseTool] tool=Agent agentId=main inputClosed=false -- agent spawned
21:07:12 [CLI createCanUseTool] tool=Bash agentId=aa43c... inputClosed=false -- subagent tries tool
21:07:16 [SDK readMessages] GOT RESULT: isSingleUserTurn=true firstResultReceived_before=false -- main turn completes
21:07:16 [CLI read()] stdin loop ENDED. Setting inputClosed=true -- SDK closed stdin
21:07:23 [CLI createCanUseTool] tool=Bash agentId=main inputClosed=true -- next request
21:07:23 [CLI sendRequest] BLOCKED by inputClosed=true! subtype=can_use_tool -- DEAD
Reproduction
- Use
query()with background agents - Spawn a background agent that takes longer than the main turn to complete
- The main turn emits a
result, SDK closes stdin - Background agent (or next turn) tries a control request -> "Stream closed"
The race window is small when the background agent runs successfully (it usually finishes around the same time as the main turn). The window widens significantly when the background agent fails quickly (e.g. a tool denial), because the main turn completes while the agent is still being cleaned up inside the CLI.
Impact
- All Bash and MCP tool calls fail for the remainder of the CLI process
- Built-in tools (Read, Grep, Glob, Edit) continue working (they don't use
sendRequest) - Self-heals when a new CLI process is spawned
- Affects all SDK consumers using
query()with background agents - Observed across multiple independent bot instances with different configurations
Suggested fixes
Option A - Don't close stdin while background tasks are running. streamInput should wait for all in-flight background tasks to complete before calling endInput(). The CLI could report active task count, or the SDK could track agent lifecycle.
Option B - Don't close stdin at all for multi-turn sessions. When resume is used to continue conversations across query() calls, stdin closure between turns kills background work. Keep stdin open until the session is explicitly closed.
Option C - Use a control message instead of stdin closure. Instead of closing stdin (irreversible, races with in-flight work), send a { type: "turn_complete" } control message. The CLI can act on it without losing the communication channel.
Related
This interacts with #27203 (background subagent Bash denial). The permission denial in #27203 causes subagents to fail fast, which dramatically widens the race window for this stdin-close bug. But this bug exists independently - any background agent that outlives the main turn's result message can trigger it.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗