Skip to content

Comments

[dotnet] Fix SendAndWaitAsync to throw OperationCanceledException on external cancellation#543

Merged
SteveSandersonMS merged 4 commits intomainfrom
copilot/fix-sendandwaitasync-timeout
Feb 23, 2026
Merged

[dotnet] Fix SendAndWaitAsync to throw OperationCanceledException on external cancellation#543
SteveSandersonMS merged 4 commits intomainfrom
copilot/fix-sendandwaitasync-timeout

Conversation

Copy link
Contributor

Copilot AI commented Feb 23, 2026

  • Understand the issue: SendAndWaitAsync always throws TimeoutException when the linked CTS fires, even if the user's cancellation token was cancelled externally
  • Fix Session.cs: check if original cancellationToken is cancelled; if so call TrySetCanceled, else throw TimeoutException
  • Update XML doc comment to mention OperationCanceledException
  • Add E2E test SendAndWait_Throws_OperationCanceledException_When_Token_Cancelled that properly waits for ToolExecutionStartEvent before cancelling
  • Add snapshot YAML auto-generated by running the test (not manually written)
  • All SendAndWait tests pass, CodeQL clean
Original prompt

This section details on the original issue you should resolve

<issue_title>[dotnet] SendAndWaitAsync throws timeout for cancelled tokens</issue_title>
<issue_description>Ref:

tcs.TrySetException(new TimeoutException($"SendAndWaitAsync timed out after {effectiveTimeout}")));
return await tcs.Task;

If the cancellation token passed to SendAndWaitAsync is cancelled externally it always throws a TimeoutException - instead of a OperationCanceledException.

This make it hard to tell to the user what's the real issue - if it's just a user cancellation or if the action indeed timed out.</issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

… cancelled

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix SendAndWaitAsync to handle cancelled tokens correctly [dotnet] Fix SendAndWaitAsync to throw OperationCanceledException on external cancellation Feb 23, 2026
…elling

Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
Co-authored-by: SteveSandersonMS <1101362+SteveSandersonMS@users.noreply.github.com>
@github-actions
Copy link

Cross-SDK Consistency Review

This PR fixes a .NET-specific bug in SendAndWaitAsync where external cancellation was incorrectly reported as a timeout. The fix properly distinguishes between OperationCanceledException (user cancellation) and TimeoutException (actual timeout).

Cross-SDK Comparison

After reviewing the equivalent sendAndWait/send_and_wait/SendAndWait implementations across all SDKs:

✅ Go SDK - Already handles this correctly

  • Uses context.Context which combines timeout and cancellation
  • Returns ctx.Err() on line 211, which is either context.Canceled or context.DeadlineExceeded
  • Reference: go/session.go:160-213

⚠️ Node.js SDK - Limited cancellation support

  • Only accepts a timeout parameter (no AbortSignal)
  • Cannot distinguish between user cancellation and timeout
  • Reference: nodejs/src/session.ts:153-206

⚠️ Python SDK - Limited cancellation support

  • Only accepts a timeout parameter (no cancellation token)
  • Uses asyncio.wait_for() which raises asyncio.TimeoutError for all timeout scenarios
  • Reference: python/copilot/session.py:139-198

.NET SDK - Now fixed with this PR ✅

  • Accepts both TimeSpan? timeout and CancellationToken
  • Properly distinguishes external cancellation from timeout after this fix

Recommendation

No action required for this PR - the fix is correct and maintains the existing .NET API surface. The .NET SDK's ability to accept an explicit CancellationToken is actually a .NET-specific pattern that aligns with framework conventions (e.g., HttpClient.SendAsync, Task.Delay).

The other SDKs use different patterns appropriate to their ecosystems:

  • Go's context.Context is idiomatic and already correct
  • Node.js and Python could potentially add cancellation support in future feature work, but this is not a blocking inconsistency

This PR maintains cross-SDK consistency by ensuring .NET behaves correctly within its own idiomatic patterns. ✅

AI generated by SDK Consistency Review Agent

@SteveSandersonMS SteveSandersonMS merged commit f0909a7 into main Feb 23, 2026
36 of 37 checks passed
@SteveSandersonMS SteveSandersonMS deleted the copilot/fix-sendandwaitasync-timeout branch February 23, 2026 15:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes .NET SDK CopilotSession.SendAndWaitAsync cancellation semantics so that externally-canceled CancellationTokens surface as cancellation (instead of being reported as a timeout), aligning the API behavior with .NET expectations and improving diagnosability for callers.

Changes:

  • Update SendAndWaitAsync to complete the wait task as canceled when the caller’s CancellationToken is canceled; otherwise keep throwing TimeoutException on timeout.
  • Update XML docs to include OperationCanceledException.
  • Add a new E2E test + snapshot covering external cancellation during an in-flight SendAndWaitAsync.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
dotnet/src/Session.cs Adjusts SendAndWaitAsync completion to distinguish external cancellation vs timeout and documents cancellation behavior.
dotnet/test/SessionTests.cs Adds an E2E regression test validating cancellation behavior.
test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml Adds the replay snapshot for the new E2E test scenario.

Comment on lines +409 to +413
var session = await Client.CreateSessionAsync();

// Set up wait for tool execution to start BEFORE sending
var toolStartTask = TestHelper.GetNextEventOfTypeAsync<ToolExecutionStartEvent>(session);

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test creates a session without an OnPermissionRequest handler but then waits for ToolExecutionStartEvent from a shell command. In this repo, tool operations are expected to be denied-by-default when no permission handler is provided (see PermissionTests.Should_Deny_Tool_Operations_By_Default_When_No_Handler_Is_Provided), which can prevent tool.execution_start from ever arriving or can cause the turn to complete quickly with “Permission denied”, making this test flaky/slow (60s timeout in GetNextEventOfTypeAsync). Consider creating the session with new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll } (or a handler that approves shell), and optionally pass a shorter timeout to GetNextEventOfTypeAsync to fail fast.

Copilot uses AI. Check for mistakes.
}

[Fact]
public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Cancelled()
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method name uses Token_Cancelled (double-l). In .NET APIs and the rest of the framework, the standard spelling is Canceled (single-l), e.g. OperationCanceledException / CancellationToken. Consider renaming the test (and corresponding snapshot filename) to ..._When_Token_Canceled for consistency/searchability.

Suggested change
public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Cancelled()
public async Task SendAndWait_Throws_OperationCanceledException_When_Token_Canceled()

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[dotnet] SendAndWaitAsync throws timeout for cancelled tokens

2 participants