Repository navigation
Fix intermittent warm workspace switch latency - #9244
Conversation
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
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:
📝 WalkthroughWalkthroughWorkspace switching now uses a transaction coordinator that waits for destination presentation and source retirement, protects destination renderers from reclamation, reports profiling milestones, and validates terminal/browser readiness with tests. ChangesWorkspace switch coordination
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant TabManager
participant WorkspaceSwitchCoordinator
participant RendererRealizationController
participant ContentView
TabManager->>WorkspaceSwitchCoordinator: Begin workspace selection
WorkspaceSwitchCoordinator->>RendererRealizationController: Protect destination surface
ContentView->>WorkspaceSwitchCoordinator: Report portal, frame, and interaction readiness
WorkspaceSwitchCoordinator->>RendererRealizationController: Release presentation protection
ContentView->>WorkspaceSwitchCoordinator: Report source retirement
WorkspaceSwitchCoordinator-->>ContentView: Complete workspace handoff
Possibly related PRs
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 1 warning)
✅ Passed checks (22 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 14-80: Add behavior-level tests that drive the actual
WorkspaceSwitchCoordinator lifecycle from selectionWillCommit through
beginPresentation, readiness milestones, and sourceDidRetire, rather than
testing Readiness or RendererRealizationPlanner directly. Verify readiness
remains false until all required milestones arrive, active clears only after
sourceDidRetire, and renderer leases are released exactly once on portal
presentation, cancel(), targetSurfaceID replacement, and rapid reselection. Use
the injectable RendererRealizationController dependency instead of the shared
singleton so lease assertions remain isolated.
In `@Sources/ContentView.swift`:
- Around line 3704-3716: Extract the shared terminal interaction-readiness
predicate from selectedWorkspaceInputIsReady and
workspaceSwitchPresentationTarget into a single helper, preserving the
hosted-view-first-responder check followed by the text-box input view fallback.
Update both call sites to use that helper so snapshot and notification-driven
readiness remain consistent.
- Line 3664: Update the presentation setup around firstFramePresented so it is
scoped to the current presentation rather than using the lifetime-based
terminalHasRenderedFrame result. Capture and compare the frame signal at
beginPresentation, or initialize firstFramePresented to false and let
ghosttyDidRenderFrame set it after the destination renders, ensuring
WorkspaceSwitchCoordinator waits for a real destination frame.
In `@Sources/WorkspaceSwitchCoordinator.swift`:
- Around line 95-97: Remove the readinessForTesting computed property from
WorkspaceSwitchCoordinator. Do not replace it with another test-only accessor;
tests should access the existing active state through `@testable` import if
needed.
- Around line 36-46: Update the workspace-switch coordinator’s readiness flow
around Readiness and renderTarget so key-window state is handled dynamically
rather than captured in immutable requiresInteraction. Observe
NSWindow.didResignKeyNotification directly in the coordinator and clear or
bypass the interaction gate for the affected target when its window resigns key,
allowing active to become ready and source portalRenderingEnabled to be
released.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 52fc980c-fe88-43cd-9215-25b08be6e707
📒 Files selected for processing (10)
Sources/App/CmuxMainWindow.swiftSources/App/MainWindowVisibilityController.swiftSources/App/RendererRealizationController.swiftSources/App/RendererRealizationPlanner.swiftSources/ContentView.swiftSources/TabManager.swiftSources/Workspace.swiftSources/WorkspaceSwitchCoordinator.swiftcmux.xcodeproj/project.pbxprojcmuxTests/WorkspaceSwitchCoordinatorTests.swift
💤 Files with no reviewable changes (1)
- Sources/Workspace.swift
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
cmuxTests/WorkspaceSwitchCoordinatorTests.swift (1)
145-165: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAssert that the eligible warm renderer is reclaimed.
Line 165 passes if the planner returns an empty set, so this does not verify normal reclamation continues while the protected target is excluded. Assert the complete result includes
warmSurfaceIDand excludesswitchTargetID.As per coding guidelines, “When a reported bug was missed by tests, add behavior-level coverage for the exact reproduction path.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift` around lines 145 - 165, Update the test around RendererRealizationPlanner.selectedSurfaceIds to assert the complete selected set: require warmSurfaceID to be included and switchTargetID to be excluded. Preserve the existing protected-target scenario while ensuring the test verifies eligible warm renderer reclamation rather than passing on an empty result.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 49-59: Update sourceRetirementDoesNotWaitForTerminalFocusTransfer
in WorkspaceSwitchCoordinatorTests so it no longer expects readiness when
requiresInteraction is true and interactionReady is false. Align the assertion
with the input-readiness contract and existing terminal readiness behavior, and
add behavior-level coverage for the reproduction path where source retirement
must wait until interactionReady is true; do not model focus transfer through
Readiness.
- Around line 93-97: Update WorkspaceSwitchCoordinator’s
noteFirstFrame(surfaceID:) flow to retain a matching frame notification received
before portalPresented, using pending transaction state as needed. When
noteTerminalPortalPresented(surfaceID:) marks the portal available, consume the
preserved frame so firstFramePresented and isReadyForSourceRetirement reflect
both events; keep the existing regression expectation unchanged.
---
Outside diff comments:
In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 145-165: Update the test around
RendererRealizationPlanner.selectedSurfaceIds to assert the complete selected
set: require warmSurfaceID to be included and switchTargetID to be excluded.
Preserve the existing protected-target scenario while ensuring the test verifies
eligible warm renderer reclamation rather than passing on an empty result.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2f25e2c3-d19a-4dd8-8609-238708b00e3b
📒 Files selected for processing (1)
cmuxTests/WorkspaceSwitchCoordinatorTests.swift
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/WorkspaceSwitchCoordinator.swift (1)
314-335: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winKey the retirement callbacks to
retiringWorkspaceId.
sourceWillRetire()/sourceDidRetire()currently only guard that “some” transaction is active. SinceselectionWillCommit()cancels and installs a new transaction, a late retirement callback for the previous source can mark the new active transaction as retired and complete it before its real source has retired. PassretiringWorkspaceIdfromcompleteWorkspaceHandoff()through these callbacks and return when the active transaction’ssourceWorkspaceIDdoes not match.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/WorkspaceSwitchCoordinator.swift` around lines 314 - 335, Update completeWorkspaceHandoff() and the sourceWillRetire()/sourceDidRetire() callbacks to accept and propagate retiringWorkspaceId. Before mutating the active transaction, require its sourceWorkspaceID to match retiringWorkspaceId; otherwise return without recording retirement or completing the transaction.Source: Coding guidelines
♻️ Duplicate comments (1)
cmuxTests/WorkspaceSwitchCoordinatorTests.swift (1)
231-233: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAssert that cancellation does not release the lease twice.
The assertion precedes
cancel(), so a duplicateendRendererProtectionduring cancellation still passes. Reassert after cancellation to preserve the exactly-once teardown contract.Proposed test addition
`#expect`(releasedRequestIDs == protectedRequestIDs) coordinator.cancel() + `#expect`(releasedRequestIDs == protectedRequestIDs)As per coding guidelines, “When a reported bug was missed by tests, add behavior-level coverage for the exact reproduction path.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift` around lines 231 - 233, Update the test around WorkspaceSwitchCoordinator cancellation to assert releasedRequestIDs equals protectedRequestIDs after coordinator.cancel(), not only before it. Preserve the pre-cancellation assertion if useful, and ensure the post-cancellation assertion verifies that cancellation does not invoke endRendererProtection twice.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/ContentView.swift`:
- Around line 2984-2989: Update the NSWindow.didResignKeyNotification handler in
the view’s onReceive registration to avoid subscribing globally when
observedWindow is nil; only attach or process the resign-key event when
observedWindow identifies the relevant window, preserving
noteInteractionNoLongerRequired for that window’s notification.
---
Outside diff comments:
In `@Sources/WorkspaceSwitchCoordinator.swift`:
- Around line 314-335: Update completeWorkspaceHandoff() and the
sourceWillRetire()/sourceDidRetire() callbacks to accept and propagate
retiringWorkspaceId. Before mutating the active transaction, require its
sourceWorkspaceID to match retiringWorkspaceId; otherwise return without
recording retirement or completing the transaction.
---
Duplicate comments:
In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 231-233: Update the test around WorkspaceSwitchCoordinator
cancellation to assert releasedRequestIDs equals protectedRequestIDs after
coordinator.cancel(), not only before it. Preserve the pre-cancellation
assertion if useful, and ensure the post-cancellation assertion verifies that
cancellation does not invoke endRendererProtection twice.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 732c5b95-34d8-4290-bbe4-04cf52142a7d
📒 Files selected for processing (8)
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Engine/Rendering/GhosttyMetalLayer.swiftSources/ContentView.swiftSources/GhosttyNotificationScrollRuntimeBridge.swiftSources/TabManager.swiftSources/Workspace.swiftSources/WorkspaceSwitchCoordinator.swiftcmux.xcodeproj/project.pbxprojcmuxTests/WorkspaceSwitchCoordinatorTests.swift
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmuxTests/WorkspaceSwitchCoordinatorTests.swift`:
- Around line 289-306: Update the test around
WorkspaceSwitchCoordinator.cancel() to start a replacement transaction by
invoking selectionWillCommit again before beginPresentation. Keep the passive
presentation setup unchanged, then assert isReadyForSourceRetirement to verify a
subsequent handoff is not blocked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: fe79d998-8471-4026-a8d8-ae3f997c1b8a
📒 Files selected for processing (5)
Sources/App/CmuxMainWindow.swiftSources/App/MainWindowVisibilityController.swiftSources/ProfilingSignposts.swiftSources/WorkspaceSwitchCoordinator.swiftcmuxTests/WorkspaceSwitchCoordinatorTests.swift
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Sources/WorkspaceSwitchCoordinator.swift (2)
204-211: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winKeep renderer protection until first-frame readiness.
These paths release the request-scoped renderer lease as soon as
portalPresentedis true, even whenfirstFramePresentedis still false.finishIfPossibleexplicitly waits for the first drawable frame, so the renderer can be reclaimed during that gap and recreate the intermittent presentation delay this transaction is meant to prevent. LetfinishIfPossiblerelease the lease only after visual readiness is complete.Also applies to: 231-244
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/WorkspaceSwitchCoordinator.swift` around lines 204 - 211, Update the portal presentation handling around the transaction branches and the corresponding path at the noted secondary location so renderer protection is not released merely when portalPresented is true. Remove or defer the releaseRendererProtection call and let finishIfPossible release the request-scoped lease only after firstFramePresented indicates visual readiness.
301-334: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winScope source-retirement callbacks to the active workspace request.
selectionWillCommit()callscancel()and then installs a new transaction, so an in-flight delayed source-retirement callback can currently mutate that new transaction:sourceDidRetire()setssourceRetired = trueandnoteInteractionNoLongerRequired()clearsrequiresInteractionand ends the interactive interval. Have these callbacks carry and validate the retiring source/target IDs, or make them no-ops once the current transaction has moved on.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/WorkspaceSwitchCoordinator.swift` around lines 301 - 334, Scope source-retirement callbacks to the transaction that initiated them: update sourceWillRetire, sourceDidRetire, and noteInteractionNoLongerRequired to carry or validate the retiring source/target workspace IDs against the active transaction, and return without mutation when they no longer match. Ensure stale callbacks cannot alter readiness, interactive intervals, portal-hide intervals, or sourceRetired on a newly installed transaction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Sources/WorkspaceSwitchCoordinator.swift`:
- Around line 204-211: Update the portal presentation handling around the
transaction branches and the corresponding path at the noted secondary location
so renderer protection is not released merely when portalPresented is true.
Remove or defer the releaseRendererProtection call and let finishIfPossible
release the request-scoped lease only after firstFramePresented indicates visual
readiness.
- Around line 301-334: Scope source-retirement callbacks to the transaction that
initiated them: update sourceWillRetire, sourceDidRetire, and
noteInteractionNoLongerRequired to carry or validate the retiring source/target
workspace IDs against the active transaction, and return without mutation when
they no longer match. Ensure stale callbacks cannot alter readiness, interactive
intervals, portal-hide intervals, or sourceRetired on a newly installed
transaction.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 01113c65-e1c0-4795-be60-ea9c1bd761e5
📒 Files selected for processing (4)
Sources/AppDelegate+WindowDock.swiftSources/AppDelegate.swiftSources/TabManager.swiftSources/WorkspaceSwitchCoordinator.swift
💤 Files with no reviewable changes (1)
- Sources/TabManager.swift
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
1 similar comment
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
All reported issues were addressed across 29 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
Review completed against the latest diff
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 30 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 30 files
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
|
CI status note: the only red check, |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
99c932a to
99c4884
Compare
4e1c41e Fix intermittent warm workspace switch latency (manaflow-ai#9244)
* Add workspace switch phase signposts * test: cover workspace switch readiness * Wait for workspace presentation readiness * test: cover workspace switch signal ordering * Fix workspace switch readiness signal ordering * Deduplicate workspace interaction readiness * Fix shared workspace switch signpost owner * Test canceled workspace switch handoff * Retire handoff without an active switch transaction * Cancel workspace switch at window teardown * Test portal readiness after layout * Signal workspace presentation from portal layout * Test source retirement independent of presentation * Make mount reconciliation own workspace handoff * Close workspace switch lifecycle review gaps * Make workspace switch leases self-expiring * Handle coalesced switch and portal rebind lifecycle * Close workspace switch review findings * Tighten workspace switch resource ownership * Split workspace switch state types * Use portal-owned presentation readiness * Scope portal readiness diagnostics * Avoid workspace lookup outside switch diagnostics * test: cover coalesced portal visibility edge * Fix coalesced terminal portal presentation edges * Scope workspace switch signposts to an owner * Move workspace switch signposts to their own file * test: cover destination pane focus changes * End switch interaction measurement at workspace boundary * fix: retire workspace handoff after focus pass * fix: bind handoff retirement to authoritative source * perf: scope workspace switch diagnostics to owners * fix: complete browser workspace readiness * perf: observe only active workspace portals * fix: retain scoped portal observers through handoff * fix: publish workspace switch completion * fix: retain renderer protection through portal presentation * fix: bound unresolved workspace switch handoff * fix: bound post-retirement workspace switches * fix: harden workspace switch presentation lifecycle * perf: avoid disabled switch signpost formatting * fix: scope workspace switch callbacks to their target * fix: capture workspace switch request before expiry scheduling * fix: restore workspace switch build under Swift 6 * fix: keep merged test target compiling * fix: remove duplicate exact session lookup * fix: preserve hibernation liveness in record initialization * fix: initialize hibernation process liveness explicitly * fix: close workspace switch lifecycle findings * test: keep cloud port regression target compiling * test: wire cloud machine resize action fixture
fbbe32a raised the mount lru to 4 (and 4 while cycling) so a switch back to a recent workspace was a visibility flip instead of a cold mount. that predates #9244 (warm switch latency fix, 2026-09-06), which fixed the switch lag at the source with readiness signals and dropped the cycle pair on main. the merge brought maxMountedWorkspacesDuringCycle back as an unused constant. 4 is not worth it: - every mounted workspace keeps portal rendering on (WorkspacePortalRenderingPlan -> setPortalRenderingEnabled(true)), so its terminal and browser portal views keep their geometry follow-ups and stay in the layer tree the window walks on every frame and typing update - each kept terminal keeps its surface render targets in memory, times 4 - the switch handoff is already covered: the retiring workspace stays pinned through mountedBackgroundWorkspaceLoadIds while the new one loads - WorkspaceMountPolicyTests.testDefaultPolicyMountsOnlySelectedWorkspace asserts the budget of 1 and fails at 4 so the file goes back to main byte for byte.
Summary
ws.switch.selection-commit,ws.switch.portal-show,ws.switch.portal-hide,ws.switch.renderer-realization,ws.switch.first-frame,ws.switch.interactive,ws.switch.window-focus, andws.switch.window-become-key.Root cause
The 0.64.20 handoff path split lifecycle ownership between model selection and presentation/focus callbacks. It retained the old workspace behind
hasLoadedTerminalSurface()—a native-pointer proxy rather than a presentation guarantee—or a 150 ms timeout. During rapid or delayed SwiftUI delivery, an earlier selection callback could also reconcile after the model had already advanced.That made mount ownership depend on timing: the destination was selected, but the old and new view graphs and portals could remain in a handoff state until a callback or timeout happened. PR #7231 deduplicated repeated portal-hide work and PR #7236 stopped hibernation presentation from restoring unrelated portals, but neither removed this split ownership.
The reported 0.64.20 build predates renderer reclamation, so reclamation is not claimed as the original cause. Current
maindoes reclaim renderers, however, so the switch transaction protects its destination through the authoritative mount transition.The new invariant is:
sourceDidRetireimmediately after reconciliation, even when the source workspace model was already deleted. A transaction mismatch cancels immediately, and a dead coordinator's weak owner makes any missed release ineligible for protection.No sleep, polling loop, loaded-pointer proxy, callback-owned mount decision, or unbounded protection lease remains.
Cold destinations
The live source workspace is intentionally not retained as a loading cover for an already-reclaimed destination. Doing so would recreate the bug's timing-dependent dual-mounted view graph. A cold destination may expose the normal window background during its measured
ws.switch.renderer-realization/ws.switch.first-frameinterval; warm destinations stay on the immediate path.The former cycle-hot 220 ms mount-pair timer and its now-dead two-workspace budget are removed; rapid cycling uses the same single authoritative mount path as every other switch.
The required tagged dogfood after CI should include an intentionally reclaimed destination and use these intervals to judge that visual interval. This PR does not claim an unmeasured p99 bound, and it does not trade the warm-switch ownership fix for another timer or callback-gated mount.
Diagnostics
The intervals use the existing
DynamicTracingSignposts/OSSignposterpath. Names and messages are not constructed unless a tracing tool is recording, and the phase names are readable directly in a Points of Interest trace.Terminal realization ends on an observed drawable rather than when a nonblocking rebuild request is accepted.
noteFirstFramereleases the frame observer and its notification-demand lease immediately, independent of source retirement, portal readiness, or interaction readiness; if no frame is produced, there is no per-frame notification work. The transaction owns at most one target-frame observer and one renderer lease per window; a later switch cancels and releases the previous transaction. The observer/demand pair also tears itself down on deallocation. Protected renderers are pinned inside, and count toward, the configured warm-renderer budget.Regression coverage
The commits preserve a failing-test-before-fix boundary:
1a69a65869adds the behavioral requirement that source retirement release switch-time renderer protection independently of presentation readiness.cfdaa39d10makes mount reconciliation authoritative, rejects stale callbacks, rebases coalesced selection sources, and moves renderer-lease release to source retirement.e919239040removes the dead live-source presentation path and adds request-scoped frame delivery and teardown coverage.307bca49d9makes protection self-expiring, reconciles authoritative stale deliveries, and covers presentation-target mismatch cancellation.76a24646aecovers coalesced round trips and portal rebind presentation.5f4b6931cbkeeps selection side effects unconditional, makes disabled tracing allocation-free, and removes the obsolete source-cover policy.8be3f7a6c0makes frame observation RAII-owned, counts protected renderers inside the warm budget, and removes dead cycle-hot timer state.f1a50c3e79splits the coordinator state model into one-type-per-file definitions.5564407b5bmakes the terminal portal registry the single source of presentation readiness.dacf39d15aadds the matching browser predicate and gates process-wide portal diagnostics to active, matching-window switches.The test-only full CI run is pinned to
1a69a65869: https://github.com/manaflow-ai/cmux/actions/runs/30599009307Additional focused coverage exercises portal/frame ordering, stale frame callbacks, reclaimed-renderer frame sequencing, rapid-switch cancellation, stale source callbacks, mismatch cancellation, owner expiration, deallocation cleanup, warm-budget accounting, and both idle and memory-pressure reclamation protection. I did not add a fake p99 UI assertion: intermittent SwiftUI/AppKit/Ghostty scheduling still requires a live trace, which is why permanent phase signposts are part of the fix.
Validation
swiftc -parsefor all changed Swift and test files./scripts/check-pbxproj.sh./scripts/lint-pbxproj-test-wiring.shpython3 scripts/check-test-determinism.pypython3 scripts/lint-stored-dispatch-work-items.pygit diff --checkNo local app build, local
xcodebuild test, or XCUITest was run, per task constraints. Currentmainno longer contains.github/swift-file-length-budget.tsvorscripts/swift_file_length_budget.py; the new coordinator and its test file both remain below 500 lines, and.github/swift-warning-budget.tsvis unchanged.Localization audit: no user-facing strings were added or changed, so
Resources/Localizable.xcstringsand web message catalogs are unchanged.Closes #9231
Closeout notes (2026-09-01)