Skip to content

fix(server): prevent send-on-closed-channel panic in session title generation - #3832

Open
Piyush0049 wants to merge 1 commit into
docker:mainfrom
Piyush0049:fix/session-manager-concurrency-panic
Open

fix(server): prevent send-on-closed-channel panic in session title generation#3832
Piyush0049 wants to merge 1 commit into
docker:mainfrom
Piyush0049:fix/session-manager-concurrency-panic

Conversation

@Piyush0049

Copy link
Copy Markdown
Contributor

Description

This PR fixes a critical runtime concurrency bug in SessionManager.RunSession where asynchronous session title generation could attempt to send events on a closed channel, causing the server process to crash.

Root Cause

When a session turn required title generation, sm.generateTitle was spawned asynchronously as a background goroutine writing title events to streamChan. If the conversation stream (RunStream) terminated early due to cancellation, completion, or error, the function returned immediately and triggered defer close(streamChan). When the background title goroutine subsequently attempted to emit runtime.SessionTitle to streamChan, sending on the closed channel caused a runtime panic.

Key Changes

  • WaitGroup Lifecycle Coordination: Added a sync.WaitGroup around the asynchronous generateTitle goroutine and placed wg.Wait() before the return paths in RunSession. This guarantees that close(streamChan) cannot execute until title generation completes or safely aborts on context cancellation.
  • Graceful Stream Consumer Loop: Replaced a direct return with break inside the RunStream consumer loop when encountering a context error. This ensures execution always falls through to wg.Wait(), guaranteeing background synchronization even during early client disconnects or stream aborts.

Verification

  • Ran task lint and golangci-lint run cleanly with zero issues reported.
  • Verified that context cancellation in generateTitle unblocks immediately via select on ctx.Done(), preventing hangs or deadlocks.

@Piyush0049
Piyush0049 requested a review from a team as a code owner July 25, 2026 15:48
@aheritier aheritier added area/core Core agent runtime, session management kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Jul 25, 2026
@Piyush0049
Piyush0049 force-pushed the fix/session-manager-concurrency-panic branch from 9065123 to 89df77d Compare July 25, 2026 18:29
@aheritier

Copy link
Copy Markdown
Collaborator

Same here @Piyush0049

Please provide more details about the issue you are trying to solve. Is it something which happens often? What are the symptoms?

Right now the PR doesn't provide enough context to understand if you are fixing a bug or if these are just code reviews by an LLM which could easily hallucinate bugs.

Thanks

@aheritier
aheritier requested a review from docker-agent July 26, 2026 08:55
@Piyush0049

Copy link
Copy Markdown
Contributor Author

@aheritier This fixes a runtime panic in docker agent serve api when a client disconnects during a streaming session.

Root Cause

SessionManager.RunSession() closes streamChan when the main stream goroutine exits. However, generateTitle() is still running in a separate goroutine and may try to send runtime.SessionTitle after the channel has already been closed, resulting in:

panic: send on closed channel

Fix

I synchronized the background generateTitle() goroutine using a sync.WaitGroup and wait for it to finish before closing streamChan.

All unit tests in pkg/server pass cleanly

This guarantees the channel isn't closed while it's still being written to, preventing the panic.

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟢 APPROVE

The concurrency fix is correctly implemented. The sync.WaitGroup coordinates the async generateTitle goroutine with the close(streamChan) defer, eliminating the send-on-closed-channel panic. Key points verified:

  • wg.Go() availability: Go 1.26.5 (per go.mod) includes sync.WaitGroup.Go (introduced in Go 1.25) — compiles cleanly.
  • Defer ordering: wg.Wait() is called inline (not deferred), so it blocks the goroutine before any of the LIFO defers (streaming.Unlock, cancel, close(streamChan)) fire — correct.
  • returnbreak: The change correctly allows execution to fall through to wg.Wait() on context cancellation, preserving the early-exit semantics while ensuring the WaitGroup is honoured.
  • All return paths covered: Whether the stream completes normally or context is cancelled, wg.Wait() is reached before the deferred close(streamChan).
  • One drafter hypothesis (stream producer goroutine-leak on break) was investigated and dismissed: the break has identical effect to the previous return — both abandon the range loop at the same point — so no new risk is introduced.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Core agent runtime, session management kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants