Skip to content

Commit 44132f4

Browse files
stephentoubCopilot
andcommitted
Fix Go broadcast handler blocking event delivery
Fire broadcast handlers (tool/permission) in separate goroutines while keeping user event handlers serialized on the consumer goroutine. This prevents a stalled broadcast handler (e.g., a secondary client whose permission handler intentionally never completes) from blocking event delivery to user code — matching the fire-and-forget semantics of Node.js and Python SDKs. The consumer goroutine (processEvents) still runs off the readLoop, so RPC deadlocks are also avoided. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 137fa95 commit 44132f4

1 file changed

Lines changed: 11 additions & 8 deletions

File tree

go/session.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -461,14 +461,16 @@ func (s *Session) dispatchEvent(event SessionEvent) {
461461
}
462462

463463
// processEvents is the single-goroutine consumer loop that processes events
464-
// from the channel. Ensures all user code — event handlers, tool handlers,
465-
// permission handlers — is invoked serially and in FIFO order. Broadcast work
466-
// (tool calls, permission requests) completes before user handlers see the event.
464+
// from the channel. Ensures user event handlers are invoked serially and in
465+
// FIFO order. Broadcast work (tool calls, permission requests) is fired
466+
// concurrently so that a stalled handler does not block event delivery.
467467
func (s *Session) processEvents() {
468468
for event := range s.eventCh {
469-
// Handle broadcast request events (serialized with user handlers).
470-
// Runs off the readLoop goroutine so RPC requests won't deadlock.
471-
s.handleBroadcastEvent(event)
469+
// Fire broadcast handlers in a separate goroutine so they don't block
470+
// event delivery. This preserves pre-existing fire-and-forget semantics
471+
// (important when a secondary client's handler intentionally never
472+
// completes), while running off the readLoop to avoid deadlocks.
473+
go s.handleBroadcastEvent(event)
472474

473475
s.handlerMutex.RLock()
474476
handlers := make([]SessionEventHandler, 0, len(s.handlers))
@@ -494,8 +496,9 @@ func (s *Session) processEvents() {
494496
// and responding via RPC. This implements the protocol v3 broadcast model where tool
495497
// calls and permission requests are broadcast as session events to all clients.
496498
//
497-
// Handlers are executed on the processEvents goroutine (not the JSON-RPC read loop)
498-
// so that RPC responses can be received without deadlocking.
499+
// Handlers are executed in their own goroutine (not the JSON-RPC read loop or the
500+
// event consumer loop) so that a stalled handler does not block event delivery or
501+
// cause RPC deadlocks.
499502
func (s *Session) handleBroadcastEvent(event SessionEvent) {
500503
switch event.Type {
501504
case ExternalToolRequested:

0 commit comments

Comments
 (0)