Recover routed Claude sessions through cmux restore; journal closed panes - #15324
Conversation
Closing a Claude pane kills the agent before its SessionEnd hook runs, so the journal keeps the session open and crash recovery reopens it. The test closes a pane carrying a Claude session, then runs recovery against the same journal and hook store; the closed session must not be a candidate. Adds the journal seam the test injects; the close path does not use it yet, so this commit fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d panes Crash recovery typed its own launcher command for routed Claude sessions, so it skipped three things the normal restore does: setting CMUX_AGENT_RESTORE_LAUNCH and CMUX_CUSTOM_CLAUDE_PATH for the wrapper, checking the routed launcher is on PATH, and reapplying the observed permission mode. Recovery now opens each session in a panel carrying its restore record and types `cmux restore claude <id>`, so the planner the normal restore uses covers all three. Only an undeclared launcher that the restore record cannot rebuild still resumes through its recorded prefix, now with the observed permission mode. When the routed launcher is missing, the restore planner already fell back to a direct resume; it now reports why on stderr. Closing a pane records agent.session.ended for the Claude session it carried, so a crash afterwards no longer reopens it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
Warning Review limit reachedNext included review available in 11 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (14)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
All contributors have signed the CLA ✍️ ✅ |
|
Dogfood build of cmux DEV pr-15324-05bf5cb0.app The link opens this exact commit in the cmux dev menu bar app. The build starts on each push and the page waits until it is ready; a newer push replaces it. It signs in against production, so Cloud or backend changes still need a tagged build with a development backend. |
…ions carried elsewhere A closed panel no longer journals the end of a session another panel still carries, such as a restore that lost to a live owner. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
CI failure attributionCI passes on Written by |
|
Regression proof for
Subagent review: the first pass found one blocker (a stale Not verified: no tagged-build GUI dogfood. Coverage is the app-host and package tests above. — Icicle g1 ⚙️ (run_worker_20260927_686a3a99) |
|
Merge receipt for |
7b6ad14 PR media: tour-only pushes load the earlier build of the same inputs (manaflow-ai#15361) 842d580 ci: prebuild selected Swift packages in parallel before the serial test pass (manaflow-ai#15114) 1a7467a dogfood: give the agent activity reorder tour paths globs (manaflow-ai#15360) 31b2619 Recover routed Claude sessions through cmux restore; journal closed panes (manaflow-ai#15324) 5a3ca1a ci: price mini contention in distance routing and keep SwiftPM builds on owned Macs (manaflow-ai#14804) b8afe20 Add notifications.suppressWhenAppFocused setting (manaflow-ai#14701) 3843dd6 ci: queue main's whole full-suite run on the owned pool (manaflow-ai#15356) # Conflicts: # .github/workflows/ci-guards.yml # .github/workflows/ci-macos.yml # .github/workflows/pr-media.yml # .github/workflows/test-e2e.yml
…s own, journal Dock closes (#15375) * Regression tests: paced crash recovery, recorded account pin, Dock close journaling Three leftovers from crash recovery (#15363), each pinned by a test that fails on main: - Recovery starts every lost session at once. The tests expect only a few to start now (sessions from a workspace on screen, then the most recently active) and the rest to open their workspace and start on first visit. Adds the start plan seam, which still starts everything. - A routed launcher that appends arguments after the forwarded tail leaves no launcher prefix, so the resumed session lost its account pin. The tests expect the wrapper to record the pin from the launcher's own routing headers and the routed restore to use it. - Closing a Claude pane in the Dock does not journal agent.session.ended. Adds the Dock's journal seam; the close path does not use it yet. Refs #15363 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Start only a few recovered agent sessions at once Crash recovery started every lost session together, so a heavy user's relaunch spawned dozens of agents at the same moment. Recovery now starts a few right away: sessions from a workspace on screen, then the most recently active. The rest open their workspace now and resume on its first visit, the way startup restore treats background workspaces. Their panels carry the session from the start, so a second recovery still skips them. A session resumed through its recorded launcher still starts now, since its launch claim is taken when the command is typed. Refs #15363 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Record the routed launch's account pin on its own The pin a routed Claude restore reapplies came from the launcher prefix, which is what is left of the launcher's argv after stripping the tail it forwarded to Claude. A routed launcher that appends arguments after that tail breaks the match, so no prefix was recorded and the resumed session went back to the pool. The wrapper now records the pinned account itself. It reads the pin from the routing headers the launcher wrote into its private settings file for this launch, so the pin no longer depends on how the launcher was invoked. The hook keeps it with the launch record, and the routed restore passes it as `--account`. A prefix pin still applies to records that predate this. The value is launch metadata only and is never replayed into a resumed process's environment. Refs #15363 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Journal Dock pane closes through the shared close path Closing a workspace pane records agent.session.ended for the Claude session it carried (#15324), but a Dock pane close did not, so a Claude pane closed in the Dock could come back after a crash. The close journaling moves off Workspace onto a small protocol both panel owners conform to, and the Dock calls it from the teardown every close takes. A panel the Dock hands to another container is detached first and is not journaled. Refs #15363 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Address review: hold deferred recoveries until visit; carry the pin through queued hooks A deferred recovered session still started at once: a panel that carries a restore record is admitted when its workspace commits, and admission starts the terminal headless whether or not the workspace is loaded. Recovery now stages those panels with admission deferred, and the workspace admits them when it is first selected. The app test checks the held terminals and the release on visit. The wrapper's recorded account pin never reached the session-start capture: Claude's lifecycle hooks are queued, and each queued layer keeps only an allowlist of environment keys. Claude's queued hooks now carry the routed launch metadata (the pin and the marker pair), and the app's hook ingress accepts it. The capture still validates each value before recording it. Refs #15363 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Pass the first-visit hold through workspace creation Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Address review: journal the Dock's aside agent-hook binding on close When Dock process detection shows a tmux binding, the agent-hook binding waits in managedAgentResumeBindingsByPanelId, and a close read only the effective one. The host now lists every binding that can name the session. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Fix workspace creation test overrides * Test carried Dock sessions survive stale pane close * Keep managed Dock sessions open during stale close * Fix workspace create review test override --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follow-up to #14870.
Crash recovery typed its own launcher command for Claude sessions started through a routed launcher. That skipped three things a normal
cmux restoredoes: it never setCMUX_AGENT_RESTORE_LAUNCHorCMUX_CUSTOM_CLAUDE_PATHfor the wrapper, it never checked that the routed launcher is on PATH, and it dropped the permission mode the session was last observed in.Recovery now routes those sessions through the regular restore path. Each recovered session opens in a workspace whose terminal carries the session as its startup restore agent (the mechanism Vault resume uses), and types
cmux restore claude <id>. The CLI reads that surface's restore record and plans with the sameAgentRestorePlannera normal restore uses, so wrapper authorization, the PATH check with fallback to a direct resume, and the permission mode come from one code path. Plain Claude sessions take the same path. The launch claim is taken at the CLI's admission boundary, as in a normal restore. Only a session started through an undeclared launcher that the restore record cannot rebuild still resumes through its recorded prefix, and that command now reapplies the observed permission mode too.When the routed launcher is missing, the planner already fell back to a direct resume silently. It now returns a notice, and
cmux restoreprints the reason on stderr before starting the agent: "this session was started through the routed launcher 'sr', which is not on PATH. Resuming the agent directly instead." The string is localized in all 9 required locales.Closing a Claude pane kills the agent before its own end hook reports, so the journal kept the session open and a later crash reopened a pane the user had closed. Closing a panel now journals
agent.session.endedfor the Claude session it carried (its agent-hook binding, restored snapshot, or deferred restore). It skips panels moved to another workspace and closes during app quit, where startup restore owns the sessions. Dock panels are not covered yet.Testing
Regression test, two commits:
cmuxTests/AgentSessionRecoveryAppTests/closedClaudePaneIsNotRecoveredAfterACrash()closes a pane carrying a Claude session in a realWorkspace, waits for the journal write, then runs recovery against the same journal and hook store as the next launch after a crash. Only the session that died with the app may come back. Commit 1 (red, ba10d4f) adds the test and the journal seam without wiring it into the close path. Commit 2 adds the fix. CI receipts are posted below.Also added:
routedSessionsResumeThroughTheRestoreVerb(app host), package tests that a routed candidate yields no launcher argv, that the launcher command reapplies the observed permission mode unless the launch pinned one, that the planner carries the mode, and that a missing routed launcher produces the notice.Nothing was compiled or run on the author's machine beyond the localization check (
localization_catalog.py check: 0 parity errors) and the pbxproj check. Compiles and suites run in CI.— Icicle g1 ⚙️ (run_worker_20260927_686a3a99)
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Recovers routed Claude sessions through the normal
cmux restorepath and journals closed Claude panes so crash recovery no longer reopens sessions the user closed.Bug Fixes
cmux restore claude <id>from a panel carrying the session's restore record, so routed sessions now get wrapper authorization, a PATH check, and the observed permission mode like a normal restore.agent.session.endedfor that session (skipped for panels moved between workspaces, sessions another panel still carries, and during app quit), so a later crash recovery won't reopen it.Written for commit 05bf5cb. Summary will update on new commits.