From 9cfb17b64f941f80525369df45f957341a42eb27 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 16 Feb 2026 10:48:20 +0000 Subject: [PATCH] Fix ObjectDisposedException when disposing session after client.StopAsync() Make CopilotSession.DisposeAsync() idempotent and tolerant to already-disposed connections by adding an _isDisposed flag and catching ObjectDisposedException and IOException during disposal. Fixes #306 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- 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 1f8bfd4b9..34f4d02d5 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 e3419f981..9e336a7e5 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(); + } }