Repository navigation
Fix session restore resuming agents in the wrong directory (drifted cwd) - #5300
2 commits merged into
Conversation
Two regression tests through RestorableAgentSessionIndex.load that fail against the current resolver: - Claude: when the transcript exists but encodes to neither candidate and the config probe can't confirm, restore must prefer the launch cwd, not the drifted recorded cwd. - A directory-namespaced non-Claude agent (Gemini) with a drifted cwd must resolve to the launch cwd; today the Claude-only gate returns the drift. These reproduce the "No conversation found with session ID" failure on session restore for cwd-namespaced agents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe PR classifies agent kinds by session-store namespacing, adds Claude transcript verification to avoid drift, centralizes CLI resume cwd normalization, updates resume command construction to use the computed working directory, and adds tests and fixtures validating these behaviors. ChangesResume Working Directory Selection and Verification
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 17 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (17 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d9919d30bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let resumeWorkingDirectory = Self.agentResumeBindingWorkingDirectory( | ||
| runtimeCwd: cwd, | ||
| launchWorkingDirectory: launchCommand?.workingDirectory | ||
| ) |
There was a problem hiding this comment.
Preserve runtime cwd for id-keyed resume bindings
When the hook is for a cwd-in-file/id-keyed agent such as Codex, OpenCode, Amp, Rovo Dev, or Hermes, this unconditionally replaces the hook-reported runtime cwd with the launch cwd before baking the surface resume command and cwd payload. That contradicts the app-side resolver added in this same change (.cwdInFile returns the recorded runtime cwd) and means a restored surface for a Codex session that started in /repo then worked in /repo/worktree will auto-resume from /repo instead of reopening where the agent was working; kind is available here, so the launch-cwd preference should be limited to cwd-namespaced kinds.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR generalises the earlier Claude-only session-restore cwd fix (from #5154) to all 16 agent kinds and to the second resume path (
Confidence Score: 5/5Safe to merge; the fix is well-scoped, all three production paths are covered by tests, and the conservative default for unrecognised agent kinds avoids regressions. The shared AgentResumeWorkingDirectory resolver is the single source of truth for both the CLI and app paths. The old Claude-only guard is cleanly replaced by a kind-aware switch, transcript verification correctly returns nil on failure (driving the new launchCwd preference), and the cwdInFile fallback to runtime cwd is preserved. No actor-isolation, blocking-runtime, or logging issues were introduced. No files require special attention; the consistency test in cmuxTests/RestorableAgentSessionIndexTests.swift has a minor enum coverage gap for the registry-owned kinds. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Session restore triggered] --> B{Which resume path?}
B -->|CLI surface-binding| C[publishAgentSurfaceResumeBinding]
B -->|App-side resolver| D[restorableWorkingDirectory]
C --> E[AgentResumeWorkingDirectory.resolve\nkind · runtimeCwd · launchWorkingDirectory]
D --> F{kind.cwdNamespacing}
F -->|.cwdInFile\nCodex · Amp · OpenCode · …| G[return recordedCwd ?? launchCwd]
F -->|.byDirectory\nClaude · Gemini · Grok · …| H{kind == .claude?}
H -->|Yes| I[claudeVerifiedRestorableWorkingDirectory\nprobe transcript path + config dir]
I -->|verified candidate found| J[return verified candidate]
I -->|nil — unverifiable| K[return launchCwd ?? recordedCwd]
H -->|No — other byDirectory agent| K
E --> L{cwdNamespacing for kind}
L -->|.byDirectory| M[return launch ?? runtime]
L -->|.cwdInFile| N[return runtime ?? launch]
M --> O[surface.resume.set cwd = launch dir]
N --> P[surface.resume.set cwd = runtime dir]
Reviews (3): Last reviewed commit: "Fix agent session resume cd-ing into the..." | Re-trigger Greptile |
| let resumeWorkingDirectory = Self.agentResumeBindingWorkingDirectory( | ||
| runtimeCwd: cwd, | ||
| launchWorkingDirectory: launchCommand?.workingDirectory | ||
| ) |
There was a problem hiding this comment.
CLI resume binding uses launch cwd for id-keyed agents
agentResumeBindingWorkingDirectory is intentionally agent-agnostic, so it always prefers the launch dir for every agent type, including cwdInFile kinds (Codex, Amp, OpenCode, etc.). The app-side resolver in restorableWorkingDirectory correctly distinguishes these and returns recordedCwd for them — preserving "the directory the agent was last working in." The CLI binding path now silently starts those shells in the launch dir instead.
Resume itself won't fail for id-keyed agents (the session ID is sufficient), but a Codex/Amp user who drifted into a worktree mid-session will find their CLI-restored terminal unexpectedly back at the launch root, while a command-palette restore would put them at the worktree. Passing kind (or its cwdNamespacing) into this helper and returning recordedCwd ?? launchCwd for .cwdInFile would close the gap.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3a7ba80. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a7ba80682
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case .codex, .opencode, .amp, .antigravity, .rovodev, .hermesAgent: | ||
| return .cwdInFile |
There was a problem hiding this comment.
Classify registry-owned Antigravity before falling back
For restored hook sessions, built-in Antigravity does not reach this .antigravity case: RestorableAgentKind.allCases intentionally omits registry-owned agents, so RestorableAgentSessionIndex.load adds the registry's antigravity entry as .custom("antigravity"). That means restorableWorkingDirectory treats it as .unknown and now prefers the launch cwd, even though this branch documents Antigravity as id-keyed/cwd-in-file; an Antigravity session that drifted into a worktree will be relaunched from the launch directory in the app-side restore/fork/command-palette path instead of reopening where it was working.
Useful? React with 👍 / 👎.
3a7ba80 to
1895940
Compare
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmuxTests/RestorableAgentSessionIndexTests.swift">
<violation number="1" location="cmuxTests/RestorableAgentSessionIndexTests.swift:482">
P2: This test iterates over `RestorableAgentKind.allCases`, which intentionally omits registry-owned agents. A registry-owned Antigravity session enters the app-side resolver as `.custom("antigravity")` → treated as `.unknown` → prefers launch cwd. Meanwhile, the CLI string switch correctly classifies `"antigravity"` as cwd-in-file → keeps runtime cwd. The two paths disagree for registry-owned Antigravity, and this test won't catch it. Consider adding explicit test cases for `.custom("antigravity")` (and any other registry-owned kinds listed in the CLI switch) to verify they resolve consistently across both paths.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| func testAgentResumeBindingWorkingDirectoryMatchesCwdNamespacing() { | ||
| let launch = "/Users/x/repo" | ||
| let runtime = "/Users/x/repo/worktrees/feature" | ||
| for kind in RestorableAgentKind.allCases { |
There was a problem hiding this comment.
P2: This test iterates over RestorableAgentKind.allCases, which intentionally omits registry-owned agents. A registry-owned Antigravity session enters the app-side resolver as .custom("antigravity") → treated as .unknown → prefers launch cwd. Meanwhile, the CLI string switch correctly classifies "antigravity" as cwd-in-file → keeps runtime cwd. The two paths disagree for registry-owned Antigravity, and this test won't catch it. Consider adding explicit test cases for .custom("antigravity") (and any other registry-owned kinds listed in the CLI switch) to verify they resolve consistently across both paths.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmuxTests/RestorableAgentSessionIndexTests.swift, line 482:
<comment>This test iterates over `RestorableAgentKind.allCases`, which intentionally omits registry-owned agents. A registry-owned Antigravity session enters the app-side resolver as `.custom("antigravity")` → treated as `.unknown` → prefers launch cwd. Meanwhile, the CLI string switch correctly classifies `"antigravity"` as cwd-in-file → keeps runtime cwd. The two paths disagree for registry-owned Antigravity, and this test won't catch it. Consider adding explicit test cases for `.custom("antigravity")` (and any other registry-owned kinds listed in the CLI switch) to verify they resolve consistently across both paths.</comment>
<file context>
@@ -425,40 +425,81 @@ final class RestorableAgentSessionIndexTests: XCTestCase {
+ func testAgentResumeBindingWorkingDirectoryMatchesCwdNamespacing() {
+ let launch = "/Users/x/repo"
+ let runtime = "/Users/x/repo/worktrees/feature"
+ for kind in RestorableAgentKind.allCases {
+ let resolved = CMUXCLI.agentResumeBindingWorkingDirectory(
+ kind: kind.rawValue,
</file context>
Agent session stores are namespaced by the directory the agent was launched in; cmux was resuming from the agent's drifted runtime cwd (a worktree it cd'd into mid-session), so `<agent> --resume <id>` looked in the wrong namespace and failed with "No conversation found with session ID". Two independent paths carried the drift, fixed via one shared engine: New shared package logic (Packages/CMUXAgentLaunch/AgentResumeWorkingDirectory): classifies each agent kind (cwd-in-file vs directory-namespaced) and resolves the resume working directory, preferring the launch cwd for directory- namespaced agents (Claude, Gemini, Cursor, Grok, Pi, Qoder) and keeping the runtime cwd for id-keyed agents (Codex, OpenCode, Amp, ...). Linked by both the app and the standalone cmux-cli, so there is one source of truth. 1. CLI surface resume binding (publishAgentSurfaceResumeBinding) — the path that re-runs the agent on a restored surface — now pins the binding to the launch cwd via the shared resolver instead of baking the drifted runtime cwd. This is the path that actually failed on a build that already had the Claude-only #5154 fix. 2. App-side resolver (restorableWorkingDirectory) generalized beyond Claude via RestorableAgentKind.cwdNamespacing, which now delegates to the shared classifier. Claude still verifies against the transcript first. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1895940 to
49babcf
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49babcf72f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| // The launch cwd matches the session namespace and never drifts; fall back to the | ||
| // recorded cwd only when no launch cwd was captured. | ||
| return launchCwd ?? recordedCwd |
There was a problem hiding this comment.
Preserve cwd for registered custom agents
When a registered custom Vault agent has a launch cwd and later reports a different runtime cwd, .custom(<id>) falls through the shared classifier as .byDirectory, so this now stores launchCwd instead of the recorded cwd. Custom registrations default to cwd: .preserve, and their resume templates can also expand {{cwd}}, so a custom agent that cds into a worktree will auto-resume from the original directory and substitute the wrong cwd; before this change every non-Claude hook record kept record.cwd.
Useful? React with 👍 / 👎.
Review (#5300 P2): a custom Vault agent defaults to cwd: .preserve and its resume template can expand {{cwd}}, but restorableWorkingDirectory classified the unknown custom id as .byDirectory and returned the launch dir — so a custom agent that cd'd into a worktree would resume in the wrong directory. Thread the registration into restorableWorkingDirectory and keep the runtime cwd whenever a registration is present (covers both .preserve and .ignore). (The sibling antigravity P2 is already handled: .custom("antigravity").rawValue is "antigravity", which the shared cwd classifier lists as .cwdInFile.) Test: testCustomVaultAgentPreservesRuntimeCwdOnRestore — a drifted custom agent restores into its runtime cwd, not the launch dir. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause (adversarially-verified): the CLI agent launchers stamped the operator's FOCUSED pane into CMUX_WORKSPACE_ID/CMUX_SURFACE_ID instead of the launch surface, while leaving CMUX_PANEL_ID/CMUX_TAB_ID at the launch surface. So a codex launched in surface B while surface A is focused recorded surface A and restored into the wrong surface after reload (the "jumble"). Flows through the codex-family omx/codex-teams launchers; plain `codex` is unaffected because its env comes from the matched per-surface terminal startup env. - New shared resolver CMUXAgentLaunch/AgentSpawnIdentity: prefer the launcher's OWN surface/workspace identity, falling back to the focused pane only when the launcher has none. Pure value logic with golden tests. - configureTmuxCompatEnvironment (CLI/cmux.swift): resolve via AgentSpawnIdentity so omx/claude-teams stamp the launch surface, keeping CMUX_SURFACE_ID matched with the inherited CMUX_PANEL_ID. - runCodexTeams watcher args: derive the root workspace/surface from the launch env, not focusedContext, so the codex-teams watcher records the right surface. Independent of the cwd-resolution work (#5300/#5312): that is the directory axis, this is the surface-mapping axis. Full root cause + e2e survives-reload test plan in plans/feat-codex-surface-jumble/DESIGN.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-agent resume-verb switch was duplicated across the app target (AgentResumeCommandBuilder.resumeArguments) and the standalone cmux-cli target (agentSurfaceResumeArguments + helpers), because cmux-cli cannot import the app's Sources. Extract the launcher pre-resolution and the built-in-kind verb switch into one pure builder, AgentResumeArgv, in the shared CMUXAgentLaunch package (linked by app + cli + tests). Behavior-preserving: the app and CLI switches were verified identical, and the shared builder is golden-tested per built-in kind and launcher wrapper (AgentResumeArgvTests). The app keeps its Vault/.custom path locally (CmuxVaultAgentRegistration lives in the app target); only the built-in and wrapper resolution is shared. Deletes ~95 lines of duplicated CLI helpers. Stacks on the resume cwd-drift fix (task-session-restore-resume / manaflow-ai#5300). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…5350) (#5351) * Fix codex sessions restoring into the wrong surface after reload (#4920) Root cause (adversarially-verified): the CLI agent launchers stamped the operator's FOCUSED pane into CMUX_WORKSPACE_ID/CMUX_SURFACE_ID instead of the launch surface, while leaving CMUX_PANEL_ID/CMUX_TAB_ID at the launch surface. So a codex launched in surface B while surface A is focused recorded surface A and restored into the wrong surface after reload (the "jumble"). Flows through the codex-family omx/codex-teams launchers; plain `codex` is unaffected because its env comes from the matched per-surface terminal startup env. - New shared resolver CMUXAgentLaunch/AgentSpawnIdentity: prefer the launcher's OWN surface/workspace identity, falling back to the focused pane only when the launcher has none. Pure value logic with golden tests. - configureTmuxCompatEnvironment (CLI/cmux.swift): resolve via AgentSpawnIdentity so omx/claude-teams stamp the launch surface, keeping CMUX_SURFACE_ID matched with the inherited CMUX_PANEL_ID. - runCodexTeams watcher args: derive the root workspace/surface from the launch env, not focusedContext, so the codex-teams watcher records the right surface. Independent of the cwd-resolution work (#5300/#5312): that is the directory axis, this is the surface-mapping axis. Full root cause + e2e survives-reload test plan in plans/feat-codex-surface-jumble/DESIGN.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add e2e test for the spawn-env surface fix via a hidden debug seam testTmuxCompatEnvStampsLaunchSurfaceNotFocusedPane drives the real `cmux` binary through the real configureTmuxCompatEnvironment (via a hidden __debug-tmux-compat-env subcommand) with a mock socket reporting focused=surface A and the launcher's own env=surface B. Asserts the stamped CMUX_SURFACE_ID == launch surface B (== CMUX_PANEL_ID), not the focused surface A. Exercises the actual launcher env path, not just the resolver. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review: drop stray design doc, fix debug-seam socket source, surface-scope test tab id - Remove plans/feat-codex-surface-jumble/DESIGN.md (design docs live in the control repo, not cmux). - cubic P2: debugDumpTmuxCompatEnvironment now injects the passed socketPath into the env before resolving the focused context, so it uses the socket the command was pointed at. - CodeRabbit: the e2e fixture uses a surface-scoped CMUX_TAB_ID (distinct from the workspace id) and asserts it passes through, matching the repo's surface-scoped tab identity contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: run CMUXAgentLaunch package tests as a real gate The CI swift-test step ran a hardcoded package list that omitted CMUXAgentLaunch, so its tests (AgentSpawnIdentity resolver, AgentLaunchSanitizer, Hermes/RovoDev resolvers, and the resume-engine builders once they land) only compiled, never executed. The package resolves standalone via SwiftPM (no GhosttyKit/app dep), so add it to the gate. Verified locally: 59 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Capture CODEX_HOME for plain codex resume/fork (#5350) Plain codex (no cmux launcher, no resolvable PID at hook time) produced an empty launchCommand, so the agent's launch env was never captured. When codex runs under the subrouter account manager, CODEX_HOME points at ~/.codex-accounts/<account>, not ~/.codex; dropping it made resume/fork emit a bare `codex resume/fork <id>` against the default home and fail with "No saved session found". agentLaunchCommandFromEnvironment now still emits an env-only record carrying the selected launch env (CODEX_HOME, CLAUDE_CONFIG_DIR, ...) when argv is unavailable but the env is non-empty. AgentResumeCommandBuilder already prefixes that env ahead of the kind's fallback verb, so resume and fork reproduce `CODEX_HOME=<home> codex resume/fork <id>`. Default home stays nil (unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Override leaked env surface id with PID/TTY truth in agent hooks (#5333) The codex surface jumble: a launcher or inherited subprocess can leak the operator's FOCUSED pane into CMUX_SURFACE_ID, and the generic hook trusted that env surface whenever both env ids were present (the corrector was suppressed), routing the session to the wrong pane. The no-pid-gate resume binding then persisted the wrong surface across reload. The hook now always resolves the agent process's own terminal binding (TTY first, then PID) and, when it points to a different accessible surface inside the SAME workspace, overrides the ambient-env surface with that ground truth. It stays a no-op when the surface came from an explicit --surface flag, when the TTY/PID binding is unavailable (remote/SSH), or when it already agrees, so the common path and remote sessions are unaffected. Adds two app-hosted CLI integration tests: CODEX_HOME survives an unavailable launch command (#5350), and a leaked env surface is overridden by the TTY-bound pane (#5333). Both are RED before their respective fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Review: clean up debug-seam shim dir; make new hook tests hermetic - debugDumpTmuxCompatEnvironment leaked a /tmp shim dir per invocation (incl. each CI run of the tmux-compat seam test); it is the one-shot dump path that never spawns a long-lived agent, so defer-remove the dir on exit. (greptile, cubic) - The two new codex-hook integration tests inherited the runner HOME; set HOME to the per-test temp root before spawning cmux, matching the existing runGenericHookPersistenceScenario pattern. (cubic) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Persist the env-only launch record so fork/resume keep CODEX_HOME The hook session store's update() only assigned launchCommand when arguments was non-empty, so the new env-only record (empty argv, carrying CODEX_HOME) was never written. A later hook event that falls back to mapped?.launchCommand, or the fork path reading the persisted store after reload, could then drop CODEX_HOME and fail with "No saved session found" again. Persist an argv-less env-only record when no argv-bearing record exists yet, without ever downgrading a richer earlier capture. Extends the G2 test to assert the persisted launchCommand carries CODEX_HOME. (cursor Bugbot) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Keep AgentSpawnIdentity a coherent workspace/surface pair resolve() fell back independently per axis, so a launcher inheriting only CMUX_WORKSPACE_ID while focus is in a different workspace produced an impossible (own workspace, focused surface) pair. Call sites stamped that into the spawn env, and hook routing then rejected the surface-not-in-workspace pair and dropped the hook (no resume binding). Borrow the focused surface only when the focused pane is in the resolved workspace; otherwise leave the surface nil so the hook's PID/TTY resolution picks the agent's real pane. Composes with the G3 hook override. Updates the per-axis test to the coherent semantics and adds the same-workspace case. (autoreview Codex / greptile / codex bot) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Let a stale ambient surface fall through to PID/TTY instead of dropping the hook The G3 correction only ran when the ambient CMUX_SURFACE_ID resolved to an accessible surface. A stale/invalid ambient surface (closed, or in another workspace) made resolvedDirectSurfaceArg nil, which set hasInvalidDirectSurfaceArg and hasUnusableDirectBinding, so resolveAgentHookTarget aborted BEFORE the TTY/PID binding could recover — the stale-env variant of the codex jumble still no-op'd the hook and lost the session across reload. Treat an invalid AMBIENT env id differently from an invalid EXPLICIT flag: only an explicit --workspace/--surface that fails to resolve is a hard error; a stale ambient id is treated as absent so routing falls through to the PID/TTY binding (ground truth). Adds a stale-env regression test. (autoreview Codex P1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Make AgentSpawnIdentity coherent in both partial-env directions The prior coherence fix handled (own workspace, no own surface) but the mirror remained: an orphan own surface (inherited CMUX_SURFACE_ID with no CMUX_WORKSPACE_ID) was still paired with the focused workspace, producing a cross-workspace (focused workspace, own surface) identity. Treat the own surface as authoritative only paired with its own workspace; an orphan own surface is not trusted against the focused context, so the resolver uses the coherent focused pair or leaves it nil for PID/TTY recovery. Adds two orphan-surface tests (8 total, all green). (autoreview Codex P2) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Keep non-restorable launches non-resumable: env-only fallback only for absent argv The env-only CODEX_HOME fallback also fired when sanitizedAgentLaunchArguments REJECTED a captured argv. That rejection is exactly how AgentLaunchSanitizer suppresses non-restorable invocations (codex exec/review/login, claude config, ...), so routing them through the env-only record persisted a bogus resumable record (and could publish a default `codex resume <id>`) for one-shot commands. Restrict the env-only fallback to the genuinely-unavailable-argv case (no CMUX_AGENT_LAUNCH_ARGV_B64 and an unresolved/exited PID). A captured-but-rejected argv returns nil again, so non-restorable launches stay non-resumable even when CODEX_HOME is present. The G2 test forces the no-argv path via a dead PID, so it stays green. Adds a non-restorable-exec regression test. (autoreview Codex P1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: lawrencecchen <lawrence@cmux.com>
Summary
Session restore was running
<agent> --resume <id>from the wrong directory, so agents failed with "No conversation found with session ID …" and the terminal fell back to a fresh login shell.Agent session/transcript stores are namespaced by the directory the agent was launched in. cmux was resuming from the agent's drifted runtime cwd (the dir it
cd'd into mid-session, e.g. a worktree), so--resumelooked in the wrong namespace. The launch working directory (frozen at launch) matches the namespace and is the correctcdtarget.PR #5154 fixed this for Claude in one of two parallel resume mechanisms. This generalizes it to every cwd-namespaced agent and to the mechanism that actually re-runs the agent on a restored surface.
Two independent carriers of the drift, both fixed:
CLI surface resume binding (
CLI/cmux.swift, the path that re-runs the agent on a restored surface).publishAgentSurfaceResumeBindingpreferred the hook-reported runtime cwd over the launch cwd for both the bakedcdand the binding cwd. Now prefers the launch cwd via a shared, unit-testedagentResumeBindingWorkingDirectoryhelper. Agent-agnostic, so it fixes restore for every agent type. This is the path that produced the reported failure on a build that already had Fix Claude fork/resume failing when session changed directories #5154.App-side resolver (
restorableWorkingDirectory, used by fork / command palette / relaunch index). It was Claude-only (guard kind == .claude else { return recordedCwd }) and both unverified fallbacks returned the drifted recorded cwd. Now classifies each kind via a newRestorableAgentKind.cwdNamespacing:Investigated with a multi-agent workflow that classified all 16 agent kinds against cmux's own resume code and on-disk stores, and confirmed the subagent-hook angle is not involved (Claude's
SubagentStopis wired to telemetry only and subagent events carry the parent session id).Testing
Regression tests in
cmuxTests/RestorableAgentSessionIndexTests.swift, two-commit red→green (failing tests first commit, fix second):testClaudeResumePrefersLaunchCwdWhenTranscriptUnverifiable— Claude, out-of-tree transcript that verifies against neither candidate: must prefer the launch cwd, not the drift.testDirectoryNamespacedNonClaudeAgentResolvesDriftToLaunchCwd— Gemini drift resolves to the launch cwd (was short-circuited to the drift by the Claude-only gate).testAgentResumeBindingWorkingDirectoryPrefersLaunchOverRuntimeCwd— the CLI helper prefers the launch working directory over the runtime cwd.CI
testsjob compiles + runs the unit bundle. Tagged dogfood build provided for manual restore verification.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Touches all agent resume/restore paths and per-kind cwd heuristics; wrong classification could break resume for some agents, but behavior is centralized, documented, and covered by new unit/integration tests.
Overview
Fixes agent session restore choosing the wrong working directory when the hook-reported runtime cwd drifts (e.g. into a worktree) while the session store is keyed by the launch directory—so
--resumefailed with "No conversation found."Introduces shared
CMUXAgentLaunchpolicy (AgentCwdNamespacing,AgentResumeWorkingDirectory.resolve): directory-namespaced agents (Claude, Gemini, Cursor, Grok, Pi, Qoder, unknown) prefer launch cwd; id-keyed / cwd-in-file agents (Codex, OpenCode, Amp, etc.) keep runtime cwd.CLI
publishAgentSurfaceResumeBindingnow uses that resolver for resume commands andsurface.resume.setcwdinstead of favoring drifted runtime cwd.App
restorableWorkingDirectoryapplies the same classification; non-Claude directory agents no longer return recorded cwd only; Claude still verifies transcript location when possible, but falls back to launch cwd when verification fails (not drift).Reviewed by Cursor Bugbot for commit 49babcf. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Fixes session restore resuming agents from a drifted runtime cwd. Resume now picks the correct directory per agent kind so
--resume <id>finds the right namespace.CMUXAgentLaunchwithAgentResumeWorkingDirectoryandAgentCwdNamespacing. Directory‑namespaced kinds (Claude, Gemini, Cursor, Grok, Pi, Qoder) prefer the launch cwd; id‑keyed/cwd‑in‑file kinds (Codex, OpenCode, Amp, Antigravity, Rovo Dev, Hermes) keep the runtime cwd; unknown kinds default to launch cwd.publishAgentSurfaceResumeBindingandrestorableWorkingDirectoryboth delegate to the shared resolver; Claude still verifies the transcript location and falls back to the launch cwd when unverifiable.Written for commit 49babcf. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes
Tests