handleRequest() CE catch shadows stopReason:cancelled on session/cancel
When session/cancel is received, the SDK sends a JSON-RPC error -32800 ("StandaloneCoroutine was cancelled") instead of the expected {"stopReason":"cancelled"} response.
This causes IDE clients to remain stuck in a "thinking" state after cancellation or catchAgain this pattern Client side
Expected behavior
After session/cancel, the session/prompt response should be:
{"id":5,"result":{"stopReason":"cancelled"}}
Actual behavior
The response is:
{"id":5,"error":{"code":-32800,"message":"StandaloneCoroutine was cancelled"}}
Root cause
In Agent.kt, SessionWrapper.cancel() does two things sequentially:
suspend fun cancel() {
agentSession.cancel() // step 1
protocol.cancelPendingIncomingRequest(activePrompt.requestId) // step 2
}
Step 2 cancels the Job that runs the prompt handler. The CancellationException is caught by two competing handlers:
Protocol.handleRequest() (low-level) — catches CE and sends -32800
SessionWrapper.prompt() (high-level) — catches CE and returns
PromptResponse(StopReason.CANCELLED)
Handler 1 always wins because it wraps the entire handler execution.
Handler 2 is effectively dead code for the cancel path — the CE never
reaches it because handleRequest() catches it first.
Reproduction
- Start a
session/prompt that triggers a long-running tool (e.g., a build command)
- Send
session/cancel while the tool is executing
- Observe the prompt response: it's
-32800 instead of stopReason:cancelled
handleRequest() CE catch shadows stopReason:cancelled on session/cancel
When
session/cancelis received, the SDK sends a JSON-RPC error-32800("StandaloneCoroutine was cancelled") instead of the expected{"stopReason":"cancelled"}response.This causes IDE clients to remain stuck in a "thinking" state after cancellation or catchAgain this pattern Client side
Expected behavior
After
session/cancel, thesession/promptresponse should be:{"id":5,"result":{"stopReason":"cancelled"}}Actual behavior
The response is:
{"id":5,"error":{"code":-32800,"message":"StandaloneCoroutine was cancelled"}}Root cause
In
Agent.kt,SessionWrapper.cancel()does two things sequentially:Step 2 cancels the Job that runs the prompt handler. The CancellationException is caught by two competing handlers:
Protocol.handleRequest()(low-level) — catches CE and sends-32800SessionWrapper.prompt()(high-level) — catches CE and returnsPromptResponse(StopReason.CANCELLED)Handler 1 always wins because it wraps the entire handler execution.
Handler 2 is effectively dead code for the cancel path — the CE never
reaches it because
handleRequest()catches it first.Reproduction
session/promptthat triggers a long-running tool (e.g., a build command)session/cancelwhile the tool is executing-32800instead ofstopReason:cancelled