Skip to content

Keep session panes mounted when returning to the conversation list (terminals lose their reap protection on navigation) #2385

Description

@2witstudios

Context

PR #2383 gives a session a way out — clicking Agents lands on the conversation list, and a session gets an explicit escape. Both paths clear selectedSessionId, and AgentsSurface.tsx is a ternary, so the entire AgentPanes tree unmounts rather than being hidden. That unmounts XtermTerminal, whose cleanup emits shell:disconnect (apps/web/src/components/agents/shell/XtermTerminal.tsx:390).

Raised as a P1 on #2383 by chatgpt-codex-connector. It was not a blocker for that PR, for the reasons below, but it is worth doing deliberately now that leaving a session is a routine action rather than a rare one.

What is NOT the problem

Worth stating so this doesn't get over-fixed. Detaching removes a viewer, not the session:

  • disconnectConnectionremoveViewer(...) (apps/realtime/src/terminal/shell-handler.ts:1529-1537). The PTY keeps running.
  • Scrollback is buffered server-side and persisted on teardown (shell-handler.ts:188-203, MAX_SCROLLBACK_BYTES in terminal-session-map.ts:4).
  • Reattach is a built-in fast path: "JOIN this connection to an ALREADY-RUNNING session and hand the viewer its buffered scrollback: cancel any pending idle reap…" (shell-handler.ts:1552-1561).
  • The idle reap is quiet-based, not detachment-based, and agent input into a viewer-less session re-arms it — armIdleReap's own docblock: "an agent driving a headless shell is activity, and reaping mid-command thirty minutes after it started would kill work in progress" (shell-handler.ts:481-500).

So navigating away does not lose terminal data, and a headless agent-driven shell is explicitly protected.

What IS the exposure

DETACHED_IDLE_MS = 30 * 60 * 1000 (apps/realtime/src/terminal/terminal-session-map.ts:5). Once the last viewer leaves, the timer arms and, if it fires, kills the session (session.command.kill('idle-reap') + endShellSession, shell-handler.ts:508-519).

Two real cases:

  1. A human-started long-running command, with no agent input, left unattended for >30 minutes → reaped. Nothing re-arms the timer for it.
  2. A command that finishes while nobody is attached — the process is fine and the tail is persisted, but the live shell:closed exit code has no viewer to reach, so the exit status can be missed.

Why it is worth doing now

This cost is not new — AgentPanes is key={selectedSessionId} (apps/web/src/components/agents/AgentsSurface.tsx:176), so switching between sessions has always paid exactly the same price. What changed is frequency: leaving a session used to be nearly impossible (the only routes out destroyed it), and is now the default landing behaviour. The same mechanism moved onto the happy path.

Fixing it would also make session switching non-destructive, which is the larger win and is not achievable by special-casing the back button.

Proposed approach

Keep the session's panes mounted while showing the conversation list — e.g. an invisible keep-alive container, the same technique SessionPanes already uses within a session (apps/web/src/components/agents/panes/SessionPanes.tsx:86,108invisible/visibility:hidden, deliberately not hidden/display:none, for this exact reason).

Note this is effectively the MachineKeepAliveHost pattern, which the agents surface's docblock retired on the grounds that it "has no successor here because it has no problem to solve." That justification no longer holds, so reintroducing it is a deliberate design change and should carry its own review — which is why it was kept out of #2383 rather than bolted on.

Open questions for whoever picks this up:

  • Scope: keep-alive only the most recent session, or an LRU of N? Unbounded keep-alive holds concurrency slots and billing heartbeats open.
  • Interaction with forgetWorkspace and the GC path at AgentsSurface.tsx:70-74 (a session that no longer exists server-side must not be kept alive).
  • Whether a kept-alive-but-hidden session should still count as "attached" for the reap — if it does, the timer never arms, which is the point, but it also means a backgrounded session holds its slot indefinitely.

References

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions