Skip to content

Commit 76cb292

Browse files
qing-antclaude
andauthored
fix: pass initialize_timeout from env var in query() (#743)
## Problem `InternalClient.process_query()` (used by the top-level `query()` function) creates a `Query` object without passing `initialize_timeout`, so it always defaults to the hardcoded 60-second timeout. Meanwhile, `ClaudeSDKClient.connect()` correctly reads `CLAUDE_CODE_STREAM_CLOSE_TIMEOUT` from the environment and passes it to `Query`. This means users who set `CLAUDE_CODE_STREAM_CLOSE_TIMEOUT` to handle slow initialization (e.g., large MCP server setups) see the timeout honored when using `ClaudeSDKClient`, but silently ignored when using `query()`. ## Fix Add the same timeout calculation to `InternalClient.process_query()`: ```python initialize_timeout_ms = int( os.environ.get("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "60000") ) initialize_timeout = max(initialize_timeout_ms / 1000.0, 60.0) ``` Then pass `initialize_timeout=initialize_timeout` to the `Query` constructor. ## Testing - Added two unit tests verifying that `query()` forwards custom and default timeout values to `Query` - All 348 existing tests pass - E2E verified with a live SDK instance: `query()` successfully completes with `CLAUDE_CODE_STREAM_CLOSE_TIMEOUT=120000` set Fixes #741 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 19e1f53 commit 76cb292

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

src/claude_agent_sdk/_internal/client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Internal client implementation."""
22

33
import json
4+
import os
45
from collections.abc import AsyncIterable, AsyncIterator
56
from dataclasses import asdict, replace
67
from typing import Any
@@ -98,6 +99,12 @@ async def process_query(
9899
for name, agent_def in configured_options.agents.items()
99100
}
100101

102+
# Match ClaudeSDKClient.connect() — without this, query() ignores the env var
103+
initialize_timeout_ms = int(
104+
os.environ.get("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "60000")
105+
)
106+
initialize_timeout = max(initialize_timeout_ms / 1000.0, 60.0)
107+
101108
# Create Query to handle control protocol
102109
# Always use streaming mode internally (matching TypeScript SDK)
103110
# This ensures agents are always sent via initialize request
@@ -109,6 +116,7 @@ async def process_query(
109116
if configured_options.hooks
110117
else None,
111118
sdk_mcp_servers=sdk_mcp_servers,
119+
initialize_timeout=initialize_timeout,
112120
agents=agents_dict,
113121
)
114122

tests/test_client.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for Claude SDK client functionality."""
22

3+
import os
34
from unittest.mock import AsyncMock, Mock, patch
45

56
import anyio
@@ -127,3 +128,67 @@ async def mock_receive():
127128
assert call_kwargs["options"].cwd == "/custom/path"
128129

129130
anyio.run(_test)
131+
132+
def _run_query_with_mocked_internals(self, env_patch, expected_timeout):
133+
"""Helper: run query() with mocked transport/Query and verify initialize_timeout."""
134+
135+
async def _test():
136+
with (
137+
patch(
138+
"claude_agent_sdk._internal.client.SubprocessCLITransport"
139+
) as mock_transport_class,
140+
patch("claude_agent_sdk._internal.client.Query") as mock_query_class,
141+
patch.dict(os.environ, env_patch, clear=False),
142+
):
143+
mock_transport = AsyncMock()
144+
mock_transport_class.return_value = mock_transport
145+
mock_transport.connect = AsyncMock()
146+
mock_transport.close = AsyncMock()
147+
mock_transport.end_input = AsyncMock()
148+
mock_transport.write = AsyncMock()
149+
mock_transport.is_ready = Mock(return_value=True)
150+
151+
mock_query = AsyncMock()
152+
mock_query_class.return_value = mock_query
153+
mock_query.start = AsyncMock()
154+
mock_query.initialize = AsyncMock()
155+
mock_query.close = AsyncMock()
156+
mock_query._tg = None
157+
158+
async def mock_receive():
159+
yield {
160+
"type": "result",
161+
"subtype": "success",
162+
"duration_ms": 100,
163+
"duration_api_ms": 80,
164+
"is_error": False,
165+
"num_turns": 1,
166+
"session_id": "test",
167+
}
168+
169+
mock_query.receive_messages = mock_receive
170+
171+
async for _ in query(prompt="test", options=ClaudeAgentOptions()):
172+
pass
173+
174+
call_kwargs = mock_query_class.call_args.kwargs
175+
assert call_kwargs["initialize_timeout"] == expected_timeout
176+
177+
anyio.run(_test)
178+
179+
def test_query_passes_initialize_timeout_from_env(self):
180+
"""Test that query() reads CLAUDE_CODE_STREAM_CLOSE_TIMEOUT and passes it to Query."""
181+
self._run_query_with_mocked_internals(
182+
env_patch={"CLAUDE_CODE_STREAM_CLOSE_TIMEOUT": "120000"},
183+
expected_timeout=120.0,
184+
)
185+
186+
def test_query_uses_default_initialize_timeout(self):
187+
"""Test that query() defaults to 60s initialize timeout when env var is not set."""
188+
# Ensure env var is absent for this test
189+
with patch.dict(os.environ, {}, clear=False):
190+
os.environ.pop("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", None)
191+
self._run_query_with_mocked_internals(
192+
env_patch={},
193+
expected_timeout=60.0,
194+
)

0 commit comments

Comments
 (0)