fix(cloud): make Cloud workspace reconciliation always settle - #16158
Conversation
…thing Reconcile reprojects every missing placement through SurfaceCatalog.project. When the reused pane already carries that placement, attachRemoteView still removes and reinserts it, bumps the projection revision twice, and requests the next reconcile of the same machine. Any disagreement between the plan and project() then becomes a main-actor livelock, which is how nightly b36a9b3 spun at 98% CPU and grew to tens of GB (fixed at the plan level by #16025). Fails on main: projectionVersions advances by 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
attachRemoteView rewrote a reused projection even when its remote workspace and tab were already the requested ones: it removed and reinserted it (clearing and resetting the panel directory, rerunning sidebar git probes, bumping the guest routing revision twice) and requested another reconcile of the machine. Since reconcile itself reprojects through project(), any plan that reports a shown pane as missing became an endless main-actor loop. Return early when the coordinates are unchanged, and apply a real change as one projections assignment so observers never see the pane unprojected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same guard as attachRemoteView for setRemotePlacement: skip views whose coordinates already match, and apply real changes as one projections assignment. Unchanged placements no longer bump the projection revision or post a catalog change that wakes the device layout coordinator. The test now states its fixture precondition explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
All contributors have signed the CLA ✍️ ✅ |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Important Review skippedReview was skipped as selected files did not have any reviewable changes. ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review. 📝 WalkthroughWalkthroughRemote placement updates skip projections whose coordinates already match the requested placement. Reconciliation now limits repeated passes for a graph state and reports non-convergence. Tests cover projection reuse, pass limits, and graph-state changes. Two Actions discovery titles are added to the localization omission list. ChangesRemote projection reconciliation
Localization omission entries
Priority: ⬆️ High Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to The change avoids redundant placement updates and bounds repeated reconciliation. No actionable merge-blocking risk is identified; merge after normal checks pass. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change bounds repeated work and avoids transient pane removal without demonstrating increased access or permissions. Remaining uncertainty concerns the new diagnostic data and exposure to repeated external events. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 2 warnings)
✅ Passed checks (22 passed)
Full details: Description checkExplanation The description explains the problem, implementation, regression tests, and changelog entry. It does not follow the required template because it omits the Summary, Testing, Demo Video, and Checklist sections, and it states that some test results are still pending. Resolution Restructure the description using the repository template. Add explicit Summary and Testing sections, report the final results for all listed test runs, include a Demo Video or explain why it is not applicable, and complete the Checklist items. State any remaining verification limits clearly. Full details: Docstring CoverageExplanation Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 3 files. (1 skipped: 1 unsupported.) Full details: Cmux Swift Package BoundariesExplanation The diff adds Resolution Extract ✨ 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 |
|
Dogfood build of cmux DEV pr-16158-3f270680.app The link opens this exact commit in the cmux dev menu bar app; the page waits until the build is ready. Builds run only while this PR has the Dogfood tours of
|
…ther A consumer that requests another reconcile without changing the accepted graph keeps CloudWorkspaceProjectionCoordinator's loop running forever on the main actor, which is how nightly b36a9b3 hung at 100% CPU and grew to tens of GB. Fails on main: the loop runs until the test stub stops asking (1000 passes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CloudWorkspaceProjectionCoordinator re-ran reconcile while anything kept requesting it, with no progress check. Any consumer that asks for another pass without changing the graph (attachRemoteView before this PR, a plan that reports a shown pane as missing in #16025) held the main actor forever: nightly b36a9b3 pinned a core, grew to tens of GB, and could not even run its updater. Count passes over the same accepted CloudVMState. A converging graph needs two or three; after eight, stop, report a Sentry warning, and wait for the next graph or request, which starts a new count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. |
…graph Review of the previous bound: counting every pass over an unchanged graph could stop a reconcile that was still making progress (a staggered restore of several bound workspaces re-requests the same graph), stranding panes until the next graph. CloudWorkspaceReconcileBudget now stops after three consecutive passes that start from the same graph, projection revision and bindings (a pass that changed nothing cannot make the next one different), with a hard ceiling of 64 passes per graph for a loop that rewrites projections every pass, as nightly b36a9b3 did. Non-convergence is reported once per graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: keying the dedupe on the full CloudVMState retained a whole graph per machine for the process lifetime (cancel never cleared it) and still reported once per revision. Key on the cursor generation, include generation and revision in the event, and clear it when the machine is cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same change as #16175: main's localization parity check fails on actions.discovery.menuTitle and dialogTitle (fr is identical to English), which blocks this PR's static preflight and every gate behind it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failure attributionCI failed on
Matched log linesNot re-run automatically: Written by |
|
Merge receipt for
Labeled |
* test(cloud): reusing a projection at its current placement changes nothing Reconcile reprojects every missing placement through SurfaceCatalog.project. When the reused pane already carries that placement, attachRemoteView still removes and reinserts it, bumps the projection revision twice, and requests the next reconcile of the same machine. Any disagreement between the plan and project() then becomes a main-actor livelock, which is how nightly b36a9b3 spun at 98% CPU and grew to tens of GB (fixed at the plan level by #16025). Fails on main: projectionVersions advances by 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): reattaching a projection's current placement is a no-op attachRemoteView rewrote a reused projection even when its remote workspace and tab were already the requested ones: it removed and reinserted it (clearing and resetting the panel directory, rerunning sidebar git probes, bumping the guest routing revision twice) and requested another reconcile of the machine. Since reconcile itself reprojects through project(), any plan that reports a shown pane as missing became an endless main-actor loop. Return early when the coordinates are unchanged, and apply a real change as one projections assignment so observers never see the pane unprojected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): setting a projection's current remote placement is a no-op Same guard as attachRemoteView for setRemotePlacement: skip views whose coordinates already match, and apply real changes as one projections assignment. Unchanged placements no longer bump the projection revision or post a catalog change that wakes the device layout coordinator. The test now states its fixture precondition explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): reconciling one graph stops when every pass requests another A consumer that requests another reconcile without changing the accepted graph keeps CloudWorkspaceProjectionCoordinator's loop running forever on the main actor, which is how nightly b36a9b3 hung at 100% CPU and grew to tens of GB. Fails on main: the loop runs until the test stub stops asking (1000 passes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): bound reconciliation passes over one accepted graph CloudWorkspaceProjectionCoordinator re-ran reconcile while anything kept requesting it, with no progress check. Any consumer that asks for another pass without changing the graph (attachRemoteView before this PR, a plan that reports a shown pane as missing in #16025) held the main actor forever: nightly b36a9b3 pinned a core, grew to tens of GB, and could not even run its updater. Count passes over the same accepted CloudVMState. A converging graph needs two or three; after eight, stop, report a Sentry warning, and wait for the next graph or request, which starts a new count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): bound reconciliation by progress, not by passes over one graph Review of the previous bound: counting every pass over an unchanged graph could stop a reconcile that was still making progress (a staggered restore of several bound workspaces re-requests the same graph), stranding panes until the next graph. CloudWorkspaceReconcileBudget now stops after three consecutive passes that start from the same graph, projection revision and bindings (a pass that changed nothing cannot make the next one different), with a hard ceiling of 64 passes per graph for a loop that rewrites projections every pass, as nightly b36a9b3 did. Non-convergence is reported once per graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): report projection non-convergence once per daemon generation Review: keying the dedupe on the full CloudVMState retained a whole graph per machine for the process lifetime (cancel never cleared it) and still reported once per revision. Key on the cursor generation, include generation and revision in the event, and clear it when the machine is cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(l10n): document the French Actions discovery titles as invariant Same change as #16175: main's localization parity check fails on actions.discovery.menuTitle and dialogTitle (fr is identical to English), which blocks this PR's static preflight and every gate behind it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
488eaf7 fix(cloud): make Cloud workspace reconciliation always settle (manaflow-ai#16158) 02b80fe ci: cmux-tui artifact publishing runs in its own artifacts environment (manaflow-ai#16267) 71bb553 ci: propagate settled fast-guard failures (manaflow-ai#16258) 3121d49 ci: fold Testbox guard checks into fast guard lane (manaflow-ai#16247) # Conflicts: # .github/workflows/ci-guards.yml # .github/workflows/cmux-tui-artifacts.yml # .github/workflows/testbox-broker-guard.yml
…ope) (#16260) * fix: share OpenCodePaths with the CLI through CMUXAgentLaunch #16229 made CLI/cmux.swift call OpenCodePaths, but the enum lived in Sources/SessionIndexModels.swift, which only the app target compiles, so the CLI target fails with "cannot find 'OpenCodePaths' in scope". Move the unchanged path logic into CMUXAgentLaunch, which the app, the CLI and cmuxTests already import, and make its two entry points public. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Pass the temporary-config flag to the Codex provider override parser #16201 made providerOverrides(from:) skip provider entries when the caller uses a temporary CODEX_HOME, but read `usesTemporaryConfig`, a parameter of build(configToml:usesTemporaryConfig:) that is not in scope there, so the CLI no longer compiles. Pass the flag through. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test: match temporary Codex config argument scope * Make OpenCodePaths a value type to satisfy package conventions Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci: cmux-tui artifact publishing runs in its own artifacts environment (#16267) * test(ci): cmux-tui artifact publishing must run in the artifacts environment #16171 put the cmux-tui publish job in the release environment, whose policy allows only main and v* tags, so helper-branch pin publishes fail before any step runs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ci): cmux-tui artifact publishing runs in the artifacts environment The artifacts environment holds only the R2 upload credentials and allows main, feat-cmux-next and cmux-tui-pin-* helper branches, so daemon pin publishes work again while signing, Sparkle, Homebrew and Apple secrets stay in release (main and v* tags only). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): make Cloud workspace reconciliation always settle (#16158) * test(cloud): reusing a projection at its current placement changes nothing Reconcile reprojects every missing placement through SurfaceCatalog.project. When the reused pane already carries that placement, attachRemoteView still removes and reinserts it, bumps the projection revision twice, and requests the next reconcile of the same machine. Any disagreement between the plan and project() then becomes a main-actor livelock, which is how nightly b36a9b3 spun at 98% CPU and grew to tens of GB (fixed at the plan level by #16025). Fails on main: projectionVersions advances by 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): reattaching a projection's current placement is a no-op attachRemoteView rewrote a reused projection even when its remote workspace and tab were already the requested ones: it removed and reinserted it (clearing and resetting the panel directory, rerunning sidebar git probes, bumping the guest routing revision twice) and requested another reconcile of the machine. Since reconcile itself reprojects through project(), any plan that reports a shown pane as missing became an endless main-actor loop. Return early when the coordinates are unchanged, and apply a real change as one projections assignment so observers never see the pane unprojected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): setting a projection's current remote placement is a no-op Same guard as attachRemoteView for setRemotePlacement: skip views whose coordinates already match, and apply real changes as one projections assignment. Unchanged placements no longer bump the projection revision or post a catalog change that wakes the device layout coordinator. The test now states its fixture precondition explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): reconciling one graph stops when every pass requests another A consumer that requests another reconcile without changing the accepted graph keeps CloudWorkspaceProjectionCoordinator's loop running forever on the main actor, which is how nightly b36a9b3 hung at 100% CPU and grew to tens of GB. Fails on main: the loop runs until the test stub stops asking (1000 passes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): bound reconciliation passes over one accepted graph CloudWorkspaceProjectionCoordinator re-ran reconcile while anything kept requesting it, with no progress check. Any consumer that asks for another pass without changing the graph (attachRemoteView before this PR, a plan that reports a shown pane as missing in #16025) held the main actor forever: nightly b36a9b3 pinned a core, grew to tens of GB, and could not even run its updater. Count passes over the same accepted CloudVMState. A converging graph needs two or three; after eight, stop, report a Sentry warning, and wait for the next graph or request, which starts a new count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): bound reconciliation by progress, not by passes over one graph Review of the previous bound: counting every pass over an unchanged graph could stop a reconcile that was still making progress (a staggered restore of several bound workspaces re-requests the same graph), stranding panes until the next graph. CloudWorkspaceReconcileBudget now stops after three consecutive passes that start from the same graph, projection revision and bindings (a pass that changed nothing cannot make the next one different), with a hard ceiling of 64 passes per graph for a loop that rewrites projections every pass, as nightly b36a9b3 did. Non-convergence is reported once per graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): report projection non-convergence once per daemon generation Review: keying the dedupe on the full CloudVMState retained a whole graph per machine for the process lifetime (cancel never cleared it) and still reported once per revision. Key on the cursor generation, include generation and revision in the event, and clear it when the machine is cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(l10n): document the French Actions discovery titles as invariant Same change as #16175: main's localization parity check fails on actions.discovery.menuTitle and dialogTitle (fr is identical to English), which blocks this PR's static preflight and every gate behind it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(tests): name the app's window-chrome sidebar options explicitly #11539 reverted #14991's qualification in SidebarWidthPolicyTests, so SidebarMaterialOption.sidebar is ambiguous between CmuxSettings and the app's typealias to WindowChromeSidebarMaterialOption. Use the WindowChrome names again, as #14991 did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Austin Wang <austinwang115@gmail.com>
…ope) (#16260) * fix: share OpenCodePaths with the CLI through CMUXAgentLaunch #16229 made CLI/cmux.swift call OpenCodePaths, but the enum lived in Sources/SessionIndexModels.swift, which only the app target compiles, so the CLI target fails with "cannot find 'OpenCodePaths' in scope". Move the unchanged path logic into CMUXAgentLaunch, which the app, the CLI and cmuxTests already import, and make its two entry points public. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Pass the temporary-config flag to the Codex provider override parser #16201 made providerOverrides(from:) skip provider entries when the caller uses a temporary CODEX_HOME, but read `usesTemporaryConfig`, a parameter of build(configToml:usesTemporaryConfig:) that is not in scope there, so the CLI no longer compiles. Pass the flag through. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test: match temporary Codex config argument scope * Make OpenCodePaths a value type to satisfy package conventions Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci: cmux-tui artifact publishing runs in its own artifacts environment (#16267) * test(ci): cmux-tui artifact publishing must run in the artifacts environment #16171 put the cmux-tui publish job in the release environment, whose policy allows only main and v* tags, so helper-branch pin publishes fail before any step runs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(ci): cmux-tui artifact publishing runs in the artifacts environment The artifacts environment holds only the R2 upload credentials and allows main, feat-cmux-next and cmux-tui-pin-* helper branches, so daemon pin publishes work again while signing, Sparkle, Homebrew and Apple secrets stay in release (main and v* tags only). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(cloud): make Cloud workspace reconciliation always settle (#16158) * test(cloud): reusing a projection at its current placement changes nothing Reconcile reprojects every missing placement through SurfaceCatalog.project. When the reused pane already carries that placement, attachRemoteView still removes and reinserts it, bumps the projection revision twice, and requests the next reconcile of the same machine. Any disagreement between the plan and project() then becomes a main-actor livelock, which is how nightly b36a9b3 spun at 98% CPU and grew to tens of GB (fixed at the plan level by #16025). Fails on main: projectionVersions advances by 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): reattaching a projection's current placement is a no-op attachRemoteView rewrote a reused projection even when its remote workspace and tab were already the requested ones: it removed and reinserted it (clearing and resetting the panel directory, rerunning sidebar git probes, bumping the guest routing revision twice) and requested another reconcile of the machine. Since reconcile itself reprojects through project(), any plan that reports a shown pane as missing became an endless main-actor loop. Return early when the coordinates are unchanged, and apply a real change as one projections assignment so observers never see the pane unprojected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): setting a projection's current remote placement is a no-op Same guard as attachRemoteView for setRemotePlacement: skip views whose coordinates already match, and apply real changes as one projections assignment. Unchanged placements no longer bump the projection revision or post a catalog change that wakes the device layout coordinator. The test now states its fixture precondition explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cloud): reconciling one graph stops when every pass requests another A consumer that requests another reconcile without changing the accepted graph keeps CloudWorkspaceProjectionCoordinator's loop running forever on the main actor, which is how nightly b36a9b3 hung at 100% CPU and grew to tens of GB. Fails on main: the loop runs until the test stub stops asking (1000 passes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): bound reconciliation passes over one accepted graph CloudWorkspaceProjectionCoordinator re-ran reconcile while anything kept requesting it, with no progress check. Any consumer that asks for another pass without changing the graph (attachRemoteView before this PR, a plan that reports a shown pane as missing in #16025) held the main actor forever: nightly b36a9b3 pinned a core, grew to tens of GB, and could not even run its updater. Count passes over the same accepted CloudVMState. A converging graph needs two or three; after eight, stop, report a Sentry warning, and wait for the next graph or request, which starts a new count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): bound reconciliation by progress, not by passes over one graph Review of the previous bound: counting every pass over an unchanged graph could stop a reconcile that was still making progress (a staggered restore of several bound workspaces re-requests the same graph), stranding panes until the next graph. CloudWorkspaceReconcileBudget now stops after three consecutive passes that start from the same graph, projection revision and bindings (a pass that changed nothing cannot make the next one different), with a hard ceiling of 64 passes per graph for a loop that rewrites projections every pass, as nightly b36a9b3 did. Non-convergence is reported once per graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cloud): report projection non-convergence once per daemon generation Review: keying the dedupe on the full CloudVMState retained a whole graph per machine for the process lifetime (cancel never cleared it) and still reported once per revision. Key on the cursor generation, include generation and revision in the event, and clear it when the machine is cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(l10n): document the French Actions discovery titles as invariant Same change as #16175: main's localization parity check fails on actions.discovery.menuTitle and dialogTitle (fr is identical to English), which blocks this PR's static preflight and every gate behind it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(tests): name the app's window-chrome sidebar options explicitly #11539 reverted #14991's qualification in SidebarWidthPolicyTests, so SidebarMaterialOption.sidebar is ambiguous between CmuxSettings and the app's typealias to WindowChromeSidebarMaterialOption. Use the WindowChrome names again, as #14991 did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Austin Wang <austinwang115@gmail.com>
#16158's budget test passes budget.admit(...) straight to #expect. Xcode 26.3's macro expands the argument inside a closure where budget is immutable ("cannot use mutating member on immutable value"), so the macOS 15 lane fails at TEST BUILD. Bind each result first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Add failing regression test for discarded browser pane page state A hidden browser pane discarded for memory comes back through a fresh URL navigation, so it loses native back/forward history, scroll position and typed form input (#15069). This test discards a scrolled page with typed input and asserts all three survive the restore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore discarded browser panes from WebKit session state Discarding a hidden browser pane kept only its URL, history URL list and zoom, so returning to it replayed a fresh navigation: scroll position, typed input and SPA route were lost (#15069). Discard now captures the page's WebKit interactionState, a snapshot image and the typed form values reported by an isolated-world user script. Restore assigns the interaction state to the replacement web view, paints the snapshot with a "Restoring" label until the first paint, and replays form values once the document loads. URL replay stays as the fallback when no state was captured, the state belongs to another document, or WebKit does not start a load from it. Interaction state is persisted in session snapshots so relaunch restores the same way, except for private profiles, form submissions and state over the size limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add failing regression test for hidden WebContent termination restore A WebContent process that dies while its browser pane is hidden leaves the pane behind the manual Reload overlay, and recovery reloads the URL (#15069). These tests expect revealing the pane to restore the last session state instead, including when an uncommitted load was in flight at termination. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore hidden panes whose WebContent process died from session state A WebContent process that terminates while its pane is hidden no longer parks the pane behind the manual Reload overlay. The termination records that the pane was hidden; showing it converts the dead view into a discarded one and restores the WebKit interaction state, so history, scroll and form input come back without a URL reload. A load that had not committed when the process died restores the committed page instead. A crash while the pane is visible keeps the Reload overlay so a page that crashes its own process cannot reload in a loop. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add failing regression test for timer-free hidden web view discard default Issue #15069 asks for Chrome-style tab discard: a hidden pane keeps its page until hidden web content exceeds a memory budget, and the fixed hidden-time timer becomes opt-in. Today an idle pane hidden past the delay is discarded by the default policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Discard hidden web views oldest-first under a memory budget Hidden browser panes used to be discarded by a fixed timer. The default policy is now a hidden WebContent memory budget (browser.hiddenWebViewMemoryBudgetMB, default 2048). On each memory sample, BrowserHiddenWebViewMemoryBudgetCoordinator evicts the pane that has been hidden longest until the total fits. The timer is still available as browser.hiddenWebViewDiscardMode = "timer". The memory-pressure responder is unchanged. The mode and budget are wired through CmuxSettings, Settings > Browser, the cmux.json schema and the settings file, with localized strings. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add failing regression test for hidden pane discard blockers Issue #15069 asks the memory budget to leave alone a hidden pane whose state a restore cannot bring back. Typed input the restore never replays, such as a password or a rich-text editor, should keep the pane until the system is under memory pressure. Picture in Picture should keep it alive like playing media. Today the budget discards all three. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep hidden panes whose state a restore would lose The hidden memory budget could discard a pane holding typed input the form restore never replays (a password, a rich-text edit), close a page's Picture in Picture window, or drop a screen share. Those now block a routine discard: - The form-state observer flags unrestorable input, including values dropped by the capture caps, and the pane re-evaluates its discard schedule when that flag changes. System memory pressure still frees such a pane. - The media hook reports Picture in Picture per frame, and a paused Picture in Picture video keeps the pane alive. - Display and system-audio capture count as media capture next to camera and microphone. An explicit urgency (routine or system memory pressure) replaces the boolean that let pressure override a recoverable WebContent termination, so every pressure-only bypass reads from one place. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the hidden discard mode enum on one line in the cmux.json schema Match the schema's inline enum style and keep the embedded copy smaller. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a per-pane pin that keeps a hidden browser page active "Keep Page Active While Hidden" in the command palette pins the focused browser pane. A pinned page is never discarded while hidden, not even under system memory pressure, and the pin survives relaunch through the session snapshot. Toggling it re-evaluates the pane's discard schedule. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add failing regression tests for manual restore of unloaded pages With browser.autoRestoreUnloadedPages off (#9561), showing a discarded pane, or one whose WebContent process died while hidden, must leave it unloaded until the user restores it, and that restore must still bring back history, scroll and typed input. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a setting to keep unloaded browser pages until the user restores them browser.autoRestoreUnloadedPages (default true) decides whether a page unloaded to save memory, or whose WebContent process ended while hidden, restores as soon as its pane is shown. With it off, the pane keeps the page's last snapshot, dimmed, with a Restore button. Restore brings back history, scroll position and typed input from the captured interaction state, like the automatic path. This is the placeholder #9561 asked for, on the same restore path instead of a separate reload. A relaunched pane's deferred first load never waits, since nothing was unloaded. The page recovery overlay now owns both the crashed-page Reload prompt and the unloaded-page placeholder. The setting is in Settings > Browser, cmux.json and the settings file, with strings in all nine locales. Refs #15069 Refs #9561 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add failing regression tests for discard restore gaps from review Covers four gaps in the #15069 restore path: - a form-submission result page restored from session state resubmits the form, so it must replay by URL; - a WebContent process that dies while hidden after Stop is not restored; - a back/forward cache return never reports typed input again; - Dock browser panes are left out of the memory budget. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore form submission results by URL and report input after a cache return Assigning session state for a document that came from a form submission, in the main frame or a subframe, makes WebKit send the form again. The restoration state now tracks form submissions per document: a main-frame request sets the pending document's mark and a redirect re-decides it, a subframe submission marks the live document, and a commit moves the pending mark to the live one. A capture whose document is marked replays by URL. A back/forward cache return commits natively, which clears the pane's copy of typed input. The form state script now reports again on a persisted pageshow. Live session state is persisted only while its current entry is the URL the session snapshot saves, since a relaunch restores it for that URL. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore a page whose process died while hidden even after Stop Stop keeps a live page from reloading, but a WebContent process that died while the pane was hidden left no live page to keep. Drop the terminated web view before the Stop check, which clears Stop, so showing the pane restores the page. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Count Dock browser panes in the hidden page budget and pressure sweep The budget coordinator and the memory-pressure responder walked only workspace panes, so hidden Dock browsers never counted or unloaded. Both now use one app-wide enumeration that covers workspace panes, workspace Docks and window Docks, which also replaces the separate list the detached inspector routing kept. The per-manager and per-workspace pressure helpers go away. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Create the Dock budget test's workspace through addWorkspaceIfActive The deprecated addWorkspace call added a test-target warning. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a failing test for a new-window request clearing a form submission mark A request with no target frame loads in another web view, but the pane counted it as its own main-frame request. A new-window GET landing between a POST's decision and its commit cleared the pending mark, so a discard of the submission result page restored it with interaction state and sent the form again. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ignore new-window requests when tracking form submissions A navigation request with no target frame opens a new window, so its method says nothing about this pane's documents. Treating it as a main-frame request let a GET new-window request clear the mark set by a pending POST, and a POST new-window request mark a page that never submitted a form. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Splice Memory Saver search entries with a call instead of + After merging main, `[...] + browserMemorySaverEntries + [...]` in cmuxDefault(catalog:) no longer type-checks in reasonable time. Pass both literals to a function, as appendingDevicesEntries(to:) does, so each keeps a concrete contextual type. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Move the form-state scripts onto the WebKit types that run them The package conventions lint rejects BrowserFormStateScript, an enum with only static members. The content world, observer script, handler registration and restore call are now extensions on WKContentWorld, WKUserScript, WKUserContentController and WKWebView, so BrowserPanel no longer holds the content world or the handler name. The JavaScript is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add failing tests for a browser view outside a window marking its pane visible SwiftUI can build a browser view whose host never reaches a window and then dismantle it. Its visible report leaves a hidden pane marked visible, so Memory Saver never discards it (#15069). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Report a browser pane visible only while its view is in a window SwiftUI can build a browser panel view or portal host that never enters a window and then dismantle it. Its visible report left a hidden pane marked visible, so Memory Saver never discarded it (#15069). Visible reports from the panel view and the portal lifecycle now require the view to be in a window, and each reports visible when it enters one. Hidden reports are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Let the window visibility tests run main-actor tasks The tests ran synchronously as a main-actor job, so the nested run loop never ran the main-actor tasks that report a panel visible: the portal lifecycle update and the window-entry report. The "outside a window" checks passed without those tasks running, and the "enters a window" checks failed. The tests are now async and yield after each settle pass, as SidebarScrollViewConfiguratorTests does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cover popup page-state reports and a remote pane's queued restore A popup built from the opener's configuration shares its content controller, so its form and media reports reach the opener's handlers and replace the opener's typed input or keep it from being discarded. A remote pane whose proxy is reconnecting queues the URL replay; when the queued load starts it clears the in-flight restore that was noted up front, so the typed input never comes back. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bind page-state reports to their web view and note queued restores The form-state and media-playback handlers now drop reports from any other web view. A popup shares the opener's content controller, so its reports used to replace the opener's typed input and could keep the opener from being discarded after the popup closed. A URL-replay restore notes itself once its load starts. A remote pane queues that load until its proxy is back, and the queued load's start cleared the restore noted up front, so the typed input never came back. Budget enforcement returns before the per-pane checks when no pane is hidden. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Wait on causes, not intervals, in the discard and visibility tests The restore fixture waits for both typed values to be reported, the blocker tests wait for the form-state report or its unrestorable flag, and the visibility tests wait for the host or window-presence view before checking that no visible report arrived. Refs #15069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Wait for WebKit to save the scroll before discarding in the restore tests WebKit saves a page's scroll position into its history item 300 ms after scrolling stops, and the discard restore replays that item. Waiting for the typed-input report, which is debounced from before the scroll, let the test discard first, so the restore brought back the unscrolled position. Wait until the scroll shows up in the web view's session history instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the Import Choose… button's accessibility identifier The Import Browser Data block put identifiers on its actions row and on the whole block, neither of which was an accessibility element. SwiftUI applies a container's identifier to the children of such a container, so the Choose… button lost `SettingsBrowserImportChooseButton` and `testImportChooseButtonOpensImportWizard` could not find it. Both containers now contain their children, the pattern the right sidebar tab rows already use. Red: SettingsBrowserBehaviorUITests.testImportChooseButtonOpensImportWizard fails at d192505 in E2E runs 36427228637 and 36430175176. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: cover agent commands on hidden panes that need a restore A hidden pane whose WebContent process died keeps a dead web view, so a browser automation command waits for a document that never commits and times out until the user shows the pane. A pane an agent is driving also stays the memory budget's first pick because only hiding counts as use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: restore hidden browser panes for agent commands A browser pane whose WebContent process died while hidden kept the dead web view until someone showed the pane. Socket commands captured that view, and their document-readiness wait could never see a commit, so they timed out. The resolver now turns such a pane into a discarded one before the command captures its web view, the way showing the pane does, and the readiness wait restores it from its interaction state without showing it. A command also counts as use of the pane: hidden-pane discards measure the delay from the later of the hide and the last command, and the memory budget evicts by that time, so it no longer frees a page an agent is driving. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: record the backdated hide in the agent budget test The workspace can record a new pane hidden before the test backdates the hide, and a repeated hidden report keeps the first hide time. Show the pane first so the backdated hide is always recorded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that agent-driven restore cycles free dropped web views An agent that keeps waking a hidden pane must not grow memory: each discard has to release the web view it drops so its WebContent process can exit, the restored page must fall back under the memory budget once idle, and the captured page state must be freed when the restore commits. Also cover the web view whose process died while hidden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: tear down replaced browser webviews * test: assert browser teardown attachments * test: align flaky host assertions with current behavior * test: drain async browser teardown before leak checks * fix: let replaced browser views drain observer tasks * test: name browser lifetime checks precisely * fix: hoist async readiness before XCTest assertions * test: provide remote browser proxy credential * test: align restored browser and SSH fixtures with main * test: avoid sleep in browser restore wait * fix: await browser automation fixture setup * test: assert resolved SSH route settings * Test immediate cleanup of pane drag previews * Stabilize unrelated app-host fixture waits * Keep font fixture assertions within test budget * Capture dock fixture after window setup * Use the loader signal in the correct fork fixture * Keep settings merge within source budgets * Fit accessibility fix within settings budget * Restore ghostty and bonsplit pointers dropped by a main merge A merge of main committed stale submodule checkouts, rolling ghostty back to 9961d09 and bonsplit back to b32f48b. Point both at main's commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore Memory Saver settings rows dropped by a main merge Merging main took main's BrowserSection and curated search entries, which brought back the old toggle and delay rows and orphaned BrowserMemorySaverSettingsRows and insertingBrowserMemorySaverEntries. Mode, budget and auto-restore had no Settings UI, and search results for them pointed at missing rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that every Browser Memory Saver row is searchable A main merge took main's curated settings entries and dropped the mode, budget and auto-restore rows. Against those entries this test fails for Memory Saver Mode, Hidden Tab Memory Budget and Restore Unloaded Pages; it passes with 36efe7a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test that imported sessions drop WebKit page state SessionSnapshotImportTrust.sanitizedBrowserPanel filters imported history to http(s) but keeps interactionState, whose own back/forward list would restore the entries the filter removed. This test fails until the sanitizer clears it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop WebKit page state from imported sessions restore-session --from filters imported browser history to http(s), but interactionState carries WebKit's own back/forward list, and seedPageRestoration assigned it to the web view on first load. The sanitizer now clears it and reports the panel as changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge latest main and preserve browser regression coverage * Harden browser import and web view teardown * Fix temporary Codex config provider forwarding * Keep CLI OpenCode config path self contained * Align close-tab test helper with latest main * Repair latest main test target wiring * Fix billing seat nudge web assertion * test(web): pin the seats-follow-membership billing copy The billing panel's over-seat line is asserted here, and this test has been red on main since the dashboard SPA port: it already checks that no add-seats link is offered, and the port brought one back. Widen it to the copy the rule actually calls for, so both halves of the regression are covered. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(web): restore the seats-follow-membership copy the dashboard port dropped The Team subscription quantity follows the member count, so an over-seat line has nothing for an admin to act on: the reconciler updates Stripe on the next membership fact. That was settled in 06f4a7c, which reworded the line in all 20 locales, removed the add-seats link beside it, and dropped the members-page seat nudge. The dashboard SPA port rebuilt the billing panel from the pre-06f4a7c version at a new path, so git saw no conflict and the link came back, and the locale files went back to the soft-seat wording. `web/tests/ dashboard-billing-screen.test.tsx` has been red on main ever since, which fails the required `ci-status` on every web pull request. Restores the wording and drops the link. `seatNudge` and `seatNudgeAction` go too: the nudge they belonged to is gone from the members page and nothing reads them. `docs/team-settings-and-invites.md` already records the rule, and the stale "seats are soft" comment left hanging over an unrelated type in `team-members.tsx` is removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(web): pin the new-team seat copy too The same merge-resolution path that reverted the billing panel's copy also reverted this line, and nothing asserted on it. Pin the sentence and the old wording's absence so a stale merge side fails the shard instead of shipping. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(dev): apply concurrent-index migrations outside transactions The GCP development backend runs migrations on startup, but drizzle-kit wraps every migration in a transaction and PostgreSQL rejects CREATE INDEX CONCURRENTLY. Share a local migration runner between bun db:migrate, DB tests, and the tagged backend so startup can complete safely while preserving atomic transactions for ordinary migrations. * fix(ci): use transaction-safe migrations and restore queue timeout helper The web migration lane must use the local runner for CREATE INDEX CONCURRENTLY migrations, and the latest main branch's tests still call the removed drainMainQueue(timeout:) overload. Keep both migration passes safe and preserve the timeout-aware test helper for existing suites. * Fix pinned request uploads on Bun 1.3 * fix(ci): route every local migration lane through safe runner * Cancel pinned uploads when requests close * fix(web): insert a real JSON null in the malformed cleanup-row test "Cloud VM database schema > rejects malformed transferred cleanup rows" (#15423) never ran on main, because main's migrations failed before the database behavior tests. With migrations fixed it fails: expect((insertError)?.code).toBe("23514") Expected: "23514" Received: "23502" Its first malformed value is `null`, and postgres.js binds `sql.json(null)` as SQL NULL. The NOT NULL column rejects that (23502) before the check constraint the test is about. The row under test is a JSON null document, so that case now inserts `'null'::jsonb`, and the check rejects it with 23514 like the other nine. Checked on a scratch Postgres 14 with postgres.js: sql.json(null) gives 23502, the JSON null gives 23514, all ten malformed values give 23514, and {modelPlane: true} and {homeVolume: "v"} are accepted. Refs #15488 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cloud: pin the team date wire shapes in a test The team API writes every date with `Date.toISOString()`, so the strings on the wire always carry milliseconds. This test decodes that shape, the whole-second shape, and a non-date string, and fails today because `TeamsClient.decoder` uses `.iso8601`, which rejects fractional seconds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * cloud: accept the team API's millisecond timestamps `TeamsClient.decoder` used `.iso8601`, which rejects fractional seconds, while every team date on the wire comes from `Date.toISOString()` and so always carries milliseconds. Team detail, sent invitations, received invitations and invite links therefore could not decode at all, and `macos / swift-package-tests` is red on main because of it. Parse the fractional-second shape first and fall back to whole seconds, matching `VMClient.dateValue` in the same package. `Date.ISO8601FormatStyle` is Sendable, unlike `ISO8601DateFormatter`, so it can live on the static decoder. A string that is not a date still fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test(cloud): pin sub-second precision and a dated invite link A review pointed out that every fixture date sits on a whole second, so a decoder that parses the milliseconds and then throws them away passed the whole suite. Verified on Linux: a truncating decoder now fails. Also covers `CloudTeamInviteLink.expiresAt` as a string, which was only ever null in the fixture, and guards the whole-second replacement against silently becoming a no-op. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep queue helper within test file budget * Avoid duplicate SessionEntry test target source * fix(tests): call the mutating reconcile budget outside #expect #16158's budget test passes budget.admit(...) straight to #expect. Xcode 26.3's macro expands the argument inside a closure where budget is immutable ("cannot use mutating member on immutable value"), so the macOS 15 lane fails at TEST BUILD. Bind each result first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test: check the vm ready poll interval in cmuxCLITests The same change as #16242 (90851e5), carried so this PR can restore main's cmuxTests build in one piece. #15381 called CMUXCLI.vmReadyPollInterval from the app-hosted CLIVMTransferTests, where CMUXCLI names the app's routing type, not the CLI. The policy check moves to cmuxCLITests, which builds the CLI target. Refs #15488 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: cover vm poll interval boundaries * test: compile the vm ready poll policy into cmuxCLITests #16245 moved the vm poll-interval check into cmuxCLITests with `@testable import cmux_cli`, which cannot compile or link for the same reason as the hook store tests: cmux_cli is the CLI executable. The pure policy now lives in CLI/VMReadyPollInterval.swift, compiled into both the CLI and cmuxCLITests (the CMUXCLI+AutoNaming precedent), and CMUXCLI.vmReadyPollInterval delegates to it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * test: drive hook state recovery through the bundled CLI #16196 added ClaudeHookSessionStoreRecoveryTests with `@testable import cmux_cli`. cmux_cli is the cmux-cli executable, which cmuxCLITests does not link and cannot host, so the target stopped compiling ("Unable to find module dependency: CmuxControlSocketAtomicsC / CmuxSimulatorSystem"), and adding those packages would only move the failure to link time. The two tests now seed the hook state file, run a real `cmux hooks claude session-start` against a mock socket, and read what the CLI left on disk, like the rest of cmuxCLITests: - a malformed sibling record no longer discards a valid session mapping, and a salvageable file is not quarantined; - each of two unreadable state files is moved to its own quarantine backup with its original bytes, and the store keeps working afterwards. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Keep reconcile test within file budget * Give every app-host test process its own preferences domain App-host test processes all run the same cmux DEV bundle, so they shared the runner user's real com.cmuxterm.app.debug domain. CFFIXED_USER_HOME does not move it: cfprefsd resolves the path from the user account. What one process saved became the next process's starting state. The Dock tests (DockSocketLifecycleTests and others) save rightSidebar.mode=dock and fileExplorer.isVisible=true through FileExplorerState(), so a later process's createMainWindow() mounted the Dock while creating the window and testWorkspaceTerminalFontSizeShortcutSeedsDockCreatedAfterShortcut found a Dock before the shortcut. CmuxMain.main() now first calls TestProcessDefaults.installIfHostingTests(). In an XCTest host (CMUX_TEST_PROCESS, XCTestConfigurationFilePath or XCInjectBundleInto) it replaces +[NSUserDefaults standardUserDefaults] with a suite named <bundle id>.xctest.<pid>. A suite's search list has the argument, suite, global and registration domains but not the app's domain, so the process starts from registered defaults and keeps its writes. The suite is removed at exit; suites of crashed processes are removed by the next test process. @AppStorage, NSUserDefaultsController and package code all read UserDefaults.standard, so they follow. XCUITest target apps are not XCTest hosts and keep the real domain their tests seed. Code that names the app's domain explicitly (CloudTreeExpansionPreferences through Core Foundation, the scroll-bar preference and LanguageSettingsStore through persistentDomain(forName:)) now uses ProcessDefaultsDomain, so it sees the same domain as UserDefaults.standard. The window-frame reset from #15985 is removed: the process no longer sees an earlier process's frame. Its regression test now plants the frame in the shared domain the way an earlier process saved it. The Dock font-size test also sets the right sidebar hidden before it creates its window, because tests in the same process can still leave the Dock showing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Stabilize app-host CI regressions * Keep app-host defaults test within budget * Repair remaining app-host CI regressions * Test locale preference writes during background navigation * Keep background locale responses from changing language preferences * Cover normalized fetch metadata and cross-tab locale races * Use browser fetch metadata after Next.js request normalization * Exercise background cookie writes with a real HTML fetch * Correct locale navigation test metadata * Address browser restoration review findings * Harden browser discard edge cases * Document browser form state caps * Fix migration script trailing whitespace * Address browser review cleanup findings * Correct hidden memory budget planner fixture * Repair accent color access after main catch-up * Fix browser window presence callback capture --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Nightly
b36a9b3(0.64.25-nightly.3672807119701) ran one core at 98% and grew its memory footprint from 396 MB to 3.3 GB in 92 s, reaching about 64 GB before it was quit. It relaunched straight into the same state and could not run its updater, because Sparkle runs on the pinned main thread. Symbolicated with the Sentry dSYM, the CPU report and a livesampleshow one main-actor cycle:CloudWorkspaceProjectionCoordinator.reconcile→SurfaceCatalog.project(reuse branch) →attachRemoteView→reconcileCloudWorkspaceBinding→requestCloudWorkspaceProjection→reconcile.#16025 fixed the trigger: the plan reported a pane the workspace already showed as missing. This PR makes the loop impossible regardless of trigger, in two layers.
attachRemoteViewandsetRemotePlacementreturn early when the remote workspace and tab are unchanged. Before, they removed and reinserted the projection (clearing and restoring the panel directory, rerunning sidebar git probes, bumping the guest routing revision twice), andattachRemoteViewrequested another reconcile. A real change is now oneprojectionsassignment, so observers never see the pane briefly unprojected.CloudWorkspaceProjectionCoordinatorbounds the passes it runs over one acceptedCloudVMState. A converging graph needs two or three. AftermaxPassesPerState(8) it stops, sends a Sentry warning (cloud.projection, "Cloud workspace projection did not converge"), and waits for the next graph or request, which starts a new count. A future bug of this class becomes one reported warning instead of a hung app.Regression tests, each committed before its fix:
CloudWorkspaceLiveProjectionTests/reusingCurrentPlacementIsNoOp(): red on 3f220d9 (projectionVersions1 → 3), https://github.com/manaflow-ai/cmux/actions/runs/36761717085CloudWorkspaceLiveProjectionTests/reconcileOfOneGraphIsBounded(): red run on 2307c49, https://github.com/manaflow-ai/cmux/actions/runs/36766307690CloudWorkspaceLiveProjectionTestsandCloudPlacementCoordinatorTests: https://github.com/manaflow-ai/cmux/actions/runs/36766367050Results for the last two runs are pending; this description will be updated with them.
Changelog
Fixed: cmux no longer freezes and grows its memory without bound while syncing a Cloud workspace.
🤖 Generated with Claude Code
Summary by CodeRabbit