From 40aa45cfc1108276ce2777d069c8acff89c3caac Mon Sep 17 00:00:00 2001 From: parthtrivedi2492 Date: Wed, 4 Feb 2026 15:22:32 +0000 Subject: [PATCH] Fix ObjectDisposedException when disposing session after client.StopAsync() Make CopilotSession.DisposeAsync() idempotent and tolerant to already-disposed connections. This fixes the crash when 'await using var session' cleanup runs after client.StopAsync() has already disposed the connection. Changes: - Add _isDisposed flag with Interlocked.Exchange for thread-safe idempotency - Catch ObjectDisposedException and IOException during dispose (connection gone) - Add regression test to ClientTests Fixes #306 --- dotnet/src/Session.cs | 21 +++++++++++++++++++-- dotnet/test/ClientTests.cs | 9 +++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) 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(); + } }