Stop firing duplicate agent resumes on relaunch

Merged
AU
3mo ago
+455-4

Summary

Closes #8446.

On app relaunch, Workspace.createPanel decided per-panel, independently, whether to auto-resume a restored agent session (codex resume <id> / claude --resume <id>), using only the persisted wasAgentRunning flag with no check of whether a process for that session was already alive. Plain (non-tmux) resumes had no liveness check at all, and nothing prevented two panels referencing the same session from both firing a resume in the same restore pass. After a crash, this could fire a resume for every restored panel simultaneously — the reported repro was 22 concurrent codex resume processes within a 30-second window, all idle an hour later, each holding an open handle to codex's SQLite state, causing intermittent "database is locked" failures on unrelated later Codex launches.

  • Before constructing restoredAgentResumeLaunch, consult the same live-process index already used for "reopen closed tab" and Fork Conversation availability (SharedLiveAgentIndex / RestorableAgentSessionIndex.entry(workspaceId:panelId:)) via the new AgentResumeLiveness.hasLiveProcess(for:kind:sessionId:). If a process for this exact session is already alive, skip the resume.
  • Add AgentResumeLaunchGuard, a per-process-lifetime dedup guard: the first panel to resume a given (kind, sessionId) claims it; any other panel referencing the same session in the same restore pass is turned away, even before the freshly spawned process becomes visible to the live-process index (which only updates from a periodic process scan).
  • Generalize reconcileSurfaceResumeBindings, previously tmux-only, to also drop a plain agent-hook resume binding when AgentResumeLiveness reports its session is no longer live, so a normal exit of a non-tmux agent doesn't leave a stale binding that gets replayed as a resume on the next relaunch.

wasAgentRunning's nil-defaults-to-true backwards-compatibility behavior is intentionally untouched — a genuine crash-restart with nothing else alive still auto-resumes normally, per the issue's guidance to fix this via the liveness check rather than that default.

Scope note: this targets the restorableAgent-driven resume path specifically (AgentResumeCommandBuilder / resumeStartupCommand()), which is what the issue identifies as the actual unconditional/unchecked mechanism. The separate resumeBinding-driven (agent-hook binding) launch path already has its own tmux-specific isProcessDetected liveness gate at launch time; this PR generalizes only its persist-time stale-binding cleanup, not its launch-time gate, to keep the diff scoped to the reported bug.

Test plan

  • cmuxTests/AgentResumeLaunchGuardTests.swift / cmuxTests/AgentResumeLivenessTests.swift: new regression suites for the dedup guard and liveness-check logic in isolation (pure, no Workspace/app dependency). Added in a separate commit against deliberately naive stub implementations first (confirmed red — xcodebuild test -scheme cmux-unit -only-testing:cmuxTests/AgentResumeLaunchGuardTests -only-testing:cmuxTests/AgentResumeLivenessTests, 3 failing expectations), then the real implementation + Workspace.swift wiring lands in the second commit (confirmed green — same command, 9/9 passing).
  • xcodebuild -scheme cmux build — app target compiles cleanly, no new warnings.
  • python3 scripts/normalize-pbxproj.py, ./scripts/check-pbxproj.sh, ./scripts/lint-pbxproj-test-wiring.sh — all pass.
  • Full end-to-end restore/spawn behavior (an actual PTY resuming an actual codex/claude process across a real app relaunch) isn't unit-testable without a running app, so it isn't covered by an automated test here — this needs manual dogfood verification (relaunch cmux with multiple resumable agent sessions open, confirm only one codex resume/claude --resume fires per underlying session).

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Prevents duplicate agent resumes on app relaunch and avoids rebinding inactive panels. Fixes #8446 so only one codex resume/claude --resume runs per session, preserves remote .persistentSSH bindings during cleanup, and pulls in an upstream CmuxGit init fix to unblock builds.

  • Bug Fixes
    • Gate restorable-agent resumes in Workspace.createPanel via AgentResumeLiveness.hasLiveProcess(...) using SharedLiveAgentIndex/RestorableAgentSessionIndex, and skip the session rebind when the gate suppresses the launch.
    • Add AgentResumeLaunchGuard to dedup across panels during the same restore pass, with a 60s TTL so later legitimate resumes aren’t blocked; release claims early when a launch fails and prune expired claims to bound growth.
    • Drop stale plain agent-hook resume bindings in reconcileSurfaceResumeBindings when the session isn’t live, using the fresh RestorableAgentSessionIndex from the same scan snapshot; restrict pruning to .local bindings and never delete .persistentSSH remote bindings.
    • Fix cmuxTests build by importing Foundation in AgentResumeLaunchGuardTests.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented duplicate agent resume launches during overlapping restore flows with TTL-based in-memory deduplication and early release on failed panel creation.
    • Stopped stale agent-hook resume bindings from being replayed when the referenced agent session is no longer live.
    • Improved auto-resuming terminal restore to suppress redundant resume attempts and avoid stealing authoritative session ownership when liveness/dedup blocks resuming.
  • Tests
    • Added regression coverage for launch deduplication (including TTL and release behavior) and liveness/stale binding detection.