Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public partial class CopilotSession : IAsyncDisposable
private SessionHooks? _hooks;
private readonly SemaphoreSlim _hooksLock = new(1, 1);
private SessionRpc? _sessionRpc;
private int _isDisposed;

/// <summary>
/// Gets the unique identifier for this session.
Expand Down Expand Up @@ -560,8 +561,24 @@ await InvokeRpcAsync<object>(
/// </example>
public async ValueTask DisposeAsync()
{
await InvokeRpcAsync<object>(
"session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None);
if (Interlocked.Exchange(ref _isDisposed, 1) == 1)
{
return;
}

try
{
await InvokeRpcAsync<object>(
"session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None);
}
catch (ObjectDisposedException)
{
// Connection was already disposed (e.g., client.StopAsync() was called first)
}
catch (IOException)
{
// Connection is broken or closed
}
Comment on lines +574 to +581
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

Consider catching all exceptions here rather than specific types. Dispose methods should be resilient and never throw exceptions as per IAsyncDisposable best practices. While ObjectDisposedException and IOException cover the expected cases, other unexpected exceptions (e.g., from custom JsonRpc implementations) could still propagate. Consider using a catch-all:

catch (Exception)
{
    // Suppress all exceptions during disposal
}

This matches the pattern used in CopilotClient.CleanupConnectionAsync which catches all exceptions during cleanup.

Suggested change
catch (ObjectDisposedException)
{
// Connection was already disposed (e.g., client.StopAsync() was called first)
}
catch (IOException)
{
// Connection is broken or closed
}
catch (Exception)
{
// Suppress all exceptions during disposal (connection may already be closed or broken)
}

Copilot uses AI. Check for mistakes.

_eventHandlers.Clear();
_toolHandlers.Clear();
Expand Down
9 changes: 9 additions & 0 deletions dotnet/test/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,13 @@ public void Should_Throw_When_UseLoggedInUser_Used_With_CliUrl()
});
});
}

[Fact]
public async Task Should_Not_Throw_When_Disposing_Session_After_Stopping_Client()
{
await using var client = new CopilotClient(new CopilotClientOptions());
await using var session = await client.CreateSessionAsync();

await client.StopAsync();
}
}
Loading