Repository navigation
Fix Pi resume bindings so relaunch restores keep working - #12115
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThis 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. ChangesAgent resume and lifecycle reliability
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Severity of issue fixed: Medium Merge Risk: 🟡 Moderate · up to 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
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (10 errors, 2 warnings)
✅ Passed checks (13 passed)
Full details: Out of Scope Changes checkExplanation The PR includes changes unrelated to [ 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 CoverageExplanation 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 IsolationExplanation The production diff adds pure declarations inside the explicitly Resolution Mark the two value-only constants and Full details: Cmux Swift Blocking RuntimeExplanation The PR adds production timing-based synchronization. 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 Full details: Cmux Expensive Synchronous LoadExplanation The PR adds a synchronous hook-store filesystem scan to the 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 Full details: Cmux Algorithmic ComplexityExplanation The PR adds an unbounded directory scan to a file-watcher refresh path. 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 Full details: Cmux Swift `@Concurrent`Explanation The PR adds synchronous file-system work to a MainActor-isolated async refresh path. Resolution Keep the stateful refresh orchestration on Full details: Cmux Swift Package BoundariesExplanation The PR adds independently testable agent-liveness domain logic directly to the app target. Resolution Extract the pure liveness decision layer from the app target into a small SwiftPM target named Full details: Cmux Full InternationalizationExplanation The new CLI message uses the correct localized API and adds a matching catalog key, but Full details: Cmux Architecture RethinkExplanation The PR adds a production timing repair for a shared-state race. Resolution Make Full details: Cmux Source ArtifactsExplanation The diff changes the dependency submodule pointer at Full details: Cmux No Ambient Global StateExplanation The PR adds three static-only namespace types in production Swift. Resolution Replace
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
All contributors have signed the CLA ✍️ ✅ |
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
…-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
…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
|
Review audit rechecked against
|
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>
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
…#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>
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
surface resume set/getsequence aftersession_start; the shared hook publishes the binding, launch command, working directory, and environment.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.swiftbecause stale tests called the removedindexRefreshingNow(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.signalInterruptsReconnectBackoffPromptlySIGINT 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:
CMUX_*variables are scrubbed. Each child’sHOME,CFFIXED_USER_HOME, andXDG_CONFIG_HOMEnow 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
7a16326ce7fails the nativeCLIChildEnvironmentTestssuite on the fleet; fix commit175add8ecfpasses 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 (
d967a861tocb9bcfa8).b2108d6f46restores 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:
AgentNotificationMutationBoundaryTestsexec'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/shrefuses to exec, so the post-exec scope read could never seeCMUX_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, then304a67efa6to stay within the file-length budget). It now passes on that mini.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/Japanesecli.restore.admission.waitingmessage; 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;9c819ac6b0removes 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 onb2108d6f46and 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 (isolatedPI_CODING_AGENT_DIR), one prompt each, then three quit/relaunch cycles back to back:session.restore.agent.invalidatestartup-restore-cancelledEvery 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.refreshLiveSessionKeyswhen 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.ClaudeHookLifecycleCleanupTestsandClaudeBackgroundWorkNotifyTests: 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 bye75135a501):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/getpath onsession_start; the shared hook remains the publisher. Workspace, Dock, andcontrolSurfaceResumeSetnow reject same-session agent-hook writes that would demote a trustedauto_resumebinding to manual, and return the kept binding on the socket so older extensions cannot undo trust.downgradesTrustedAgentHookBindingencodes 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.CachedAgentProcessIdentityValidatorgains Pi argv rules andcurrentHookRecordvouching when Pi hides session id in argv.Restore admission: v2 errors expose
v2RetryableonCLIError; restore admission usesAgentRestoreAdmissionRetrywith a localized “waiting…” message on retryablebusywhile 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 intoWorkspace+AgentResumeOwnershipandDockSplitStore+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:CLIChildEnvironmentfor 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.