feat(run): expose spawned TUI tabs on the --listen control plane - #3819
Conversation
docker-agent
left a comment
There was a problem hiding this comment.
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.
| if !exposed { | ||
| sm = server.NewSessionManager(ctx, nil, rt.SessionStore(), 0, &f.runConfig) | ||
| } | ||
| guard := sm.AttachRuntime(ctx, sess.ID, rt, sess) |
There was a problem hiding this comment.
[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 |
| // 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 |
There was a problem hiding this comment.
[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 |
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.
ed6e139 to
388f3dc
Compare
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 privateSessionManagerthat 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
SessionManageron the run flags before the TUI starts, so no tab can be spawned before the manager is wired up.recallCoordinatorOptis 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>/eventsstreams the tab'suser_message,stream_started, andstream_stoppedevents, each stamped with the correctsession_id.No breaking changes; the control-plane API surface is unchanged.