diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 1f8bfd4b..34f4d02d 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -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; /// /// Gets the unique identifier for this session. @@ -560,8 +561,24 @@ await InvokeRpcAsync( /// public async ValueTask DisposeAsync() { - await InvokeRpcAsync( - "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None); + if (Interlocked.Exchange(ref _isDisposed, 1) == 1) + { + return; + } + + try + { + await InvokeRpcAsync( + "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 + } _eventHandlers.Clear(); _toolHandlers.Clear(); diff --git a/dotnet/test/ClientTests.cs b/dotnet/test/ClientTests.cs index e3419f98..9e336a7e 100644 --- a/dotnet/test/ClientTests.cs +++ b/dotnet/test/ClientTests.cs @@ -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(); + } }