Cancel completed host tool invocations across SDKs - #2509
Conversation
Track request-scoped cancellation across each SDK and terminate pending host callbacks when the runtime completes a request or the session transport shuts down. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Six moderate cancellation and cleanup defects remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Balanced
Findings: 6
New issues introduced by this change (6)
| Severity | Finding |
|---|---|
dotnet/src/Client.cs — JsonRpc.Completion can be faulted by an abnormal transport/protocol failure. In that case this… |
|
dotnet/src/Session.cs — CancellationTokenSource.Cancel() runs registered callbacks synchronously. Because… |
|
java/sdk/src/main/java/com/github/copilot/CopilotSession.java — This drains only entries currently visible in the map and does not prevent a later insertion.… |
|
nodejs/src/session.ts — After a normal handler result or error, _claimExternalTool has already removed this controller,… |
|
rust/src/session.rs — This create path can return from the later register_mcp_auth_interest(...).await? before a… |
|
rust/src/session.rs — The event-loop shutdown path does not cancel this token. In particular,… |
What changed in this PR
Adds request-scoped cancellation for host-owned external tool invocations across all six SDKs.
Changes:
- Cancels pending callbacks on completion, teardown, and connection loss.
- Atomically claims requests to prevent stale or duplicate responses.
- Adds lifecycle, race, retry, and cancellation coverage.
Required fixes:
dotnet/src/Client.cs(moderate, 1 vote): ObserveJsonRpc.Completionfailures and continue cancellation cleanup.dotnet/src/Session.cs(moderate, 1 vote): Move synchronous token cancellation off the JSON-RPC read loop.java/.../CopilotSession.java(moderate, 1 vote): Prevent tool registration after force-stop draining begins.nodejs/src/session.ts(moderate, 1 vote): Always abort invocation controllers in both affectedfinallyblocks.rust/src/session.rs:1619(moderate, 1 vote): Cancel external-tool tasks during event-loop shutdown.rust/src/session.rs:1115(moderate, 1 vote): Add cleanup protection for create-session registration failures.
| File | Description |
|---|---|
rust/tests/session_test.rs |
Tests cancellation and connection-loss behavior. |
rust/src/session.rs |
Tracks and cancels tool futures; has two unresolved cleanup issues. |
rust/src/jsonrpc.rs |
Exposes connection-closure cancellation. |
python/test_session.py |
Tests task cancellation and disconnect behavior. |
python/test_client.py |
Tests client shutdown cleanup. |
python/copilot/session.py |
Tracks and claims external-tool tasks. |
python/copilot/client.py |
Cancels session work during teardown. |
nodejs/test/external-tool-cancellation.test.ts |
Covers cancellation, retries, duplicates, and late results. |
nodejs/test/client.test.ts |
Tests transport-close cancellation. |
nodejs/src/types.ts |
Exposes invocation abort signals. |
nodejs/src/session.ts |
Manages invocation controllers; final abort remains incomplete. |
nodejs/src/client.ts |
Handles connection-close cleanup. |
java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java |
Tests Java tool cancellation. |
java/sdk/src/test/java/com/github/copilot/JsonRpcClientTest.java |
Tests close-handler delivery. |
java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java |
Tests force-stop cancellation. |
java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java |
Adds connection-close callbacks. |
java/sdk/src/main/java/com/github/copilot/CopilotSession.java |
Tracks tool futures; force-stop has a registration race. |
java/sdk/src/main/java/com/github/copilot/CopilotClient.java |
Propagates teardown to sessions. |
go/types.go |
Documents invocation-context cancellation. |
go/session.go |
Implements request-scoped contexts and claiming. |
go/session_test.go |
Tests completion-driven cancellation. |
go/client.go |
Cancels tools during shutdown and connection loss. |
go/client_test.go |
Tests client lifecycle cancellation. |
dotnet/test/Unit/ClientSessionLifetimeTests.cs |
Covers cancellation and teardown scenarios. |
dotnet/src/Session.cs |
Adds cancellable tool calls; cancellation can block dispatch. |
dotnet/src/Client.cs |
Propagates teardown; faulted completion can skip cleanup. |
CHANGELOG.md |
Documents cross-SDK cancellation behavior. |
Suppressed comments (1)
nodejs/src/session.ts:2068
- A concurrent caller returns immediately while the first destroy RPC is still pending. If that RPC later fails, the second caller has already observed a successful
disconnect()even though the session remains connected and retryable. Store the in-flight disconnect promise and have concurrent callers await the same operation, clearing it on failure.
if (this.disconnected || this.disconnecting) {
return;
}
this.disconnecting = true;
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use lexical ownership for request cancellation sources and observe JSON-RPC completion faults without a generic catch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Add new standalone E2E coverage for long-running external tools being cancelled when the session is terminated/disposed, without modifying existing E2E tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Apply language formatters and remove an unused Vitest import from the cross-SDK E2E coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
SteveSandersonMS
left a comment
There was a problem hiding this comment.
Thanks for the solid cross-SDK fix and for keeping behavior aligned across runtimes.
I added follow-up standalone E2E tests in commit b6ac7f4 to cover the concrete cancellation scenario (long-running external tool + session termination) across all six SDKs:
- .NET: dispose path
- Node/Python/Go/Rust/Java: disconnect/close path
Locally, the new targeted tests pass in Node, Python, Go, Rust, and .NET. Java test execution is blocked in this environment by Maven TLS trust-chain resolution of org.sonatype.central:central-publishing-maven-plugin, so we should rely on CI for the Java lane signal.
Given this, I recommend we proceed to merge once CI is green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Avoid swallowing fatal runtime exceptions while retaining best-effort cleanup after connection startup failures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Preserve request-scoped external tool cancellation alongside PreparedSession registration ownership and the latest Python client tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cross-SDK Consistency Review — PR #2509Reviewed the authoritative diff ( Result: Consistent. This PR implements the feature in parallel across all six SDKs with idiomatic-per-language cancellation primitives, matching the CHANGELOG description precisely:
All six implementations:
No cross-SDK gaps or naming inconsistencies found. This is a good example of maintaining feature parity across the six language implementations.
|

Host-owned external tool callbacks could continue running after the runtime had already completed or drained their request, wasting resources and risking stale responses. This adds request-scoped cancellation across every SDK language while preserving existing
SendAndWaitwait-only cancellation semantics.Summary
requestIdand pass the language-native cancellation primitive to the host callback.external_tool.completedarrives, when sessions or clients terminate, and when the transport closes unexpectedly.Testing
netstandard2.0,net8.0, andnet10.0; 57 focused lifetime tests.Generated by Copilot