fix(host-service): source workspace session lists from daemon+DB truth so background agents survive restarts
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.countBackgroundSessionsnow 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:
- In-memory sessions first — they own liveness, titles, socket-attachment, and respect
listedfor hidden internal sessions (teardown runners). - 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. - 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→[]andcountBackgroundSessions→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 failbun run typecheck(host-service + desktop),bun run lintclean
Known Limitations
- A restored session shows
title: null("Terminal") andDetacheduntil something attaches — the daemon doesn't know renderer-side titles. Auto-adoption now sees the session and attaches, which restores the title. listSessions/countnow do onedaemon.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.writeInputto 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
listSessionsandcountBackgroundSessionsuselistWorkspaceTerminalSessions; count excludesattachedTerminalIdsand 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.