fix(host-service): source workspace session lists from daemon+DB truth so background agents survive restarts

Merged
KI
4mo ago
+188-40

Summary

  • After any host-service restart (app update, tray restart, crash), every pane-less terminal session — i.e. background agents — vanished from the session dropdown, the background-terminals dropdown, and pane auto-adoption, while the agent kept running under the detached pty-daemon. Verified live: 17 daemon-owned PTYs (9 running agents: 8 Claude, 1 Codex) invisible right after the 1.16.0 update restarted the host-service.
  • terminal.listSessions / terminal.countBackgroundSessions now answer from the source of truth (in-memory sessions ∪ daemon-alive ⨝ DB-active rows) instead of the in-memory session map alone.

Why / Context

The in-memory session map is attachment plumbing: it empties on restart and only repopulates when a renderer attaches a pane. Reading it alone to answer "what sessions exist" made the answer wrong exactly when it matters — a background agent has no pane, so nothing ever re-registers it. It's also a deadlock: auto-adoption (#5740/#5808) is the thing that would attach a pane, but it reads the same blind listSessions. Meanwhile the sidebar agent chips (DB-joined bindings) still showed the agent, so users saw "there's a Claude in the background" while every terminal surface denied it existed.

This is the session-map twin of the port-detection gap fixed in #5325/#5430 — same root: host-service restart empties process-local state while the detached daemon keeps PTYs alive.

How It Works

New listWorkspaceTerminalSessions(db, workspaceId) in terminal.ts:

  1. In-memory sessions first — they own liveness, titles, socket-attachment, and respect listed for hidden internal sessions (teardown runners).
  2. Union with every other alive daemon session joined to an active, workspace-owned, non-dispose-stamped row (attached: false, title: null — honest state for a session nothing has attached to since the restart). Dispose-stamped rows are the reaper's scheduled kills (workspace close, #5752) and are never resurfaced.
  3. If the daemon is unreachable, degrade to the memory view — never worse than the old behavior.

Both router endpoints derive from this one function, so the dropdown and its count can never disagree. The now-callerless map-based countTerminalSessions is deleted. An earlier draft warmed the map back up via a reaper reconcile instead; rejected in favor of fixing the read at its source — no eventual-consistency window, no standing daemon subscriptions for unattached sessions.

Manual QA — real before/after

Ran an isolated end-to-end harness: real host-service (source), its own production-style detached pty-daemon, one session, then a real kill -TERM + respawn of the host-service (the app-update scenario), no renderer ever attaching.

  • Before (pre-fix code): post-restart the daemon (same pid) still owns the alive PTY, but listSessions → [] and countBackgroundSessions → 0
  • After (this fix): post-restart listSessions → the session, count → 1, immediately on the first query — same daemon pid across the restart, PTY untouched
  • Fresh queries are read-only: daemon PTY pid unchanged before/after listing

Testing

  • New integration test (real pty-daemon): after a simulated restart the session is listed immediately with attached:false/title:null, counted as background, exclusion works, PTY pid untouched, and a dispose-stamped session never resurfaces. Mutation-checked: forcing the read back to map-only fails the test.
  • bun test (packages/host-service): 892 pass / 0 fail
  • bun run typecheck (host-service + desktop), bun run lint clean

Known Limitations

  • A restored session shows title: null ("Terminal") and Detached until something attaches — the daemon doesn't know renderer-side titles. Auto-adoption now sees the session and attaches, which restores the title.
  • listSessions/count now do one daemon.list() round trip per query (local unix socket, same call the resources panel already makes). Renderer polls are 2s/10s while the respective dropdown is open.
  • terminal.writeInput to a not-yet-attached restored session still returns NOT_FOUND (pre-existing); renderer flows attach before writing. Adopt-on-miss there is a possible follow-up.

Summary by cubic

Fixes session listing to read from the daemon + DB so background agents remain visible after host-service restarts. Prevents missing sessions in dropdowns and restores auto-adoption behavior.

  • Bug Fixes
    • Session lists now union in-memory sessions with alive daemon sessions joined to active, non-disposed DB rows.
    • Restored sessions appear with attached:false and title:null until something attaches.
    • Snapshot in-memory state after the daemon lookup to avoid returning sessions disposed mid-flight.
    • Both listSessions and countBackgroundSessions use listWorkspaceTerminalSessions; count excludes attachedTerminalIds and falls back to in-memory with a warning if the daemon is unreachable.

Summary by CodeRabbit

  • New Features
    • Workspace terminal session listings now incorporate active sessions managed by the PTY service, including after host service restarts.
    • Returned sessions present consistent “detached” metadata (such as blank titles and default exit values) when applicable.
  • Bug Fixes
    • Sessions with a pending disposal request no longer appear in active session lists or background-session counts.
    • Background-session counts remain accurate by excluding sessions already attached to this host process.
    • Session discovery gracefully falls back to in-process results if the PTY service is temporarily unreachable.
  • Tests
    • Added an integration test covering post-restart listing/count behavior and ensuring daemon PTY state is unchanged.