Skip to content

Persist Codex title session resumes - #3500

Closed
austinywang wants to merge 2 commits into
mainfrom
issue-3499-codex-session-persist
Closed

austinywang wants to merge 2 commits into
mainfrom
issue-3499-codex-session-persist

Conversation

@austinywang

@austinywang austinywang commented May 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • persist Codex title slugs as restorable agent snapshots when hook state is missing
  • resume title-derived Codex sessions through the existing workspace restore path
  • clear title-derived resume IDs on prompt return so normal exits and stale resume failures do not loop

Regression

  • traced the regression to the session restore feature landing hook-only in 7f4d8c3 / merge 7102fdf, which added agent resume persistence but did not persist Codex title slugs
  • first commit f95c033 is the red regression coverage commit

Verification

  • git diff --check
  • python3 -m py_compile tests/test_session_relaunch_resumes_agent_sessions.py
  • plutil -lint GhosttyTabs.xcodeproj/project.pbxproj
  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv

Fixes #3499


Note

Medium Risk
Changes session snapshot/restore behavior in Workspace by inferring/restoring Codex agent sessions from terminal titles and by tracking auto-resume state transitions, which could affect when agent resumes are persisted or cleared across relaunches.

Overview
Adds support for persisting/restoring Codex sessions when the normal hook-based state is unavailable by parsing codex-... session slugs from a terminal’s surface title and converting them into SessionRestorableAgentSnapshots (new CodexSessionTitleParser).

Updates Workspace snapshotting and shell-activity handling to only keep these title-derived Codex resumes while a command is running, clearing them when the shell returns to promptIdle, and introduces restoredAgentAutoResumeRunningPanelIds to avoid looping/stale auto-resumes. Adds unit/integration coverage (RestorableCodexTitleTests, updated relaunch test) and wires new sources into the Xcode project + Swift file budget.

Reviewed by Cursor Bugbot for commit 2b46dc8. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Restore Codex sessions by parsing the session slug from the terminal title and persisting it as a restorable agent snapshot when hook state is missing. Resumes use the existing workspace path and clear on prompt return to prevent loops.

  • Bug Fixes
    • Persist Codex title slugs as restorable agent snapshots when the hook store is empty, then resume via the normal workspace restore flow.
    • Issue codex resume --dangerously-bypass-approvals-and-sandbox <session-id> using the stored snapshot; invalidate if the title changes.
    • Clear title-derived resume state when the shell returns to promptIdle to avoid looping on normal exits or stale sessions.
    • Adds coverage to ensure relaunch resumes Codex-from-title sessions alongside other agents.
    • Fixes Codex session not resumed after cmux restart — resume ID is lost #3499.

Written for commit 2b46dc8. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added support for Codex sessions to persist and be restored through window titles
  • Tests

    • Extended test coverage for session restoration to validate title-based session recovery during application relaunch

Codex exposes its resumable session slug in the terminal title, but the persisted workspace snapshot currently only restores hook-recorded agent sessions. This regression coverage exercises the workspace snapshot path with an empty hook index and extends the relaunch harness with a title-only Codex surface.

Constraint: CI should show this commit red before the fix lands.\nRejected: Grep for title parsing code | source-text tests would not prove workspace persistence behavior.\nConfidence: high\nScope-risk: narrow\nTested: Not run locally; this is the intentionally failing regression commit.\nNot-tested: Local xcodebuild and UI/E2E per repository policy.
Workspace snapshots now treat the Codex title slug as an agent resume source when hook state is missing. The title-derived snapshot is stored in the existing workspace session JSON path, restored through the existing agent startup input path, and cleared when the resumed or live title-derived command returns to a prompt.

Constraint: Agent resume must remain owned by SessionPersistence/Workspace state rather than adding a parallel store.\nConstraint: Codex CLI resume command keeps existing cmux command-builder shape: codex resume --dangerously-bypass-approvals-and-sandbox <session-id>.\nRejected: UserDefaults keyed by workspace/surface | duplicates the durable workspace snapshot owner and complicates cleanup.\nRejected: Persist every Codex-looking title forever | normal exits and stale resume failures would loop.\nConfidence: high\nScope-risk: moderate\nDirective: Keep title-derived Codex resume state tied to shell activity; promptIdle is the cleanup signal for stale or completed title sessions.\nTested: git diff --check; python3 -m py_compile tests/test_session_relaunch_resumes_agent_sessions.py; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj; python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv\nNot-tested: Local xcodebuild/unit/UI tests per task and repository constraints; final tagged reload still required.
@vercel

vercel Bot commented May 4, 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 May 4, 2026 9:36am
cmux-staging Building Building Preview, Comment May 4, 2026 9:36am

@coderabbitai

coderabbitai Bot commented May 4, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds Codex session persistence and restoration from window title slugs. A new CodexSessionTitleParser extracts session IDs from codex-<hex>-<suffix> title patterns and creates restorable agent snapshots. Workspace tracking manages a new restoredAgentAutoResumeRunningPanelIds set to control when restored agents are persisted or cleared based on shell activity and title matching. Tests validate title-slug persistence, clearing, and restoration across workspace restarts.

Changes

Codex Title-Based Session Persistence and Restoration

Layer / File(s) Summary
Data Shape & Runtime State
Sources/Workspace.swift
New private set restoredAgentAutoResumeRunningPanelIds tracks panels with "running" restored agent state to drive auto-resume transitions and invalidation.
Parser Implementation
Sources/CodexSessionTitleParser.swift
New CodexSessionTitleParser enum parses codex-<hex>-<suffix> from titles via regex, builds restorable snapshots with fixed launch command, and provides helpers to detect and match surface-title-sourced snapshots.
Workspace Integration
Sources/Workspace.swift
sessionPanelSnapshot(...) uses title parsing to conditionally update restored agent snapshots and move panels between auto-resume pending/running sets based on shellActivityState; updatePanelShellActivityState(...) refactored to drive transitions via title matching vs. fingerprint invalidation; panel creation removes from opposite auto-resume set; teardown and pruning clear the running set.
Project Wiring
GhosttyTabs.xcodeproj/project.pbxproj
CodexSessionTitleParser.swift added to main app Sources; RestorableCodexTitleTests.swift added to test target Sources.
Tests & Validation
cmuxTests/RestorableCodexTitleTests.swift, tests/test_session_relaunch_resumes_agent_sessions.py
Swift tests verify title slug persistence stops after .promptIdle and restored sessions clear appropriately; Python integration test validates title-derived Codex resume command on relaunch.

Sequence Diagram

sequenceDiagram
    participant Panel as Panel
    participant Workspace as Workspace
    participant Parser as CodexSessionTitleParser
    participant Snapshot as Session Snapshot
    
    Panel->>Workspace: updatePanelShellActivityState(state: .commandRunning)
    
    Panel->>Panel: title = "codex-019df0a1-6"
    Workspace->>Parser: sessionId(from: title)
    Parser-->>Workspace: "019df0a1"
    
    Workspace->>Parser: snapshot(sessionId: "019df0a1", workingDirectory: "...")
    Parser-->>Workspace: SessionRestorableAgentSnapshot(.codex, launchCommand, source: "surface-title")
    
    Workspace->>Workspace: restoredAgentAutoResumeRunningPanelIds.insert(panelId)
    Workspace->>Snapshot: Store restored agent snapshot
    
    Note over Workspace: ... later, on shell .promptIdle ...
    
    Panel->>Workspace: updatePanelShellActivityState(state: .promptIdle)
    Workspace->>Workspace: restoredAgentAutoResumeRunningPanelIds.remove(panelId)
    Workspace->>Snapshot: Clear restored agent snapshot
    
    Note over Workspace: ... on restart ...
    Workspace->>Snapshot: Restore snapshot with codex agent
    Workspace->>Workspace: Launch with codex --resume 019df0a1
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly Related PRs

Poem

🐰 The title now holds a secret code,
Session IDs in the window's abode,
When Codex sleeps and restarts ring true,
A prompt returns it to what it knew!
Magic in slugs—a rabbit's delight! ✨


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error CodexSessionTitleParser is a pure utility enum with only static methods for parsing and value construction, lacking the explicit nonisolated marking required by project actor isolation rules for such helpers. Mark CodexSessionTitleParser as nonisolated enum to explicitly indicate it's a pure utility helper without MainActor isolation requirements, consistent with existing project patterns.
Cmux Architecture Rethink ❌ Error PR introduces two new mutable Sets (restoredAgentAutoResumePendingPanelIds and restoredAgentAutoResumeRunningPanelIds) creating parallel ownership of auto-resume state that should be owned by the single dictionary restoredAgentSnapshotsByPanelId, violating the architectural rule against multiple owners of the same state. Embed pending/running status directly into SessionRestorableAgentSnapshot as an optional enum field, or compute it dynamically from snapshot.launchCommand.source and shellActivityState to make the snapshot the single owner.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Persist Codex title session resumes' is concise and clearly summarizes the main objective of the PR: adding persistence for Codex session IDs derived from terminal titles.
Description check ✅ Passed The PR description provides a comprehensive summary of changes and why they were made, includes verification steps, and references the fixed issue, but lacks explicit testing details and a demo video section.
Linked Issues check ✅ Passed All code changes directly address issue #3499 requirements: CodexSessionTitleParser extracts session IDs from titles, Workspace changes persist/restore these snapshots and clear on promptIdle, and tests verify persistence and resumption behavior.
Out of Scope Changes check ✅ Passed All changes are scoped to issue #3499: new parser for title-based Codex sessions, session restore/cleanup logic in Workspace, comprehensive test coverage, and project wiring. No unrelated modifications detected.
Cmux Swift Blocking Runtime ✅ Passed The PR introduces no blocking or timing-based synchronization in production code. New pure logic regex parsing and state management through set operations without semaphores, locks, or blocking waits.
Cmux Swift Concurrency ✅ Passed Pull request introduces no legacy async patterns; only synchronous state management with appropriate @MainActor test annotations.
Cmux Swift @Concurrent ✅ Passed The Swift changes introduce only synchronous, pure helper methods in CodexSessionTitleParser.swift with no async functions or missing @concurrent annotations.
Cmux Swift Logging ✅ Passed Pull request adheres to swift-logging.md rules with no logging in new files, DEBUG-guarded cmuxDebugLog calls in modified Workspace.swift, and all session identifiers redacted using .prefix().
Cmux Swiftui State Layout ✅ Passed This PR does not violate SwiftUI state layout rules. New code introduces utility functions and test files with no deprecated state patterns or render-time mutations.
✨ 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 issue-3499-codex-session-persist

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

@greptile-apps

greptile-apps Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes the regression from #7f4d8c38 by persisting Codex session title slugs (extracted from OSC terminal title escape sequences) as restorable SessionRestorableAgentSnapshot entries when hook-based state is absent, and clears those entries when the shell returns to a prompt so normal exits do not re-trigger a stale resume loop.

  • P1 – actor isolation: All static members of CodexSessionTitleParser are pure helpers but lack nonisolated, coupling them to the main actor under @MainActor-by-default module isolation.
  • P2 – snapshot side-effects: The new restorableAgent == nil block inside sessionPanelSnapshot mutates restoredAgentSnapshotsByPanelId and companion sets as a side effect of a read path, making sessionSnapshot() non-idempotent and spreading state-transition ownership across three call sites.

Confidence Score: 3/5

Not safe to merge without resolving the nonisolated actor isolation gap on CodexSessionTitleParser and reviewing the snapshot-path side-effects.

One P1 (missing nonisolated on CodexSessionTitleParser statics in a @MainActor-by-default module) caps the score at 4; the P2 architectural concern about non-idempotent snapshot reads expanding existing debt pulls it to 3.

Sources/CodexSessionTitleParser.swift (actor isolation) and the restorableAgent == nil block in Sources/Workspace.swift (~line 378).

Important Files Changed

Filename Overview
Sources/CodexSessionTitleParser.swift New parser enum extracting Codex session slug from a terminal title string; all static members lack nonisolated, coupling pure helpers to the main actor in a @MainActor-by-default module
Sources/Workspace.swift Adds restoredAgentAutoResumeRunningPanelIds tracking and title-derived snapshot logic; new state-mutation block inside sessionPanelSnapshot worsens an existing pattern by making snapshot reads non-idempotent
cmuxTests/RestorableCodexTitleTests.swift New XCTest suite covering slug-persist, prompt-return clear, and restored-workspace round-trip; adequate coverage for the three new code paths
tests/test_session_relaunch_resumes_agent_sessions.py Integration test extended to set a Codex title via OSC escape and verify resume after relaunch; sleep-based synchronization is test-only scaffolding per the blocking-runtime rule
GhosttyTabs.xcodeproj/project.pbxproj Registers CodexSessionTitleParser.swift (source) and RestorableCodexTitleTests.swift (test) in both file references and build phases; no structural issues
.github/swift-file-length-budget.tsv Budget bumped by 52 lines for Workspace.swift to account for additions; within expected range

Sequence Diagram

sequenceDiagram
    participant T as Terminal OSC Title
    participant W as Workspace
    participant P as CodexSessionTitleParser
    participant S as restoredAgentSnapshotsByPanelId

    T->>W: updatePanelTitle(panelId, "codex-019df0a1-6")
    W->>P: restorableSnapshot(fromTitle:, workingDirectory:)
    P-->>W: SessionRestorableAgentSnapshot(kind:.codex, source:"surface-title")
    W->>S: [panelId] = snapshot (via sessionPanelSnapshot side-effect)

    Note over W: sessionSnapshot() called (e.g. on app suspend)
    W->>P: isSurfaceTitleSnapshot(currentSnapshot)?
    P-->>W: true → keep snapshot in output

    Note over W: shellState → .commandRunning
    W->>P: snapshot(restoredAgent, matchesTitle:)?
    P-->>W: true → insert panelId into AutoResumeRunning

    Note over W: shellState → .promptIdle (codex exited)
    W->>S: removeValue(panelId) — clear surface-title snapshot
    W-->>T: resume loop prevented
Loading

Reviews (1): Last reviewed commit: "Persist Codex title sessions through wor..." | Re-trigger Greptile

Comment on lines +3 to +7
enum CodexSessionTitleParser {
static let launchSource = "surface-title"

private static let sessionSlugRegex = try! NSRegularExpression(
pattern: #"(?i)(?:^|[^A-Z0-9_-])(codex-[0-9a-f]{8,}(?:-[A-Z0-9]+)+)(?=$|[^A-Z0-9_-])"#

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.

P1 Missing nonisolated on pure static helpers

CodexSessionTitleParser is a pure string-parsing utility with no actor-bound state. In a module with @MainActor-by-default isolation, all static members — launchSource, sessionSlugRegex, sessionId(from:), restorableSnapshot(fromTitle:workingDirectory:), isSurfaceTitleSnapshot(_:), snapshot(_:matchesTitle:), and normalized(_:) — will be implicitly @MainActor. That unnecessarily couples calls to them through the main actor, prevents use from background contexts, and violates the actor-isolation rule that pure helpers and file-scoped constants should be explicitly nonisolated. Marking each static member nonisolated (and the enum itself nonisolated to make the annotation structural rather than per-member) is the fix.

File Used: .github/review-bot-rules/swift-actor-isolation.md (source)

Comment on lines +6 to +8
private static let sessionSlugRegex = try! NSRegularExpression(
pattern: #"(?i)(?:^|[^A-Z0-9_-])(codex-[0-9a-f]{8,}(?:-[A-Z0-9]+)+)(?=$|[^A-Z0-9_-])"#
)

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 try! on static regex initialization

The regex is a compile-time-constant literal so this will never trap in practice, but try! on a static let is fragile — any future edit to the pattern string (typo, feature addition) silently becomes a launch crash instead of a compile or test failure. Swift's native Regex literal (#/pattern/) validates the pattern at compile time and eliminates the forced try entirely; if that spelling isn't available on the OS minimum target, a static var with a do/catch and preconditionFailure gives a clear crash site and message.

Comment thread Sources/Workspace.swift
Comment on lines +378 to +395
if restorableAgent == nil {
let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
if shellActivityState == .promptIdle,
let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if shellActivityState != .promptIdle,
let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
fromTitle: panelTitles[panelId] ?? panel.displayTitle,
workingDirectory: panelDirectories[panelId]
) {
restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}
}

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 State mutation as a side-effect of snapshot reads

The new restorableAgent == nil block mutates restoredAgentSnapshotsByPanelId, restoredAgentAutoResumePendingPanelIds, restoredAgentAutoResumeRunningPanelIds, and invalidatedRestoredAgentFingerprintsByPanelId inside what is otherwise a read-path snapshot computation. This extends an already present (but limited) mutation in the restorableAgent != nil branch. The practical consequence is that sessionSnapshot() is not idempotent — the first call when shellActivityState == .promptIdle clears the surface-title snapshot, and a second call then observes different state — and the source of truth for "does this panel have a title-derived codex snapshot?" is now spread across three different call sites (updatePanelTitle, updatePanelShellActivityState, and sessionPanelSnapshot). These transitions belong in the updatePanelTitle/updatePanelShellActivityState handlers so the snapshot function is a pure reader of already-correct state.

File Used: .github/review-bot-rules/swift-architectural-rethink.md (source)

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Workspace.swift`:
- Around line 378-395: When restorableAgent is nil, add handling so a stored
surface-title snapshot is cleared if the live title no longer parses as a Codex
restorable snapshot even when the hook state is .unknown: inside the existing
block that checks restoredAgentSnapshotsByPanelId[panelId], keep the current
branch that clears on shellActivityState == .promptIdle, and in the else-if
branch (the shellActivityState != .promptIdle branch that currently tries
CodexSessionTitleParser.restorableSnapshot(...)), add logic so that if the
parser returns nil AND the existing currentSnapshot is a surface-title snapshot
(use CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot)), you
remove restoredAgentSnapshotsByPanelId[panelId],
restoredAgentAutoResumePendingPanelIds.remove(panelId),
restoredAgentAutoResumeRunningPanelIds.remove(panelId), and
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId); this
ensures stale title-derived snapshots are cleared when the live title stops
matching even if panelShellActivityStates[panelId] remains .unknown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 406dfdd7-e197-4081-9c88-203706c7aff2

📥 Commits

Reviewing files that changed from the base of the PR and between 890686f and 2b46dc8.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/CodexSessionTitleParser.swift
  • Sources/Workspace.swift
  • cmuxTests/RestorableCodexTitleTests.swift
  • tests/test_session_relaunch_resumes_agent_sessions.py

Comment thread Sources/Workspace.swift
Comment on lines +378 to +395
if restorableAgent == nil {
let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
if shellActivityState == .promptIdle,
let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if shellActivityState != .promptIdle,
let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
fromTitle: panelTitles[panelId] ?? panel.displayTitle,
workingDirectory: panelDirectories[panelId]
) {
restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale title-derived snapshots when the live title stops matching.

If hook state never comes back after restore, panelShellActivityStates[panelId] can stay .unknown. In that case this block only clears surface-title snapshots on .promptIdle, so once the panel title stops parsing as a Codex slug the old snapshot is kept and re-persisted on the next workspace save. That recreates the stale auto-resume loop this PR is trying to eliminate.

Suggested fix
         if restorableAgent == nil {
             let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
             if shellActivityState == .promptIdle,
                let currentSnapshot,
                CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
                 restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
                 restoredAgentAutoResumePendingPanelIds.remove(panelId)
                 restoredAgentAutoResumeRunningPanelIds.remove(panelId)
                 invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
             } else if shellActivityState != .promptIdle,
                       let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
                         fromTitle: panelTitles[panelId] ?? panel.displayTitle,
                         workingDirectory: panelDirectories[panelId]
                       ) {
                 restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
                 invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
+            } else if let currentSnapshot,
+                      CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
+                restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
+                restoredAgentAutoResumePendingPanelIds.remove(panelId)
+                restoredAgentAutoResumeRunningPanelIds.remove(panelId)
+                invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if restorableAgent == nil {
let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
if shellActivityState == .promptIdle,
let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if shellActivityState != .promptIdle,
let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
fromTitle: panelTitles[panelId] ?? panel.displayTitle,
workingDirectory: panelDirectories[panelId]
) {
restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}
}
if restorableAgent == nil {
let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
if shellActivityState == .promptIdle,
let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if shellActivityState != .promptIdle,
let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
fromTitle: panelTitles[panelId] ?? panel.displayTitle,
workingDirectory: panelDirectories[panelId]
) {
restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 378 - 395, When restorableAgent is nil,
add handling so a stored surface-title snapshot is cleared if the live title no
longer parses as a Codex restorable snapshot even when the hook state is
.unknown: inside the existing block that checks
restoredAgentSnapshotsByPanelId[panelId], keep the current branch that clears on
shellActivityState == .promptIdle, and in the else-if branch (the
shellActivityState != .promptIdle branch that currently tries
CodexSessionTitleParser.restorableSnapshot(...)), add logic so that if the
parser returns nil AND the existing currentSnapshot is a surface-title snapshot
(use CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot)), you
remove restoredAgentSnapshotsByPanelId[panelId],
restoredAgentAutoResumePendingPanelIds.remove(panelId),
restoredAgentAutoResumeRunningPanelIds.remove(panelId), and
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId); this
ensures stale title-derived snapshots are cleared when the live title stops
matching even if panelShellActivityStates[panelId] remains .unknown.

@austinywang austinywang closed this May 4, 2026

@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 2b46dc8. Configure here.

Comment thread Sources/Workspace.swift
) {
restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Title snapshot overwrites richer hook-based restored snapshots

Medium Severity

When restorableAgent is nil (hook state missing) and the shell isn't at prompt idle, the else if branch unconditionally overwrites restoredAgentSnapshotsByPanelId[panelId] with a title-based snapshot — even when there's already a richer hook-based snapshot from a session restore. This replaces detailed launch command data (actual executable path, real arguments, environment) with hardcoded values ("codex" path and --dangerously-bypass-approvals-and-sandbox). A guard checking that no existing non-title snapshot is present would prevent this data loss.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2b46dc8. Configure here.

This branch was successfully deployed

1 active deployment
Preview – cmux — 2b46dc8f Deployed May 4, 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.

Codex session not resumed after cmux restart — resume ID is lost

1 participant