8.1 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/quick-terminal/spec.md | 2026-08-06 | draft |
Implementation Plan: Quick Terminal Durable Session Lifecycle and Menu Alignment
Overview
Two user-reported defects remain after the refresh-persistence work shipped:
- A quick terminal does not survive a page reload in practice. A refreshed tab re-numbers ("Terminal 7") and a new shell is created instead of reattaching to the existing one.
- The plus-button creation menu opens toward the leading (left) edge of the tab strip, overhanging the workspace edge, which is awkward.
The persistence infrastructure (durable descriptors, boot payload, resync, reattach by sessionId)
already exists and is correct. The reload failure is not a missing-descriptor bug; it is a
session-lifetime bug: quick-terminal PTYs share the agent-login IdleTimeout = 10m /
HardTimeout = 30m in loginpty. A quiet or detached terminal is reaped, so on the next boot/resync
reconcileTab finds no live session, calls markUnavailable, and persists session_id = NULL /
status = exited. The user then starts a fresh terminal, and sequence keeps climbing.
The fix gives quick-terminal host-shell sessions their own lifetime policy: no idle or hard
timeout. They end only on explicit tab close (already stops the PTY), natural process exit
(already reported via HandleSessionExit), or backend shutdown. To avoid leaking PTYs on shutdown
when the timeout no longer reaps them, the loginpty.Manager gains a StopAll wired into graceful
shutdown. The menu fix is a one-line alignment change plus coverage.
Confirmed root cause
apps/backend/internal/agent/loginpty/manager.gosupervise()appliesIdleTimeout(10m) and aHardTimeout(30m)context.WithTimeoutto every session, including host-shell sessions started by quick terminals. A quiet or detached quick terminal is terminated by these.apps/backend/internal/quickterminal/service.goreconcileTab()(run byList, which boot state and GET/quick-terminal-tabsboth call) sees the reaped session as!Runningand callsmarkUnavailable, persistingsession_id = ""andstatus = exited. This is why reload shows the terminal as gone and a new one is created.apps/web/components/quick-chat/quick-tab-add-menu.tsxsetsalign="end"on aw-64DropdownMenuContent; the trigger is at the leading edge of the strip, so the menu opens leftward.
Backend
Per-session lifetime policy in loginpty
- Add an explicit lifetime policy to
Session/StartWithKeyso a session can opt out of the idle and hard timeouts. Host-shell sessions (agentID == HostShellAgentID) use the no-timeout policy; all other agent-login sessions keep the existing 10m/30m behavior. Prefer a small, explicit policy value (e.g. anoTimeout boolor alifetimestruct on the session) set fromagentIDinsideStartWithKey, rather than readingagentIDstring comparisons scattered insupervise. - In
supervise(), when the policy disables timeouts, do not arm the idle timer and do not wrap the context inWithTimeout; still handle natural process exit (exited) and externalstop()so cleanup, map removal, andonExitremain correct. Keep the existing drain/readDonebehavior. - Add
Manager.StopAll()that stops every live session (idempotent), for graceful shutdown.
Graceful shutdown wiring
- Register a cleanup in
backendappthat callsloginMgr.StopAll()during graceful shutdown, so no-timeout quick-terminal PTYs are reaped when the backend stops (they previously relied on the 30m hard timeout). Follow the existingaddCleanup/runCleanupspattern used for the lifecycle manager.
Frontend
Menu alignment
- In
quick-tab-add-menu.tsx, changealign="end"toalign="start"onDropdownMenuContentso the menu opens toward the trailing edge. Preserve the existing mobile bottom-sheet treatment.
Reload reattachment (verification, likely no behavior change)
- The reattach path already exists (stable
tabId→managerKey, reconcile trusts backendrunning). With sessions no longer reaped, confirm no additional frontend change is needed for reload reattachment; only add regression coverage. If a residual client-side path still starts a fresh shell for arunningrestored descriptor, fix it in the terminal tab view / reconcile so a restored live descriptor only attaches bysessionId.
Tests
- What: host-shell sessions are not reaped by idle or hard timeout, while agent-login sessions
still are;
StopAllstops all live sessions and is idempotent; natural exit and external stop still clean up manager maps and fireonExitunder the no-timeout policy. Files:apps/backend/internal/agent/loginpty/manager_test.go. How:testing/synctestto advance fake time past 10m/30m and assert a host-shell session is still running while an agent-login session has exited; directStopAllassertions. - What:
reconcileTabkeeps a still-running host-shell session asrunningwith itssessionIdafter simulated long idle (no reaping), and onlymarkUnavailablewhen the manager truly has no entry. Files:apps/backend/internal/quickterminal/service_test.go. How: table-driven service tests against a manager holding a live long-idle session and one with no entry. - What: graceful-shutdown cleanup invokes
loginMgr.StopAll(). Files: the relevantapps/backend/internal/backendapp/*_test.gofor cleanup wiring. How: assert the registered cleanup stops a seeded live session. - What: the add menu renders with start alignment and existing items.
Files: an existing
apps/web/components/quick-chat/*test (extend, do not add a new file unless none covers this component). How: assert theDropdownMenuContentalignment prop / rendered side and the two menu items.
E2E Tests
- Scenario: GIVEN a running quick terminal left idle past the old 10m idle window (fast-forward
or a shortened test policy hook), WHEN the user reloads and reopens Quick Chat, THEN the same tab
and sequence reattach to the same PTY without a new numbered terminal.
File:
apps/web/e2e/tests/terminal/quick-terminal.spec.ts(extend the existing reattach spec). What to verify: single tab, unchanged sequence/label, same-session marker output, and no second shell. Close every surviving terminal infinally. - Scenario: GIVEN the plus menu on a desktop pointer viewport, WHEN it opens, THEN it opens
toward the trailing edge and stays within the viewport.
File:
apps/web/e2e/tests/terminal/quick-terminal.spec.tsor the existing quick-chat menu spec. What to verify: menu bounding box is to the right of the trigger and within the viewport.
Verification Results
To be filled in during implementation.
Implementation Waves And Parallel Candidates
Wave 1 (parallel-safe, independent surfaces):
Wave 2 (depends on Task 01):
Task 02 is fully independent of the backend tasks and can land in parallel. Task 03 depends on the
Task 01 policy/StopAll surface.
Risks
- Removing timeouts means a runaway or forgotten shell can live for the entire backend uptime.
Mitigation: explicit tab close already stops the PTY, natural exit is reported, and
StopAllreaps everything on shutdown. This matches the user's explicit request and stays bounded by process lifetime. - The lifetime policy must not weaken agent-login OAuth reaping. Keep the policy keyed strictly to
HostShellAgentIDand assert the agent-login path still times out. supervisecleanup correctness (map removal,onExit) must hold when the idle timer and timeout context are absent. Cover natural exit and external stop under the no-timeout policy.- E2E time-advancement for a 10m idle window is expensive; prefer a test-only shortened policy hook or fake-clock injection rather than a real 10-minute wait.