Skip to content

Fix session restore resuming agents in the wrong directory (drifted cwd) - #5300

Merged
2 commits merged into
mainfrom
task-session-restore-resume
Jun 4, 2026
Merged

2 commits merged into
mainfrom
task-session-restore-resume

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

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 --resume looked in the wrong namespace. The launch working directory (frozen at launch) matches the namespace and is the correct cd target.

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:

  1. CLI surface resume binding (CLI/cmux.swift, the path that re-runs the agent on a restored surface). publishAgentSurfaceResumeBinding preferred the hook-reported runtime cwd over the launch cwd for both the baked cd and the binding cwd. Now prefers the launch cwd via a shared, unit-tested agentResumeBindingWorkingDirectory helper. 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.

  2. 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 new RestorableAgentKind.cwdNamespacing:

    • directory-namespaced (Claude, Gemini, Cursor, Grok, Pi, Qoder) → prefer the launch cwd (Claude still verifies against the transcript's project dir first);
    • id-keyed / cwd-in-file (Codex, OpenCode, Amp, Antigravity, Rovo Dev, Hermes) → keep the recorded cwd, so they reopen where they were and nothing regresses.

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 SubagentStop is 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 tests job compiles + runs the unit bundle. Tagged dogfood build provided for manual restore verification.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with 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 --resume failed with "No conversation found."

Introduces shared CMUXAgentLaunch policy (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 publishAgentSurfaceResumeBinding now uses that resolver for resume commands and surface.resume.set cwd instead of favoring drifted runtime cwd.

App restorableWorkingDirectory applies 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.

  • Bug Fixes
    • Shared resolver: added CMUXAgentLaunch with AgentResumeWorkingDirectory and AgentCwdNamespacing. 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.
    • CLI and app: publishAgentSurfaceResumeBinding and restorableWorkingDirectory both delegate to the shared resolver; Claude still verifies the transcript location and falls back to the launch cwd when unverifiable.
    • Tests: added package and app coverage for kind‑aware cwd selection, Claude unverifiable transcripts, Gemini drift resolving to launch cwd, and classifier parity between app and shared logic.

Written for commit 49babcf. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Introduced kind-aware resume classification so agents are treated as directory-sensitive or cwd-stored when determining resume working directory.
  • Bug Fixes

    • Resume now prefers the stable launch directory when recorded cwd is unreliable.
    • Claude resume verification probes storage locations and avoids using drifted recorded cwd if transcript can’t be verified.
  • Tests

    • Added tests for kind-aware resume behavior and Claude unverifiable-transcript scenarios.

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>
@vercel

vercel Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 3, 2026 8:42pm
cmux-staging Building Building Preview, Comment Jun 3, 2026 8:42pm

@coderabbitai

coderabbitai Bot commented Jun 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Resume Working Directory Selection and Verification

Layer / File(s) Summary
Working directory namespacing classification
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentCwdNamespacing.swift, Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentResumeWorkingDirectory.swift, Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentResumeWorkingDirectoryTests.swift, Sources/RestorableAgentTypes.swift
Adds AgentCwdNamespacing enum and AgentResumeWorkingDirectory resolver that classify kinds and pick runtime vs launch cwd; exposes RestorableAgentKind.cwdNamespacing and tests the resolver.
Session restorable working directory with Claude verification
Sources/RestorableAgentSession.swift
restorableWorkingDirectory now uses namespacing; adds claudeVerifiedRestorableWorkingDirectory that validates session id and confirms transcript presence on disk, returning nil when unverifiable.
Resume binding working directory integration (CLI)
CLI/cmux.swift
Adds agentResumeBindingWorkingDirectory usage: computes resumeWorkingDirectory, passes it to resume command construction, and sets auto-resume params["cwd"] from that value when present.
Test coverage and fixture helpers for resume cwd behavior
cmuxTests/RestorableAgentSessionIndexTests.swift
Adds tests for Claude unverifiable-transcript behavior and directory-namespaced agents, introduces driftedAgentHookRecord helper, and refactors hook-store writer to accept configurable filenames.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through cwds both new and old,
Trimming drifted paths until they're bold,
Claude checks the disk to find the thread,
Launch dir wins when transcripts are said,
Resume springs back—neat, warm, and gold.

🚥 Pre-merge checks | ✅ 17 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing session restore to use the correct (launch) directory instead of the drifted runtime directory for agent resumption.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed AgentCwdNamespacing and AgentResumeWorkingDirectory properly marked Sendable. No implicit MainActor models, shared mutable Sendable without isolation, or UI access violations.
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing synchronization added. New resolver logic uses only string normalization and switch statements, no semaphores, sleeps, locks, or delayed dispatch.
Cmux No Hacky Sleeps ✅ Passed All changes are in Swift files. The rule applies only to TypeScript, JavaScript, shell, and build/runtime scripts; Swift is explicitly out of scope per the rule's own statement.
Cmux Algorithmic Complexity ✅ Passed Double nested loop bounded to O(2*3) per session. No nested full-collection scans on scalable collections, no batch rescans, no hot-path sorting/filtering violations found.
Cmux Swift Concurrency ✅ Passed PR uses modern Swift concurrency patterns (Task.detached with .value for I/O work); no legacy DispatchQueue.global(), Combine, completion handlers, or fire-and-forget Tasks found.
Cmux Swift @Concurrent ✅ Passed All new functions follow concurrent annotation rules: pure sync enums, I/O work properly isolated via Task.detached(priority: .utility), no missing or invalid @concurrent annotations.
Cmux Swift File And Package Boundaries ✅ Passed PR extracts agent resume logic into CMUXAgentLaunch package with clear responsibilities, touches oversized files incidentally (+9, +33 net lines), enabling app/CLI code reuse.
Cmux Swift Logging ✅ Passed New working directory resolution logic adds no print/debugPrint/dump/NSLog calls in production code, no ad hoc file logging, no MainActor Logger issues, and no secret/personal data exposure.
Cmux User-Facing Error Privacy ✅ Passed No new user-facing error messages, alerts, or sensitive information exposure detected. PR adds internal working directory resolution logic without violating user-facing error privacy rules.
Cmux Full Internationalization ✅ Passed PR adds internal working-directory resolution logic with no new user-facing strings. All text additions are developer documentation or internal protocol tokens, not requiring i18n per the rules.
Cmux Swiftui State Layout ✅ Passed This PR contains no SwiftUI changes. All 7 modified files are pure Swift (CLI utilities, session logic, type definitions, and tests) with zero SwiftUI-related patterns detected across all files.
Cmux Architecture Rethink ✅ Passed Introduces shared AgentResumeWorkingDirectory policy enum with clear ownership, explicit invariants, full test coverage; no timing repairs, mutable state, or bad representable state.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies only agent working directory logic (enums, session resolution, CLI) with no NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup additions.
Description check ✅ Passed The pull request provides a comprehensive description that covers the bug being fixed, the two resume paths affected, the classification strategy per agent kind, testing approach, and verification method.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-session-restore-resume

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread CLI/cmux.swift Outdated
Comment on lines +24379 to +24382
let resumeWorkingDirectory = Self.agentResumeBindingWorkingDirectory(
runtimeCwd: cwd,
launchWorkingDirectory: launchCommand?.workingDirectory
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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-apps

greptile-apps Bot commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR generalises the earlier Claude-only session-restore cwd fix (from #5154) to all 16 agent kinds and to the second resume path (publishAgentSurfaceResumeBinding). The root cause — agents that namespace their session store by their launch directory fail --resume when cmux re-runs them from a drifted runtime cwd — is correctly fixed by introducing a shared AgentResumeWorkingDirectory resolver in CMUXAgentLaunch that both the app-side resolver and the CLI surface-binding publisher now call.

  • New AgentCwdNamespacing + AgentResumeWorkingDirectory package classifies all known kinds (directory-namespaced → prefer launch cwd; id-keyed/cwd-in-file → prefer runtime cwd; unknown → conservative byDirectory default) and is unit-tested in isolation.
  • restorableWorkingDirectory switches on cwdNamespacing instead of the old Claude-only guard; claudeVerifiedRestorableWorkingDirectory now returns nil on unverifiable transcripts so the caller falls through to launchCwd ?? recordedCwd, fixing the failure case described in the PR.
  • publishAgentSurfaceResumeBinding replaces its manual normalizedHookValue(cwd) ?? normalizedHookValue(launchCommand?.workingDirectory) with AgentResumeWorkingDirectory.resolve, which correctly applies the per-kind policy to both the baked cd and the surface.resume.set binding cwd.

Confidence Score: 5/5

Safe 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

Filename Overview
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentCwdNamespacing.swift New enum defining two namespacing strategies (byDirectory / cwdInFile); clean, well-documented, and correctly treats unverified kinds conservatively as byDirectory.
Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentResumeWorkingDirectory.swift New shared resolver: cwdNamespacing switch covers all 16 known kinds, normalized() trims and nil-coerces correctly, and resolve() returns the right preference per namespacing class.
Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentResumeWorkingDirectoryTests.swift Good unit coverage for all 12 named kinds (including grok and pi at the string level), fallback/empty cases, and unknown-kind defaulting; all assertions are behaviorally meaningful.
Sources/RestorableAgentTypes.swift Adds cwdNamespacing computed property by delegating to the shared classifier; delegation is correct and keeps the app and CLI in sync automatically.
Sources/RestorableAgentSession.swift restorableWorkingDirectory now switches on cwdNamespacing; Claude verification path correctly returns nil on failure so the caller prefers launchCwd over drifted recordedCwd; logic is sound.
CLI/cmux.swift publishAgentSurfaceResumeBinding now resolves both the command workingDirectory and the surface.resume.set cwd param through the shared AgentResumeWorkingDirectory helper; covers cwdInFile agents correctly too.
cmuxTests/RestorableAgentSessionIndexTests.swift Three new integration tests cover the key regression scenarios (unverifiable Claude transcript, Gemini drift, and kind/classifier parity); driftedAgentHookRecord helper and writeHookStore refactor are clean.

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]
Loading

Reviews (3): Last reviewed commit: "Fix agent session resume cd-ing into the..." | Re-trigger Greptile

Comment thread CLI/cmux.swift Outdated
Comment on lines +24379 to +24382
let resumeWorkingDirectory = Self.agentResumeBindingWorkingDirectory(
runtimeCwd: cwd,
launchWorkingDirectory: launchCommand?.workingDirectory
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread CLI/cmux.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread Sources/RestorableAgentTypes.swift Outdated
Comment on lines +129 to +130
case .codex, .opencode, .amp, .antigravity, .rovodev, .hermesAgent:
return .cwdInFile

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

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>
@lawrencecchen
lawrencecchen force-pushed the task-session-restore-resume branch from 1895940 to 49babcf Compare June 3, 2026 20:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

lawrencecchen added a commit that referenced this pull request Jun 4, 2026
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>
lawrencecchen added a commit that referenced this pull request Jun 4, 2026
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>
@lawrencecchen lawrencecchen closed this pull request by merging all changes into main in 5b7ec86 Jun 4, 2026
pull Bot pushed a commit to jasonkneen/cmux that referenced this pull request Jun 4, 2026
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>
@austinywang austinywang mentioned this pull request Jun 4, 2026
lawrencecchen added a commit that referenced this pull request Jun 4, 2026
…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>

This branch was successfully deployed

1 active deployment
Preview – cmux — 49babcf7 Deployed Jun 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant