Commit e6e07f1
fix(query): don't close stdin on a result frame while tasks are in flight (#1103)
## Summary
Fixes #1088.
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.
`Query.wait_for_result_and_end_input()` closed stdin on the *first*
result frame, which broke everything a still-running subagent needed
afterwards:
- its SDK-MCP tool calls failed with exactly **`"Stream closed"`** (the
error reported in #1088), and
- its **PreToolUse hooks were silently bypassed** — built-in tools kept
executing with no hook callback delivered, so deny-gate hooks stopped
gating.
## Reproduction
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 a `run_in_background: true` Task. Observed timeline on
`main`:
```
[19.3s] parent turn ends → ResultMessage #1 → SDK closes stdin (task still running)
[25s+] subagent's ToolSearch calls execute with NO PreToolUse callback (hooks silently bypassed)
[~25s] subagent calls the SDK-MCP tool → tool_result is_error=true: "Stream closed" (×2 retries)
[47.9s] ResultMessage #2 arrives (task completion wakes the parent for a follow-up turn)
```
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` /
terminal `task_updated` lifecycle frames (using the existing
`TERMINAL_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-shot `query()` forever. The
narrower rule preserves prompt closure:
- no background tasks → behavior unchanged (first result still closes
stdin; existing tests cover this),
- background task in flight → stdin stays open; its completion wakes the
parent for a follow-up turn that ends in another result frame, which
closes stdin (this also makes *chained* background tasks work),
- early process exit → the reader's `finally` still 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_notification` and terminal `task_updated`
patch) — 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.
- Full suite: 1059 passed, 5 skipped (asyncio + trio); ruff and mypy
clean.
- E2E re-run of the reproduction against the live CLI with the fix: hook
fires for the background subagent's calls, the SDK-MCP tool executes and
returns its result, stdin closes exactly at the final result frame, run
exits normally.
Used AI assistance; reviewed and tested by me.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: shihyayou <shihyayouyou@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>1 parent 74fe445 commit e6e07f1
2 files changed
Lines changed: 426 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
41 | 58 | | |
42 | 59 | | |
43 | 60 | | |
| |||
128 | 145 | | |
129 | 146 | | |
130 | 147 | | |
131 | | - | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
132 | 152 | | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
133 | 159 | | |
134 | 160 | | |
135 | 161 | | |
| |||
293 | 319 | | |
294 | 320 | | |
295 | 321 | | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
296 | 327 | | |
297 | 328 | | |
298 | 329 | | |
299 | 330 | | |
300 | 331 | | |
301 | 332 | | |
302 | 333 | | |
303 | | - | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
304 | 349 | | |
305 | 350 | | |
306 | 351 | | |
| |||
806 | 851 | | |
807 | 852 | | |
808 | 853 | | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
809 | 911 | | |
810 | | - | |
| 912 | + | |
811 | 913 | | |
812 | 914 | | |
813 | | - | |
814 | | - | |
815 | | - | |
816 | | - | |
817 | | - | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
818 | 924 | | |
819 | 925 | | |
820 | 926 | | |
821 | | - | |
| 927 | + | |
822 | 928 | | |
823 | 929 | | |
824 | 930 | | |
| |||
0 commit comments