Skip to content

feat(run): expose spawned TUI tabs on the --listen control plane - #3819

Merged
dgageot merged 1 commit into
docker:mainfrom
dgageot:board-tab-event-sources
Jul 24, 2026
Merged

feat(run): expose spawned TUI tabs on the --listen control plane#3819
dgageot merged 1 commit into
docker:mainfrom
dgageot:board-tab-event-sources

Conversation

@dgageot

@dgageot dgageot commented Jul 24, 2026

Copy link
Copy Markdown
Member

When the agent runs with --listen, control-plane clients (such as the kanban board) watch session events over the control-plane socket. Sessions spawned after startup — TUI tabs opened with ctrl+t — were attached to a private SessionManager that was never published, so those sessions were invisible to any connected client. A board card would stay idle/green while a tab was actively mid-turn.

The fix publishes the serving SessionManager on the run flags before the TUI starts, so no tab can be spawned before the manager is wired up. recallCoordinatorOpt is updated to attach each new spawned session to that manager, registering its App event source and follow-up injector exactly as the initial session is registered. After this change, GET /api/sessions/<tab-id>/events streams the tab's user_message, stream_started, and stream_stopped events, each stamped with the correct session_id.

No breaking changes; the control-plane API surface is unchanged.

@dgageot
dgageot requested a review from a team as a code owner July 24, 2026 07:22

@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: 🟡 NEEDS ATTENTION

The fix correctly publishes the serving SessionManager before TUI startup and refactors recallCoordinatorOpt to register spawned-tab sessions with the control-plane manager. One medium-confidence issue was found in the new code: per-tab AttachRuntime calls on the shared manager are never matched by a DeleteSession, so stale session entries accumulate for every closed tab in long-lived --listen runs.

Comment thread cmd/root/run_listen.go
if !exposed {
sm = server.NewSessionManager(ctx, nil, rt.SessionStore(), 0, &f.runConfig)
}
guard := sm.AttachRuntime(ctx, sess.ID, rt, sess)

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.

[medium] Shared SessionManager accumulates stale session entries from spawned TUI tabs

When --listen is active (exposed == true), recallCoordinatorOpt calls sm.AttachRuntime(ctx, sess.ID, rt, sess) on the shared serving SessionManager for every tab spawned via ctrl+t. AttachRuntime internally calls context.WithoutCancel(ctx), so cancelling spawnCtx does not propagate into the manager — the registered session entry survives the tab's lifetime.

The spawner's cleanup function (in createSessionSpawner) only calls stopToolSets(spawnCtx, t); there is no matching sm.DeleteSession(sess.ID) or equivalent detach call. Over a long-lived --listen run with many tab opens-and-closes, the shared SessionManager accumulates stale sessionRuntime entries, event-log entries, and follow-up injector registrations — one per closed tab — indefinitely.

Suggested fix: call sm.DeleteSession(newSess.ID) (or the appropriate cleanup method on the shared manager) in the spawner's cleanup closure after stopToolSets, so each tab's resources are released when it closes:

cleanup := func() {
    stopToolSets(spawnCtx, t)
    if f.listenSM != nil {
        f.listenSM.DeleteSession(newSess.ID)
    }
}
Confidence Score
🟢 strong 97/100

Comment thread cmd/root/run_listen.go
// this, a board watching the run sees a green card while a tab is mid-turn.
func (f *runExecFlags) recallCoordinatorOpt(ctx context.Context, rt runtime.Runtime, sess *session.Session) app.Opt {
sm := server.NewSessionManager(ctx, nil, rt.SessionStore(), 0, &f.runConfig)
sm := f.listenSM

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.

[low] Unsynchronized read of f.listenSM in recallCoordinatorOpt relies on an implicit ordering invariant

f.listenSM is written once in startSessionCoordinator (before the TUI starts) and read in recallCoordinatorOpt (called from tab spawns, after the TUI starts). The ordering is enforced today by the sequential call structure, but the Go memory model does not formally guarantee visibility of a struct-field write across goroutine boundaries without explicit synchronization (mutex, sync/atomic, sync.Once, or a channel send/receive).

If a future refactor ever creates a goroutine that reads f.listenSM before the write completes, the race detector would flag this. A sync.Once or a sync/atomic.Pointer[server.SessionManager] would make the ordering mechanically enforceable rather than relying on documentation.

This is a low-risk concern today given the strict sequential setup, but worth noting since the invariant is currently only documented in a comment.

Confidence Score
🟡 moderate 57/100

@aheritier aheritier added kind/feat PR adds a new feature (maps to feat:). Use on PRs only. area/core Core agent runtime, session management labels Jul 24, 2026
Sessions spawned after startup (TUI tabs, ctrl+t) attached to a private
SessionManager, so a control-plane client could neither tail their events
nor observe their state: a board watching the run showed the card idle
while a tab was mid-turn.

When the run serves a control plane, publish its SessionManager on the
flags (set before the TUI starts, so no spawn races it) and have
recallCoordinatorOpt attach spawned sessions to it, registering their App
event source and follow-up injector exactly like the initial session.
@dgageot
dgageot force-pushed the board-tab-event-sources branch from ed6e139 to 388f3dc Compare July 24, 2026 08:18
@dgageot
dgageot merged commit c75786d into docker:main Jul 24, 2026
8 checks passed
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/feat PR adds a new feature (maps to feat:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants