fix(desktop): garbage-collect persisted terminal scrollback in localStorage

Merged
KI
3mo ago
+480-11

Problem

Terminal scrollback is persisted to renderer localStorage (terminal-buffer:<id> + terminal-dims:<id>) so panes can restore across remounts and relaunches. Those entries were only deleted on an explicit terminal kill (terminalRuntimeRegistry.dispose()). Terminals that disappear any other way — PTY exit, workspace deleted, host reset, crash, org switch — leaked their snapshot forever.

Measured on a real install after ~2 months: 945 orphaned buffers / 23.7 MB, enough that Chromium's synchronous localStorage load wedged the renderer (652% CPU, 6.31 GB RSS, unresponsive AX queries).

Fix (two layers)

1. Destroyed PTY ⇒ scrollback deleted, immediately.

  • host-service tags attach errors for permanently-destroyed sessions (not found / disposed / exited) with code: "session-gone" on the existing error message (additive, version-skew safe).
  • The renderer transport tracks that signal plus live exit messages as sessionEnded, distinct from _terminated (which also covers access denials where the PTY may still be alive). The signal clears the persisted snapshot on the spot, and every persistence path — detach, release, parked eviction — clears instead of re-persisting a dead session's buffer. A successful re-attach resets the flag (session re-created under the same id).

2. Boot-time GC bounds the store no matter how a terminal dies.

  • Every persist/restore stamps the terminal in a single terminal-buffer-persisted-at index.
  • At renderer boot, before any terminal mounts, pruneExpiredTerminalState() removes: unstamped entries (all pre-existing bloat, cleaned on first launch after this ships — a one-time scrollback-restore loss), entries older than a 14-day TTL (covers deaths the renderer never observes, e.g. workspace deleted while the app is closed), and oldest entries beyond a 2M-char (~4 MB) total budget.

Pruning deliberately does not cross-reference pane layouts: v2WorkspaceLocalState is org-scoped and cache-first, so reading it for GC could delete valid buffers for evicted orgs / not-ready collections.

Relationship to #5965

Complementary, not overlapping fixes: #5965 guards the TanStack-DB localStorage collections write path against the QuotaExceededError rollback→retry loop once the store is already full, and reclaims orphaned snapshots only reactively on that quota path. This PR stops the terminal-snapshot store from growing unbounded in the first place and ties snapshot lifetime to PTY lifetime. Both touch terminal-runtime(-registry).ts, so whichever lands second needs a small rebase.

Testing

  • terminal-buffer-gc.test.ts: legacy pruning, TTL, size-budget eviction order, index hygiene, dims-only orphans, corrupt-index recovery. Core tests verified to fail when their behavior is mutated out.
  • terminal-ws-transport.test.ts: exit ⇒ sessionEnded + callback fired once; session-gone attach error ⇒ same; plain error ⇒ untouched; re-attach resets.
  • terminal-runtime-registry.test.ts: release of an ended session disposes with clear and never serializes.
  • Full terminal lib suite: 304 pass. bun run typecheck --filter=desktop --filter=@superset/host-service and bun run lint clean.

Summary by CodeRabbit

  • New Features
    • Added automatic garbage collection for persisted terminal scrollback and dimensions at desktop startup, including TTL-based pruning and a size cap.
    • Introduced persisted-state tracking so terminal history retention is updated as terminals are persisted, restored, or cleared.
  • Bug Fixes
    • Improved session-ended handling to reliably clear persisted terminal state and avoid retaining snapshots after terminal termination.
    • Enhanced persistence cleanup to remove stale entries and keep the persisted-at index consistent.
    • Improved robustness when persisted GC index data is missing or corrupted.