Location
src/claude_agent_sdk/_internal/query.py:828 (on main @ e21b457)
Problem
await self.transport.close() is the last statement in Query.close(), not inside a try/finally. If any of the preceding cleanup steps (lines ~812-827: transcript-mirror-batcher close, child-task cancel, read-task wait()) raise, transport.close() is never reached — the stderr reader task and the CLI subprocess are not cleaned up.
async def close(self) -> None:
...
if self._transcript_mirror_batcher is not None:
await self._transcript_mirror_batcher.close()
for task in list(self._child_tasks):
task.cancel()
if self._read_task is not None and not self._read_task.done():
self._read_task.cancel()
await self._read_task.wait()
self._read_task = None
...
self._message_send.close()
await self.transport.close() # <-- skipped if anything above raises
Impact
Potential subprocess / background-task leak on error paths during shutdown. Low likelihood in practice (the preceding calls are mostly non-raising), but _transcript_mirror_batcher.close() and _read_task.wait() are awaits that could in principle propagate.
Suggested fix
Wrap the earlier cleanup steps in try: ... finally: await self.transport.close(), or use nested try/finally (or an AsyncExitStack) so each cleanup step runs regardless of earlier failures. Example:
try:
if self._transcript_mirror_batcher is not None:
await self._transcript_mirror_batcher.close()
for task in list(self._child_tasks):
task.cancel()
if self._read_task is not None and not self._read_task.done():
self._read_task.cancel()
await self._read_task.wait()
self._read_task = None
self._message_send.close()
finally:
await self.transport.close()
Note
Discovered during review of PR #885, which does not introduce this — it's pre-existing on main.
Location
src/claude_agent_sdk/_internal/query.py:828(onmain@e21b457)Problem
await self.transport.close()is the last statement inQuery.close(), not inside atry/finally. If any of the preceding cleanup steps (lines ~812-827: transcript-mirror-batcher close, child-task cancel, read-taskwait()) raise,transport.close()is never reached — the stderr reader task and the CLI subprocess are not cleaned up.Impact
Potential subprocess / background-task leak on error paths during shutdown. Low likelihood in practice (the preceding calls are mostly non-raising), but
_transcript_mirror_batcher.close()and_read_task.wait()areawaits that could in principle propagate.Suggested fix
Wrap the earlier cleanup steps in
try: ... finally: await self.transport.close(), or use nestedtry/finally(or anAsyncExitStack) so each cleanup step runs regardless of earlier failures. Example:Note
Discovered during review of PR #885, which does not introduce this — it's pre-existing on
main.