From be74fddca241e0b5a791a89725a3a67ea6cd299e Mon Sep 17 00:00:00 2001 From: kevinWangSheng Date: Sat, 14 Mar 2026 08:36:29 -0700 Subject: [PATCH] fix(agent): prevent panic from closing events channel while readLoop may still send In 6 agent session implementations (codex, opencode, qoder, iflow, cursor, gemini), Close() unconditionally calls close(events) after an 8-second timeout, even when the readLoop goroutine may still be running and sending events to the channel. Due to Go's select non-determinism, the readLoop goroutine can choose the send case over the ctx.Done() case, causing a "send on closed channel" panic. Move close(events) into the case where wg.Wait() succeeds (readLoop has fully exited), and skip closing on timeout to prevent the race. This follows the same safe pattern already used by claudeSession, where the events channel is only closed after the readLoop goroutine has finished. --- agent/codex/session.go | 2 +- agent/cursor/session.go | 2 +- agent/gemini/session.go | 2 +- agent/iflow/session.go | 2 +- agent/opencode/session.go | 2 +- agent/qoder/session.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/agent/codex/session.go b/agent/codex/session.go index 1e7818d..7a903ba 100644 --- a/agent/codex/session.go +++ b/agent/codex/session.go @@ -497,10 +497,10 @@ func (cs *codexSession) Close() error { }() select { case <-done: + close(cs.events) case <-time.After(8 * time.Second): slog.Warn("codexSession: close timed out, abandoning wg.Wait") } - close(cs.events) return nil } diff --git a/agent/cursor/session.go b/agent/cursor/session.go index 041dbad..d163abc 100644 --- a/agent/cursor/session.go +++ b/agent/cursor/session.go @@ -464,10 +464,10 @@ func (cs *cursorSession) Close() error { }() select { case <-done: + close(cs.events) case <-time.After(8 * time.Second): slog.Warn("cursorSession: close timed out, abandoning wg.Wait") } - close(cs.events) return nil } diff --git a/agent/gemini/session.go b/agent/gemini/session.go index d99bccd..718cc7d 100644 --- a/agent/gemini/session.go +++ b/agent/gemini/session.go @@ -469,10 +469,10 @@ func (gs *geminiSession) Close() error { }() select { case <-done: + close(gs.events) case <-time.After(8 * time.Second): slog.Warn("geminiSession: close timed out, abandoning wg.Wait") } - close(gs.events) return nil } diff --git a/agent/iflow/session.go b/agent/iflow/session.go index 2488211..1d0cf96 100644 --- a/agent/iflow/session.go +++ b/agent/iflow/session.go @@ -897,9 +897,9 @@ func (s *iflowSession) Close() error { }() select { case <-done: + close(s.events) case <-time.After(8 * time.Second): slog.Warn("iflowSession: close timed out, abandoning wg.Wait") } - close(s.events) return nil } diff --git a/agent/opencode/session.go b/agent/opencode/session.go index 3ee4b4f..ebe3a4e 100644 --- a/agent/opencode/session.go +++ b/agent/opencode/session.go @@ -393,10 +393,10 @@ func (s *opencodeSession) Close() error { }() select { case <-done: + close(s.events) case <-time.After(8 * time.Second): slog.Warn("opencodeSession: close timed out, abandoning wg.Wait") } - close(s.events) return nil } diff --git a/agent/qoder/session.go b/agent/qoder/session.go index e4492e9..c98ca63 100644 --- a/agent/qoder/session.go +++ b/agent/qoder/session.go @@ -288,10 +288,10 @@ func (qs *qoderSession) Close() error { }() select { case <-done: + close(qs.events) case <-time.After(8 * time.Second): slog.Warn("qoderSession: close timed out, abandoning wg.Wait") } - close(qs.events) return nil }