feat(desktop,host-service): resume v1 pane agent sessions after migration
Summary
- Migrated v1 terminal panes lost their agent entirely — only
{paneId, cwd}crossed the v1→v2 boundary, so a pane running claude came back as a bare shell. Now the migrated pane surfaces the same resume banner and detection as a killed v2 session (#6253) and resumes through the sameagents.run({resumeSessionId})path.
Why / Context
#6253 made killed v2 agent sessions resumable by id, but the v1→v2 migration predates it and had nothing to resume from: v1 never recorded which agent session ran in a pane. The hook script's v1 fallback collapsed SessionStart/SessionEnd into Start/Stop, dropped SUPERSET_AGENT_ID, and the Electron hook server discarded the session id after logging it. This closes that gap so the v1 sunset doesn't strand in-flight agent sessions.
How It Works
Three stages:
1. Capture (v1 side, keeps working after the flip)
- Hook templates (
notify-hookv7 + cursor/copilot/gemini) sendagentIdand the un-collapsedrawEventTypeon the v1 dispatch. - New
main/lib/notifications/v1-agent-sessions.tsrecords per-pane{agentId, agentSessionId, prompted, endedAt}intoapp-state.json, mirroring host-service binding semantics: never-prompted sessions excluded (resume would hit "No conversation found"), the agent's ownSessionEndmarks a clean quit, a goodbye within 30s of the pane's terminal death is a SIGHUP death gasp and upgrades back to resumable, and 30s stragglers can't revive a clean quit. Entries prune to live pane ids at boot. - Capture runs in Electron main, so it keeps updating even after the user flips to v2 while v1 daemon sessions live on.
2. Carry (migration)
pendingMigratedTerminalsqueue entries record theirv1PaneId(nullable; old entries just skip seeding).- The capture is read at v2 pane-creation time via
migration.readV1PaneAgentSessions— not frozen into the plan — because the migration ledgers panes on every boot while the user is still on v1, and the pane's agent keeps running and reporting long after that.
3. Seed (v2 side)
- New
terminalAgents.seedResumeCandidatemutation inserts an already-ended binding (endReason: "terminal-exited",lastEventType: "Stop") for the recreated terminal, after verifying the terminal belongs to the workspace, and never overwrites a real binding.useCreatePendingMigratedTerminalscalls it best-effort after eachcreateSession; from there the existingresumeCandidatequery,TerminalAgentResumeBanner, and resume flow take over unchanged.
Manual QA Checklist
Capture
- Run claude in a v1 pane, prompt it →
v1AgentSessionsentry inapp-state.jsonwithprompted: true - Quit claude cleanly (
/quitor Ctrl+D on prompt) → entry gainsendedAt - Kill the pane's pty out from under claude →
endedAtcleared (death gasp upgrade)
Migration + resume
- Migrate the workspace, open it in v2 → recreated terminal pane shows "Claude Code was interrupted" banner
- Resume → claude reopens the v1 conversation in a fresh terminal, pane repoints, old shell disposed
- Cleanly-quit v1 sessions and never-prompted sessions get no banner
- Launching a new agent in the migrated terminal before its workspace opens is unaffected (seed no-ops on existing bindings)
Testing
bun run typecheck(desktop + host-service)bun run lint(exit 0)bun test: new transition tests (v1-agent-sessions.test.ts),resolveMigratedPaneResume+ plan tests,seedEndedTerminalAgentBindingpersistence tests; full sweeps: desktop main (573), host-service trpc (359), all green- Mutation-verified: removing the
promptedguard from resume detection makes tests fail - Manual QA above not yet run end-to-end (needs a real v1 profile); flagging for a CDP pass before merge if desired
Design Decisions
- Read capture at creation time instead of plan time: the terminal-migration ledger is one-shot per pane, but the pane keeps living in v1 after it's ledgered. Freezing agent info into the plan would ship stale (usually empty) sessions; resolving by
v1PaneIdat first v2 open uses the latest state. - Seed an ended binding instead of auto-launching
claude --resume: keeps the user in control and reuses the shipped banner/detection verbatim rather than a parallel path.
Known Limitations
- Capture only knows sessions that fire ≥1 hook event after this ships — an agent idle since before the update won't be offered until its next turn. Nothing recoverable; v1 never persisted this.
- Panes already migrated and created before this ships have no
v1PaneIdand get no resume. - Like the v2 resume flow, resume launches at the workspace root — a claude session started in a subdirectory may not be found by
--resume(claude keys sessions by cwd). - Codex coverage (verified empirically on codex-cli 0.146): TUI sessions capture via
~/.codex/hooks.jsonexactly like claude; the legacy notify callback'sthread-idis now extracted as a fallback id, so exec-launched and pre-hooks codex builds capture on their first completed turn. Only a codex session killed before any completed turn stays uncapturable.
Summary by cubic
Migrated v1 terminal panes now preserve and resume their agent sessions after moving to v2, showing the standard resume banner and flowing through agents.run({ resumeSessionId }). Adds Codex support by deriving the session id from thread-id, and hardens seeding against transient failures.
-
New Features
- Capture: v7 notify hooks send
rawEventTypeandagentId; Electron main records per‑pane{agentId, agentSessionId, prompted, endedAt}inapp-state.jsonand prunes to live panes. - Carry:
pendingMigratedTerminalsrecordsv1PaneId;migration.readV1PaneAgentSessionsreads the latest capture at v2 pane creation. - Seed:
terminalAgents.seedResumeCandidatewrites an ended binding (endReason: "terminal-exited",lastEventType: "Stop") for the recreated terminal; the renderer seeds this after create. - Codex: notify hook falls back to
thread-idas the session id on turn completion (prefers explicitsession_id).
- Capture: v7 notify hooks send
-
Bug Fixes
- Resume seeding: don’t dequeue until seed succeeds; abort the pass on capture‑read errors so the next open retries.
- Capture correctness: never inherit a session id across an agent change; ignore goodbyes from a different agent/session; skip app‑state writes when nothing changes.
- Robustness: filter
v1PaneIdby type;ensureValidShapetolerates a legacy null panes map and prunes stale v1 session records. - Hooks: bump script markers (
notifyv7,copilotv4,cursorv6,geminiv5).
Summary by CodeRabbit
-
New Features
- Added tracking for v1 agent sessions, including prompts, completion, revival, and terminal exits.
- Preserved agent-session metadata during terminal migration.
- Enabled eligible migrated sessions to resume automatically in the new workspace.
- Added support for safely seeding resumable agent bindings.
-
Bug Fixes
- Removed stale session records for panes that no longer exist.
- Improved session identification, including Codex thread IDs and raw lifecycle events.
- Updated agent notification hooks for more reliable session tracking.