diff --git a/src/claude_code_api/stream.py b/src/claude_code_api/stream.py index 5f8b944..9e2fdc3 100644 --- a/src/claude_code_api/stream.py +++ b/src/claude_code_api/stream.py @@ -377,7 +377,12 @@ class StreamClaudeProcess: await self._queue.put(record) finally: self._eof = True - await self._queue.put(None) + # `put_nowait`, not `await put`: this runs in a `finally` + # that a shutdown-time cancellation may have entered, and an + # unbounded queue never blocks anyway. Awaiting here would + # risk swallowing the sentinel and leaving a turn generator + # parked on `next_record()` forever. + self._queue.put_nowait(None) async def _read_stderr(self) -> None: proc = self._proc @@ -456,11 +461,19 @@ class StreamClaudeProcess: # ---- shutdown ----------------------------------------------------- async def terminate(self, *, grace: float = 5.0) -> int | None: - """SIGTERM → wait up to `grace` seconds → SIGKILL ladder.""" + """Close stdin → SIGTERM → wait up to `grace` → SIGKILL ladder. + + Closing stdin first is the protocol's own goodbye: headless + claude exits on EOF, so a healthy process usually never sees the + signals at all. + """ proc = self._proc if proc is None: return None if proc.returncode is None: + with contextlib.suppress(Exception): + if proc.stdin is not None and not proc.stdin.is_closing(): + proc.stdin.close() signal_group(proc.pid, signal.SIGTERM) with contextlib.suppress(TimeoutError): await asyncio.wait_for(proc.wait(), timeout=grace)