Skip to content

Keep resumed agent notification hooks armed during restore - #9064

Merged
austinywang merged 9 commits into
mainfrom
issue-9059-resume-notification-reliability
Jul 28, 2026
Merged

austinywang merged 9 commits into
mainfrom
issue-9059-resume-notification-reliability

Conversation

@austinywang

@austinywang austinywang commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Preserve Codex and Claude notification hooks for cmux-owned auto-resume launches even when the restore-time socket probe misses transiently.
  • Authorize the bypass with a one-shot token bound to the exact provider and resumed session.
  • Cover both restore sources: live agent snapshots and higher-precedence persisted agent-hook bindings.
  • Route captured live executables through the per-surface wrapper while preserving the selected binary.
  • Add privacy-safe DEBUG traces for restore binding, hook target resolution, and notification delivery decisions.

Root cause and evidence

#8839 changed auto-resume to type the resume command into the normal interactive login shell. Both wrappers made one bounded 750 ms socket ping during that startup window and used the result to decide whether cmux hooks existed for the full agent process lifetime. A missing socket file, connection backlog, or slow ping caused permanent passthrough.

For Codex, resume does not emit SessionStart. A startup probe miss therefore removed both the wrapper-generated SessionStart and the injected Stop hook. No later event could bind the resumed session or create the unread notification. Claude used the same lifetime gate and had the same notification-loss class.

The regression stress harness launches 12 missing-socket and 12 stale-socket Codex restores. The test-only commit demonstrates that all restore launches lost SessionStart and Stop injection before the fix.

Ownership fix

AgentRestoreLaunch in Packages/macOS/CMUXAgentLaunch is the single value owner for restore authorization. It can be constructed only for Codex or Claude with a UUID-shaped session ID. It supplies the provider wrapper route, custom executable environment key, portable shell wrapper, and exact CMUX_AGENT_RESTORE_LAUNCH=<provider>:<session-id> authorization.

The app composes that value with stored-command parsing in both restore paths:

  • SessionRestorableAgentSnapshot.resumeStartupInput()
  • persisted SurfaceResumeBindingSnapshot values whose source is agent-hook

Every authorized restore is routed through the managed per-surface wrapper. If persistence captured a live absolute provider binary, the wrapper receives that exact selection through CMUX_CUSTOM_CODEX_PATH or CMUX_CUSTOM_CLAUDE_PATH instead of silently selecting another binary from PATH.

Startup input transports the one-shot assignment through /usr/bin/env before the routed command. This spelling works when typed into bash, zsh, fish, csh, or tcsh. Persistent resume commands and fork commands remain unchanged. Unsupported providers, invalid session IDs, CLI bindings, and other non-hook commands remain unmarked.

Each wrapper consumes and unsets the token immediately. It bypasses socket health only when the token exactly matches the provider and explicit resume ID parsed from argv. Wrong-provider, wrong-session, legacy boolean, and missing-ID markers still require the live-socket ownership check.

Claude can legitimately resolve through a finite user-owned shim chain. The wrapper re-issues the same authorization only at a recognized wrapper-shim boundary, consumes it on the next wrapper pass, and unsets it before the real Claude process. This preserves hook settings and cmux surface context during a startup socket outage without leaking authorization into nested unrelated launches.

Once hooks remain installed, the existing target resolver validates current workspace and surface accessibility, prefers live TTY or PID binding over stale ambient surface state, and republishes the resolved resume binding at SessionStart and Stop.

Diagnostics

DEBUG builds record bounded, privacy-safe decisions through the existing 500-entry debug ring and tagged log:

  • restore discovery and typed startup presence
  • restored workspace and surface binding publication
  • hook event, resume intent, and target resolution source
  • Stop notification send, success, failure, or dedupe decision

Session and target identifiers are truncated. Message content is represented only by byte or character counts.

Tests

  • Codex stress coverage for 12 missing-socket and 12 stale-socket app-owned restores.
  • Codex and Claude coverage for exact provider/session authorization, wrong-provider tokens, wrong-session tokens, legacy boolean tokens, unmarked manual resumes, invalid IDs, and marker non-leakage.
  • Claude coverage for stale-socket authorization across a supported finite shim re-entry.
  • Swift package coverage for provider/session validation, wrapper selection, captured-binary preservation keys, portable wrapper dispatch, and /usr/bin/env authorization transport.
  • App behavior coverage for snapshot and persisted agent-hook startup input, live custom executables, tcsh dispatch, unsupported providers, invalid IDs, CLI bindings, persistent resume commands, and fork commands.
  • Full Claude wrapper, Claude mutual-shim, user-binary resolution, Codex stress, and shell dispatch suites.
  • Bash syntax, Python compile, Swift parser, Package.resolved policy, test wiring, pbxproj normalization, and diff checks.
  • Local Xcode tests were not run as required by Notifications flaky for resumed agent sessions: blue unread ring sometimes never appears (Codex and others) #9059. The focused pure Swift package suite passed with the native arm64 Swift toolchain.

CI also exposed an existing actor-isolation warning in Workspace+ForkAgentConversationAvailability.swift. A separate repair removes the actor-isolated .shared default argument and resolves it inside a no-argument overload. No warning-budget file was changed.

Scope

Fixes #9059.

#9040 is the deterministic Dock-pane ring rendering bug and is not changed here. #6184 and #1027 remain separate historical reports.

@coderabbitai

coderabbitai Bot commented Jul 28, 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

Resume-aware Claude and Codex wrappers now consume provider/session-bound restore markers, preserve authorized resume behavior across socket conditions, prevent marker leakage, and set resume diagnostics. Session persistence, tests, debug logging, CI coverage, and a workspace availability overload are updated.

Changes

Resume hook reliability

Layer / File(s) Summary
Restore launch contract
Packages/macOS/CMUXAgentLaunch/..., Sources/RestorableAgentSession.swift, Sources/SurfaceResumeCommandCanonicalizer+PortableAgentExecutable.swift, cmuxTests/...
Resume startup commands conditionally include validated provider- and session-bound restore markers, wrapper routing, and custom executable settings.
Launch-time hook ownership
Resources/bin/cmux-claude-wrapper, Resources/bin/cmux-codex-wrapper
Wrappers consume restore markers, evaluate resume ownership and socket state, preserve authorized resumes, inject hooks, and set resume markers.
Socket-outage regression coverage
tests/test_*wrapper*.py, .github/workflows/ci.yml
Tests cover stale and missing sockets, passthrough and bypass behavior, shim re-entry, marker consumption, hook injection, and resume argument ordering; CI runs the Codex suite.
Resume diagnostic logging
CLI/cmux.swift, Sources/DockSplitStore+SessionRestore.swift, Sources/Workspace.swift
Debug logs include resume state, session-resume intent details, and typed startup state.

Conversation availability API

Layer / File(s) Summary
Context-menu availability overload
Sources/Workspace+ForkAgentConversationAvailability.swift
Adds a panel-only async overload and requires indexed calls to provide liveAgentIndex explicitly.

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

Possibly related issues

  • manaflow-ai/cmux-dev-artifacts#6622: Codex resume-wrapper trust-gate changes and regression tests overlap with this PR’s Codex resume coverage.
  • manaflow-ai/cmux-dev-artifacts#6632: Codex resume-wrapper gating and regression tests address the same resume trust behavior.
  • manaflow-ai/cmux-dev-artifacts#6627: Codex resume-launch handling and resume process regression testing are directly related.

Possibly related PRs

Suggested reviewers: lawrencecchen, azooz2003-bit

Sequence Diagram(s)

sequenceDiagram
  participant RestoreCommand
  participant AgentWrapper
  participant CmuxCLI
  participant AgentProcess
  RestoreCommand->>AgentWrapper: Pass provider/session restore marker
  AgentWrapper->>AgentWrapper: Parse resume id and evaluate socket ownership
  AgentWrapper->>CmuxCLI: Request resume hook arguments
  CmuxCLI-->>AgentWrapper: Return hook arguments
  AgentWrapper->>AgentProcess: Launch resumed agent without restore-marker leakage
Loading

Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error The diff adds reusable restore-command routing (AgentRestoreLaunch.applying(toStoredCommand:)) in app-target Sources instead of the CMUXAgentLaunch package. Move the restore-launch routing/authorizing helpers into Packages/macOS/CMUXAgentLaunch (e.g. AgentRestoreLaunch) and have app code call that package API.
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.
✅ Passed checks (23 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes and tests directly address #9059 by preserving hook injection for resumed sessions despite transient restore socket failures.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes stand out; the extra warning-fix overload and debug logging still support the restore and notification work.
Cmux Swift Actor Isolation ✅ Passed The Swift production diff removes the SharedLiveAgentIndex.shared default-arg issue on @MainActor Workspace and adds only plain Sendable value utilities.
Cmux Swift Blocking Runtime ✅ Passed Production Swift changes only add debug logs and pure string/command rewriting; no new semaphores, waits, sleeps, sync dispatch, polling, or locks were introduced.
Cmux Browser Automation Off-Main ✅ Passed No browser-automation routing files or browser.* worker commands changed; PR only touches resume-token/session wrapper logic and tests.
Cmux Expensive Synchronous Load ✅ Passed PASS: The changed Swift files only rewrite resume-launch commands; they add no RestorableAgentSessionIndex.load()/JSONL/transcript loads on main or interactive paths.
Cmux Cache Substitution Correctness ✅ Passed The diff adds restore-launch authorization and portability checks, but does not replace any authoritative read with a cached/opportunistic value in snapshot or persistence paths.
Cmux No Hacky Sleeps ✅ Passed PASS: HEAD only changes Swift/test files; no covered non-Swift runtime scripts were touched, and the inspected shell wrappers contain no new sleeps/polls.
Cmux Algorithmic Complexity ✅ Passed New scans are bounded to command strings/argv/env or 16-hop shim history; no scalable workspace/session rescans or repeated hot-path sorting were introduced.
Cmux Swift Concurrency ✅ Passed The Swift deltas are launch/input string transforms and DEBUG logs; no new background queues, Combine state, completion-handler APIs, or unscoped Tasks were added.
Cmux Swift @Concurrent ✅ Passed PASS: The only new async method is Workspace’s @MainActor resolver; the changed Swift code adds no missing/invalid @concurrent usage or UI-heavy off-actor async work.
Cmux Swiftpm Lockfiles ✅ Passed Diff changes workflow and source/tests only; no Package.swift, .gitignore, xcodeproj, or Package.resolved edits, so no lockfile policy violation.
Cmux Swift Logging ✅ Passed Changed Swift logs are DEBUG-only cmuxDebugLog calls with sanitized UUID prefixes/booleans; no ad hoc print/debugPrint/NSLog additions in production code.
Cmux User-Facing Error Privacy ✅ Passed No user-facing error/alert/output changed; the restore token is confined to internal startup input and DEBUG logs, while prompts still show binding.command.
Cmux Full Internationalization ✅ Passed Only debug-only logs, tests, and literal protocol/env tokens changed; no user-facing Swift text, string catalogs, Info.plists, or web locale files were touched.
Cmux Swiftui State Layout ✅ Passed PASS: the PR diff only touches wrappers/tests and backend Swift code; no SwiftUI imports or review-bot trigger patterns appear in the changed files.
Cmux Architecture Rethink ✅ Passed PASS: The Swift changes add a narrowly owned restore-launch value type and a delegating overload; no new sleeps, polling, observer, or split-lifecycle wiring appears in production code.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes resume/token/session and test code; no standalone NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers registration was added or changed.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test/workflow/package files; none are local outputs, caches, or artifact directories.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Changed production files add restore-launch logic and DEBUG logs only; no new test-only accessor or wrapper seam was introduced.
Cmux No Ambient Global State ✅ Passed No new file-scope API, global var, or singleton was added; restore logic lives in constructable AgentRestoreLaunch and scoped extensions.
Title check ✅ Passed The title is clear and matches the main change: preserving agent notification hooks during restore/resume launches.
Description check ✅ Passed Mostly complete: it covers summary, root cause, fixes, tests, and scope, but omits the demo video, review-trigger block, and checklist.
✨ 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-9059-resume-notification-reliability

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.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_codex_wrapper_resume_hooks.py`:
- Around line 19-27: Extract the shared make_executable, read_lines, and expect
helpers from test_codex_wrapper_resume_hooks.py and test_claude_wrapper_hooks.py
into a small test-utils module, then import and reuse them in both test files.
Preserve the existing helper behavior, including the canonical read_lines
newline handling, and remove the duplicated local definitions.
🪄 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 Plus

Run ID: b13d377b-c557-4449-851d-802d649d99f3

📥 Commits

Reviewing files that changed from the base of the PR and between cdd84ba and a2233e5.

📒 Files selected for processing (8)
  • .github/workflows/ci.yml
  • CLI/cmux.swift
  • Resources/bin/cmux-claude-wrapper
  • Resources/bin/cmux-codex-wrapper
  • Sources/DockSplitStore+SessionRestore.swift
  • Sources/Workspace.swift
  • tests/test_claude_wrapper_hooks.py
  • tests/test_codex_wrapper_resume_hooks.py

Comment thread tests/test_codex_wrapper_resume_hooks.py
@cursor

cursor Bot commented Jul 28, 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.

@cursor

cursor Bot commented Jul 28, 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.

@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/SurfaceResumeCommandCanonicalizer`+PortableAgentExecutable.swift:
- Around line 96-128: Update applying(toStoredCommand:) so cmux-based commands
are recognized when the executable basename is cmux and the following recorded
argument is a supported agent launch kind such as teams, omc, or omx. Route
these commands through the same authorization flow instead of returning the
original cmux command, while preserving existing bare-agent and wrapper-token
behavior; if these launch kinds have a separate authorization path, explicitly
exclude them here and delegate accordingly.
🪄 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 Plus

Run ID: 205a6bc3-be04-44a8-bef9-9061abc2785e

📥 Commits

Reviewing files that changed from the base of the PR and between d5f5f3a and fe9033e.

📒 Files selected for processing (12)
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentRestoreLaunch.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentRestoreLaunchTests.swift
  • Resources/bin/cmux-claude-wrapper
  • Resources/bin/cmux-codex-wrapper
  • Sources/RestorableAgentSession.swift
  • Sources/SurfaceResumeCommandCanonicalizer+PortableAgentExecutable.swift
  • cmuxTests/ForkParentFallbackResidualTests.swift
  • cmuxTests/SessionPersistenceResumeBindingTests.swift
  • cmuxTests/SessionPersistenceTests.swift
  • tests/test_claude_wrapper_hooks.py
  • tests/test_claude_wrapper_mutual_shim_loop.py
  • tests/test_codex_wrapper_resume_hooks.py

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.

Notifications flaky for resumed agent sessions: blue unread ring sometimes never appears (Codex and others)

1 participant