Skip to content

Fix Pi resume bindings so relaunch restores keep working - #12115

Merged
austinywang merged 51 commits into
mainfrom
issue-12084-pi-resume-binding
Sep 10, 2026
Merged

austinywang merged 51 commits into
mainfrom
issue-12084-pi-resume-binding

Conversation

@austinywang

@austinywang austinywang commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Pi panes could lose automatic resume immediately after startup and come back as bare shells on a later cmux relaunch. This change keeps the hook as the sole resume publisher, preserves trusted same-session bindings from already-loaded old extensions, and uses process identity to distinguish Pi TUI prompt marks from an agent exiting.

Fixes #12084.

Resulting behavior

  • The generated extension no longer sends the redundant public surface resume set/get sequence after session_start; the shared hook publishes the binding, launch command, working directory, and environment.
  • Workspace and Dock reject a same-session agent-hook downgrade atomically. The socket reports the retained binding, so an old extension loaded before upgrade cannot undo trust.
  • A shell-activity transition preserves a binding while its hook-recorded process generation, validated index entry, or session-identified foreground process establishes liveness. An unidentified replacement Pi process cannot vouch for the previous session.
  • Deferred restores retain ownership while the index is loading. The CLI retries structured retryable admission failures through the existing bounded policy and prints one localized waiting message.
  • Ownership scans use main’s shared indexForOwnershipDecision() contract from Admit Amp restores on a fresh scan instead of a quiet hook-store directory (#12158) #12166: fresh scan, owner PID/argv revalidation, and an atomic launch claim. The obsolete per-kind quiescence fingerprints and settle-delay helpers have been removed.

CI repair after merging main

All six app-host shards and the semantic integration job failed to compile RestoreAdmissionRetryTests.swift because stale tests called the removed indexRefreshingNow(relevantKinds:) overload. The tests now exercise the current ownership API under Pi and Claude hook-store churn. The unused settle suite and its focused CI selector are removed. Pi dispatch expectations now reflect the removed set/get writes while continuing to verify serialization and moved-surface cleanup.

The separate SSHPTYAttachRetryScriptBuilderTests.signalInterruptsReconnectBackoffPromptly SIGINT failure did not reproduce on a leased Mac: the complete 10-test suite passed twice, including all six signal cases. No timeout relaxation or unrelated SSH change is included.

Test fixtures repaired

The next full run compiled successfully but failed the focused Pi suite plus two app-host shards. The latest repair addresses the test fixtures:

  • Correct the Pi assertion: a completed empty index, with neither pending restore nor live-process evidence, retires an old automatic binding while preserving manual resume. Missing/inconclusive indexes and pending restores remain separately covered.
  • CodeRouter commands that must not connect now use the detached mock server. They no longer create an XCTest expectation that they deliberately never wait for.
  • CLI fixture isolation is decided from the app-host environment, before child CMUX_* variables are scrubbed. Each child’s HOME, CFFIXED_USER_HOME, and XDG_CONFIG_HOME now stay together instead of leaking into the shared app-host configuration. These fixture changes do not alter application deadlines or CI assertion tolerances.

Regression commit 7a16326ce7 fails the native CLIChildEnvironmentTests suite on the fleet; fix commit 175add8ecf passes the same 3-test suite. Both suites now pass on a fleet Mac and on CI.

A further merge audit found that the branch accidentally rolled Bonsplit back five commits from main (d967a861 to cb9bcfa8). b2108d6f46 restores main’s existing revision, retaining its tab hit-testing/native-drag fixes. There is now no submodule diff in this PR.

Test breakage on macOS 26 found while closing the loop

Two app-host lanes were red on main as well as here, for reasons unrelated to resume bindings:

  • AgentNotificationMutationBoundaryTests exec'd its delivery-target fixture into /bin/sh. Since macOS 26 the kernel returns no environment for an exec'd platform binary (/bin/sh, /bin/sleep) even to a same-user reader, and a copied /bin/sh refuses to exec, so the post-exec scope read could never see CMUX_SURFACE_ID. The test failed six of six runs on a Darwin 25.5 fleet mini and on every CI shard-6 run. Real agents are node or bun processes, whose environment stays readable, and so does the Xcode toolchain's python the fixture now execs into (d8268d1162, then 304a67efa6 to stay within the file-length budget). It now passes on that mini.
  • The Claude hook harnesses bounded each CLI invocation at five or ten seconds. Twice on CI a hook printed its verdict and exited 0 but was recorded as timed out; against a responsive mock the same hook finishes in about half a second, and it lingered on none of seven fleet runs. One shared 30-second bound now covers those invocations; the CLI's own client deadlines are far shorter, so a hung hook still fails (c72f79d148).

Trade-offs

Publication depends on the existing hook path. Already-loaded extensions are protected at the shared binding mutation boundary. Hook writes during a completed scan no longer require another quiet interval; the admission boundary checks current owner identity and serializes launches. Existing timeout/cancellation outcomes and fallback notices remain authoritative. This avoids reintroducing a polling path solely to satisfy obsolete tests.

Liveness helpers and the socket resume extension are extracted into focused files to meet the existing Swift file-length limits, with explicit project wiring. Neither budget TSV is modified.

Validation

Current HEAD: 9c819ac6b0. Includes current main (1281d43ec9) and the remote branch’s JSON assertion fix (4a63059eb5). The localization merge preserves every main entry plus the existing English/Japanese cli.restore.admission.waiting message; the determinism exception retains main’s wording. JSON assertion helpers and the SSH host harness were extracted without changing behavior so all Swift file-length limits pass. No budget TSV changed. The automatic merge had retained duplicate CodeRouter mock setup blocks; 9c819ac6b0 removes the duplicate and leaves that file identical to main.

Merge validation passed: localization structure (3 tests), Swift file-length limits, project normalization, test wiring, test determinism (0 findings), test sharding, and git diff --check. GitHub reports the PR mergeable. CI run 34424542439 and current-tag app-host verification are in progress. The most recent recorded dogfood below is on b2108d6f46 and predates this main merge.

Dogfood on the tagged Debug build of b2108d6f46, on a Mac running other Claude and Codex sessions that rewrite the shared hook directory every few seconds. Three real Pi 0.82 sessions (isolated PI_CODING_AGENT_DIR), one prompt each, then three quit/relaunch cycles back to back:

cycle Pi restores logged pane reads after the restore settled (30 s) showing a live Pi TUI with an automatic binding session.restore.agent.invalidate startup-restore-cancelled
1 16 18 of 18 0 0
2 16 20 of 21 (one read hit a socket timeout) 0 0
3 16 10 of 10 0 0

Every binding read in all three cycles was automatic; no pane was ever downgraded to manual and no relaunch restored a bare shell. Socket timeouts during the runs come from this Mac's unrelated main-thread stall in SessionIndexStore.refreshLiveSessionKeys when the hook directory reloads (captured by the app's own hang watchdog for every tagged build on the machine).

Fleet Mac (Darwin 25.5, Xcode 26.3), tree at 304a67efa6 (same test sources as HEAD):

  • AgentNotificationRegressionTests: 76 of 76, twice (0 of 6 before the fixture fix).
  • AgentNotificationRegressionTests, ClaudeHookLifecycleCleanupTests, ClaudeBackgroundWorkNotifyTests, ClaudeHookSurfaceResolutionSwiftTests: 101 tests, all passing with the shared bound.
  • ClaudeHookLifecycleCleanupTests and ClaudeBackgroundWorkNotifyTests: six consecutive loops, no lingering hook process.
  • SurfaceResumeAgentHookDowngradeTests, RestoredAgentShellActivityLivenessTests, RestoreAdmissionRetryPolicyTests, LiveAgentIndexRelevantChurnTests, SurfaceResumeRestoreClaimTests, CompletedRestoredAgentGenerationTests, AgentRestoreLiveOwnerAdmissionTests: passing on the reconciled tree.
  • CMUX_CLI_BIN=<mini CLI> python3 tests/test_pi_extension_dispatch.py: passed twice (on this overloaded Mac it only reaches 12 to 16 of the 34 expected Feed starts in five seconds; CI runs it on a runner).
  • CMUX_CLI_BIN=<tagged CLI> python3 tests/test_pi_extension_install.py: passed.

Repo guards on a02cf9c039 (unchanged by e75135a501): scripts/swift_file_length_budget.py (28 changed files), scripts/check-test-determinism.py (0 active findings), scripts/lint-pbxproj-test-wiring.sh, scripts/check-pbxproj.sh, git diff --check: all pass.

Localization audit: the only semantic catalog addition against main is cli.restore.admission.waiting, with English and Japanese values. Existing localized strings moved with the socket extension unchanged; no new shortcut or UI text.

🤖 Generated with Claude Code


Note

High Risk
Changes core agent restore, resume-binding mutation, and process-identity admission paths; mistakes could leave stale auto-resume or block legitimate restores.

Overview
Fixes Pi (and similar TUI) panes losing automatic resume after relaunch (#12084) by tightening who may publish resume bindings and how shell activity is interpreted.

Resume binding ownership: The Pi extension no longer runs the redundant surface resume set/get path on session_start; the shared hook remains the publisher. Workspace, Dock, and controlSurfaceResumeSet now reject same-session agent-hook writes that would demote a trusted auto_resume binding to manual, and return the kept binding on the socket so older extensions cannot undo trust. downgradesTrustedAgentHookBinding encodes that rule.

Agent still alive vs. shell idle: New shared RestoredAgentLiveness (hook PID generation, live index, foreground process) stops OSC 133 prompt/command flips from retiring bindings or clearing restored snapshots while the agent process is still running. Dock and workspace lifecycles call this before invalidating or marking completion. CachedAgentProcessIdentityValidator gains Pi argv rules and currentHookRecord vouching when Pi hides session id in argv.

Restore admission: v2 errors expose v2Retryable on CLIError; restore admission uses AgentRestoreAdmissionRetry with a localized “waiting…” message on retryable busy while ownership scans settle. Stale-binding logic skips bindings tied to deferred restores still awaiting admission.

Structure & CI: Surface resume control socket logic moves to TerminalController+SurfaceResumeBindings.swift; ownership helpers split into Workspace+AgentResumeOwnership and DockSplitStore+AgentResumeOwnership. CI adds a non-tolerant focused app-host step for downgrade, shell-activity liveness, admission retry, and live-index churn suites. Test-only fixes: CLIChildEnvironment for app-host isolation, safer JSON asserts in notify integration tests, macOS 26 exec fixture for notification boundary tests, and shared Claude hook process timeouts.

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

@vercel

vercel Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
cmux166 Ready Ready Preview Sep 10, 2026 1:16am UTC
cmux41 Ready Ready Preview Sep 10, 2026 1:16am UTC

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Review Change StackReview 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 2b94f84e-1ea8-4ee0-b5e8-7aa497df8c46

📥 Commits

Reviewing files that changed from the base of the PR and between 9302bf8 and 6fae4ed.

📒 Files selected for processing (2)
  • Resources/Localizable.xcstrings
  • cmux.xcodeproj/project.pbxproj

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

This change protects trusted Pi resume bindings, preserves live restored agents during shell-activity changes, settles scoped live-agent indexes, and retries retryable restore-admission responses. It adds regression tests, localization, documentation, and CI coverage.

Changes

Agent resume and lifecycle reliability

Layer / File(s) Summary
Trusted binding and process identity checks
CLI/CMUXCLI+PiExtensionSourcePart2.swift, Sources/SurfaceResumeBindingSnapshot+ManagedSessionIdentity.swift, Sources/Workspace.swift, Sources/DockSplitStore+SurfaceResume.swift, Sources/CachedAgentProcessIdentityValidator.swift, Sources/RestoredAgentForegroundProcess.swift, docs/agent-hooks.md, cmuxTests/SurfaceResumeAgentHookDowngradeTests.swift, tests/test_pi_extension_install.py
Pi startup removes the redundant resume-binding write. Same-session agent-hook writes cannot downgrade trusted bindings. Hook and foreground process validation can accept caller-vouched missing identities.
Restored-agent liveness and scoped index settling
Sources/RestoredAgentLiveness.swift, Sources/Workspace+AgentLifecycle.swift, Sources/DockSplitStore+RestoredAgentLifecycle.swift, Sources/SharedLiveAgentIndex.swift, Sources/TerminalController+AgentRestoreAdmission.swift, cmuxTests/RestoredAgentShellActivityLivenessTests.swift, cmuxTests/RestoreAdmissionRetryTests.swift
Workspace and dock lifecycle paths retain live restored agents. Deferred restores preserve ownership while relevant hook-store scans settle.
Admission retry and regression wiring
CLI/CLIError.swift, CLI/CMUXCLI+RestoreAdmission.swift, CLI/cmux.swift, Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentRestoreAdmissionRetry.swift, Resources/Localizable.xcstrings, cmux.xcodeproj/project.pbxproj, .github/workflows/ci.yml, cmuxTests/AgentNotificationMutationBoundaryTests.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileTerminalLaneCoordinatorTests.swift
Structured v2 errors carry retryability. Restore admission retries bounded retryable busy responses and reports waiting status. Tests, project wiring, localization, and CI cover the updated behavior.

Priority: ➖ Normal

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

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 6fae4

Pi resume reliability is improved, but unresolved delayed replay and test-settlement issues could still misdirect or repeat resume input and allow regressions to escape coverage. These risks should be resolved before merge.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as requireRestoreLaunchAdmission
  participant Retry as AgentRestoreAdmissionRetry
  participant CMUX as agent.restore.admit
  CLI->>Retry: Send admission request
  Retry->>CMUX: Call agent.restore.admit
  CMUX-->>Retry: Return retryable busy response
  Retry->>CMUX: Retry admission request
  CMUX-->>CLI: Return admitted response
Loading

Important

Pre-merge checks failed

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

❌ Failed checks (10 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error The production diff adds pure declarations inside the explicitly @MainActor SharedLiveAgentIndex: deferredRestoreSettleAttempts, deferredRestoreSettlePauseNanoseconds, and the state-free async… Mark the two value-only constants and settledIndex as nonisolated, or move them to a nonisolated file-scoped utility. Keep indexRefreshingNow MainActor-isolated because it accesses SharedLiveAgentIndex state. Add or retain explicit …
Cmux Swift Blocking Runtime ❌ Error The PR adds production timing-based synchronization. Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentRestoreAdmissionRetry.swift:26 defaults retries to Thread.sleep, and `CLI/CMUXCLI+R… Replace both timing waits with event-driven synchronization. Make restore-admission retry asynchronous or callback-driven and resume it from a cancellation-aware scheduler, timer abstraction, socket response, or ownership-state signal inste…
Cmux Expensive Synchronous Load ❌ Error The PR adds a synchronous hook-store filesystem scan to the @MainActor SharedLiveAgentIndex. In Sources/SharedLiveAgentIndex.swift, startIndexLoaderIfNeeded() now calls `Self.hookStoreFingerpr… Move hook-store fingerprint capture and comparison off the main actor. For example, capture only the directory and requested filenames on MainActor, then perform contentsOfDirectory and all stat calls in the detached loader, a backgroun…
Cmux Algorithmic Complexity ❌ Error The PR adds an unbounded directory scan to a file-watcher refresh path. Sources/SharedLiveAgentIndex.swift:578-579 calls contentsOfDirectory(atPath:) and filters every directory entry for `-hook-s… Keep hook-store filenames in a cached snapshot maintained by the directory watcher, or derive a bounded set from the registered agent kinds and pass the scoped restore kinds into the fingerprint capture. Do not enumerate and filter the whol…
Cmux Swift @Concurrent ❌ Error The PR adds synchronous file-system work to a MainActor-isolated async refresh path. SharedLiveAgentIndex is @MainActor; its new indexRefreshingNow(relevantKinds:) calls `hookStoresUnchangedSinc… Keep the stateful refresh orchestration on @MainActor, but move hook-store fingerprint capture and comparison into a nonisolated async helper annotated @concurrent. Make the MainActor refresh methods and loader setup await that helper b…
Cmux Swift Package Boundaries ❌ Error The PR adds independently testable agent-liveness domain logic directly to the app target. Sources/RestoredAgentLiveness.swift and Sources/RestoredAgentForegroundProcess.swift import only Foundati… Extract the pure liveness decision layer from the app target into a small SwiftPM target named CmuxAgentLiveness. The first public type should be RestoredAgentLiveness; expose the smallest value/protocol API needed for an agent snapshot…
Cmux Full Internationalization ❌ Error The new CLI message uses the correct localized API and adds a matching catalog key, but Resources/Localizable.xcstrings supports 20 locale codes while cli.restore.admission.waiting contains only `… Add a real translated stringUnit for cli.restore.admission.waiting in Resources/Localizable.xcstrings for all 18 missing locale codes. Do not use copied English, placeholders, or empty values.
Cmux Architecture Rethink ❌ Error The PR adds a production timing repair for a shared-state race. Sources/SharedLiveAgentIndex.swift adds settledIndex with up to five attempts and a 1.5-second Task.sleep, and both `Workspace.def… Make SharedLiveAgentIndex the single owner of restore-admission readiness. Add an explicit loader-generation or hook-store completion signal that records the relevant hook-store fingerprint with the loaded index, requeues refreshes from t…
Cmux Source Artifacts ❌ Error The diff changes the dependency submodule pointer at vendor/bonsplit from d967a8612e52827cfef8c9343394c98e30dc8801 to cb9bcfa8b841278ec952a7c9689387b1348b4aa1. This is the only changed path matc… Remove the unrelated vendor/bonsplit submodule pointer change from the PR, or provide and document a specific required build or test-system reason for the new revision.
Cmux No Ambient Global State ❌ Error The PR adds three static-only namespace types in production Swift. Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentRestoreAdmissionRetry.swift:10 declares a caseless public enum whose … Replace AgentRestoreAdmissionRetry with a constructable type that stores the delay schedule, sleep function, retry callback, and retry classifier, then expose an instance response(sending:) method. Construct it at the CLI/package seam a…
Out of Scope Changes check ⚠️ Warning The PR includes changes unrelated to [#12084], including notification-routing test updates in cmuxTests/AgentNotificationMutationBoundaryTests.swift, an iOS lane-test change in MobileTerminalLaneCoord… Remove the unrelated notification-routing, iOS lane-test, warning-budget, and test-compile changes, or move them into separate pull requests. Keep only changes required for Pi resume bindings and their directly supporting tests, documentati…
Docstring Coverage ⚠️ Warning Docstring coverage is 49.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 81 functions across 20 files. (2 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (13 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies issue [#12084] by removing the redundant Pi binding write and protecting trusted same-session agent-hook bindings from downgrade. The added liveness, restore-admission, and regression…
Cmux Browser Automation Off-Main ✅ Passed PASS: The pull-request patch is not a browser socket automation change. The inferred PR range (91718ec..9302bf8) does not modify Sources/TerminalController.swift or `Packages/macOS/CmuxControlSock…
Cmux Cache Substitution Correctness ✅ Passed No changed code replaces a fresh authoritative read with an unguarded cache in a persistence, history, undo, or snapshot path. The new scoped restore refresh still loads the index and checks current h…
Cmux No Hacky Sleeps ✅ Passed PASS. The PR introduces no hacky sleep in covered production TypeScript, JavaScript, shell, or build/runtime-script code. The only changed non-Swift runtime-like code is the test harness in `tests/tes…
Cmux Swift Concurrency ✅ Passed PASS. The PR does not introduce a listed Swift concurrency violation. The new settling code uses async/await and Task.sleep. No added production DispatchQueue, DispatchGroup, Combine state, comp…
Cmux Swiftpm Lockfiles ✅ Passed PASS. The effective PR diff changes .github/workflows/ci.yml and adds source/test entries to cmux.xcodeproj/project.pbxproj; it does not change any Package.swift, package-local `Package.resolved…
Cmux Swift Logging ✅ Passed The PR adds one production Swift output statement: FileHandle.standardError.write(...) in CLI/CMUXCLI+RestoreAdmission.swift. It emits a localized, user-facing wait message for cmux restore, whi…
Cmux User-Facing Error Privacy ✅ Passed The introduced user-facing copy is limited to the generic message restore: waiting for cmux to verify that this agent session is not already running… (English and Japanese). It names cmux and an age…
Cmux Swiftui State Layout ✅ Passed PASS — The PR does not introduce a SwiftUI state or layout pattern covered by the rule. The added Swift code contains no SwiftUI imports, views, ObservableObject/@published state, GeometryReader, lazy…
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS. The PR diff adds or changes agent-restore, lifecycle, CLI, and test code. It does not add or materially change an NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup. Added Swi…
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS. The production-source diff adds no test-build guard, test/debug-named member, or test-only state accessor. The new AgentRestoreAdmissionRetry.response is called by production `CLI/CMUXCLI+Rest…
Title check ✅ Passed The title clearly identifies the main change: preserving Pi resume bindings so relaunch restores continue to work.
Description check ✅ Passed The description thoroughly explains the problem, implementation, testing, validation, risks, and resulting behavior. It does not include the template's Demo Video, Review Trigger, or Checklist section…
Full details: Out of Scope Changes check

Explanation

The PR includes changes unrelated to [#12084], including notification-routing test updates in cmuxTests/AgentNotificationMutationBoundaryTests.swift, an iOS lane-test change in MobileTerminalLaneCoordinatorTests.swift, and described warning-budget and test-compile fixes copied from #12168.

Resolution

Remove the unrelated notification-routing, iOS lane-test, warning-budget, and test-compile changes, or move them into separate pull requests. Keep only changes required for Pi resume bindings and their directly supporting tests, documentation, localization, and CI coverage.

Full details: Docstring Coverage

Explanation

Docstring coverage is 49.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 81 functions across 20 files. (2 skipped: 2 unsupported.)

Full details: Cmux Swift Actor Isolation

Explanation

The production diff adds pure declarations inside the explicitly @MainActor SharedLiveAgentIndex: deferredRestoreSettleAttempts, deferredRestoreSettlePauseNanoseconds, and the state-free async settledIndex helper. They do not access actor state, but callers must still cross the MainActor boundary; the new non-MainActor tests call settledIndex with await. This introduces unnecessary implicit MainActor coupling covered by the repository actor-isolation rule. The instance refresh methods remain appropriately MainActor-isolated because they access shared mutable index state.

Resolution

Mark the two value-only constants and settledIndex as nonisolated, or move them to a nonisolated file-scoped utility. Keep indexRefreshingNow MainActor-isolated because it accesses SharedLiveAgentIndex state. Add or retain explicit actor annotations only at callers that access the shared index.

Full details: Cmux Swift Blocking Runtime

Explanation

The PR adds production timing-based synchronization. Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentRestoreAdmissionRetry.swift:26 defaults retries to Thread.sleep, and CLI/CMUXCLI+RestoreAdmission.swift:57-75 uses that helper for live agent.restore.admit requests. Sources/SharedLiveAgentIndex.swift:547 adds Task.sleep between production index-settle attempts, called by the Workspace and Dock deferred-restore paths. The rule explicitly fails Thread.sleep and Task.sleep in shipped runtime code, including retry backoff and readiness waits. These sites are additions relative to the PR base, not pre-existing behavior.

Resolution

Replace both timing waits with event-driven synchronization. Make restore-admission retry asynchronous or callback-driven and resume it from a cancellation-aware scheduler, timer abstraction, socket response, or ownership-state signal instead of blocking with Thread.sleep. Make index settling await the live-index loader completion or an explicit hook-store/index state transition, with cancellation and a bounded timeout owned by that scheduler or signal. Remove the production Task.sleep and Thread.sleep calls and retain delay injection only in test-only scaffolding.

Full details: Cmux Expensive Synchronous Load

Explanation

The PR adds a synchronous hook-store filesystem scan to the @MainActor SharedLiveAgentIndex. In Sources/SharedLiveAgentIndex.swift, startIndexLoaderIfNeeded() now calls Self.hookStoreFingerprints(...) before creating the existing Task.detached; the helper calls FileManager.contentsOfDirectory and runs stat for every *-hook-sessions.json file. The PR also calls the same synchronous helper from hookStoresUnchangedSinceCompletedLoad() while indexRefreshingNow() runs on the @MainActor. This is new relative to the PR base. The path is reachable from the socket handler agent.restore.admit through SharedLiveAgentIndex.shared.indexRefreshingNow(relevantKinds:), and from deferred restore handling. The actual indexLoader() remains detached, but the new metadata scan is not. This matches the rule's prohibited broad directory scan and per-file syscall on a main-actor/socket path.

Resolution

Move hook-store fingerprint capture and comparison off the main actor. For example, capture only the directory and requested filenames on MainActor, then perform contentsOfDirectory and all stat calls in the detached loader, a background actor, or a repository service, and return Sendable fingerprints with the load result. Make the settled-refresh comparison await that background operation. Keep MainActor work limited to updating cached state and launching or applying the restore.

Full details: Cmux Algorithmic Complexity

Explanation

The PR adds an unbounded directory scan to a file-watcher refresh path. Sources/SharedLiveAgentIndex.swift:578-579 calls contentsOfDirectory(atPath:) and filters every directory entry for -hook-sessions.json; startIndexLoaderIfNeeded invokes this scan at lines 948-952 before every index loader. The watcher routes hook-store events through handleHookStoreChange and startReload at lines 2544-2552, so each processed refresh rebuilds this collection and then stats every matching file. RestorableAgentKind also supports custom(String), so the file set is not an explicit tiny fixed-size collection. The PR introduces this behavior relative to the second parent. No benchmark or cached directory snapshot bounds the work.

Resolution

Keep hook-store filenames in a cached snapshot maintained by the directory watcher, or derive a bounded set from the registered agent kinds and pass the scoped restore kinds into the fingerprint capture. Do not enumerate and filter the whole hook-store directory in startIndexLoaderIfNeeded; fingerprint only the cached or requested filenames. Preserve missing-file detection and update the cache when watcher link, rename, or write events occur.

Full details: Cmux Swift `@Concurrent`

Explanation

The PR adds synchronous file-system work to a MainActor-isolated async refresh path. SharedLiveAgentIndex is @MainActor; its new indexRefreshingNow(relevantKinds:) calls hookStoresUnchangedSinceCompletedLoad, which calls hookStoreFingerprints and performs FileManager.default.contentsOfDirectory and repeated stat calls. The new startIndexLoaderIfNeeded also fingerprints the directory before starting its Task.detached loader. Workspace and Dock invoke the refresh from Task { @mainactor ... }, and the Terminal admission path awaits the same MainActor helper. No hop surrounds the new fingerprint operations. This matches the rule's file-I/O-heavy async work called from UI isolation without an explicit hop. The existing @concurrent Terminal methods are valid, and the new retry helper is synchronous CLI code, so those do not change this finding.

Resolution

Keep the stateful refresh orchestration on @MainActor, but move hook-store fingerprint capture and comparison into a nonisolated async helper annotated @concurrent. Make the MainActor refresh methods and loader setup await that helper before mutating actor state. Do not annotate SharedLiveAgentIndex methods that access actor-isolated state; add the explicit await boundary around the new file-system work instead.

Full details: Cmux Swift Package Boundaries

Explanation

The PR adds independently testable agent-liveness domain logic directly to the app target. Sources/RestoredAgentLiveness.swift and Sources/RestoredAgentForegroundProcess.swift import only Foundation, inject process probes, and implement the evidence-order and foreground identity decisions. Workspace and Dock both call RestoredAgentLiveness, and the new liveness tests exercise these helpers directly. The project file registers both files in the main app Sources phase. This matches the rule's app-target boundary failure condition. The retry runner is correctly placed in the CMUXAgentLaunch SwiftPM target, but that does not isolate the new liveness feature.

Resolution

Extract the pure liveness decision layer from the app target into a small SwiftPM target named CmuxAgentLiveness. The first public type should be RestoredAgentLiveness; expose the smallest value/protocol API needed for an agent snapshot, process evidence, foreground arguments, and identity validation. Move RestoredAgentForegroundProcess with that evaluator. Keep Workspace, DockSplitStore, SharedLiveAgentIndex.shared, OS process probes, and lifecycle composition in the app target as adapters. Move the direct evaluator tests to the package test target, then make the app target depend on the package.

Full details: Cmux Full Internationalization

Explanation

The new CLI message uses the correct localized API and adds a matching catalog key, but Resources/Localizable.xcstrings supports 20 locale codes while cli.restore.admission.waiting contains only en and ja. The missing locales are ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant. This violates the repository rule requiring translated entries for every existing locale in a touched catalog.

Full details: Cmux Architecture Rethink

Explanation

The PR adds a production timing repair for a shared-state race. Sources/SharedLiveAgentIndex.swift adds settledIndex with up to five attempts and a 1.5-second Task.sleep, and both Workspace.deferAgentResumeRestore and DockSplitStore.deferAgentResumeRestore use it while hook-store churn settles. Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentRestoreAdmissionRetry.swift also defaults to blocking Thread.sleep while retrying the admission RPC. These are not test-only waits. The changed comments explicitly describe waiting for hook-store/process-scan churn to subside. This matches the architectural rule's timing/blocking repair condition and leaves readiness dependent on elapsed time.

Resolution

Make SharedLiveAgentIndex the single owner of restore-admission readiness. Add an explicit loader-generation or hook-store completion signal that records the relevant hook-store fingerprint with the loaded index, requeues refreshes from the existing file-change event, and resumes deferred-restore continuations only when that generation is current. Make the admission RPC wait on that server-side completion state and return one structured result, so the CLI does not implement Thread.sleep retries. Remove settledIndex, the fixed 1.5-second pause, and the wall-clock retry schedule. Keep tests deterministic by injecting and advancing a fake completion signal.

Full details: Cmux Source Artifacts

Explanation

The diff changes the dependency submodule pointer at vendor/bonsplit from d967a8612e52827cfef8c9343394c98e30dc8801 to cb9bcfa8b841278ec952a7c9689387b1348b4aa1. This is the only changed path matching the rule's dependency-checkout condition. The PR changes agent restore behavior and does not change Bonsplit integration, project wiring, or identify a build, release, test-system, or other deliberate reason for this dependency update. The other changed paths are intentional source, test, config, localization, or documentation files.

Full details: Cmux No Ambient Global State

Explanation

The PR adds three static-only namespace types in production Swift. Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentRestoreAdmissionRetry.swift:10 declares a caseless public enum whose API is a static retry function plus a static schedule. Sources/RestoredAgentForegroundProcess.swift:13 declares a caseless enum containing only the static matches API at line 26. CLI/CMUXCLI+RestoreAdmission.swift:132 adds RestoreAdmissionRetryPolicy, a caseless nested enum whose only behavior is the static isRetryable and response methods at lines 134 and 142. Production callers use these surfaces at lines 57 and 146 of CMUXCLI+RestoreAdmission.swift and line 90 of RestoredAgentLiveness.swift. The diff confirms all three are new relative to the PR base. This matches the rule's explicit failure condition for caseless enums used as static-helper namespaces. RestoredAgentLiveness itself is constructable and injects process probes, so it is not included as a violation.

Resolution

Replace AgentRestoreAdmissionRetry with a constructable type that stores the delay schedule, sleep function, retry callback, and retry classifier, then expose an instance response(sending:) method. Construct it at the CLI/package seam and inject test closures. Replace RestoredAgentForegroundProcess with an instance type that stores the process-argument provider and identity validator, and inject it into RestoredAgentLiveness or construct it at the workspace/Dock seam. Replace RestoreAdmissionRetryPolicy with instance behavior on CMUXCLI or with a constructable, injectable policy type. Remove the static-only namespace enums and update the production call sites and tests.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-12084-pi-resume-binding

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.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

austinywang and others added 7 commits September 8, 2026 02:24
Fresh and restored Pi panes keep issuing the pre-fix extension's public
`surface resume set --source agent-hook` write until every running Pi process
has reloaded the refreshed extension. Cover the store-level rule that such a
write must not demote a trusted auto-resume binding for the same session.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
…ngrades

The Pi extension template no longer re-publishes its binding, but every Pi
process that already loaded the old extension keeps sending the public
`surface resume set --source agent-hook` write until it restarts, and the
on-disk extension is only refreshed once the new CLI sees a session-start.
That covers every pane restored on the first relaunch after updating: cmux
stored the `auto_resume`-less write as manual, and the following relaunch
restored a bare shell instead of Pi.

Reject a same-session agent-hook write that would demote a trusted binding,
atomically with the workspace and Dock store mutations, next to the existing
Codex provenance gate. The socket reports the binding the surface kept
instead of an empty-command failure, so the old extension's follow-up
verification and third-party tooling read the effective state.

Fixes #12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
Pi's interactive TUI emits OSC 133 prompt and command marks while the agent
keeps running, so cmux sees the pane flip between idle and busy on every
turn. Cover the restored-agent lifecycle rule that such a flip must not
demote a hook-published binding while the agent's process is verifiably
alive, in both the workspace and Dock stores, while an exited agent still
retires its binding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
Pi's interactive TUI emits OSC 133 prompt and command marks while the agent
keeps running, so cmux's shell-activity state flips between idle and busy on
every turn. The restored-agent lifecycle read the first busy flip after an
idle hook publish as an unrelated command replacing the agent, and the first
idle flip after a resumed launch as the agent exiting, and demoted the
binding to manual either way. Every fresh or restored Pi pane therefore lost
automatic resume within seconds, healing only when the next prompt-submit
hook re-published it, and the following relaunch restored a bare shell.

Before a shell-activity flip invalidates or retires an agent-hook binding,
require evidence that the agent is gone. Cheapest first: the hook-registered
`<kind>.<session>` PID whose process generation still matches, the live agent
index with revalidated process evidence, and the pane's foreground process
validated against the agent identity, which covers the window before the
session-start hook registers a PID. A live agent's busy flip now records the
observed running phase instead; an exited agent still retires its binding.

Refs #12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
The full sharded app-host run tolerates Swift Testing assertion failures, so
the new resume-binding suites would not turn CI red on their own. Run them on
the focused non-tolerant shard alongside the other regression guards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
After a relaunch the only process evidence for a restored binding is the
hook-recorded PID that died with the previous app instance. While the restore
is still deferred behind its ownership scan nothing owns the binding, so the
staleness reconciliation could retire it and the scan then cancelled the
launch. Cover the rule that a deferred restore keeps its binding, while an
exited agent without a pending restore still retires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
A restore deferred behind its ownership scan has not launched yet, so the
absence of a live process is expected rather than proof that the agent
exited. Treat a pending deferred restore for the same session as in-flight
ownership in the staleness check, so the scan can still admit the launch and
a relaunch restores the agent instead of a bare shell.

Refs #12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
austinywang and others added 9 commits September 8, 2026 05:22
…-binding

# Conflicts:
#	cmux.xcodeproj/project.pbxproj
Right after a relaunch several restored panes fire their session-start hooks
at once, so the app's ownership-sensitive process scan cannot settle for a
few seconds and answers `busy` with `retryable`. Cover the CLI retry budget
for that answer, the bounded settle loop deferred restores wait on, and the
pane-foreground fallback rejecting a contradicting Pi session identity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
Three things stopped the second and later relaunches from resuming Pi:

- `cmux restore` aborted on the app's `busy` admission answer. The
  ownership-sensitive process scan fails closed when hook stores keep
  changing, which is routine while restored panes fire their session-start
  hooks; the pane was left at a bare shell and its binding retired. The CLI
  now waits through a bounded, front-loaded retry budget when the answer is
  marked retryable, and says so once on stderr.
- Deferred restores cancelled every pending launch on the first unsettled
  scan. They now wait for a bounded number of settle attempts, pausing so the
  churn can subside instead of scanning back to back.
- Since #11358 a process must show its session id in argv or the
  environment to count as a session's process. Pi overwrites its argv with a
  bare title and nothing exports that variable for it, so a fresh Pi's own
  hook-recorded PID no longer proved liveness and the staleness reconciliation
  retired its binding. The hook record, and the pane's foreground process for
  that pane's binding, now vouch for a missing session identity while a
  contradicting one still rejects, restoring the pre-#11358 behavior for
  exactly those two callers.

Refs #12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
The CLI's sources are not compiled into the unit-test bundle, where `CMUXCLI`
names an unrelated alias, so the retry loop moves into CMUXAgentLaunch as a
generic runner with an injected error classifier. The CLI keeps its structured
`busy`/retryable classifier as a thin adapter, and the tests exercise the
shared runner directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
Every agent on a Mac writes its own hook store into the shared hook
directory, so whole-directory quiescence almost never holds on a busy
machine. Cover an ownership refresh that settles on the requested kind's
hook store alone: churn in another kind's store no longer fails it closed,
churn in the requested kind's store still does, and the filename and
fingerprint helpers behind that decision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
The ownership-sensitive refresh behind `cmux restore` admission and deferred
relaunch restores failed closed whenever anything in the shared hook
directory changed across two consecutive scans. Every agent on the Mac
writes its own `<kind>-hook-sessions.json` there on every hook, so on a
machine running a few Claude or Codex sessions the directory is never quiet
for the two scans, every deferred Pi restore was cancelled, and every
`cmux restore` answered busy. Dogfood on this Mac reproduced it: all three
restored Pi panes were cancelled 27 seconds after relaunch while nothing of
theirs had run.

Judge "settled" by evidence instead: capture each hook-store file's on-disk
fingerprint when a loader starts, and let a refresh scoped to the restored
kinds return as soon as the applied load reflects those kinds' current
files. Writes for other kinds neither force another pass nor fail closed;
a write for the restored kind still does. Admission scopes to the requested
kind and deferred restores to the kinds they still hold. Unscoped callers
keep the whole-directory rule.

Also parse `--session` for the built-in Pi kind when no registration is
attached, so a pane-foreground fallback rejects a Pi resumed into a
different session while a bare fresh Pi is still vouched for.

Refs #12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
`Darwin.stat(_:_:)` resolves to the `stat` struct initializer inside a
closure, which the unit bundle build rejected. The unqualified call picks
the syscall overload.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
@austinywang austinywang changed the title Fix Pi session-start downgrading automatic resume Fix Pi resume bindings so relaunch restores keep working Sep 8, 2026
@austinywang
austinywang marked this pull request as ready for review September 8, 2026 14:40
…dmission

#11976 stopped the Claude prompt-submit and pre-tool-use hooks from sending
`clear_notifications` themselves: they journal the turn start or state
change at the resolved target and the app's reconciler clears that pane.
The two moved-pane tests still asserted the old send, so main's focused
notification-routing shard has been red since that merge. Assert the
journal event's target instead, keep the sibling-pane and workspace-wide
negatives, and leave the SessionEnd test, whose send still exists, alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH
@austinywang

Copy link
Copy Markdown
Contributor Author

Review audit rechecked against 6c2fcdca67. Main’s #12166 now owns fresh-scan admission; obsolete quiescence code and its tests have been removed. Replies on each thread explain current dispositions.

Comment ID Author File:line (review location) Ask Disposition Commit
3959323545 CodeRabbit CachedAgentProcessIdentityValidator.swift:167 Require foreground session identity already-fixed: strict foreground identity 33b3cbd
3959323551 CodeRabbit SharedLiveAgentIndex.swift:547 Remove fixed settlement delay fix: deleted superseded delay helper 6c2fcdc
3959323556 CodeRabbit SharedLiveAgentIndex.swift:953 Avoid main-actor fingerprint I/O fix: deleted superseded fingerprints 6c2fcdc
3959323560 CodeRabbit Workspace+AgentLifecycle.swift:398 Share liveness evaluator already-fixed: both surfaces use RestoredAgentLiveness 9302bf8
3959435672 Cursor RestoredAgentLiveness.swift:63 Stale PID blocks foreground evidence already-fixed: explicit matching foreground identity works; bare replacement stays rejected 33b3cbd
3963266985 CodeRabbit RestoredAgentLiveness.swift:13 Make evaluator constructable already-fixed: injected probe instance 9302bf8
3963315677 CodeRabbit MobileTerminalLaneCoordinatorTests.swift:149 Await causal provider selection already-fixed on main; inherited unchanged 7a0ba63
3963490113 CodeRabbit MobileTerminalLaneCoordinatorTests.swift:149 Assert provider selection before cleanup already-fixed on main; inherited unchanged 7a0ba63
3963490123 CodeRabbit RestoredAgentLiveness.swift:88 Reject unidentified replacement Pi already-fixed with negative regression cases 33b3cbd
3963832771 Cursor CachedAgentProcessIdentityValidator.swift:210 Parse -s as session disagree: Pi 0.82.1 rejects -s; linked upstream parser in reply 6c2fcdc
3965928016 Cursor SharedLiveAgentIndex.swift:2511 Scoped refresh ignores kinds fix: remove orphaned API, test shared fresh-scan outcome 6c2fcdc
3965928028 Cursor Workspace+AgentLifecycle.swift:602 Deferred restore skips old settle loop fix: delete obsolete loop; keep main’s deadline/cancellation outcome 6c2fcdc

austinywang and others added 6 commits September 9, 2026 15:34
Since macOS 26 the kernel returns no environment for an exec'd platform
binary such as `/bin/sh` or `/bin/sleep`, even to a same-user reader, and a
copied `/bin/sh` refuses to exec at all. The delivery-target fixture exec'd
into `/bin/sh`, so its post-exec scope read could never see
CMUX_SURFACE_ID and the test failed on every Darwin 25 host (six of six
runs on a fleet mini, and the app-host shard on CI). Real agents are node or
bun processes, whose environment stays readable, and so does the Xcode
toolchain's python this fixture now execs into.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Claude hook harnesses bounded each CLI invocation at five or ten
seconds. A hook that prints its verdict and exits normally still needs a
few socket round-trips through a mock server scheduled on the test
process's global queues, and loaded CI runners twice took longer than that:
the process exited 0 with the expected output but was recorded as timed
out. The CLI's own non-actionable client deadlines are far shorter than 30
seconds, so a hook that genuinely hangs still fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The platform-binary note grew the file past its merge-base length, which
scripts/swift_file_length_budget.py rejects. Fold it into the two comment
lines the fixture already had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`set -m` puts the reporters the shell integration forks into their own
process groups, so killing the interactive shell's group leaves them briefly
alive and still writing into the scratch tree. `TemporaryDirectory` then
raced them on a shared CI runner and failed with ENOTEMPTY, taking
linux-preflight and every macOS lane with it. Remove the tree once the
reporters have stopped, and tolerate leftovers after five seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t suite

SSHConfiguredRemoteCommandHostTests is a Swift Testing suite that borrows a
hand-built CLINotifyProcessIntegrationRegressionTests instance for its mock
socket helpers. Its four `processSupport.wait(for:timeout:)` calls route a
timeout through XCTestCase issue recording, and that instance has no
testCaseRun, so XCTest raises NSInternalInconsistencyException and aborts the
whole app host. In CI run 34402005479 shard 1 that abort turned an ordinary
5-second mock-socket timeout into "unexpected" failures and a red shard.

Wait through a standalone XCTWaiter instead and report the timeout as a
Swift Testing expectation, so a slow CLI fails only this test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…own error

`jsonPayload(from:)` and `notificationRows(from:)` let `JSONSerialization`
throw when the CLI under test printed nothing. XCTest records that as a thrown
error, and `scripts/ci/classify-app-host-test-output.py` counts thrown errors
as "unexpected" failures, the same bucket as an app-host crash, so a single
slow CLI invocation turned the whole tolerant app-host shard red (CI runs
34402005479 and 34414657622, shard 1, via
testSSHPersistentPTYJSONResolvesSessionIDWhenWorkspaceCreateOmitsSurfaceID).

Parse with `try?` and let `XCTUnwrap` report the missing payload, so the test
still fails with the raw output in the message but as an ordinary failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@austinywang
austinywang merged commit 1614156 into main Sep 10, 2026
35 of 36 checks passed
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 10, 2026
7517376 Fix DOMRect crashes in browser.eval on macOS 15 (manaflow-ai#12237)
1614156 Fix Pi resume bindings so relaunch restores keep working (manaflow-ai#12115)
1281d43 release: unblock stable releases after manaflow-ai#11342 (reusable-workflow permissions guard, screenshot decoupling, notarization hardening) (manaflow-ai#12157)
65ac4c2 Cloud tree: flatten terminal tabs and surface Displays (manaflow-ai#12227)

# Conflicts:
#	.github/workflows/ci.yml
#	.github/workflows/ios-screenshots.yml
#	.github/workflows/release.yml
#	.github/workflows/test-depot.yml
aerickson pushed a commit to aerickson/cmux that referenced this pull request Sep 13, 2026
…#12115)

* test: cover Pi session-start resume binding

* fix: keep Pi session-start resume binding trusted

* test: cover same-session agent-hook resume binding downgrades

Fresh and restored Pi panes keep issuing the pre-fix extension's public
`surface resume set --source agent-hook` write until every running Pi process
has reloaded the refreshed extension. Cover the store-level rule that such a
write must not demote a trusted auto-resume binding for the same session.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* fix: keep trusted agent-hook resume bindings through same-session downgrades

The Pi extension template no longer re-publishes its binding, but every Pi
process that already loaded the old extension keeps sending the public
`surface resume set --source agent-hook` write until it restarts, and the
on-disk extension is only refreshed once the new CLI sees a session-start.
That covers every pane restored on the first relaunch after updating: cmux
stored the `auto_resume`-less write as manual, and the following relaunch
restored a bare shell instead of Pi.

Reject a same-session agent-hook write that would demote a trusted binding,
atomically with the workspace and Dock store mutations, next to the existing
Codex provenance gate. The socket reports the binding the surface kept
instead of an empty-command failure, so the old extension's follow-up
verification and third-party tooling read the effective state.

Fixes manaflow-ai#12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* test: cover TUI prompt marks retiring live agent resume bindings

Pi's interactive TUI emits OSC 133 prompt and command marks while the agent
keeps running, so cmux sees the pane flip between idle and busy on every
turn. Cover the restored-agent lifecycle rule that such a flip must not
demote a hook-published binding while the agent's process is verifiably
alive, in both the workspace and Dock stores, while an exited agent still
retires its binding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* fix: keep live agent resume bindings across TUI prompt marks

Pi's interactive TUI emits OSC 133 prompt and command marks while the agent
keeps running, so cmux's shell-activity state flips between idle and busy on
every turn. The restored-agent lifecycle read the first busy flip after an
idle hook publish as an unrelated command replacing the agent, and the first
idle flip after a resumed launch as the agent exiting, and demoted the
binding to manual either way. Every fresh or restored Pi pane therefore lost
automatic resume within seconds, healing only when the next prompt-submit
hook re-published it, and the following relaunch restored a bare shell.

Before a shell-activity flip invalidates or retires an agent-hook binding,
require evidence that the agent is gone. Cheapest first: the hook-registered
`<kind>.<session>` PID whose process generation still matches, the live agent
index with revalidated process evidence, and the pane's foreground process
validated against the agent identity, which covers the window before the
session-start hook registers a PID. A live agent's busy flip now records the
observed running phase instead; an exited agent still retires its binding.

Refs manaflow-ai#12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* ci: gate the agent resume binding regressions

The full sharded app-host run tolerates Swift Testing assertion failures, so
the new resume-binding suites would not turn CI red on their own. Run them on
the focused non-tolerant shard alongside the other regression guards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* test: cover deferred restores surviving staleness reconciliation

After a relaunch the only process evidence for a restored binding is the
hook-recorded PID that died with the previous app instance. While the restore
is still deferred behind its ownership scan nothing owns the binding, so the
staleness reconciliation could retire it and the scan then cancelled the
launch. Cover the rule that a deferred restore keeps its binding, while an
exited agent without a pending restore still retires.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* fix: keep deferred restores from being retired as stale

A restore deferred behind its ownership scan has not launched yet, so the
absence of a live process is expected rather than proof that the agent
exited. Treat a pending deferred restore for the same session as in-flight
ownership in the staleness check, so the scan can still admit the launch and
a relaunch restores the agent instead of a bare shell.

Refs manaflow-ai#12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* test: cover restore admission retries and settling deferred restores

Right after a relaunch several restored panes fire their session-start hooks
at once, so the app's ownership-sensitive process scan cannot settle for a
few seconds and answers `busy` with `retryable`. Cover the CLI retry budget
for that answer, the bounded settle loop deferred restores wait on, and the
pane-foreground fallback rejecting a contradicting Pi session identity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* fix: keep resumes of resumes working through restore-time hook storms

Three things stopped the second and later relaunches from resuming Pi:

- `cmux restore` aborted on the app's `busy` admission answer. The
  ownership-sensitive process scan fails closed when hook stores keep
  changing, which is routine while restored panes fire their session-start
  hooks; the pane was left at a bare shell and its binding retired. The CLI
  now waits through a bounded, front-loaded retry budget when the answer is
  marked retryable, and says so once on stderr.
- Deferred restores cancelled every pending launch on the first unsettled
  scan. They now wait for a bounded number of settle attempts, pausing so the
  churn can subside instead of scanning back to back.
- Since manaflow-ai#11358 a process must show its session id in argv or the
  environment to count as a session's process. Pi overwrites its argv with a
  bare title and nothing exports that variable for it, so a fresh Pi's own
  hook-recorded PID no longer proved liveness and the staleness reconciliation
  retired its binding. The hook record, and the pane's foreground process for
  that pane's binding, now vouch for a missing session identity while a
  contradicting one still rejects, restoring the pre-manaflow-ai#11358 behavior for
  exactly those two callers.

Refs manaflow-ai#12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* refactor: share the restore admission retry runner with the unit bundle

The CLI's sources are not compiled into the unit-test bundle, where `CMUXCLI`
names an unrelated alias, so the retry loop moves into CMUXAgentLaunch as a
generic runner with an injected error classifier. The CLI keeps its structured
`busy`/retryable classifier as a thin adapter, and the tests exercise the
shared runner directly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* test: cover restore refreshes scoped to the restored agent's hook store

Every agent on a Mac writes its own hook store into the shared hook
directory, so whole-directory quiescence almost never holds on a busy
machine. Cover an ownership refresh that settles on the requested kind's
hook store alone: churn in another kind's store no longer fails it closed,
churn in the requested kind's store still does, and the filename and
fingerprint helpers behind that decision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* fix: settle restore ownership on the restored agent's hook store alone

The ownership-sensitive refresh behind `cmux restore` admission and deferred
relaunch restores failed closed whenever anything in the shared hook
directory changed across two consecutive scans. Every agent on the Mac
writes its own `<kind>-hook-sessions.json` there on every hook, so on a
machine running a few Claude or Codex sessions the directory is never quiet
for the two scans, every deferred Pi restore was cancelled, and every
`cmux restore` answered busy. Dogfood on this Mac reproduced it: all three
restored Pi panes were cancelled 27 seconds after relaunch while nothing of
theirs had run.

Judge "settled" by evidence instead: capture each hook-store file's on-disk
fingerprint when a loader starts, and let a refresh scoped to the restored
kinds return as soon as the applied load reflects those kinds' current
files. Writes for other kinds neither force another pass nor fail closed;
a write for the restored kind still does. Admission scopes to the requested
kind and deferred restores to the kinds they still hold. Unscoped callers
keep the whole-directory rule.

Also parse `--session` for the built-in Pi kind when no registration is
attached, so a pane-foreground fallback rejects a Pi resumed into a
different session while a bare fresh Pi is still vouched for.

Refs manaflow-ai#12084

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* fix: call the stat syscall unqualified in hook-store fingerprinting

`Darwin.stat(_:_:)` resolves to the `stat` struct initializer inside a
closure, which the unit bundle build rejected. The unqualified call picks
the syscall overload.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* chore: normalize project file after merging main

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* test: align Claude hook pane-clear tests with semantic notification admission

manaflow-ai#11976 stopped the Claude prompt-submit and pre-tool-use hooks from sending
`clear_notifications` themselves: they journal the turn start or state
change at the resolved target and the app's reconciler clears that pane.
The two moved-pane tests still asserted the old send, so main's focused
notification-routing shard has been red since that merge. Assert the
journal event's target instead, keep the sibling-pane and workspace-wide
negatives, and leave the SessionEnd test, whose send still exists, alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* fix: bound foreground vouching to the session's recorded process

Review follow-ups on the restored-agent liveness evidence:

- A pane's foreground process that shows no session identity vouched for
  the binding unconditionally. Once the session has registered its own
  process, a different bare Pi in the same pane is another agent, so only
  that registered process may vouch without naming the session; an
  explicit `--session` still decides either way.
- The workspace and Dock lifecycles each re-implemented the same evidence
  order. `RestoredAgentLiveness` is now the one evaluator; each container
  supplies only its runtime table, workspace identity, and foreground pid.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* fix: let a dead recorded process stop bounding foreground vouching

The previous bound forwarded the session's recorded PID to the foreground
check unconditionally, so a dead PID left in the runtime table blocked
vouching for a Pi resumed in place (hibernation resume, manual `cmux
restore`) whose argv is already a bare title, and the first prompt mark
could retire it again. Bound bare vouching to the recorded process only
while that process still exists. A wrongly kept binding self-heals within a
second when a stranger's own session-start hook replaces it; a wrongly
retired one stays retired until the next prompt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* chore: mirror the warning-budget and test-compile fixes from manaflow-ai#12168

main's macOS lane is red on the Swift warning budget (five over-budget
warnings) and the CmuxTerminal test bundle's compile; manaflow-ai#12168 fixes both but
has not merged yet. Carry the same six source hunks here so this PR's CI can
pass on its own; they are identical to manaflow-ai#12168 and merge away when it lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EvVAUmXgi8CoNwXyrbCugH

* test: avoid exec race in PID routing regression

* test: remove nondeterministic lane assertion sleep

* review: make liveness evaluator constructable and settle lane test

* test: reflect inconclusive Pi session index semantics

* ci: allow injected fake retry sleeper in determinism gate

* test: reject unidentified replacement Pi processes

* fix: require session identity for foreground Pi liveness

* fix: reconcile Pi restore coverage with fresh-scan admission

* test: cover scrubbed CLI child environment isolation

* fix: isolate CLI fixtures and correct resume test expectations

* fix: retain main Bonsplit revision after merge

* test: exec the PID-routing fixture into a non-platform image

Since macOS 26 the kernel returns no environment for an exec'd platform
binary such as `/bin/sh` or `/bin/sleep`, even to a same-user reader, and a
copied `/bin/sh` refuses to exec at all. The delivery-target fixture exec'd
into `/bin/sh`, so its post-exec scope read could never see
CMUX_SURFACE_ID and the test failed on every Darwin 25 host (six of six
runs on a fleet mini, and the app-host shard on CI). Real agents are node or
bun processes, whose environment stays readable, and so does the Xcode
toolchain's python this fixture now execs into.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: give hook CLI invocations one 30-second wall bound

The Claude hook harnesses bounded each CLI invocation at five or ten
seconds. A hook that prints its verdict and exits normally still needs a
few socket round-trips through a mock server scheduled on the test
process's global queues, and loaded CI runners twice took longer than that:
the process exited 0 with the expected output but was recorded as timed
out. The CLI's own non-actionable client deadlines are far shorter than 30
seconds, so a hook that genuinely hangs still fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: keep the routing fixture within its file-length budget

The platform-binary note grew the file past its merge-base length, which
scripts/swift_file_length_budget.py rejects. Fold it into the two comment
lines the fixture already had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: let the bash-integration guard outlive its background reporters

`set -m` puts the reporters the shell integration forks into their own
process groups, so killing the interactive shell's group leaves them briefly
alive and still writing into the scratch tree. `TemporaryDirectory` then
raced them on a shared CI runner and failed with ENOTEMPTY, taking
linux-preflight and every macOS lane with it. Remove the tree once the
reporters have stopped, and tolerate leftovers after five seconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: stop XCTest wait timeouts from crashing the app host in SSH host suite

SSHConfiguredRemoteCommandHostTests is a Swift Testing suite that borrows a
hand-built CLINotifyProcessIntegrationRegressionTests instance for its mock
socket helpers. Its four `processSupport.wait(for:timeout:)` calls route a
timeout through XCTestCase issue recording, and that instance has no
testCaseRun, so XCTest raises NSInternalInconsistencyException and aborts the
whole app host. In CI run 34402005479 shard 1 that abort turned an ordinary
5-second mock-socket timeout into "unexpected" failures and a red shard.

Wait through a standalone XCTWaiter instead and report the timeout as a
Swift Testing expectation, so a slow CLI fails only this test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: report empty CLI JSON output as an assertion failure, not a thrown error

`jsonPayload(from:)` and `notificationRows(from:)` let `JSONSerialization`
throw when the CLI under test printed nothing. XCTest records that as a thrown
error, and `scripts/ci/classify-app-host-test-output.py` counts thrown errors
as "unexpected" failures, the same bucket as an app-host crash, so a single
slow CLI invocation turned the whole tolerant app-host shard red (CI runs
34402005479 and 34414657622, shard 1, via
testSSHPersistentPTYJSONResolvesSessionIDWhenWorkspaceCreateOmitsSurfaceID).

Parse with `try?` and let `XCTUnwrap` report the missing payload, so the test
still fails with the raw output in the message but as an ordinary failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor: keep merged test helpers within file budgets

* fix: remove duplicate CodeRouter fixture from main merge

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview – cmux41 — 9c819ac6 Deployed Sep 10, 2026 by vercel[bot]
Preview – cmux166 — 9c819ac6 Deployed Sep 10, 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.

Pi hook: extension's second surface resume set downgrades the resume binding to manual until the user submits a prompt

1 participant