From 9a1f80d0bf7e1c50e71e060f7d513e40fa9d6627 Mon Sep 17 00:00:00 2001 From: garythung Date: Sun, 1 Mar 2026 05:06:46 +0000 Subject: [PATCH] fix: wait for CLI graceful shutdown before sending SIGTERM SubprocessCLITransport.close() was immediately sending SIGTERM after closing stdin, not giving the CLI subprocess time to flush the session transcript to disk. This caused session resume to fail with "No conversation found" because the .jsonl file was nearly empty. Now waits up to 10 seconds for the process to exit on its own after stdin EOF before falling back to SIGTERM. Co-Authored-By: Claude Opus 4.6 --- .../_internal/transport/subprocess_cli.py | 16 ++++++++++------ tests/test_transport.py | 11 +++++++++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 1f0aac58..1ff47496 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -463,14 +463,18 @@ async def close(self) -> None: await self._stderr_stream.aclose() self._stderr_stream = None - # Terminate and wait for process + # Wait for process to exit gracefully after stdin EOF, + # giving the CLI time to flush the session transcript. + # Only resort to SIGTERM if it doesn't exit in time. if self._process.returncode is None: - with suppress(ProcessLookupError): - self._process.terminate() - # Wait for process to finish with timeout - with suppress(Exception): - # Just try to wait, but don't block if it fails + try: + with anyio.fail_after(10): await self._process.wait() + except TimeoutError: + with suppress(ProcessLookupError): + self._process.terminate() + with suppress(Exception): + await self._process.wait() self._process = None self._stdout_stream = None diff --git a/tests/test_transport.py b/tests/test_transport.py index 65e6ada7..c1d3518a 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -213,7 +213,12 @@ async def _test(): mock_process = MagicMock() mock_process.returncode = None mock_process.terminate = MagicMock() - mock_process.wait = AsyncMock() + + # Simulate graceful exit: wait() sets returncode to 0 + async def _graceful_wait(): + mock_process.returncode = 0 + + mock_process.wait = AsyncMock(side_effect=_graceful_wait) mock_process.stdout = MagicMock() mock_process.stderr = MagicMock() @@ -235,7 +240,9 @@ async def _test(): assert transport.is_ready() await transport.close() - mock_process.terminate.assert_called_once() + # Process should exit gracefully without needing SIGTERM + mock_process.terminate.assert_not_called() + mock_process.wait.assert_called() anyio.run(_test)