From a84a95b994060f1d7658c236c5ddc64c53e17bd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:27:15 +0000 Subject: [PATCH 1/2] Initial plan From 993cc710274cb25162be2f173e56060133f5ac39 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 2 Mar 2026 10:23:30 -0500 Subject: [PATCH 2/2] Use event delegate composition for thread-safe, insertion-ordered handler dispatch Replace HashSet with a private event (multicast delegate). The compiler-generated add/remove accessors use a lock-free CAS loop, dispatch reads the field once for an inherent snapshot, and invocation order matches registration order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Session.cs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index e906554a..8798565f 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -44,7 +44,13 @@ namespace GitHub.Copilot.SDK; /// public partial class CopilotSession : IAsyncDisposable { - private readonly HashSet _eventHandlers = new(); + /// + /// Multicast delegate used as a thread-safe, insertion-ordered handler list. + /// The compiler-generated add/remove accessors use a lock-free CAS loop over the backing field. + /// Dispatch reads the field once (inherent snapshot, no allocation). + /// Expected handler count is small (typically 1–3), so Delegate.Combine/Remove cost is negligible. + /// + private event SessionEventHandler? _eventHandlers; private readonly Dictionary _toolHandlers = new(); private readonly JsonRpc _rpc; private volatile PermissionRequestHandler? _permissionHandler; @@ -243,8 +249,8 @@ void Handler(SessionEvent evt) /// public IDisposable On(SessionEventHandler handler) { - _eventHandlers.Add(handler); - return new ActionDisposable(() => _eventHandlers.Remove(handler)); + _eventHandlers += handler; + return new ActionDisposable(() => _eventHandlers -= handler); } /// @@ -256,11 +262,8 @@ public IDisposable On(SessionEventHandler handler) /// internal void DispatchEvent(SessionEvent sessionEvent) { - foreach (var handler in _eventHandlers.ToArray()) - { - // We allow handler exceptions to propagate so they are not lost - handler(sessionEvent); - } + // Reading the field once gives us a snapshot; delegates are immutable. + _eventHandlers?.Invoke(sessionEvent); } /// @@ -550,7 +553,7 @@ await InvokeRpcAsync( // Connection is broken or closed } - _eventHandlers.Clear(); + _eventHandlers = null; _toolHandlers.Clear(); _permissionHandler = null;