Preserve recoverable windows in session snapshots

Merged
AU
2mo ago
+5142-951

Addresses the restore-loss path in #9666.

Summary

  • Persist every live registered or recoverable main-window route through one authoritative projection.
  • Distinguish orphan recovery from intentional window teardown so a visible-at-willClose window cannot be resurrected by autosave.
  • Preserve recovered sidebar state and a frozen Dock snapshot before the owning context tears its Dock down.
  • Use per-window application-hide participation instead of global NSApp.isHidden.
  • Keep targeted socket and focus lookups direct instead of rebuilding and sorting the full recovery projection.
  • Use the same key-window-first ordering for autosave fingerprinting and snapshot truncation.

Investigation boundary

Cross-window workspace select resolves the destination through TerminalController+ControlWorkspaceContext.swift, focuses through AppDelegate.focusMainWindow, and selects on the main actor. That path does not directly mutate WebKit or display-link state. The supplied crash is on the CVDisplayLink thread in WebKit, and the reporter could not reproduce it on 0.64.22 after five trials, so a causal link remains unproven.

The restore loss is independently proven: routing included terminal-backed recovery routes while session autosave enumerated only mainWindowContexts. A live window whose context was orphaned could therefore remain routable but disappear from the saved topology.

Regression coverage

The first commit is test-only, followed by the production fix in a separate commit. Focused coverage now verifies:

  • a recovered visible window and workspace remain in the session snapshot;
  • sidebar visibility, width, selection, and frozen Dock payload survive context teardown;
  • an intentionally closing window is excluded even while AppKit still reports it visible;
  • an app-hidden recovered window participates only through its own captured restore state.

Validation

  • Static checks:
    • Swift frontend parse for every touched Swift file
    • scripts/check-pbxproj.sh
    • scripts/lint-pbxproj-test-wiring.sh
    • scripts/check-package-resolved-policy.py
    • scripts/check-workspace-package-groups.py --check
    • cmux architecture policy check: clean
  • Regression provenance:
    • Test-only commit f4270ad51b failed on the newly added verified-binding freeze regression: run 31289054856.
    • Fix commit aeae1af9aa passes all nine RecoverableMainWindowLifecycleTests: run 31289688004.
    • The matching MainWindowLifecycleCoordinatorTests suite also passes: run 31289688841.
  • Tagged dev-build dogfood on current HEAD:
    • Built and launched the isolated issue-9666-crosswindow-restore-crash tag.
    • Five pre-restore and five post-restore cross-window workspace selections each returned OK workspace:N; every health check returned PONG, with a stable process PID within each launch.
    • Persisted a five-window/five-workspace snapshot containing all four one-workspace scratch windows, sent SIGKILL only to the tagged process, relaunched through the tagged opener, and confirmed all five window/workspace UUID pairs restored.
    • The restored topology autosaved all five windows before scratch cleanup. Tagged logs contained no restore/save failure, fatal, unusable-snapshot, or dropped-route match, and macOS created no new diagnostic report.
    • Closed only the four scratch windows; the baseline window remains healthy.
  • Per task constraints, no local Xcode tests or XCUITests were run; focused compilation and test execution ran in GitHub Actions.
  • No user-facing strings changed; the localization audit is clean and no catalog updates are required.

[!NOTE]
High Risk
Changes core window registration, close/orphan transitions, and session snapshot construction—areas where subtle lifecycle bugs can drop windows from restore or resurrect closing UI.

Overview
Fixes session restore loss where orphaned main windows stayed routable but were omitted from autosave because snapshots only walked registered mainWindowContexts.

MainWindowLifecycleCoordinator replaces the separate recovery ledger and context map with one phased model (registered → orphaned → closing). Session autosave and fingerprinting now use orderedSessionRouteSnapshots, merging live registered routes with recoverable orphans (including frozen value snapshots after AppKit loses the window).

Orphan recovery preserves sidebar state and can freeze windowless routes asynchronously (tear down live panels, retain bounded snapshots). Intentional closes go through closing so they are excluded from persistence even if AppKit still reports the window visible. Process-detected resume bindings without verification are downgraded to manual during those freezes.

Routing, focus, teardown, and closed-window history are updated to use route snapshots and tabManagerForWindowTeardown where teardown-only managers must not receive live work.

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

Summary by CodeRabbit

  • New Features

    • Improved restoration of hidden, dismissed, and windowless sessions, including sidebar selections, workspace layouts, dock contents, and window placement.
    • Preserved browser-only windows and their routing information during recovery and closure.
    • Added more reliable session autosave ordering and bounded state tracking.
  • Bug Fixes

    • Closed windows are now excluded from active summaries while remaining available for required cleanup.
    • Safer recovery of process-detected sessions when verification is unavailable, requiring manual approval before resuming.
    • Improved handling of scrollback inclusion when creating session snapshots.

Final closeout revalidation (2026-09-03)

  • Final pushed HEAD: 5abbd2033008f5bbbe312d3c391a19f4517dc92b; it includes the clean three-way synchronization merge with current origin/main 69cea3077841885e667d9e670400f781a44745d5. No iOS paths are in the PR diff.
  • The current-main merge exposed ten compile-only fixture mismatches in newly landed Cloud tests under Xcode 26.3. 8342f7e6e8 makes only the minimal test-target compatibility corrections; it changes no production behavior.
  • Exact-HEAD remote E2E suites passed: Recoverable lifecycle (9 tests, run 33844355399), coordinator (16 tests, run 33844355257), and closed-window routing (6 tests, run 33844355088).
  • Tagged real-app dogfood used the required cloud reload command. The default backend attempt failed only because cmux-dev-backend-1 did not resolve; I retried on the fleet with CMUX_DEV_BACKEND_MODE=off, built successfully on cmux11s-mac-mini.1 in 431s, and launched the isolated tagged build. Four scratch windows were persisted; five cross-window selections before restart and four after restart returned PONG; killing/relaunching only the tagged process restored all four exact window/workspace UUID pairs. The tagged log contained no restore/save/fatal errors or new diagnostic report.
  • Static gates passed: package-resolved policy, workspace package grouping, pbxproj normalization/check, 763-file test wiring, strict test determinism (0 active findings), and git diff --check. The historical Swift file-length script is not present because it was removed in #8125.
  • Deliberate trade-offs: three-way merges preserve the reviewed commit history while keeping the branch current; the backend was disabled only for lifecycle dogfood because no web/cloud behavior is under test; no local Xcode tests or XCUITests were run.