Skip to content

Fix session scrollback loss on Sparkle updates and red-X window close - #2894

Closed
KochMartin wants to merge 2 commits into
manaflow-ai:mainfrom
KochMartin:fix/session-persistence-update-redx
Closed

KochMartin wants to merge 2 commits into
manaflow-ai:mainfrom
KochMartin:fix/session-persistence-update-redx

Conversation

@KochMartin

@KochMartin KochMartin commented Apr 14, 2026 •

Copy link
Copy Markdown

Summary

Two independent bugs caused the session snapshot (including scrollback) to be lost in ways that matched real-world reports of "cmux forgets my terminal content after restart":

  • Sparkle update relaunch race. updaterWillRelaunchApplication wrapped its cleanup in Task { @MainActor in … }, which schedules the body asynchronously and returns to Sparkle immediately. Sparkle then proceeded to terminate + relaunch while the snapshot save raced with applicationWillTerminate's own save + TerminalController.stop() sequence. Depending on the interleaving, the update could either skip the scrollback save entirely or overwrite a good snapshot with one captured after terminals had been torn down. Fix: run the body synchronously with MainActor.assumeIsolated so the snapshot is flushed to disk before Sparkle touches anything else. Pattern matches existing usage in GhosttyTerminalView.swift.
  • Red-X window close dropped scrollback and deleted the snapshot. unregisterMainWindow saved with includeScrollback: false and removeWhenEmpty: true for every red-X close while the app stayed running. That meant (a) remaining windows' scrollback was cleared on every close, and (b) closing the last window deleted the snapshot file entirely — so the next launch started blank instead of restoring the prior session. Fix: capture scrollback for the remaining windows and keep the snapshot file around even when no windows remain. The shouldPersistSnapshotOnWindowUnregister gate still skips this save during app termination, where applicationWillTerminate has already written a full snapshot with scrollback. The now-unused shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregister policy function and its test assertions are removed.

Scope is intentionally limited to these two fixes. Broader capture-path improvements (scrollback on autosave, sleep/wake observers, etc.) are out of scope for this PR.

Test plan

  • Unit: `cmux-unit` scheme — existing testWindowUnregisterSnapshotPersistencePolicy still passes with the trimmed assertions.
  • Manual: open multiple windows with terminal content, close one with the red X, verify the remaining window's scrollback survives a Cmd+Q / relaunch cycle.
  • Manual: open cmux with terminal content, close the last window with the red X, relaunch the app, verify workspaces + scrollback are restored (previously: empty start).
  • Manual: trigger a Sparkle update install-and-relaunch, verify scrollback survives the update.
  • Manual: confirm ~/Library/Application Support/cmux/session-*.json contains scrollback fields after each of the above flows.

Notes for reviewers

I could not run a full local build — the machine doesn't have zig installed and GhosttyKit.xcframework is not prebuilt. The Swift diff is small (~20 lines across 3 files) and has been reviewed via the code-reviewer agent. Please run CI and a local build before merging.


Summary by cubic

Fixes two cases where session scrollback was lost: during Sparkle update relaunch and when closing windows with the red X. Scrollback now survives updates and relaunches, even after closing the last window.

  • Bug Fixes

    • Made updaterWillRelaunchApplication run synchronously via MainActor.assumeIsolated so the snapshot is flushed before Sparkle terminates, avoiding races with applicationWillTerminate/TerminalController.stop().
    • On unregisterMainWindow, save with includeScrollback: true and removeWhenEmpty: false while the app stays running, preserving scrollback and keeping the snapshot after last-window close; still skipped during app termination.
  • Refactors

    • Removed shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregister and its test assertions.

Written for commit 04846b4. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved session persistence reliability when closing windows to ensure data is consistently saved.
    • Enhanced the application update relaunch process to guarantee sessions and restorable state are properly persisted before restart, preventing potential data loss during updates.
  • Refactor

    • Streamlined internal session management code.

`updaterWillRelaunchApplication` wrapped its cleanup in `Task { @mainactor in … }`,
which schedules the body asynchronously and returns to Sparkle immediately.
Sparkle then proceeded to terminate + relaunch while the snapshot save raced
with `applicationWillTerminate`'s own save + `TerminalController.stop()`
sequence, occasionally losing scrollback on update flows.

Run the body synchronously with `MainActor.assumeIsolated` so the snapshot
is flushed to disk before Sparkle tears anything down.
When a user closed a window with the traffic-light button while the app
stayed running, `unregisterMainWindow` saved the session with
`includeScrollback: false` and `removeWhenEmpty: true`. The effects:

1. Remaining windows' scrollback was dropped on every close.
2. Closing the last window deleted the snapshot file entirely, so the
   next launch started blank instead of restoring the prior session.

Capture scrollback for the remaining windows and keep the snapshot file
around even when no windows remain. The `shouldPersistSnapshotOnWindowUnregister`
gate still skips this save during app termination (where
`applicationWillTerminate` has already written a full snapshot).

Drop the now-unused `shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregister`
policy function and its test assertions.
@vercel

vercel Bot commented Apr 14, 2026

Copy link
Copy Markdown

@KochMartin is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 14, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d068b035-0039-403b-b82e-89891d0d4938

📥 Commits

Reviewing files that changed from the base of the PR and between bc91b47 and 04846b4.

📒 Files selected for processing (3)
  • Sources/AppDelegate.swift
  • Sources/Update/UpdateDelegate.swift
  • cmuxTests/SessionPersistenceTests.swift
💤 Files with no reviewable changes (1)
  • cmuxTests/SessionPersistenceTests.swift

📝 Walkthrough

Walkthrough

Window-unregister snapshot persistence logic is simplified by removing a helper function, and update-relaunch operations are converted from asynchronous Task scheduling to synchronous main-actor execution to prevent races with application termination.

Changes

Cohort / File(s) Summary
Session State Management
Sources/AppDelegate.swift, Sources/Update/UpdateDelegate.swift
Removed shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregister() helper and updated window-unregister to always pass removeWhenEmpty: false. Changed update-relaunch delegate from async Task scheduling to synchronous MainActor.assumeIsolated execution for session persistence and terminal stop operations.
Session Persistence Tests
cmuxTests/SessionPersistenceTests.swift
Removed assertions for the deleted shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregister() function while preserving tests for shouldPersistSnapshotOnWindowUnregister() behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A snapshot helper hops away,
While sync and async now obey,
Windows close with grace so true,
Updates relaunch, swift and new! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the two main bugs being fixed: session scrollback loss during Sparkle updates and red-X window closes.
Description check ✅ Passed The description covers the Summary and Testing sections well, with detailed explanations of both bugs and their fixes. However, the Demo Video section and Review Trigger block are missing, and the Checklist is incomplete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

@greptile-apps

greptile-apps Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two independent snapshot-loss bugs: a Sparkle relaunch race where Task { @MainActor in … } let Sparkle proceed before the save completed, and a red-X window close that wrote includeScrollback: false, removeWhenEmpty: true, stripping scrollback and deleting the snapshot file when the last window closed. Both fixes are mechanically correct and scoped tightly to their respective paths.

The only nit is a comment at unregisterMainWindow that still describes the old "snapshot removed when last window closes" rationale, which no longer applies.

Confidence Score: 5/5

  • Safe to merge — both fixes are mechanically correct and well-isolated; the only finding is a stale inline comment.
  • Both bug fixes address real race/ordering defects. The MainActor.assumeIsolated pattern is consistent with existing codebase usage. The removeWhenEmpty: false + includeScrollback: true change correctly preserves the snapshot and captures scrollback on red-X close. The sole P2 finding is a cosmetic comment update that does not affect behavior.
  • Sources/AppDelegate.swift — stale comment at line 12933 only.

Important Files Changed

Filename Overview
Sources/Update/UpdateDelegate.swift Replaces Task { @MainActor in … } with MainActor.assumeIsolated { … } in updaterWillRelaunchApplication so the session flush runs synchronously before Sparkle terminates the process. Pattern and availability consistent with the rest of the codebase.
Sources/AppDelegate.swift Changes unregisterMainWindow to save with includeScrollback: true, removeWhenEmpty: false instead of the prior false/true pair, fixing both scrollback loss and snapshot deletion on red-X close. One comment at line 12933 is now stale.
cmuxTests/SessionPersistenceTests.swift Trims testWindowUnregisterSnapshotPersistencePolicy to remove assertions for the now-deleted shouldRemoveSnapshotWhenNoWindowsRemainOnWindowUnregister function. Remaining assertions are still behaviorally correct.

Sequence Diagram

sequenceDiagram
    participant U as User / Sparkle
    participant UD as UpdateDelegate
    participant AD as AppDelegate
    participant SP as SessionPersistenceStore

    Note over U,SP: Sparkle update relaunch (fixed)
    U->>UD: updaterWillRelaunchApplication()
    UD->>AD: "MainActor.assumeIsolated { persistSessionForUpdateRelaunch() }"
    AD->>SP: saveSessionSnapshot(includeScrollback: true) [sync]
    SP-->>AD: snapshot written ✓
    AD-->>UD: return
    UD-->>U: return
    U->>U: terminate + relaunch

    Note over U,SP: Red-X window close (fixed)
    U->>AD: window willClose (NSWindow.willCloseNotification)
    AD->>AD: unregisterMainWindow(window)
    AD->>AD: "shouldPersistSnapshotOnWindowUnregister? (isTerminatingApp=false → true)"
    AD->>SP: saveSessionSnapshot(includeScrollback: true, removeWhenEmpty: false) [async]
    SP-->>AD: snapshot preserved (or no-op if last window) ✓
Loading

Comments Outside Diff (1)

  1. Sources/AppDelegate.swift, line 12933-12935 (link)

    P2 Stale comment after behavior change

    The inline comment says geometry is persisted "even if the full session snapshot is removed when the last window closes," but this PR removes that removal behavior — removeWhenEmpty is now always false, so the snapshot file is never deleted on a red-X close. The comment's rationale no longer applies.

Reviews (1): Last reviewed commit: "fix: preserve scrollback and snapshot wh..." | Re-trigger Greptile

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Thanks for this! Session snapshots now persist on the red-X close and before the Sparkle relaunch landed on main in #3419 and #5132. You opened this first, so you got there first. Closing since main covers it now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants