diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index aa2d5b04..05f7e424 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -52,6 +52,7 @@ public partial class CopilotSession : IAsyncDisposable private readonly SemaphoreSlim _userInputHandlerLock = new(1, 1); private SessionHooks? _hooks; private readonly SemaphoreSlim _hooksLock = new(1, 1); + private int _isDisposed; /// /// Gets the unique identifier for this session. @@ -553,8 +554,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 f433e677..651f1f81 100644 --- a/dotnet/test/ClientTests.cs +++ b/dotnet/test/ClientTests.cs @@ -243,4 +243,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 { CliPath = _cliPath }); + await using var session = await client.CreateSessionAsync(); + + await client.StopAsync(); + } }