Skip to content

Commit 137fa95

Browse files
stephentoubCopilot
andcommitted
Fix broadcast handler blocking event delivery in multi-client scenario
Keep broadcast handlers fire-and-forget (matching original behavior) while serializing user event handler dispatch. A stalled broadcast handler (e.g., a secondary client whose permission handler intentionally never completes) must not block event delivery to user code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 89d2236 commit 137fa95

1 file changed

Lines changed: 24 additions & 14 deletions

File tree

dotnet/src/Session.cs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -306,25 +306,19 @@ internal void DispatchEvent(SessionEvent sessionEvent)
306306

307307
/// <summary>
308308
/// Single-reader consumer loop that processes events from the channel.
309-
/// Ensures all user code — event handlers, tool handlers, permission handlers —
310-
/// is invoked serially and in FIFO order. Broadcast work (tool calls, permission
311-
/// requests) is awaited inline before dispatching to user handlers.
309+
/// Ensures user event handlers are invoked serially and in FIFO order.
310+
/// Broadcast work (tool calls, permission requests) is fired concurrently
311+
/// so that a stalled handler does not block event delivery to user code.
312312
/// </summary>
313313
private async Task ProcessEventsAsync()
314314
{
315315
await foreach (var sessionEvent in _eventChannel.Reader.ReadAllAsync())
316316
{
317-
// Await broadcast work inline so tool/permission handlers are serialized
318-
// with everything else. If two tool requests arrive back-to-back, the second
319-
// won't start until the first completes.
320-
try
321-
{
322-
await HandleBroadcastEventAsync(sessionEvent);
323-
}
324-
catch (Exception ex) when (ex is not OperationCanceledException)
325-
{
326-
LogBroadcastHandlerError(ex);
327-
}
317+
// Fire broadcast work concurrently (fire-and-forget with error logging).
318+
// This preserves the pre-existing behavior where broadcast handlers do not
319+
// block event delivery — important when a secondary client's handler
320+
// intentionally never completes (multi-client permission scenario).
321+
_ = HandleBroadcastEventSafe(sessionEvent);
328322

329323
// Invoke user handlers serially. Catch exceptions per handler to keep the
330324
// loop alive and ensure all subscribers see the event even if one fails.
@@ -346,6 +340,22 @@ private async Task ProcessEventsAsync()
346340
}
347341
}
348342

343+
/// <summary>
344+
/// Wraps <see cref="HandleBroadcastEventAsync"/> with error logging so it can
345+
/// be safely fire-and-forget.
346+
/// </summary>
347+
private async Task HandleBroadcastEventSafe(SessionEvent sessionEvent)
348+
{
349+
try
350+
{
351+
await HandleBroadcastEventAsync(sessionEvent);
352+
}
353+
catch (Exception ex) when (ex is not OperationCanceledException)
354+
{
355+
LogBroadcastHandlerError(ex);
356+
}
357+
}
358+
349359
/// <summary>
350360
/// Registers custom tool handlers for this session.
351361
/// </summary>

0 commit comments

Comments
 (0)