Commit 9ee52df
authored
fix(transport): use spawn_detached for stderr reader (trio nursery corruption) (#885)
## Problem
When `query()` is iterated inside a trio nursery task with
`options.stderr`
set, cancelling or breaking out of the iteration can raise:
RuntimeError: Nursery stack corrupted: Nursery surrounding <Task ...>
was closed before the task exited
`SubprocessCLITransport.connect()` spawns the stderr reader by manually
calling `__aenter__` on an `anyio.TaskGroup`:
```python
self._stderr_task_group = anyio.create_task_group()
await self._stderr_task_group.__aenter__()
self._stderr_task_group.start_soon(self._handle_stderr)
```
On the trio backend this pushes a nursery onto the calling task's
nursery
stack. The matching `__aexit__` lives in `close()`, which is reached via
`process_query`'s `finally` → `inner.aclose()` → `transport.close()`. If
the
caller cancels the iteration (e.g. wraps the `async for` in a
`CancelScope`
and breaks early), the awaits along that cleanup path checkpoint while
the
scope is cancelled and re-raise `Cancelled` before `__aexit__` runs. The
nursery is left on the task's stack; when the task exits, trio raises
the
corruption error.
This is the same task-affinity hazard that
`_task_compat.spawn_detached()`
already solves for the read loop (`Query._read_task`). The stderr reader
predates that helper and was not migrated.
## Fix
Spawn the stderr reader via `spawn_detached()` and clean it up with
`TaskHandle.cancel()` + `await wait()`, matching the read-loop pattern
in
`_internal/query.py`. This removes the manual `__aenter__`/`__aexit__`
and
the trio task-affinity requirement.
## Repro
```python
import trio
from claude_agent_sdk import query, ClaudeAgentOptions
async def one():
opts = ClaudeAgentOptions(stderr=lambda line: None, max_turns=1)
with trio.CancelScope() as cs:
async for msg in query(prompt="hi", options=opts):
cs.cancel() # early-break after first message
async def main():
async with trio.open_nursery() as n:
for _ in range(5):
n.start_soon(one)
trio.run(main)
```
Before: `RuntimeError: Nursery stack corrupted` from the nursery
`__aexit__`.
After: clean exit.1 parent e21b457 commit 9ee52df
1 file changed
Lines changed: 11 additions & 11 deletions
Lines changed: 11 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | 16 | | |
18 | 17 | | |
19 | 18 | | |
20 | 19 | | |
21 | 20 | | |
22 | 21 | | |
23 | 22 | | |
| 23 | + | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
| 53 | + | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| |||
463 | 463 | | |
464 | 464 | | |
465 | 465 | | |
466 | | - | |
467 | | - | |
468 | | - | |
469 | | - | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
470 | 470 | | |
471 | 471 | | |
472 | 472 | | |
| |||
515 | 515 | | |
516 | 516 | | |
517 | 517 | | |
518 | | - | |
519 | | - | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
520 | 521 | | |
521 | | - | |
522 | | - | |
523 | | - | |
| 522 | + | |
| 523 | + | |
524 | 524 | | |
525 | 525 | | |
526 | 526 | | |
| |||
0 commit comments