Stop firing duplicate agent resumes on relaunch
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 newAgentResumeLiveness.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 whenAgentResumeLivenessreports 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.swiftwiring 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 --resumefires per underlying session).
🤖 Generated with Claude Code
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.createPanelviaAgentResumeLiveness.hasLiveProcess(...)usingSharedLiveAgentIndex/RestorableAgentSessionIndex, and skip the session rebind when the gate suppresses the launch. - Add
AgentResumeLaunchGuardto 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
reconcileSurfaceResumeBindingswhen the session isn’t live, using the freshRestorableAgentSessionIndexfrom the same scan snapshot; restrict pruning to.localbindings and never delete.persistentSSHremote bindings. - Fix
cmuxTestsbuild by importingFoundationinAgentResumeLaunchGuardTests.
- Gate restorable-agent resumes in
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.