You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When the CLI sends a control_cancel_request message to cancel an in-flight hook callback, the Python SDK ignores it entirely. The handler at _internal/query.py L205-208 is a no-op:
CLI-side AbortError noise -- The CLI fires its abort signal, rejects the pending hook request with AbortError, and logs Error in hook callback hook_N: ... AbortError to stderr on every cancelled hook. This is noisy but non-fatal.
Python runs cancelled callbacks -- Since Python never receives the cancel, it continues executing hook callbacks that the CLI has already abandoned. The eventual response write either gets dropped silently or hits a closed transport.
_read_messages() receives control_cancel_request but takes no action. Meanwhile, _handle_control_request() tasks spawned via self._tg.start_soon() (L202) are not tracked by request ID, so there is no mechanism to cancel a specific in-flight request even if the handler were implemented.
The CLI sends control_cancel_request when:
A subagent completes while a parent-level hook is still pending
The overall query's abort controller fires
A hook callback exceeds the CLI's internal timeout
Steps to Reproduce
Create a multi-agent pipeline with SDK hooks (PreToolUse/PostToolUse)
Dispatch multiple subagents via the Agent tool
When subagents complete or during shutdown, observe stderr:
Error in hook callback hook_2: ...
AbortError:
at A (/$bunfs/root/src/entrypoints/cli.js:13318:473)
at K (/$bunfs/root/src/entrypoints/cli.js:6723:7433)
Minimal reproduction:
importasynciofromclaude_agent_sdkimport (
query, ClaudeAgentOptions, HookMatcher,
AgentDefinition,
)
asyncdefslow_hook(input_data, tool_use_id, context):
"""Hook that takes time -- will be cancelled by CLI."""awaitasyncio.sleep(2)
return {}
asyncdefmain():
options=ClaudeAgentOptions(
permission_mode="bypassPermissions",
hooks={
"PostToolUse": [
HookMatcher(matcher=".*", hooks=[slow_hook]),
],
},
agents={
"researcher": AgentDefinition(
description="Quick research task",
prompt="Search the web for 'Python asyncio' and summarize in one sentence.",
),
},
)
asyncformsginquery(
prompt="Use the researcher agent to look up Python asyncio, then summarize.",
options=options,
):
print(type(msg).__name__)
asyncio.run(main())
Expected Behavior
When the CLI sends control_cancel_request:
The SDK cancels the matching in-flight _handle_control_request task
The cancelled task does not attempt to write a response
No AbortError noise in stderr
Clean shutdown without orphaned hook callbacks
Actual Behavior
control_cancel_request is silently ignored
Hook callbacks run to completion even after cancellation
CLI logs Error in hook callback hook_N: ... AbortError to stderr
Track in-flight _handle_control_request tasks by request ID using anyio.CancelScope, and cancel the matching scope when control_cancel_request arrives:
# In __init__:self._inflight_tasks: dict[str, anyio.CancelScope] = {}
# In _read_messages, replace the TODO:elifmsg_type=="control_cancel_request":
cancel_id=message.get("request_id")
ifcancel_id:
scope=self._inflight_tasks.get(cancel_id)
ifscope:
scope.cancel()
continue# In _handle_control_request, wrap body in CancelScope:asyncdef_handle_control_request(self, request):
ifself._closed:
returnrequest_id=request["request_id"]
withanyio.CancelScope() asscope:
self._inflight_tasks[request_id] =scopetry:
# ... existing dispatch logic ...ifscope.cancel_calledorself._closed:
returnawaitself.transport.write(json.dumps(success_response) +"\n")
exceptExceptionase:
ifself._closedorscope.cancel_called:
return# ... error response ...finally:
self._inflight_tasks.pop(request_id, None)
SDK version: claude-agent-sdk 0.1.48 (Python), Bundled CLI 2.1.78
Description
When the CLI sends a
control_cancel_requestmessage to cancel an in-flight hook callback, the Python SDK ignores it entirely. The handler at_internal/query.pyL205-208 is a no-op:This causes three problems:
CLI-side AbortError noise -- The CLI fires its abort signal, rejects the pending hook request with
AbortError, and logsError in hook callback hook_N: ... AbortErrorto stderr on every cancelled hook. This is noisy but non-fatal.Python runs cancelled callbacks -- Since Python never receives the cancel, it continues executing hook callbacks that the CLI has already abandoned. The eventual response write either gets dropped silently or hits a closed transport.
Shutdown desync -- During
close(), in-flight hooks that should have been cancelled are still running. Combined with theExceptionGroupissue from CLIConnectionError: ProcessTransport is not ready for writing when using SDK MCP servers with string prompts #578 / PR fix: handle transport close race condition in control request responses #492, this creates cascading failures.Root Cause
_read_messages()receivescontrol_cancel_requestbut takes no action. Meanwhile,_handle_control_request()tasks spawned viaself._tg.start_soon()(L202) are not tracked by request ID, so there is no mechanism to cancel a specific in-flight request even if the handler were implemented.The CLI sends
control_cancel_requestwhen:Steps to Reproduce
AgenttoolMinimal reproduction:
Expected Behavior
When the CLI sends
control_cancel_request:_handle_control_requesttaskAbortErrornoise in stderrActual Behavior
control_cancel_requestis silently ignoredError in hook callback hook_N: ... AbortErrorto stderrCLIConnectionError(related: CLIConnectionError: ProcessTransport is not ready for writing when using SDK MCP servers with string prompts #578, PR fix: handle transport close race condition in control request responses #492)Suggested Fix
Track in-flight
_handle_control_requesttasks by request ID usinganyio.CancelScope, and cancel the matching scope whencontrol_cancel_requestarrives:Related
CLIConnectionErrorin_handle_control_request(downstream consequence)