fix(query): don't close stdin on a result frame while tasks are in flight#1103
Open
yayayouyou wants to merge 2 commits into
Open
fix(query): don't close stdin on a result frame while tasks are in flight#1103yayayouyou wants to merge 2 commits into
yayayouyou wants to merge 2 commits into
Conversation
…ight A result frame marks the end of one turn, not the end of the run: a background Task keeps running past it and still needs stdin for hook and SDK-MCP control responses. wait_for_result_and_end_input() closed stdin on the first result frame, so a still-running subagent's SDK-MCP tool calls failed with "Stream closed" and its PreToolUse hooks were silently bypassed (built-in tools kept executing without the hook callback). Track in-flight tasks from the task_started / task_notification / terminal task_updated lifecycle frames (via the existing TERMINAL_TASK_STATUSES) and only treat a result frame as run-ending when no tasks are in flight. Each task completion wakes the parent for a follow-up turn that ends in another result frame, so stdin still closes promptly — including for chained background tasks — and the reader's finally block still guarantees the waiter is unblocked if the process exits early. Fixes anthropics#1088. Used AI assistance; reviewed and tested by me (repro'd the "Stream closed" failure end-to-end against the live CLI before the fix and verified the same scenario passes after; regression tests fail on current main; ruff, mypy, and the full unit suite green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes a streaming-mode stdin teardown bug where Query.wait_for_result_and_end_input() closed stdin on the first result frame even when background Tasks were still running, breaking subsequent hook / SDK-MCP control responses (issue #1088).
Changes:
- Track in-flight background task IDs from
task_started/ terminaltask_notification/ terminaltask_updatedframes. - Treat a
resultframe as run-ending (and thus eligible to trigger stdin closure) only when no tasks are in flight. - Add regression + unit tests covering the intermediate-result + background-task lifecycle.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/claude_agent_sdk/_internal/query.py |
Track task lifecycle and defer stdin closure until a result arrives with no in-flight tasks. |
tests/test_query.py |
Add tests asserting stdin remains open across intermediate results while tasks are in flight, plus lifecycle unit coverage. |
Comments suppressed due to low confidence (1)
src/claude_agent_sdk/_internal/query.py:877
- This debug log still says "Waiting for first result", but the code now waits for a run-ending result (a result with no tasks in flight). Adjusting the message will make logs match actual behavior during debugging.
logger.debug(
"Waiting for first result before closing stdin "
f"(sdk_mcp_servers={len(self.sdk_mcp_servers)}, "
f"has_hooks={bool(self.hooks)})"
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+132
to
+134
| # Track first result for proper stream closure with SDK MCP servers | ||
| self._first_result_event = anyio.Event() | ||
| # Task IDs of started-but-not-finished tasks. A result frame only ends |
Address Copilot review on anthropics#1103: the _first_result_event comment and the debug log still said "first result", but the waiter now wakes on a run-ending result (a result frame with no tasks in flight). Wording only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
Fixes #1088.
A
resultframe marks the end of one turn, not the end of the run: a background Task keeps running past it and still needs stdin for hook and SDK-MCP control responses.Query.wait_for_result_and_end_input()closed stdin on the first result frame, which broke everything a still-running subagent needed afterwards:"Stream closed"(the error reported in SDK closes stdin on first result frame, breaking bidirectional control from nested Task/Agent subagents #1088), andReproduction
Reproduced end-to-end against the live CLI (2.1.206) with
query()+ a PreToolUse hook + an in-process SDK-MCP server whose tool sleeps 12s, called from arun_in_background: trueTask. Observed timeline onmain:The second result frame is the key observation: result frames are per-turn, so closing stdin on the first one is the bug. (On current CLI, two sequential foreground subagents — the issue's literal scenario — no longer emit intermediate result frames; background tasks are the deterministic trigger.)
Fix
Track in-flight tasks from the
task_started/task_notification/ terminaltask_updatedlifecycle frames (using the existingTERMINAL_TASK_STATUSES, whose docstring already prescribes exactly this clearing behavior), and only treat a result frame as run-ending when no tasks are in flight.Why not the issue's first suggestion (keep stdin open until
close()whenever hooks/SDK-MCP are registered): the CLI in stream-json mode only exits on stdin EOF, so that would hang one-shotquery()forever. The narrower rule preserves prompt closure:finallystill unblocks the waiter, so no new hang mode is introduced.Tests
test_result_with_inflight_task_keeps_stdin_open(parametrized over both drain frames:task_notificationand terminaltask_updatedpatch) — asserts stdin stays open across an intermediate result and closes on the first result with no tasks in flight. Fails on current main, passes with the fix.test_track_task_lifecycle_unit— add/non-terminal/terminal/unknown-id/missing-id transitions.Used AI assistance; reviewed and tested by me.
🤖 Generated with Claude Code