@@ -45,14 +45,18 @@ namespace GitHub.Copilot.SDK;
4545/// </example>
4646public partial class CopilotSession : IAsyncDisposable
4747{
48- private readonly HashSet < SessionEventHandler > _eventHandlers = new ( ) ;
48+ /// <summary>
49+ /// Multicast delegate used as a thread-safe, insertion-ordered handler list.
50+ /// The compiler-generated add/remove accessors use a lock-free CAS loop over the backing field.
51+ /// Dispatch reads the field once (inherent snapshot, no allocation).
52+ /// Expected handler count is small (typically 1–3), so Delegate.Combine/Remove cost is negligible.
53+ /// </summary>
54+ private event SessionEventHandler ? _eventHandlers ;
4955 private readonly Dictionary < string , AIFunction > _toolHandlers = new ( ) ;
5056 private readonly JsonRpc _rpc ;
5157 private readonly CopilotTelemetry . AgentTurnTracker ? _turnTracker ;
52- private PermissionRequestHandler ? _permissionHandler ;
53- private readonly SemaphoreSlim _permissionHandlerLock = new ( 1 , 1 ) ;
54- private UserInputHandler ? _userInputHandler ;
55- private readonly SemaphoreSlim _userInputHandlerLock = new ( 1 , 1 ) ;
58+ private volatile PermissionRequestHandler ? _permissionHandler ;
59+ private volatile UserInputHandler ? _userInputHandler ;
5660 private SessionHooks ? _hooks ;
5761 private readonly SemaphoreSlim _hooksLock = new ( 1 , 1 ) ;
5862 private SessionRpc ? _sessionRpc ;
@@ -292,8 +296,8 @@ void Handler(SessionEvent evt)
292296 /// </example>
293297 public IDisposable On ( SessionEventHandler handler )
294298 {
295- _eventHandlers . Add ( handler ) ;
296- return new OnDisposeCall ( ( ) => _eventHandlers . Remove ( handler ) ) ;
299+ _eventHandlers += handler ;
300+ return new ActionDisposable ( ( ) => _eventHandlers -= handler ) ;
297301 }
298302
299303 /// <summary>
@@ -307,11 +311,8 @@ internal void DispatchEvent(SessionEvent sessionEvent)
307311 {
308312 _turnTracker ? . ProcessEvent ( sessionEvent ) ;
309313
310- foreach ( var handler in _eventHandlers . ToArray ( ) )
311- {
312- // We allow handler exceptions to propagate so they are not lost
313- handler ( sessionEvent ) ;
314- }
314+ // Reading the field once gives us a snapshot; delegates are immutable.
315+ _eventHandlers ? . Invoke ( sessionEvent ) ;
315316 }
316317
317318 /// <summary>
@@ -349,15 +350,7 @@ internal void RegisterTools(ICollection<AIFunction> tools)
349350 /// </remarks>
350351 internal void RegisterPermissionHandler ( PermissionRequestHandler handler )
351352 {
352- _permissionHandlerLock . Wait ( ) ;
353- try
354- {
355- _permissionHandler = handler ;
356- }
357- finally
358- {
359- _permissionHandlerLock . Release ( ) ;
360- }
353+ _permissionHandler = handler ;
361354 }
362355
363356 /// <summary>
@@ -367,16 +360,7 @@ internal void RegisterPermissionHandler(PermissionRequestHandler handler)
367360 /// <returns>A task that resolves with the permission decision.</returns>
368361 internal async Task < PermissionRequestResult > HandlePermissionRequestAsync ( JsonElement permissionRequestData )
369362 {
370- await _permissionHandlerLock . WaitAsync ( ) ;
371- PermissionRequestHandler ? handler ;
372- try
373- {
374- handler = _permissionHandler ;
375- }
376- finally
377- {
378- _permissionHandlerLock . Release ( ) ;
379- }
363+ var handler = _permissionHandler ;
380364
381365 if ( handler == null )
382366 {
@@ -403,15 +387,7 @@ internal async Task<PermissionRequestResult> HandlePermissionRequestAsync(JsonEl
403387 /// <param name="handler">The handler to invoke when user input is requested.</param>
404388 internal void RegisterUserInputHandler ( UserInputHandler handler )
405389 {
406- _userInputHandlerLock . Wait ( ) ;
407- try
408- {
409- _userInputHandler = handler ;
410- }
411- finally
412- {
413- _userInputHandlerLock . Release ( ) ;
414- }
390+ _userInputHandler = handler ;
415391 }
416392
417393 /// <summary>
@@ -421,16 +397,7 @@ internal void RegisterUserInputHandler(UserInputHandler handler)
421397 /// <returns>A task that resolves with the user's response.</returns>
422398 internal async Task < UserInputResponse > HandleUserInputRequestAsync ( UserInputRequest request )
423399 {
424- await _userInputHandlerLock . WaitAsync ( ) ;
425- UserInputHandler ? handler ;
426- try
427- {
428- handler = _userInputHandler ;
429- }
430- finally
431- {
432- _userInputHandlerLock . Release ( ) ;
433- }
400+ var handler = _userInputHandler ;
434401
435402 if ( handler == null )
436403 {
@@ -635,24 +602,11 @@ await InvokeRpcAsync<object>(
635602 // Connection is broken or closed
636603 }
637604
638- _eventHandlers . Clear ( ) ;
605+ _eventHandlers = null ;
639606 _toolHandlers . Clear ( ) ;
640607 _turnTracker ? . CompleteOnDispose ( ) ;
641608
642- await _permissionHandlerLock . WaitAsync ( ) ;
643- try
644- {
645- _permissionHandler = null ;
646- }
647- finally
648- {
649- _permissionHandlerLock . Release ( ) ;
650- }
651- }
652-
653- private class OnDisposeCall ( Action callback ) : IDisposable
654- {
655- public void Dispose ( ) => callback ( ) ;
609+ _permissionHandler = null ;
656610 }
657611
658612 internal record SendMessageRequest
0 commit comments