Skip to content

Fix /clear#2207

Open
dgageot wants to merge 1 commit intodocker:mainfrom
dgageot:fix-clear
Open

Fix /clear#2207
dgageot wants to merge 1 commit intodocker:mainfrom
dgageot:fix-clear

Conversation

@dgageot
Copy link
Member

@dgageot dgageot commented Mar 21, 2026

Properly re-initialize the TUI after a /clear

@dgageot dgageot requested a review from a team as a code owner March 21, 2026 17:35
Copy link

@docker-agent docker-agent bot left a comment

Choose a reason for hiding this comment

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

Assessment: 🔴 CRITICAL

This PR refactors duplicated code into a reEmitStartupInfo() method, but the refactoring introduces a critical concurrency issue by removing context cancellation.

pkg/app/app.go Outdated
startupEvents := make(chan runtime.Event, 10)
go func() {
defer close(startupEvents)
a.runtime.EmitStartupInfo(context.Background(), a.session, startupEvents)
Copy link

Choose a reason for hiding this comment

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

🔴 HIGH SEVERITY: Goroutine leak - context cancellation removed

The refactored reEmitStartupInfo() method uses context.Background() instead of accepting a context parameter. This removes the cancellation capability that existed in the original code.

Original code (lines 690-703 in SetCurrentAgentModel and 850-862 in ReplaceSession):

case <-ctx.Done():
    return

New code (line 582):

a.runtime.EmitStartupInfo(context.Background(), a.session, startupEvents)

Impact:

  1. Goroutine leaks on shutdown: When the app is closed, these goroutines cannot be cancelled and will continue running until they complete naturally
  2. Stale event delivery: If users rapidly call /clear (which triggers NewSession()), multiple uncancellable goroutines will stack up, potentially sending events from old sessions to the new session
  3. Resource exhaustion: Repeated /clear commands will spawn goroutines that cannot be cleaned up

Recommendation:
Change reEmitStartupInfo() to accept a context.Context parameter:

func (a *App) reEmitStartupInfo(ctx context.Context) {
    a.runtime.ResetStartupInfo()
    go func() {
        startupEvents := make(chan runtime.Event, 10)
        go func() {
            defer close(startupEvents)
            a.runtime.EmitStartupInfo(ctx, a.session, startupEvents)
        }()
        for event := range startupEvents {
            select {
            case a.events <- event:
            case <-ctx.Done():
                return
            default:
            }
        }
    }()
}

For NewSession() which doesn't have a context, you could use a.ctx if the App struct has one, or create a cancellable context that's stored and cancelled on the next /clear.

}()
for event := range startupEvents {
select {
case a.events <- event:
Copy link

Choose a reason for hiding this comment

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

🟡 MEDIUM SEVERITY: Silent event drops without cancellation fallback

The select statement at line 586 has a default case that silently drops events when a.events is full, but lacks the ctx.Done() case that was present in the original code.

Original code (lines 698, 856):

select {
case a.events <- event:
case <-ctx.Done():
    return
}

New code (line 586):

select {
case a.events <- event:
default:
}

Impact:
Without the ctx.Done() case, if the goroutine is blocked or slow, there's no way to interrupt it during app shutdown or session replacement. This makes the pattern less robust than the original implementation.

Recommendation:
Add context cancellation to the select statement (see the recommendation in the previous comment for the complete fix).

rumpl
rumpl previously approved these changes Mar 21, 2026
Signed-off-by: David Gageot <david.gageot@docker.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants