Skip to content

Restore Codex sessions through user-owned roots - #8321

Closed
lawrencecchen wants to merge 23 commits into
mainfrom
feat-durable-codex-session-restore
Closed

lawrencecchen wants to merge 23 commits into
mainfrom
feat-durable-codex-session-restore

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8890.

Also addresses the stale restore behavior reported in #6209.

Supersedes #4543.

Root cause

Codex hooks can report indexed and unindexed internal workers in addition to the user-owned root. An indexed child could replace the durable root identity, while truncated or nested metadata could hide an exec worker. Legacy kindless bindings could also skip verification. Snapshot reconciliation then erased the binding when a process scan temporarily found no agent. After resume, concurrent hook events could prefer the stored pre-resume PID over the wrapper's current PID, so a later snapshot saved the dead process generation and the second restart stopped.

Fix

  • Verify Codex candidates through provider metadata and follow parent links to the user-owned root.
  • Read complete metadata lines within a 4 MiB bound and reject nested codex exec, automation, and subagent identities as restore roots.
  • Infer and verify legacy kindless Codex bindings, then rewrite saved child checkpoints and commands to the canonical root.
  • Preserve durable agent-hook bindings across empty process scans while keeping wasAgentRunning as the automatic-launch gate.
  • Prefer the wrapper's current agent PID when hook events rebind a resumed session, retaining generation identity for stale-PID protection.

Verification

  • Added regression-test commits before each corresponding fix.
  • swift test --package-path Packages/macOS/CMUXAgentLaunch: 249 tests in 36 suites passed.
  • ./scripts/lint-pbxproj-test-wiring.sh: all 585 test files are wired.
  • Final cloud build passed: https://github.com/manaflow-ai/cmux/actions/runs/30184945977.
  • Live tagged test used a root Codex session plus a nested codex exec, then completed two Command-Q relaunch cycles on the final head. Both relaunches restored root 019f9c3f-330e-72e2-9afd-2d45d45e5985, preserved its transcript, accepted fresh prompts, and left wasAgentRunning: true.
  • The tag-bound shared quit shortcut and final panel snapshot were verified. Mouse-driven Computer Use remains unverified because macOS window capture returned cgWindowNotFound.

Dictionary: A binding associates a cmux surface with a durable agent session. A process generation distinguishes a live process from a later process that reused its PID.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Codex resume handling now verifies session evidence from indexed rollout records or transcript metadata. Transcript paths propagate through stored resume records, session snapshots, CLI ownership checks, and terminal restoration, while unverified or mismatched agent-hook bindings are rejected.

Changes

Codex resume verification

Layer / File(s) Summary
Evidence verifier and fixtures
Packages/macOS/CMUXAgentLaunch/Sources/..., Packages/macOS/CMUXAgentLaunch/Tests/...
Adds SQLite thread-index and transcript-metadata verification, parent-session resolution, file validation, normalization, and indexed/legacy evidence tests.
Restorable snapshot eligibility
Sources/RestorableAgentSession.swift, cmuxTests/RestorableAgentSessionIndexCodexWeakRecordTests.swift
Stores transcriptPath in restorable snapshots and requires verifier evidence for Codex restoration.
CLI resume ownership and persistence
CLI/CMUXCLI+AgentHookRestoreEvidence.swift, CLI/cmux.swift, CLI/CMUXCLI+SessionsList.swift, cmuxTests/CLICodexWeakEnvironmentRestoreBindingTests.swift
Canonicalizes Codex resume targets, propagates transcript paths, stores canonical checkpoint identifiers, and clears unindexed bindings instead of publishing them.
Verified terminal restoration
Sources/Workspace.swift, Sources/DockSplitStore+SessionRestore.swift, cmuxTests/SessionPersistenceResumeBindingTests.swift
Filters restorable agents and resume bindings through Codex evidence checks and rejects poisoned agent-hook bindings during terminal restore.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AgentHook
  participant CMUXCLI
  participant CodexSessionResumeVerifier
  participant Workspace
  participant ResumeStore
  AgentHook->>CMUXCLI: submit session and transcript metadata
  CMUXCLI->>CodexSessionResumeVerifier: verify resume evidence
  CodexSessionResumeVerifier-->>CMUXCLI: canonical session or nil
  CMUXCLI->>ResumeStore: store or clear binding
  Workspace->>CodexSessionResumeVerifier: verify restore evidence
  CodexSessionResumeVerifier-->>Workspace: accepted evidence or nil
  Workspace->>ResumeStore: resolve verified terminal binding
Loading

Possibly related PRs

  • manaflow-ai/cmux#8885 — Both modify terminal session restoration and agent-hook resume binding decisions.

Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Expensive Synchronous Load ❌ Error The PR calls CodexSessionResumeVerifier.evidence() (SQLite + transcript parsing) from @MainActor workspace restore and the prompt-submit socket handler. Move verifier/evidence work off-main via a cached/background loader (e.g. Task.detached or repository cache), then hop back only for UI/process-launch work.
Cmux No Test Or Debug Seam In Production Source ❌ Error Sources/Workspace.swift adds #if DEBUG sessionRestoreInputsForTesting, a test-only accessor in production source with no production caller; cmuxTests calls it directly. Remove the production seam; expose needed state as internal and read it via @testable import from tests, or move any real debug-only helper into a dedicated debug file/folder.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is relevant, but it does not follow the required template sections like Summary, Testing, Demo Video, Review Trigger, or Checklist. Restructure the PR description to match the template and add the missing Summary, Testing, Demo Video, Review Trigger, and Checklist sections.
✅ Passed checks (21 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes canonicalize subagent checkpoints to user-owned roots, reject unindexed or invalid workers, and add regression coverage for indexed subagents and parents.
Out of Scope Changes check ✅ Passed All changed files support the Codex restore ownership fix or its tests, with no unrelated feature work apparent.
Cmux Swift Actor Isolation ✅ Passed PASS: New value models are plain structs/Sendable; Workspace/DockSplitStore are already @MainActor, and the new restore helpers are explicitly nonisolated—no new isolation debt.
Cmux Swift Blocking Runtime ✅ Passed Touched production Swift adds no new sleeps, semaphores, main-queue syncs, polling, or locks; the only waits are preexisting or test-only.
Cmux Browser Automation Off-Main ✅ Passed Diff only touches Codex restore/session code; no browser.* routing, socketWorkerMethods, or off-main WebKit/AppKit waits appear.
Cmux Cache Substitution Correctness ✅ Passed The new snapshot/store fallbacks are re-verified against Codex sqlite/transcripts, and the only caches are command-local UI helpers.
Cmux No Hacky Sleeps ✅ Passed The diff only touches Swift files, and the no-hacky-sleeps rule applies to non-Swift runtime/build scripts, so there’s nothing in scope to flag.
Cmux Algorithmic Complexity ✅ Passed New Codex verification uses keyed SQLite lookups and bounded 32-step/file-prefix scans; no nested full scans or repeated rescans were added to scalable paths.
Cmux Swift Concurrency ✅ Passed The PR only adds synchronous restore-verification logic; it doesn’t introduce new background queues, Combine state, completion handlers, or fire-and-forget Tasks.
Cmux Swift @Concurrent ✅ Passed No rule violation found: the only new async helper offloads via Task.detached, and no invalid @concurrent annotations appear in the diff.
Cmux Swift Package Boundaries ✅ Passed The reusable Codex resume verifier was extracted into CMUXAgentLaunch; the app-target edits are thin wiring around app-specific restore/session snapshots.
Cmux Swiftpm Lockfiles ✅ Passed Diff only changes Swift source files; no Package.swift, .gitignore, Xcode project, or Package.resolved files were touched, so the lockfile rule isn't violated.
Cmux Swift Logging ✅ Passed No added or changed print/debugPrint/dump/NSLog/Logger code appeared in the Swift diff; the touched runtime files only changed restore logic.
Cmux User-Facing Error Privacy ✅ Passed The new production code adds internal Codex verification only; I found no added alerts, prints, or error copy exposing vendor/env/db/session details.
Cmux Full Internationalization ✅ Passed The patch only changes internal Swift resume-verification logic; no user-facing strings, string-catalog, plist, or web locale files were added or modified.
Cmux Swiftui State Layout ✅ Passed PR only adds restore/CLI logic; no new SwiftUI state, GeometryReader, lazy-row store refs, or render-time mutations were introduced.
Cmux Architecture Rethink ✅ Passed PASS: The PR centralizes Codex restore ownership via verifiedSessionRestoreInputs/CodexSessionResumeVerifier and adds no new sleeps, locks, observers, or duplicate entrypoints.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Only session-restore/CLI/test files changed; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers edits, so the auxiliary-window rule isn’t implicated.
Cmux Source Artifacts ✅ Passed The only changed paths are deliberate Swift source files; no logs, temp dirs, caches, screenshots, or other source-control artifacts were added.
Cmux No Ambient Global State ✅ Passed No new ambient global state: the verifier is an instance type, helpers live on owning types, and the additions are data/default params—not singleton or file-scope API.
Title check ✅ Passed The title clearly summarizes the main change: restoring Codex sessions through user-owned roots.
✨ 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 feat-durable-codex-session-restore

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.

@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: df802cb396

ℹ️ 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/Workspace.swift Outdated
Comment on lines +1055 to +1061
guard let restorableAgent, restorableAgent.kind == .codex else { return restorableAgent }
return codexResumeVerifierOwnsSession(
restorableAgent.sessionId,
environment: restorableAgent.launchCommand?.environment,
transcriptPath: restorableAgent.transcriptPath,
verifier: codexResumeVerifier
) ? restorableAgent : nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve legacy sessions saved before this transcript field

On upgrade, every previously persisted SessionRestorableAgentSnapshot decodes with transcriptPath == nil. For users on an older Codex installation without state_5.sqlite, this validation has no rollout path to check and returns nil, while the separately revalidated old agent-hook binding likewise has no transcript path; the restored terminal therefore becomes a plain shell instead of resuming an otherwise valid legacy Codex session. Resolve the rollout from the hook-store/index (or scan the legacy session files by ID) before applying this validation so the claimed legacy fallback also covers snapshots created by earlier cmux releases.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two related Codex session-restore bugs: an indexed child worker could replace the durable root identity, and snapshot reconciliation erased hook bindings when a transient empty process scan temporarily found no agent. The fix introduces CodexSessionResumeVerifier, which validates Codex sessions against the SQLite thread index and follows parent links to the user-owned root, rejecting automation, exec, and subagent identities as restore roots. Hook bindings are now treated as durable session identity; process liveness only controls the automatic-launch gate (wasAgentRunning), not whether the binding is preserved.

  • CodexSessionResumeVerifier (new package type) verifies sessions via state_5.sqlite and a legacy rollout fallback, resolving subagent child sessions to their interactive parent. It is wired into both the CLI hook-event path and the app-side RestorableAgentSessionIndex.load() and session-restore pipeline.
  • isStaleAgentHookBinding is removed from reconcileSurfaceResumeBindings; the resolvedAgentPID closure in the hook handler now prefers the wrapper's live PID over the pre-resume stored PID to protect against stale-PID snapshots after a Command-Q relaunch cycle.
  • SurfaceResumeCommandCanonicalizer gains command-parsing helpers to extract and replace the Codex session ID in resume commands (including shell-wrapper unwrapping), enabling binding retargeting when a subagent session resolves to its parent.

Confidence Score: 5/5

Safe to merge; the restore path is well-covered by regression tests and the durable-binding change is deliberate and sound.

The core logic — verifying session ownership via the SQLite thread index, following parent links, rejecting automation/exec/subagent roots, and making hook bindings durable across empty process scans — is correct and thoroughly tested. The one new finding (per-iteration SQLite reconnect inside the parent-chain traversal loop) is a cold-path efficiency issue, not a correctness bug. Previously-flagged concerns (synchronous reads on the main-actor restore path, NSHomeDirectory vs HOME mismatch) are known and carried over rather than introduced by this PR.

Files Needing Attention: CodexSessionResumeVerifier.swift (indexedEvidence reconnects per parent step); Workspace.swift and DockSplitStore+SessionRestore.swift carry the synchronous SQLite-on-main-actor concern noted in earlier threads.

Important Files Changed

Filename Overview
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexSessionResumeVerifier.swift New type that verifies Codex session ownership via SQLite thread index and legacy rollout. Parent-chain traversal reopens the database per step; all other logic (SQLite usage, rollout parsing, automation/exec rejection, cycle guard) is correct.
Sources/Workspace.swift Adds provider-verified session restore pipeline; removes isStaleAgentHookBinding to make hook bindings durable across empty process scans. Previously-flagged issues remain: synchronous SQLite reads on main-actor panel-restore path and NSHomeDirectory() vs HOME env mismatch in codexResumeEvidence.
Sources/RestorableAgentSession.swift Integrates CodexSessionResumeVerifier into RestorableAgentSessionIndex.load(); adds transcriptPath to SessionRestorableAgentSnapshot and threads it through providerVerifiedHookRecord. Off-main caller path is correct.
Sources/SurfaceResumeCommandCanonicalizer+CodexUpdateCheck.swift Adds command-parsing helpers to extract and replace the Codex session ID in resume commands, including shell-wrapper unwrapping up to depth 4. Logic is correct for the cmux-controlled command formats.
CLI/CMUXCLI+AgentHookRestoreEvidence.swift Adds agentHookCanonicalResumeSessionId which creates a fresh CodexSessionResumeVerifier per call; fixes empty-arguments crash when appending permission flags to environment-only captures. Previously-flagged stale-cache issue on the static verifier remains.
CLI/cmux.swift Introduces resolvedAgentPID closure to prefer the wrapper's live PID over the pre-resume stored PID; threads transcriptPath through resume-binding update sites. Priority ordering (hook-env > stored > inferred) is correct.
Sources/DockSplitStore+SessionRestore.swift Routes panel restoration through verifiedSessionRestoreInputs; uses default CodexSessionResumeVerifier() (no verifier passed), compounding the synchronous SQLite-on-main-actor issue flagged in the prior thread.
Sources/Workspace+AgentLifecycle.swift Removes isStaleAgentHookBinding — hook bindings are now durable session identity. Tests were updated to reflect the new behavior (binding preserved across empty scans, wasAgentRunning controls auto-launch).
cmuxTests/SessionPersistenceResumeBindingTests.swift New tests cover poisoned binding rejection, legacy unindexed rejection, kind migration for kindless bindings, and subagent-to-parent retargeting. SQLite fixture correctly leaves connection open for the test lifetime.
cmuxTests/CLICodexWeakEnvironmentRestoreBindingTests.swift Adds regression tests for unindexed-review rejection and indexed-subagent parent promotion via live CLI invocations with in-process SQLite fixtures.

Sequence Diagram

sequenceDiagram
    participant Hook as CLI Hook Event
    participant CLI as CMUXCLI
    participant Verifier as CodexSessionResumeVerifier
    participant SQLite as state_5.sqlite
    participant Store as Hook Session Store
    participant App as Workspace (App)
    participant Restore as RestorableAgentSessionIndex

    Hook->>CLI: session-start / prompt-submit (sessionId, transcriptPath)
    CLI->>Verifier: agentHookCanonicalResumeSessionId(sessionId)
    Verifier->>SQLite: "SELECT rollout_path, thread_source WHERE id = sessionId"
    alt Indexed subagent
        SQLite-->>Verifier: "rollout_path, thread_source=subagent"
        Verifier->>Verifier: read session_meta to parentSessionId
        Verifier->>SQLite: "SELECT rollout_path WHERE id = parentSessionId"
        SQLite-->>Verifier: "parent rollout_path, thread_source=user"
        Verifier-->>CLI: "evidence(sessionId=parentId)"
    else Indexed user root
        SQLite-->>Verifier: "rollout_path, thread_source=user"
        Verifier-->>CLI: "evidence(sessionId=sessionId)"
    else Not indexed legacy
        Verifier->>Verifier: read transcriptPath to session_meta
        Verifier-->>CLI: evidence via legacyRollout
    end
    CLI->>Store: updateAgentSurfaceResumeBinding(resumeSessionId)

    Note over App,Restore: App restart / panel restore

    App->>Restore: RestorableAgentSessionIndex.load()
    Restore->>Verifier: providerVerifiedHookRecord(record)
    Verifier->>SQLite: verify and resolve parent chain
    Verifier-->>Restore: verified record (parentSessionId, rolloutPath)
    Restore-->>App: RestorableAgentSessionIndex

    App->>App: verifiedSessionRestoreInputs(binding, restorableAgent)
    App->>Verifier: codexResumeEvidence(sessionId, environment)
    Verifier->>SQLite: verify session ownership
    Verifier-->>App: CodexSessionResumeEvidence
    App->>App: retarget binding command to parentSessionId
    App->>App: createPanel(restorableAgent, resumeBinding)
Loading

Reviews (12): Last reviewed commit: "fix: verify legacy Codex restore binding..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift Outdated
Comment on lines +1026 to +1041
#if DEBUG
nonisolated static func sessionRestoreInputsForTesting(
binding: SurfaceResumeBindingSnapshot?,
restorableAgent: SessionRestorableAgentSnapshot?
) -> (binding: SurfaceResumeBindingSnapshot?, restorableAgent: SessionRestorableAgentSnapshot?) {
let effectiveBinding = resumeBindingForSessionRestore(
binding,
restorableAgent: restorableAgent,
codexResumeVerifier: CodexSessionResumeVerifier()
)
return (
effectiveBinding,
restorableAgentForSessionRestore(restorableAgent, resumeBinding: effectiveBinding)
)
}
#endif

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 #if DEBUG test seam in production Sources/

sessionRestoreInputsForTesting matches the exact prohibited naming pattern (…ForTesting) and is guarded by #if DEBUG with no production caller — it only exists so SessionPersistenceResumeBindingTests can exercise resumeBindingForSessionRestore and restorableAgentForSessionRestore together. The canonical fix is to widen those two private static helpers to internal (they're already nonisolated), drop this shim entirely, and call the helpers directly from the test target via @testable import Cmux.

Rule Used: Flag Swift files under a production Sources path (... (source)

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!

private static let codexSessionResumeVerifier = CodexSessionResumeVerifier()

func agentHookProviderOwnsResumeTarget(
kind: String,

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 Stale thread-index cache for long-lived process

codexSessionResumeVerifier is a process-lifetime static with an internal CodexThreadIndexCache that loads state_5.sqlite once per database path and never re-reads it. Any Codex session created after the first hook fires won't appear in the frozen dictionary; those hooks fall through to the transcriptPath / legacyRollout path. For modern Codex hooks that include transcript_path this is a silent degradation, but for hooks that don't include it (e.g. stripped environments) the valid new session is incorrectly rejected as a review UUID. A targeted re-query on cache miss — rather than treating an empty result as final — would keep the cache warm for subsequent look-ups while still loading new entries that Codex indexed after startup.

Comment on lines +127 to +170
private func regularNonEmptyFileExists(atPath path: String, fileManager: FileManager) -> Bool {
guard let attributes = try? fileManager.attributesOfItem(atPath: path),
attributes[.type] as? FileAttributeType == .typeRegular,
let size = attributes[.size] as? NSNumber else {
return false
}
return size.int64Value > 0
}
}

private func rolloutContainsSessionMetadata(
sessionId: String,
path: String,
fileManager: FileManager
) -> Bool {
guard regularNonEmptyFileExists(atPath: path, fileManager: fileManager),
let handle = FileHandle(forReadingAtPath: path) else {
return false
}
defer { try? handle.close() }

let prefix = handle.readData(ofLength: 256 * 1024)
guard let text = String(data: prefix, encoding: .utf8) else { return false }
for line in text.split(whereSeparator: \Character.isNewline).prefix(32) {
guard let data = String(line).data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
object["type"] as? String == "session_meta",
let payload = object["payload"] as? [String: Any],
payload["id"] as? String == sessionId else {
continue
}
return true
}
return false
}

private func regularNonEmptyFileExists(atPath path: String, fileManager: FileManager) -> Bool {
guard let attributes = try? fileManager.attributesOfItem(atPath: path),
attributes[.type] as? FileAttributeType == .typeRegular,
let size = attributes[.size] as? NSNumber else {
return false
}
return size.int64Value > 0
}

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 Duplicate regularNonEmptyFileExists implementation

regularNonEmptyFileExists(atPath:fileManager:) is defined identically — same body, same signature — on both the private CodexThreadIndexCache class (line 127) and on CodexSessionResumeVerifier itself (line 163). The outer struct's copy is used only by rolloutContainsSessionMetadata; the inner class uses its own copy. Consider extracting a single fileprivate free function or a shared internal helper to avoid drift if the implementation needs to change.

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!

Comment thread Sources/Workspace.swift
Comment on lines +1070 to +1073
let codexHome = normalizedResumeBindingValue(environment?["CODEX_HOME"])
?? URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
.appendingPathComponent(".codex", isDirectory: true)
.path

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 NSHomeDirectory() skips environment HOME override

codexResumeVerifierOwnsSession falls back directly to NSHomeDirectory(), but the CLI counterpart (agentHookProviderOwnsResumeTarget) first checks environment["HOME"] before calling NSHomeDirectory(). In integration tests and sandboxed environments, HOME is overridden in the process environment while NSHomeDirectory() returns the real home directory, so the two helpers would derive different codexHome paths for the same session.

Suggested change
let codexHome = normalizedResumeBindingValue(environment?["CODEX_HOME"])
?? URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
.appendingPathComponent(".codex", isDirectory: true)
.path
let codexHome = normalizedResumeBindingValue(environment?["CODEX_HOME"])
?? URL(
fileURLWithPath: normalizedResumeBindingValue(ProcessInfo.processInfo.environment["HOME"])
?? NSHomeDirectory(),
isDirectory: true
).appendingPathComponent(".codex", isDirectory: true).path

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/RestorableAgentSession.swift (1)

1119-1141: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve the verifier’s authoritative rollout path in the snapshot.

hookRecordIsRestorable discards CodexSessionResumeEvidence, then Line 1141 stores only the hook record’s optional path. Indexed records without record.transcriptPath therefore lose the verified rollout path. Return the evidence and persist evidence.rolloutPath.

As per path instructions, resume authority must come from the provider-owned verifier rather than a secondary saved value.

Also applies to: 1356-1368

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/RestorableAgentSession.swift` around lines 1119 - 1141, Update
hookRecordIsRestorable to return the provider-owned CodexSessionResumeEvidence
rather than discarding it, and use the returned evidence when constructing each
RestorableAgentSnapshot. Persist evidence.rolloutPath for transcriptPath at both
affected snapshot-building paths, including indexed records without
record.transcriptPath, while retaining the existing non-Codex behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLI/CMUXCLI`+AgentHookRestoreEvidence.swift:
- Around line 8-26: Update agentHookProviderOwnsResumeTarget to accept the
caller-selected Codex home or environment override and stop resolving Codex home
from ambient ProcessInfo state. In CLI/CMUXCLI+AgentHookRestoreEvidence.swift
lines 8-26, use the explicit value for verification; in
CLI/CMUXCLI+SessionsList.swift lines 245-250, pass the record-specific codexHome
already computed for that row so ownership verification and launch_backed use
the same authoritative source.

In `@cmuxTests/CLICodexWeakEnvironmentRestoreBindingTests.swift`:
- Around line 419-434: Strengthen the assertions in the restore-binding test
around the observed commands: collect every surface.resume.clear request, then
require that all cleared checkpoint_id values equal the rejected sessionId.
Preserve the existing assertion that the invalid checkpoint is cleared, while
ensuring no unrelated valid resume binding is removed.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexSessionResumeVerifier.swift`:
- Around line 57-83: The CodexThreadIndexCache rolloutPath lookup must not rely
on permanently cached SQLite mappings. Update rolloutPath to reload via
loadRolloutPaths whenever the database may have changed, or remove cross-call
caching entirely, ensuring SQLite remains authoritative and preventing stale
empty, removed, or remapped session entries from being accepted.

In `@Sources/Workspace.swift`:
- Around line 1026-1041: Remove the DEBUG-only sessionRestoreInputsForTesting
helper from Workspace.swift. Move any needed test scaffolding into the test
target and exercise the production restore helpers through `@testable` import,
ensuring tests use providerVerifiedRestorableAgentForSessionRestore and cannot
retain Codex snapshots that production drops.

---

Outside diff comments:
In `@Sources/RestorableAgentSession.swift`:
- Around line 1119-1141: Update hookRecordIsRestorable to return the
provider-owned CodexSessionResumeEvidence rather than discarding it, and use the
returned evidence when constructing each RestorableAgentSnapshot. Persist
evidence.rolloutPath for transcriptPath at both affected snapshot-building
paths, including indexed records without record.transcriptPath, while retaining
the existing non-Codex behavior.
🪄 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: 4e955234-5d23-4a9b-92d0-d4f22f4c485a

📥 Commits

Reviewing files that changed from the base of the PR and between 0b70fa2 and df802cb.

📒 Files selected for processing (11)
  • CLI/CMUXCLI+AgentHookRestoreEvidence.swift
  • CLI/CMUXCLI+SessionsList.swift
  • CLI/cmux.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexSessionResumeVerifier.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/CodexSessionResumeVerifierTests.swift
  • Sources/RestorableAgentSession.swift
  • Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift
  • Sources/Workspace.swift
  • cmuxTests/CLICodexWeakEnvironmentRestoreBindingTests.swift
  • cmuxTests/RestorableAgentSessionIndexCodexWeakRecordTests.swift
  • cmuxTests/SessionPersistenceResumeBindingTests.swift

Comment thread CLI/CMUXCLI+AgentHookRestoreEvidence.swift Outdated
Comment on lines +419 to +434
let commands = state.snapshot()
XCTAssertFalse(
commands.contains { self.jsonObject($0)?["method"] as? String == "surface.resume.set" },
"unindexed review UUID must not become restore authority: \(commands)"
)
XCTAssertTrue(
commands.contains { command in
guard let payload = self.jsonObject(command),
payload["method"] as? String == "surface.resume.clear",
let params = payload["params"] as? [String: Any] else {
return false
}
return params["checkpoint_id"] as? String == sessionId
},
"the invalid checkpoint should be cleared without touching another session: \(commands)"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that no unrelated resume binding is cleared.

The test passes if the CLI clears sessionId and another valid checkpoint. Collect all surface.resume.clear requests and require every one to target the rejected ID.

Proposed assertion
-        XCTAssertTrue(
-            commands.contains { command in
+        let clearRequests = commands.compactMap { command -> [String: Any]? in
                 guard let payload = self.jsonObject(command),
                       payload["method"] as? String == "surface.resume.clear",
                       let params = payload["params"] as? [String: Any] else {
-                    return false
+                    return nil
                 }
-                return params["checkpoint_id"] as? String == sessionId
-            },
+                return params
+        }
+        XCTAssertEqual(clearRequests.count, 1, "\(commands)")
+        XCTAssertTrue(
+            clearRequests.allSatisfy { $0["checkpoint_id"] as? String == sessionId },
             "the invalid checkpoint should be cleared without touching another session: \(commands)"
         )

As per path instructions, correctness-critical resume authority must use one reliable source and fail closed without disturbing another binding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/CLICodexWeakEnvironmentRestoreBindingTests.swift` around lines 419
- 434, Strengthen the assertions in the restore-binding test around the observed
commands: collect every surface.resume.clear request, then require that all
cleared checkpoint_id values equal the rejected sessionId. Preserve the existing
assertion that the invalid checkpoint is cleared, while ensuring no unrelated
valid resume binding is removed.

Source: Path instructions

Comment thread Sources/Workspace.swift Outdated
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Dogfood passed on tagged build cdrst.

  • Started root codex --yolo, then had it run a nested codex exec --yolo that completed with NESTED_OK.
  • Root session: 019f6f52-44c8-7f63-8d1b-ef404dab0dd0. Nested session: 019f6f52-7a92-75e2-991e-3681267a0da5.
  • The live surface resume checkpoint remained the root session after the nested session completed.
  • Killed and relaunched the tagged app twice. Both restores reopened the root conversation with ROOT_DONE; neither showed No saved session found.
  • Captured and inspected debug window screenshot 2026-07-18T01-53-10Z_BFCC8E7D after restore.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
CLI/CMUXCLI+AgentHookRestoreEvidence.swift (1)

6-6: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Avoid introducing a process-wide verifier singleton.

private static let codexSessionResumeVerifier creates ambient runtime state for a verifier that owns shared caching. Make the verifier an injected or instance-scoped dependency so callers and tests can control its lifetime and isolation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/CMUXCLI`+AgentHookRestoreEvidence.swift at line 6, Remove the
process-wide codexSessionResumeVerifier static singleton and make
CodexSessionResumeVerifier an injected or instance-scoped dependency for the
callers that use it. Update those callers and tests to supply or construct the
verifier explicitly, preserving cache isolation and controllable lifetime.

Source: Coding guidelines

CLI/cmux.swift (1)

27695-27736: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Send the canonical checkpoint id when clearing resume bindings.

publishAgentSurfaceResumeBinding now stores checkpoint_id as resumeSessionId, while the fail-closed clear path still sends the normalized raw sessionId. clearAgentSurfaceResumeBinding should receive or derive the same canonical checkpoint id used by publish, or the clear should accept it explicitly, so a canonical Codex resume binding is not left uncleared.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/cmux.swift` around lines 27695 - 27736, The fail-closed clear paths in
publishAgentSurfaceResumeBinding use the raw sessionId instead of the canonical
resumeSessionId stored as checkpoint_id. Derive
agentHookCanonicalResumeSessionId before clearing or update
clearAgentSurfaceResumeBinding to accept the canonical checkpoint id, and pass
that value in every relevant clear call so Codex bindings are consistently
removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@CLI/cmux.swift`:
- Around line 27695-27736: The fail-closed clear paths in
publishAgentSurfaceResumeBinding use the raw sessionId instead of the canonical
resumeSessionId stored as checkpoint_id. Derive
agentHookCanonicalResumeSessionId before clearing or update
clearAgentSurfaceResumeBinding to accept the canonical checkpoint id, and pass
that value in every relevant clear call so Codex bindings are consistently
removed.

In `@CLI/CMUXCLI`+AgentHookRestoreEvidence.swift:
- Line 6: Remove the process-wide codexSessionResumeVerifier static singleton
and make CodexSessionResumeVerifier an injected or instance-scoped dependency
for the callers that use it. Update those callers and tests to supply or
construct the verifier explicitly, preserving cache isolation and controllable
lifetime.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fc0cc1b1-c561-4b41-8bdd-bd8727e23ab3

📥 Commits

Reviewing files that changed from the base of the PR and between df802cb and bb1022b.

📒 Files selected for processing (2)
  • CLI/CMUXCLI+AgentHookRestoreEvidence.swift
  • CLI/cmux.swift

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@lawrencecchen lawrencecchen changed the title Make Codex session restore require provider-owned evidence Restore Codex sessions through user-owned roots Jul 25, 2026

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Workspace.swift (1)

930-944: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fail closed for every unverified Codex hook binding.

This check runs only when kind is exactly "codex" and checkpointId normalizes. A kindless/whitespace-padded Codex hook binding—or a Codex binding with an empty checkpoint—falls through Line 943 and is restored unchanged without provider evidence. Reject such bindings unless a provider-verified Codex snapshot establishes the same session; add regressions for missing kind and missing checkpoint IDs.

As per path instructions, correctness-critical identity must use a reliable structured source and fail closed when absent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Workspace.swift` around lines 930 - 944, Update the resume-binding
validation around codexResumeVerifierOwnsSession so every Codex agent-hook
binding is rejected unless it has a normalized kind of codex, a nonempty
normalized checkpoint ID, and provider verification for that same session.
Ensure missing or whitespace-padded kind and missing or empty checkpoint IDs
cannot reach the existing binding restoration path, and add regressions covering
both missing kind and missing checkpoint ID cases.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Workspace.swift`:
- Around line 930-944: Update the resume-binding validation around
codexResumeVerifierOwnsSession so every Codex agent-hook binding is rejected
unless it has a normalized kind of codex, a nonempty normalized checkpoint ID,
and provider verification for that same session. Ensure missing or
whitespace-padded kind and missing or empty checkpoint IDs cannot reach the
existing binding restoration path, and add regressions covering both missing
kind and missing checkpoint ID cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 47bbb571-6b43-4b9b-af6b-9c0d0063f572

📥 Commits

Reviewing files that changed from the base of the PR and between bb1022b and 71372d0.

📒 Files selected for processing (3)
  • Sources/DockSplitStore+SessionRestore.swift
  • Sources/Workspace.swift
  • cmuxTests/SessionPersistenceResumeBindingTests.swift

Comment thread Sources/Workspace.swift
Comment on lines +1344 to 1349
let codexResumeVerifier = CodexSessionResumeVerifier()
let restoreInputs = Self.verifiedSessionRestoreInputs(
binding: snapshot.terminal?.resumeBinding,
restorableAgent: snapshot.terminal?.agent,
codexResumeVerifier: codexResumeVerifier
)

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 Synchronous SQLite + disk reads introduced on the panel-restore path

verifiedSessionRestoreInputs (via providerVerifiedRestorableAgentForSessionRestore → codexResumeEvidence → verifier.evidence()) calls sqlite3_open_v2 + SELECT id, rollout_path, thread_source FROM threads and reads up to 256 KB from the rollout JSONL file, all synchronously. createPanel(from:inPane:...) is a synchronous Workspace method accessed via main-actor-bound stored properties (agentSessionAutoResumeDefaults, surfaceResumeBindingIndex, remoteConfiguration, etc.), and the fresh CodexSessionResumeVerifier() created here has an empty CodexThreadIndexCache, so every panel restore with a Codex session opens a new SQLite connection on this path.

Before this PR, the same code path called only in-memory restorableAgentForSessionRestore/resumeBindingForSessionRestore with no disk I/O. The Codex verifier's SQLite reads already happen off-main inside RestorableAgentSessionIndex.load() via providerVerifiedHookRecord; moving them here duplicates them on the restore path. The fix is to pass the snapshot's already-verified sessionId (from RestorableAgentSessionIndex) through the persisted agent snapshot rather than re-verifying on restore, or to run verifiedSessionRestoreInputs in a detached task and await the result before mutating panel state.

Rule Used: Flag production Swift that reads, decodes, or scan... (source)

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 commented Jul 25, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

Comment thread Sources/Workspace.swift Outdated
Comment on lines +936 to +940
!codexResumeVerifierOwnsSession(
checkpointId,
environment: binding.environment,
verifier: codexResumeVerifier
) {

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 Legacy Codex sessions (not yet indexed in state_5.sqlite) silently lose their stored binding on app restart. codexResumeVerifierOwnsSession is called without a transcriptPath, so for any session whose ID doesn't appear in the SQLite index the verifier falls through to the transcript check, finds nil, and returns false — discarding the binding. But at this call site the restorableAgent parameter is already the output of providerVerifiedRestorableAgentForSessionRestore, which sets transcriptPath from evidence.rolloutPath precisely so downstream callers can use it. Passing that path here lets the same legacy-rollout fallback that approved the agent also approve its binding, keeping auto-resume intact for older Codex installations.

Suggested change
!codexResumeVerifierOwnsSession(
checkpointId,
environment: binding.environment,
verifier: codexResumeVerifier
) {
!codexResumeVerifierOwnsSession(
checkpointId,
environment: binding.environment,
transcriptPath: restorableAgent?.transcriptPath,
verifier: codexResumeVerifier
) {

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexSessionResumeVerifier.swift (1)

263-276: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject nested source.exec metadata.

Line 276 only detects "exec" when source is a string. A structured source such as {"exec": {...}} bypasses this check, so an indexed thread marked user can be restored. Reuse sourceContainsKind("exec", value: source) and add a nested-source regression case.

Proposed fix
 let source = payload["source"]
 let sourceIsSubagent = sourceContainsKind("subagent", value: source)
+let sourceIsExec = sourceContainsKind("exec", value: source)
 let sourceKind = normalized(source as? String)?.lowercased()
 let originator = normalized(payload["originator"] as? String)?.lowercased()
 ...
-    isExec: sourceKind == "exec" || originator == "codex_exec"
+    isExec: sourceIsExec || sourceKind == "exec" || originator == "codex_exec"
 )

As per path instructions, correctness-critical session identity must fail closed when ownership evidence is not reliable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexSessionResumeVerifier.swift`
around lines 263 - 276, Update SessionMetadata construction in the metadata
parsing flow to determine exec ownership with sourceContainsKind("exec", value:
source), including structured nested source.exec metadata rather than only
sourceKind string matching. Ensure indexed user threads with nested exec
metadata are rejected, and add a regression case covering this nested-source
scenario while preserving existing exec detection.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexSessionResumeVerifier.swift`:
- Around line 263-276: Update SessionMetadata construction in the metadata
parsing flow to determine exec ownership with sourceContainsKind("exec", value:
source), including structured nested source.exec metadata rather than only
sourceKind string matching. Ensure indexed user threads with nested exec
metadata are rejected, and add a regression case covering this nested-source
scenario while preserving existing exec detection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0affe49f-7403-4e52-b17a-d254e1e9edca

📥 Commits

Reviewing files that changed from the base of the PR and between 71372d0 and e0b687f.

📒 Files selected for processing (2)
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexSessionResumeVerifier.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/CodexSessionResumeVerifierTests.swift

@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: e0b687f262

ℹ️ 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".


@Suite(.serialized)
struct CodexSessionResumeVerifierTests {
@Test func indexedThreadWithExistingRolloutIsResumable() throws {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Split the regression tests from the implementation

This commit introduces both CodexSessionResumeVerifier and the tests intended to prove its regressions, so the tests never exist on the parent revision and CI cannot demonstrate that they fail without the fix. Put the failing tests in a test-only commit followed by the implementation commit, as required by the repository's regression-test policy.

AGENTS.md reference: AGENTS.md:L127-L134

Useful? React with 👍 / 👎.

Comment on lines +251 to +252
let prefix = handle.readData(ofLength: 256 * 1024)
guard let text = String(data: prefix, encoding: .utf8) else { return nil }

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 Read the complete metadata line before trusting the thread

When an indexed rollout's first session_meta line exceeds 256 KiB, this read truncates the JSON (and can also split a UTF-8 scalar), so sessionMetadata returns nil. For a codex exec rollout whose database thread_source is user—the exact shape exercised by indexedExecThreadIsNotResumable—indexedEvidence then misses source == "exec"/originator == "codex_exec" and incorrectly accepts the worker as resumable. Read through the terminating newline with a bounded streaming parser, as the existing CodexRolloutIdentityResolver does, before treating absent metadata as safe.

Useful? React with 👍 / 👎.

Comment thread Sources/Workspace.swift Outdated
Comment on lines +932 to +934
if let binding,
binding.isAgentHookBinding,
binding.kind.flatMap(RestorableAgentKind.init(rawValue:)) == .codex,

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 Verify legacy kindless Codex bindings

When restoring a legacy agent-hook binding decoded without kind—a supported format covered by legacyAgentHookBindingWithoutKindRewritesPersistedPATHManagedAgentExecutable—this condition skips provider verification entirely. If such a snapshot has no accompanying restorable-agent record, an unindexed review or worker UUID remains an automatically resumable binding and reaches startup as codex resume <unverified-id>, defeating the validation added here. Infer Codex from the legacy command/name or migrate the binding before applying this guard.

Useful? React with 👍 / 👎.

@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: 72885eaf98

ℹ️ 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/Workspace.swift Outdated
environment: environment,
transcriptPath: transcriptPath,
verifier: verifier
)?.sessionId == sessionId

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 Canonicalize saved subagent bindings instead of dropping them

When a pre-change snapshot contains only a kindful Codex agent-hook binding for an indexed subagent, evidence successfully resolves the child ID to its interactive parent, but this equality treats that canonicalization as failure because the IDs differ. verifiedSessionRestoreInputs then removes the binding and has no restorable-agent fallback, leaving a plain shell despite the database supplying a safe resume target. Retarget the saved binding's checkpoint and command to evidence.sessionId, matching the live hook publication path.

AGENTS.md reference: AGENTS.md:L148-L148

Useful? React with 👍 / 👎.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CLI/CMUXCLI+AgentHookRestoreEvidence.swift (1)

22-42: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Still resolves Codex home from ambient ProcessInfo, not the caller-selected value.

agentHookCanonicalResumeSessionId (and agentHookProviderOwnsResumeTarget, which delegates to it) falls back to ProcessInfo.processInfo.environment["CODEX_HOME"]/HOME when the launch command doesn't carry CODEX_HOME. This was already flagged on a prior commit of this function: callers with an explicit/record-specific Codex home (e.g. cmux sessions --codex-home …) can end up verifying ownership against a different state_5.sqlite index than the one actually associated with the session.

♻️ Suggested direction (from prior review)
-func agentHookCanonicalResumeSessionId(
+func agentHookCanonicalResumeSessionId(
     kind: String,
     sessionId: String,
     transcriptPath: String?,
-    launchCommand: AgentHookLaunchCommandRecord?
+    launchCommand: AgentHookLaunchCommandRecord?,
+    codexHomeOverride: String? = nil
 ) -> String? {
     guard let sessionId = normalizedHookValue(sessionId) else { return nil }
     guard kind == "codex" else { return sessionId }
     let environment = ProcessInfo.processInfo.environment
     let codexHome = normalizedHookValue(launchCommand?.environment?["CODEX_HOME"])
+        ?? normalizedHookValue(codexHomeOverride)
         ?? normalizedHookValue(environment["CODEX_HOME"])
         ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLI/CMUXCLI`+AgentHookRestoreEvidence.swift around lines 22 - 42, Update
agentHookCanonicalResumeSessionId and the delegating
agentHookProviderOwnsResumeTarget flow to use the caller-selected Codex home
consistently, rather than falling back to ambient ProcessInfo environment
values. Thread the explicit/record-specific Codex home through the relevant
launch or hook data and use it when constructing CodexSessionResumeVerifier,
preserving the existing normalization and non-Codex behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@CLI/CMUXCLI`+AgentHookRestoreEvidence.swift:
- Around line 22-42: Update agentHookCanonicalResumeSessionId and the delegating
agentHookProviderOwnsResumeTarget flow to use the caller-selected Codex home
consistently, rather than falling back to ambient ProcessInfo environment
values. Thread the explicit/record-specific Codex home through the relevant
launch or hook data and use it when constructing CodexSessionResumeVerifier,
preserving the existing normalization and non-Codex behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 43a44037-bab2-4b9c-bc7f-a1bc7e81d465

📥 Commits

Reviewing files that changed from the base of the PR and between e0b687f and 72885ea.

📒 Files selected for processing (3)
  • CLI/CMUXCLI+AgentHookRestoreEvidence.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/CodexSessionResumeVerifier.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/CodexSessionResumeVerifierTests.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex restore can resume an indexed subagent instead of its interactive parent

2 participants