Skip to content

fix(cloud): make Cloud workspace reconciliation always settle - #16158

Merged
austinywang merged 11 commits into
mainfrom
feat-cloud-reprojection-settles
Sep 30, 2026
Merged

austinywang merged 11 commits into
mainfrom
feat-cloud-reprojection-settles

Conversation

@austinywang

@austinywang austinywang commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

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 live sample show 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.

attachRemoteView and setRemotePlacement return 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), and attachRemoteView requested another reconcile. A real change is now one projections assignment, so observers never see the pane briefly unprojected.

CloudWorkspaceProjectionCoordinator bounds the passes it runs over one accepted CloudVMState. A converging graph needs two or three. After maxPassesPerState (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:

Results 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

  • Bug Fixes
    • Reusing a projection in its current workspace no longer changes its placement or triggers unnecessary projection updates.
    • Remote views now apply consistent tab-compatibility checks, while display-membership views remain tabless.
    • Workspace projection reconciliation now stops after repeated passes without progress or when it reaches its limit, helping prevent unbounded update cycles.
    • Starting reconciliation for a new workspace graph allows a fresh set of update attempts.

austinywang and others added 3 commits September 30, 2026 11:26
…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>
@github-actions

Copy link
Copy Markdown
Contributor

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

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 75c75392-0ed2-4358-ab38-d010eb486233

📥 Commits

Reviewing files that changed from the base of the PR and between e8f0e45 and 3f27068.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b721ec95-6f3d-4874-81d0-0d45b4612cd2

📥 Commits

Reviewing files that changed from the base of the PR and between 4f14ae8 and 9436437.

📒 Files selected for processing (3)
  • Sources/Surfaces/CloudWorkspaceProjectionCoordinator.swift
  • cmuxTests/CloudWorkspaceLiveProjectionTests.swift
  • scripts/localization-allowed-omissions.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

Remote 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.

Changes

Remote projection reconciliation

Layer / File(s) Summary
Placement updates and reuse
Sources/Surfaces/SurfaceCatalog.swift, cmuxTests/CloudWorkspaceLiveProjectionTests.swift
setRemotePlacement and attachRemoteView skip updates when coordinates match. Display-membership views remain tabless. Tests check that reuse preserves the projection and projection version.
Bounded reconciliation
Sources/Surfaces/CloudWorkspaceProjectionCoordinator.swift, cmuxTests/CloudWorkspaceLiveProjectionTests.swift
The coordinator limits passes for each graph state, stops after three consecutive identical marks, and reports non-convergence once per machine and daemon generation. Tests cover idle-pass and per-state pass limits, including admission of a new graph.

Localization omission entries

Layer / File(s) Summary
Actions discovery title entries
scripts/localization-allowed-omissions.json
The omission list adds menu and dialog title entries for Actions discovery, with French identity-locale explanations.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 94364

The change avoids redundant placement updates and bounds repeated reconciliation. No actionable merge-blocking risk is identified; merge after normal checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 94364

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
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated mutation scope is local projections for workspaces bound to the selected remote machine. Because reconciliation runs on the shared application main actor, pathological work can affect application-wide responsiveness. The new limit contains same-state work within a task, not aggregate work across continuously changing states or separately restarted tasks.

Trust Boundaries and Controls

  • observed — The scoped head retains state and binding checks before projection creation and before closing obsolete projections. Explicit remote views are resolved against current placement identity before reuse. These controls provide counterevidence to an ownership bypass; they do not establish the trust level of upstream cloud events.

Resilience and Maintainability Implications

  • observed — Budget rejection exits before another reconciliation pass and task cleanup is identity-checked. Cancellation removes tracked work, pending requests and transient mutation state. Provider replacement cancels before requesting reconciliation again; unregister cancels before clearing cloud state and projections. Atomic placement publication avoids an intermediate unprojected state visible to observers.

Hardening Proposals

  • proposed — Confirm the data classification of daemon generation identifiers before retaining them in warning telemetry. If they are sensitive, omit or transform them while preserving convergence counters. This is a precaution, not an observed disclosure finding.

Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error The diff adds CloudWorkspaceReconcileBudget in Sources/Surfaces/CloudWorkspaceProjectionCoordinator.swift. This is independently testable reconciliation state logic. It uses only value types from … Extract CloudWorkspaceReconcileBudget and its pure unit tests into a small SwiftPM target such as CmuxCloudProjectionCore. The target should expose public CloudWorkspaceReconcileBudget and a package-owned mark/value snapshot or protoc…
Description check ⚠️ Warning 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 s… 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 it…
Docstring Coverage ⚠️ Warning 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: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: preventing Cloud workspace reconciliation from failing to settle.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Cloud Persistent Session And Early Input ✅ Passed PASS — The diff does not add a client, carrier, event socket, readiness gate, or input-routing change. It makes unchanged remote placement attachment a no-op and applies real projection changes atomic…
Cmux Swift Actor Isolation ✅ Passed PASS. The production changes remain within the existing @MainActor CloudWorkspaceProjectionCoordinator and SurfaceCatalog UI owners. The new CloudWorkspaceReconcileBudget is a local value util…
Cmux Swift Blocking Runtime ✅ Passed PASS. The production diff adds no semaphore, blocking wait, sleep, delayed dispatch, timer, polling, main-queue sync, or manual lock. The existing clock.sleep calls in SurfaceCatalog.swift are unc…
Cmux Browser Automation Off-Main ✅ Passed PASS: The PR does not change browser socket automation. The authoritative diff changes only Cloud workspace projection/reconciliation code, tests, and localization data. `Sources/TerminalController.sw…
Cmux Expensive Synchronous Load ✅ Passed PASS: The PR changes only cloud projection reconciliation, placement updates, tests, and localization metadata. The production Swift diff adds no agent-history loader, file/directory scan, transcript …
Cmux Cache Substitution Correctness ✅ Passed PASS. The production diff changes in-memory cloud projection reconciliation and placement updates only. It does not replace a fresh authoritative read with a cached value in a persistence, history, un…
Cmux No Hacky Sleeps ✅ Passed PASS: The pull request changes only Swift files and a localization JSON file. The rule applies to non-Swift TypeScript, JavaScript, shell, and build/runtime scripts. The changed non-Swift patch adds n…
Cmux Algorithmic Complexity ✅ Passed PASS. The production changes do not add a nested per-target full-collection scan or a superlinear algorithm. setRemotePlacement and attachRemoteView perform linear scans over projections and const…
Cmux Swift Concurrency ✅ Passed The Swift diff does not introduce a flagged legacy async pattern. The existing coordinator Task remains stored in tasks, cancellable through cancel(machine:), and awaitable through `waitForIdle(…
Cmux Swift @Concurrent ✅ Passed PASS. The production changes add only synchronous reconciliation-budget and projection-update logic. CloudWorkspaceProjectionCoordinator and SurfaceCatalog remain @MainActor; the `Task { @MainAc…
Cmux Swiftpm Lockfiles ✅ Passed PASS. The pull request changes only Swift source, tests, and localization data. It changes no Package.swift, Package.resolved, .gitignore, Xcode project/workspace, workflow, or dependency configuratio…
Cmux Swift Logging ✅ Passed PASS. The only added local log is cmuxDebugLog(...), and it is guarded by #if DEBUG, which the rule allows. The production diagnostic uses the existing sentryCaptureWarning(...) path and reports…
Cmux User-Facing Error Privacy ✅ Passed PASS. The production diff adds only an internal Sentry warning and a DEBUG diagnostic for non-convergence. The warning text is generic, and its telemetry fields contain pass counts, a daemon generatio…
Cmux Full Internationalization ✅ Passed The production diff adds no user-facing UI, alert, menu, or API text. The only new Swift phrase is sent to Sentry via sentryCaptureWarning, not displayed to users; the debug log is also allowed. `Su…
Cmux Swiftui State Layout ✅ Passed PASS — The PR changes SurfaceCatalog reconciliation methods, CloudWorkspaceProjectionCoordinator, and tests. It adds no ObservableObject, @Published, @StateObject, @EnvironmentObject, `Geo…
Cmux Architecture Rethink ✅ Passed PASS. The Swift diff uses no new sleeps, delayed dispatch, polling, locks, semaphores, notification waits, duplicate entrypoint wiring, or split UI lifecycle owners. SurfaceCatalog remains the singl…
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The Swift diff changes cloud projection and reconciliation logic only. It adds no NSWindow, NSPanel, NSWindowController, SwiftUI Window, WindowGroup, close shortcut, or window identifier code. The add…
Cmux Source Artifacts ✅ Passed PASS. The diff changes only two hand-written Swift source files, one Swift test file, and the repository localization-omissions configuration. All four paths are text Git blobs under intentional sourc…
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS. The production diff adds no test/debug seam. CloudWorkspaceReconcileBudget is used by the production reconciliation path, and its members are not test-seam names or wrapper accessors for priva…
Full details: Description check

Explanation

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 Coverage

Explanation

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 Boundaries

Explanation

The diff adds CloudWorkspaceReconcileBudget in Sources/Surfaces/CloudWorkspaceProjectionCoordinator.swift. This is independently testable reconciliation state logic. It uses only value types from CmuxSurfaceCatalogModel, Foundation, and binding data. It does not use AppKit, SwiftUI, Ghostty, or app lifecycle state. The new direct budget tests also show that this logic can run without constructing app UI. The file is part of the app target, while the repository already has SwiftPM Cloud domain code and unit tests in Packages/macOS/CmuxCloud. The SurfaceCatalog and coordinator integration can remain app glue, but the budget logic crosses the package boundary.

Resolution

Extract CloudWorkspaceReconcileBudget and its pure unit tests into a small SwiftPM target such as CmuxCloudProjectionCore. The target should expose public CloudWorkspaceReconcileBudget and a package-owned mark/value snapshot or protocol instead of depending on the app-only WorkspaceCloudVMBinding. Keep CloudWorkspaceProjectionCoordinator task management, Sentry reporting, and SurfaceCatalog updates in the app target.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@austinywang austinywang added the dev-build Build a fleet dogfood build of each push (newest head under load) label Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Dogfood build of 3f270680bddbc8e819070e7af310aa4adf78f04b

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 dev-build label. Under load the fleet builds the newest push each time a worker frees up, so some pushes are skipped. It signs in against production, so Cloud or backend changes still need a tagged build with a development backend.

Dogfood tours of 3f270680

cloud-sidebar-audit-tour at 3f270680: not run

skipped: CI left no app build for this head (its compile failed or was cancelled)

Tours are picked by the paths globs in dogfood/scenarios/*.json; a Dogfood-tours: a, b line in the description picks them instead (none turns this off). Look at every frame before merging: a green tour only means no step failed.

austinywang and others added 2 commits September 30, 2026 12:30
…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>
@cursor

cursor Bot commented Sep 30, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@austinywang austinywang changed the title fix(cloud): reattaching a projection's current placement is a no-op fix(cloud): make Cloud workspace reconciliation always settle Sep 30, 2026
austinywang and others added 5 commits September 30, 2026 12:37
…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>
Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/main at 96914e7, the newest commit with green CI fast guards (6 newer skipped).

Merge-main-previous-head: db98123
Merge-main-base: 96914e7
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>
Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/main at b3ca418.

Merge-main-previous-head: 9436437
Merge-main-base: b3ca418
@github-actions

Copy link
Copy Markdown
Contributor

CI failure attribution

CI failed on e8f0e45e74 (run 36774788070 attempt 3): 1 code.

Job Verdict Why
macos / macOS compile admission code a compile error
Matched log lines
macos / macOS compile admission: /tmp/cmux-ci/src/Sources/AppDelegate+WorkspaceActionSave.swift:126:9: error: switch must be exhaustive

Not re-run automatically: macos / macOS compile admission is not a machine failure.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's.

Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/main at 579a979.

Merge-main-previous-head: e8f0e45
Merge-main-base: 579a979
@austinywang
austinywang merged commit 488eaf7 into main Sep 30, 2026
83 of 95 checks passed
@austinywang
austinywang deleted the feat-cloud-reprojection-settles branch September 30, 2026 23:03
@github-actions

Copy link
Copy Markdown
Contributor

Merge receipt for 3f270680bd, merged 2026-09-30 23:02:59 UTC

  • Not verified at merge: ci-status (not reported), macOS compile admission (in progress)
  • Verified: Web complexity, web-validation, CI fast guards, Dogfood build #​16158, Fast static checks, GhosttyKit release check, guards (18), linux-preflight, macOS admission gate, Testbox broker trust boundary
  • Skipped by policy: admission-placement, browser, Claude wrapper regressions, full-suite-coverage, remote-daemon, suite-coverage, swift-package-tests, ui-tests, web, web-build, web-database-tests, web-tests
  • Full suite: runs on main after merge.

Labeled merged-unverified: if main breaks near this merge, look here first.

@github-actions github-actions Bot added the merged-unverified A judging check was not green at merge; see the merge receipt comment label Sep 30, 2026
lawrencecchen pushed a commit that referenced this pull request Sep 30, 2026
* 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>
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 30, 2026
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
lawrencecchen added a commit that referenced this pull request Sep 30, 2026
…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>
austinywang added a commit that referenced this pull request Oct 1, 2026
…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>
austinywang pushed a commit that referenced this pull request Oct 1, 2026
#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>
austinywang added a commit that referenced this pull request Oct 1, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dev-build Build a fleet dogfood build of each push (newest head under load) merged-unverified A judging check was not green at merge; see the merge receipt comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant