feat(desktop,host-service): resume v1 pane agent sessions after migration

Merged
KI
3mo ago
+874-26

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 same agents.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-hook v7 + cursor/copilot/gemini) send agentId and the un-collapsed rawEventType on the v1 dispatch.
  • New main/lib/notifications/v1-agent-sessions.ts records per-pane {agentId, agentSessionId, prompted, endedAt} into app-state.json, mirroring host-service binding semantics: never-prompted sessions excluded (resume would hit "No conversation found"), the agent's own SessionEnd marks 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)

  • pendingMigratedTerminals queue entries record their v1PaneId (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.seedResumeCandidate mutation 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. useCreatePendingMigratedTerminals calls it best-effort after each createSession; from there the existing resumeCandidate query, TerminalAgentResumeBanner, and resume flow take over unchanged.

Manual QA Checklist

Capture

  • Run claude in a v1 pane, prompt it → v1AgentSessions entry in app-state.json with prompted: true
  • Quit claude cleanly (/quit or Ctrl+D on prompt) → entry gains endedAt
  • Kill the pane's pty out from under claude → endedAt cleared (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, seedEndedTerminalAgentBinding persistence tests; full sweeps: desktop main (573), host-service trpc (359), all green
  • Mutation-verified: removing the prompted guard 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 v1PaneId at 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 v1PaneId and 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.json exactly like claude; the legacy notify callback's thread-id is 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 rawEventType and agentId; Electron main records per‑pane {agentId, agentSessionId, prompted, endedAt} in app-state.json and prunes to live panes.
    • Carry: pendingMigratedTerminals records v1PaneId; migration.readV1PaneAgentSessions reads the latest capture at v2 pane creation.
    • Seed: terminalAgents.seedResumeCandidate writes 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-id as the session id on turn completion (prefers explicit session_id).
  • 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 v1PaneId by type; ensureValidShape tolerates a legacy null panes map and prunes stale v1 session records.
    • Hooks: bump script markers (notify v7, copilot v4, cursor v6, gemini v5).

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.