Skip to content

Avoid beachball during update relaunch#4988

Open
lawrencecchen wants to merge 4 commits into
mainfrom
issue-nightly-update-install-beachball
Open

Avoid beachball during update relaunch#4988
lawrencecchen wants to merge 4 commits into
mainfrom
issue-nightly-update-install-beachball

Conversation

@lawrencecchen
Copy link
Copy Markdown
Contributor

@lawrencecchen lawrencecchen commented May 29, 2026

Summary

  • add a bounded Ghostty embedded API for reading the formatted tail of viewport or screen text
  • use that API for session snapshot scrollback, keeping update relaunch persistence synchronous while preserving the normal scrollback/process snapshot data
  • remove the pasteboard/temp-file VT export path and skip the duplicate termination snapshot after Sparkle relaunch prep already saved one

Notes

Verification

  • git diff --check
  • git -C ghostty diff --check
  • ./tests/test_ci_ghosttykit_checksum_present.sh
  • ./scripts/reload.sh --tag updhang
  • Local Xcode tests not run, repo policy requires running tests on AWS or GitHub Actions.

Note

Medium Risk
Changes session persistence and terminal scrollback capture at quit/update relaunch; incorrect behavior could lose or truncate restored sessions, but scope is bounded and covered by new policy tests.

Overview
Fixes update relaunch UI hangs by making Sparkle’s relaunch prep run synchronously on the main thread and persisting the session snapshot with a forced synchronous write before quit.

Session snapshot scrollback now uses Ghostty’s read_text_tail API (plain/VT, byte-capped) instead of merging selection tags and the old pasteboard/temp-file VT export path. applicationWillTerminate skips a second full snapshot when update relaunch already saved one.

Bumps ghosttykit to the fork commit that adds the text-tail API; tests cover forced sync writes and duplicate termination skip.

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

Summary by CodeRabbit

  • Bug Fixes
    • More reliable session saving during updates and termination (avoids duplicate termination snapshots; supports forced synchronous snapshot writes).
  • Refactor
    • Terminal snapshot/export flow rewritten to read terminal text directly (improved scrollback capture and enforced scrollback size limits; removes temp-export-file flow).
  • Tests
    • Updated tests for synchronous snapshot policy and update-relaunch snapshot skipping.
  • Chores
    • Updated Ghostty submodule and associated checksum.

Review Change Stack

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 29, 2026 11:22am
cmux-staging Building Building Preview, Comment May 29, 2026 11:22am

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR replaces the pasteboard/temp-file VT export path for terminal scrollback snapshots with a new bounded Ghostty C API (ghostty_surface_read_text_tail), and makes the update-relaunch session snapshot write synchronous to prevent a beachball (frozen cursor) while Sparkle relaunches the app.

  • TerminalController.swift: Removes the multi-candidate selection and NSPasteboard-based VT export helpers; adds readTerminalTextTail that calls the new Ghostty API with optional line/byte limits; updates readTerminalTextForSnapshot to use the bounded API with a SessionPersistencePolicy byte cap, with a VIEWPORT fallback.
  • AppDelegate.swift: Introduces didPersistUpdateRelaunchSnapshot, forceSynchronousWrite, and saveSessionSnapshotForAppTermination to avoid writing a duplicate termination snapshot when an update-relaunch snapshot was already flushed synchronously.
  • UpdateDelegate.swift: Switches updaterWillRelaunchApplication from a fire-and-forget Task { @MainActor } to MainActor.assumeIsolated + DispatchQueue.main.sync so that the snapshot write and TerminalController.stop() finish before Sparkle proceeds with the relaunch.

Confidence Score: 4/5

Safe to merge with attention to the VIEWPORT fallback format inconsistency in readTerminalTextForSnapshot.

The core update-relaunch flow — synchronous snapshot write, autosave-timer stop, and duplicate-snapshot skip — is well-structured and backed by new unit tests. The two findings are both in the terminal text read path: the VIEWPORT fallback silently ignores allowVTExport (inconsistent format on API failure), and readTerminalTextBase64's scrollback path omits the byte cap that readTerminalTextForSnapshot correctly applies. Neither affects the primary beachball fix, but the format inconsistency could cause degraded session restore fidelity in the error path.

Sources/TerminalController.swift — the readTerminalTextForSnapshot fallback and readTerminalTextBase64 scrollback path.

Important Files Changed

Filename Overview
Sources/TerminalController.swift Removes pasteboard/temp-file VT export and multi-candidate selection logic; replaces with readTerminalTextTail using the new Ghostty C API. readTerminalTextForSnapshot's VIEWPORT fallback hardcodes PLAIN format regardless of allowVTExport. readTerminalTextBase64's scrollback path calls readTerminalTextTail without byteLimit (max_bytes=0).
Sources/AppDelegate.swift Adds didPersistUpdateRelaunchSnapshot flag, forceSynchronousWrite parameter, and saveSessionSnapshotForAppTermination helper to prevent duplicate snapshot on app termination after update relaunch. Logic and tests look correct.
Sources/Update/UpdateDelegate.swift Switches updaterWillRelaunchApplication from async Task to synchronous MainActor.assumeIsolated + DispatchQueue.main.sync to ensure snapshot completes before Sparkle relaunches. DispatchQueue.main.sync concern already noted in previous review thread.
cmuxTests/SessionPersistenceTests.swift Removes tests for deleted VT export helpers; adds forceSynchronousWrite and testUpdateRelaunchSkipsDuplicateTerminationSnapshot tests. Coverage looks appropriate for the new code paths.
ghostty Submodule pointer bumped to dac500b2e to pick up the new ghostty_surface_read_text_tail and ghostty_surface_free_text API additions.

Sequence Diagram

sequenceDiagram
    participant Sparkle
    participant UpdateDelegate
    participant MainThread as Main Thread
    participant AppDelegate
    participant TerminalController
    participant GhosttyAPI as Ghostty C API

    Sparkle->>UpdateDelegate: updaterWillRelaunchApplication()
    Note over UpdateDelegate: Thread.isMainThread ?
    alt already on main thread
        UpdateDelegate->>MainThread: prepareForRelaunch() directly
    else background thread
        UpdateDelegate->>MainThread: "DispatchQueue.main.sync { prepareForRelaunch() }"
    end

    MainThread->>AppDelegate: persistSessionForUpdateRelaunch()
    AppDelegate->>AppDelegate: "isTerminatingApp = true"
    AppDelegate->>AppDelegate: stopSessionAutosaveTimer()
    AppDelegate->>AppDelegate: saveSessionSnapshotIncludingProcessDetectedIndexes(forceSynchronousWrite: true)
    AppDelegate->>GhosttyAPI: ghostty_surface_read_text_tail(scope: SCREEN, max_bytes: limit)
    GhosttyAPI-->>AppDelegate: bounded text
    AppDelegate->>AppDelegate: write snapshot (synchronous)
    AppDelegate->>AppDelegate: "didPersistUpdateRelaunchSnapshot = true"

    MainThread->>TerminalController: stop()
    MainThread->>MainThread: NSApp.invalidateRestorableState()
    Sparkle->>Sparkle: relaunch app

    Note over AppDelegate: applicationWillTerminate fires
    AppDelegate->>AppDelegate: saveSessionSnapshotForAppTermination()
    AppDelegate->>AppDelegate: "shouldSave? didPersistUpdateRelaunchSnapshot == true → SKIP"
    Note over AppDelegate: No duplicate snapshot written
Loading

Reviews (3): Last reviewed commit: "Pin GhosttyKit checksum for bounded tail..." | Re-trigger Greptile

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

Session snapshot persistence was refactored to track update-relaunch snapshots and support forced synchronous writes. AppDelegate gained a relaunch-persisted flag and conditional termination-snapshot logic. UpdateDelegate relaunch prep was made synchronous. Terminal snapshot export now reads bounded tails via ghostty. Tests and the ghostty submodule were updated.

Changes

Update Relaunch and Termination Snapshot Persistence

Layer / File(s) Summary
AppDelegate relaunch state & termination routing
Sources/AppDelegate.swift
Adds didPersistUpdateRelaunchSnapshot and routes applicationWillTerminate through saveSessionSnapshotForAppTermination() instead of unconditional saving.
Persist session for update relaunch
Sources/AppDelegate.swift
persistSessionForUpdateRelaunch stops autosave and calls saveSessionSnapshotIncludingProcessDetectedIndexes(..., forceSynchronousWrite: true), storing the boolean result.
Synchronous-write parameter propagation & policy helpers
Sources/AppDelegate.swift
saveSessionSnapshot(...) and saveSessionSnapshotIncludingProcessDetectedIndexes(...) gain forceSynchronousWrite; shouldWriteSessionSnapshotSynchronously and shouldSaveSessionSnapshotOnApplicationTerminate consider the override and relaunch-persisted flag.
UpdateDelegate relaunch prep synchronization
Sources/Update/UpdateDelegate.swift
Replaces Task { @mainactor in ... } with a prepareForRelaunch closure executed directly on the main thread or via DispatchQueue.main.sync, performing the same persistence/stop/invalidate actions.
TerminalController: VT-export removal and tail reads
Sources/TerminalController.swift
Removes exported-screen path cleanup helpers; adds readTerminalTextTail(...); readTerminalTextBase64(...) and readTerminalTextForSnapshot(...) use bounded ghostty tail reads with format/scope selection and scrollback byte limits.
Tests & submodule
cmuxTests/SessionPersistenceTests.swift, ghostty, scripts/ghosttykit-checksums.txt
Tests extended to assert forceSynchronousWrite behavior and that update-relaunch snapshot persistence prevents duplicate termination snapshots; ghostty submodule pointer and checksum entry updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • jesstelford

Poem

🐰 A tiny hare with careful paws,
Saved snapshots beating update cause,
Synchronous writes when reboots loom,
Tail reads pull the last few rooms,
Tests hop in to guard the flow.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (2 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error UpdateDelegate.swift line 125 introduces DispatchQueue.main.sync, a blocking main-queue synchronization prohibited by swift-blocking-runtime.md. Replace DispatchQueue.main.sync with a callback, signal, or async mechanism. Sparkle's updaterWillRelaunchApplication should not block the main thread synchronously.
Cmux Swift Logging ❌ Error TerminalController.swift adds 6+ print() statements to stdout in production socket listener code without #if DEBUG, violating swift-logging.md rules requiring Apple's unified Logger. Replace print() with Logger from os.log using logger.notice()/debug(); guard non-critical diagnostics with #if DEBUG if appropriate.
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.
Description check ⚠️ Warning The pull request description covers the main objectives and includes testing details, but is missing several required template sections. Add the missing 'Testing' section with specific test details, include demo video link if applicable, add the review trigger block, and complete the verification checklist items.
✅ Passed checks (14 passed)
Check name Status Explanation
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 Swift Actor Isolation ✅ Passed PR properly maintains actor isolation: new instance variable is @MainActor-isolated, static helpers are explicitly nonisolated, no implicit MainActor types or shared mutable Sendable types introduced.
Cmux No Hacky Sleeps ✅ Passed Check applies only to TypeScript, JavaScript, shell, and build/runtime scripts. All changes are in Swift files or data files, which are out of scope.
Cmux Algorithmic Complexity ✅ Passed readTerminalTextTail uses bounded reads (400KB max, 4000 line limit) enforced via SessionPersistencePolicy constants. Loop over ≤512 panels with fixed-size bounds per terminal; no unbounded scans.
Cmux Swift Concurrency ✅ Passed PR removes fire-and-forget Task from Sparkle delegate boundary (allowed), no new legacy async patterns (Dispatch.global, Combine, completion handlers, Tasks) introduced in cmux code.
Cmux Swift @Concurrent ✅ Passed All Swift changes involve synchronous functions on MainActor classes; no nonisolated async, missing @concurrent, or improper actor isolation patterns detected.
Cmux Swift File And Package Boundaries ✅ Passed AppDelegate +40 lines (under 250 threshold), TerminalController -110 net, UpdateDelegate 145 lines. No oversized new files, no mixed responsibilities, focused bug fix for update relaunch beachball.
Cmux User-Facing Error Privacy ✅ Passed No new user-facing errors or alerts added; changes are internal refactoring for session snapshot synchronization during update relaunch with only debug logging.
Cmux Full Internationalization ✅ Passed PR removes 722 lines of fork-conversation localization keys and adds no new user-facing strings. Pre-existing incomplete socket.terminal.* keys are not worsened by this change.
Cmux Swiftui State Layout ✅ Passed PR contains no SwiftUI View files or state changes; only AppKit code modifications (AppDelegate, UpdateDelegate, TerminalController, tests, and submodule update).
Cmux Architecture Rethink ✅ Passed Clear AppDelegate ownership of termination logic, single action path, no observers/locks/timing hacks, required platform bridge, tested invariant.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR creates only main workspace windows (cmux.main.*), which are explicitly allowed. No new auxiliary windows requiring cmuxAuxiliaryWindowIdentifiers registration.
Title check ✅ Passed The title clearly and concisely summarizes the main objective: avoiding UI freezing during update relaunch by making session persistence synchronous.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-nightly-update-install-beachball

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.

if Thread.isMainThread {
prepareForRelaunch()
} else {
DispatchQueue.main.sync(execute: prepareForRelaunch)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 DispatchQueue.main.sync is a blocking primitive

DispatchQueue.main.sync is explicitly flagged by the cmux blocking-runtime rule. When Sparkle calls updaterWillRelaunchApplication from a background thread, this blocks that thread until all of the main-thread work — including persistSessionForUpdateRelaunch's synchronous file write — completes. If Sparkle holds any lock that the main thread is waiting to acquire (e.g., via another main.sync or an internal Sparkle XPC barrier), this produces a deadlock. The fix replaced a race with a hard block, but DispatchQueue.main.sync is still a timing-based synchronization primitive.

A conforming alternative is to check Sparkle's documented thread-delivery guarantee for updaterWillRelaunchApplication. If Sparkle guarantees main-thread delivery (or if the SPUStandardUpdaterController setup forces it), the Thread.isMainThread fast path already handles the call and the DispatchQueue.main.sync branch is dead code that can be removed entirely. If it is not guaranteed, a RunLoop.main.run(until:) spin or a dedicated Sparkle-owned notification fired on the main thread would be a safer coordination point.

Rule Used: Flag new blocking or timing-based synchronization ... (source)

Comment thread Sources/AppDelegate.swift Outdated
Comment on lines +3992 to +4001
if Self.shouldUseFastSessionSnapshotForUpdateRelaunch(
isUpdateRelaunchInProgress: isUpdateRelaunchInProgress
) {
_ = saveSessionSnapshot(
includeScrollback: false,
removeWhenEmpty: false,
forceSynchronousWrite: true
)
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Update-relaunch fallback snapshot silently drops ProcessDetectedResumeIndexes

When persistSessionForUpdateRelaunch ran but the synchronous write returned false (didPersistUpdateRelaunchSnapshot == false) while isUpdateRelaunchInProgress == true, saveSessionSnapshotForAppTermination falls into the fast-path branch that calls saveSessionSnapshot(includeScrollback:false, ...) directly — skipping the ProcessDetectedResumeIndexes loaded by saveSessionSnapshotIncludingProcessDetectedIndexes. The normal termination path below it does load those indexes. If saveSessionSnapshot fails during persistSessionForUpdateRelaunch (e.g., snapshot builder returns nil), the termination snapshot will be written without the restorable-agent and surface-resume-binding indexes, and the subsequent launch may restore into a degraded state. Consider whether the fallback path here should also load process-detected indexes, or whether the fast path is truly sufficient when the first write attempt failed.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

scope: GHOSTTY_TEXT_SCOPE_SCREEN,
format: allowVTExport ? GHOSTTY_TEXT_FORMAT_VT : GHOSTTY_TEXT_FORMAT_PLAIN,
lineLimit: lineLimit,
byteLimit: SessionPersistencePolicy.maxScrollbackCharactersPerTerminal
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Character-count constant misused as byte limit

Low Severity

SessionPersistencePolicy.maxScrollbackCharactersPerTerminal (400,000) is a character-count constant, used with text.count in truncatedScrollback. Here it's passed as byteLimit, which maps to max_bytes in the ghostty C API. For multi-byte UTF-8 content (e.g., CJK text), bytes-per-character can be 3–4×, so the effective limit drops to ~100K–133K characters instead of the intended 400K, silently truncating more scrollback than expected for non-ASCII terminal sessions.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0433c3c. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3e71a96. Configure here.

format: GHOSTTY_TEXT_FORMAT_PLAIN,
lineLimit: lineLimit,
byteLimit: SessionPersistencePolicy.maxScrollbackCharactersPerTerminal
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fallback loses scrollback by switching to viewport scope

Medium Severity

When includeScrollback is true, the first readTerminalTextTail call uses GHOSTTY_TEXT_SCOPE_SCREEN with VT format. If that returns nil, the fallback switches to GHOSTTY_TEXT_SCOPE_VIEWPORT (plain format), which only captures the visible portion and discards all scrollback history. If the failure was caused by the VT format rather than the screen scope, falling back to screen/plain would preserve scrollback. The current fallback undermines the purpose of includeScrollback: true for the session snapshot persistence path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e71a96. Configure here.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 1797-1801: persistSessionForUpdateRelaunch() currently calls
saveSessionSnapshotIncludingProcessDetectedIndexes(includeScrollback: true,
forceSynchronousWrite: true) which causes
TerminalController.shared.readTerminalTextForSnapshot(..., includeScrollback:
true, ...) to capture scrollback synchronously during
updaterWillRelaunchApplication(_:). To make the relaunch snapshot minimal (no
scrollback), change the includeScrollback argument to false in the
saveSessionSnapshotIncludingProcessDetectedIndexes call inside
persistSessionForUpdateRelaunch(), or alternatively adjust the session
persistence policy checks (shouldPersistScrollback /
shouldReplaySessionScrollback / hibernationState) so that
readTerminalTextForSnapshot is not invoked with includeScrollback=true on the
update-relaunch path.

In `@Sources/TerminalController.swift`:
- Around line 10051-10052: The code passes
SessionPersistencePolicy.maxScrollbackCharactersPerTerminal (a character-count
limit) into readTerminalTextTail as byteLimit, causing character/byte unit
mismatch; change the argument to the matching byte-based policy
(SessionPersistencePolicy.maxScrollbackBytesPerTerminal) or convert the
character limit to bytes before calling readTerminalTextTail so the units align;
update the call site of readTerminalTextTail in TerminalController (the
byteLimit parameter) to use the byte-based constant (or a utf8.count conversion
from the character limit) so persistence/snapshot truncation behavior matches
SessionPersistencePolicy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8252ab8d-a77d-40b8-811e-f8cfed863b9e

📥 Commits

Reviewing files that changed from the base of the PR and between ff8b446 and 0433c3c.

📒 Files selected for processing (4)
  • Sources/AppDelegate.swift
  • Sources/TerminalController.swift
  • cmuxTests/SessionPersistenceTests.swift
  • ghostty

Comment thread Sources/AppDelegate.swift
Comment on lines +1797 to +1801
didPersistUpdateRelaunchSnapshot = saveSessionSnapshotIncludingProcessDetectedIndexes(
includeScrollback: true,
removeWhenEmpty: false,
forceSynchronousWrite: true
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Show the snapshot entry point and how includeScrollback flows into the write decision.
ast-grep --pattern 'func saveSessionSnapshotIncludingProcessDetectedIndexes($$$) {
  $$$
}'

ast-grep --pattern 'func saveSessionSnapshot($$$) {
  $$$
}'

ast-grep --pattern 'static func shouldWriteSessionSnapshotSynchronously($$$) -> Bool {
  $$$
}'

# Inspect what includeScrollback triggers (e.g. bounded tail read vs full scrollback export).
rg -nP --type=swift -C4 '\bincludeScrollback\b'

# Confirm the relaunch call site runs on the main thread synchronously.
rg -nP --type=swift -C4 'persistSessionForUpdateRelaunch\s*\('

Repository: manaflow-ai/cmux

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the exact relaunch snapshot code region.
echo "=== AppDelegate.swift around persistSessionForUpdateRelaunch ==="
sed -n '1765,1825p' Sources/AppDelegate.swift

# 2) Locate updaterWillRelaunchApplication call path and where persistSessionForUpdateRelaunch is invoked.
echo "=== Find updaterWillRelaunchApplication ==="
rg -n "updaterWillRelaunchApplication" -S Sources

echo "=== Find persistSessionForUpdateRelaunch call sites ==="
rg -n "persistSessionForUpdateRelaunch\\s*\\(" -S Sources

# If UpdateDelegate.swift exists, inspect the likely area mentioned in the review comment.
if [ -f Sources/UpdateDelegate.swift ]; then
  echo "=== UpdateDelegate.swift: around 80-170 ==="
  sed -n '80,170p' Sources/UpdateDelegate.swift
fi

# 3) Inspect snapshot write-policy logic directly.
echo "=== shouldWriteSessionSnapshotSynchronously ==="
sed -n '4000,4060p' Sources/AppDelegate.swift

# 4) Confirm includeScrollback propagates into build snapshot for the relaunch snapshot helper.
echo "=== saveSessionSnapshotIncludingProcessDetectedIndexes / saveSessionSnapshot ==="
rg -n "saveSessionSnapshotIncludingProcessDetectedIndexes\\(" -S Sources/AppDelegate.swift
sed -n '3950,4020p' Sources/AppDelegate.swift

# 5) Find any existing comment about relaunch snapshot being minimal/no-scrollback or with scrollback.
echo "=== Search for 'relaunch' + 'scrollback' ==="
rg -n "relaunch.*scrollback|scrollback.*relaunch" -S Sources/AppDelegate.swift Sources

Repository: manaflow-ai/cmux

Length of output: 8910


🏁 Script executed:

#!/bin/bash
# (placeholder, only one shell block should be used)

Repository: owner/repo

Length of output: 2026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== UpdateDelegate.swift: updaterWillRelaunchApplication ==="
sed -n '70,160p' Sources/Update/UpdateDelegate.swift

echo "=== saveSessionSnapshot: sync vs async write behavior ==="
sed -n '3685,3785p' Sources/AppDelegate.swift

echo "=== persistSessionSnapshot write helper (if present) ==="
rg -n "persistSessionSnapshot\\(" -S Sources/AppDelegate.swift
# show the first match region around the definition
rg -n "private func persistSessionSnapshot\\(" -S Sources/AppDelegate.swift && sed -n '3785,3920p' Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 12336


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== persistSessionSnapshot implementation ==="
# show around first definition
rg -n "private func persistSessionSnapshot\\(" Sources/AppDelegate.swift
sed -n '4070,4180p' Sources/AppDelegate.swift

echo "=== Find where 'synchronously:' is used/checked ==="
rg -n "synchronously:\\s*|synchronously\\b" Sources/AppDelegate.swift -C2

echo "=== Search for bounded tail / scrollback export behavior tied to includeScrollback ==="
rg -n "bounded|tail|scrollback" Sources/AppDelegate.swift -S -g'*.swift' -C2

# Also search for bounded tail in the broader codebase for snapshot persistence logic.
rg -n "bounded.*tail|tail.*read|scrollback.*bounded|includeScrollback" Sources -S -g'*.swift' -C2 | head -n 80

Repository: manaflow-ai/cmux

Length of output: 25784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== SessionPersistencePolicy scrollback limits ==="
rg -n "maxScrollbackLinesPerTerminal|SessionPersistencePolicy" Sources/SessionPersistencePolicy.swift Sources -S -g'*.swift' -C1

# If SessionPersistencePolicy is in another file, the rg above will include it; also directly search:
rg -n "maxScrollbackLinesPerTerminal" Sources -S -g'*.swift' -C3

echo "=== Workspace.swift: capturedScrollback gating (shouldPersistScrollback, hibernationState) ==="
sed -n '520,640p' Sources/Workspace.swift
rg -n "shouldPersistScrollback" Sources/Workspace.swift -S -C3

echo "=== Workspace.swift: TerminalController.shared.readTerminalTextForSnapshot arguments ==="
sed -n '540,610p' Sources/Workspace.swift

echo "=== Any comments describing update relaunch snapshot goal (minimal/no-scrollback) ==="
rg -n "update relaunch|relaunch.*session|persistSessionForUpdateRelaunch|minimal.*scrollback|no scrollback" Sources/AppDelegate.swift Sources -S -g'*.swift' -C2

Repository: manaflow-ai/cmux

Length of output: 11536


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Workspace.swift: scrollback capture gating around capturedScrollback ==="
sed -n '540,620p' Sources/Workspace.swift

echo "=== Workspace.swift: shouldPersistScrollback definition/usage in this file ==="
rg -n "shouldPersistScrollback" Sources/Workspace.swift -S
# print a bit of the surrounding definitions
rg -n "shouldPersistScrollback" Sources/Workspace.swift -S -C3

echo "=== SessionPersistence.swift: scrollback limits and truncation helpers ==="
sed -n '1,120p' Sources/SessionPersistence.swift

echo "=== SessionPersistence.swift: truncatedScrollback implementation ==="
rg -n "truncatedScrollback" Sources/SessionPersistence.swift -S
rg -n "truncatedScrollback\\(" Sources/SessionPersistence.swift -S -C2 | head -n 40

# show the specific implementation region (best-effort by locating a few line ranges)
# (Using approximate range; if too small/large, re-run with adjusted bounds.)
sed -n '150,260p' Sources/SessionPersistence.swift

echo "=== Any comments/logic in Workspace about update relaunch minimal/no scrollback ==="
rg -n "update relaunch|update.*relaunch|relaunch.*scrollback|minimal.*scrollback|no scrollback" Sources/Workspace.swift Sources/AppDelegate.swift Sources/Update/UpdateDelegate.swift -S -g'*.swift'

Repository: manaflow-ai/cmux

Length of output: 15512


Fix update-relaunch session snapshot: includeScrollback: true persists scrollback in a main-thread synchronous save

persistSessionForUpdateRelaunch() sets isTerminatingApp = true and calls saveSessionSnapshotIncludingProcessDetectedIndexes(includeScrollback: true, ..., forceSynchronousWrite: true). updaterWillRelaunchApplication(_:) invokes this on the main thread (directly if already on main, otherwise via DispatchQueue.main.sync). With forceSynchronousWrite: true, saveSessionSnapshot(...) performs an immediate write (persistSessionSnapshot(..., synchronously: true) runs writeBlock() inline).

Because includeScrollback is true, snapshot building captures scrollback (bounded) via TerminalController.shared.readTerminalTextForSnapshot(..., includeScrollback: true, lineLimit: SessionPersistencePolicy.maxScrollbackLinesPerTerminal) when shouldPersistScrollback && shouldReplaySessionScrollback && hibernationState == nil. This means the relaunch snapshot is not “no scrollback” (even though it’s truncated to policy limits).

If the update-relaunch path is intended to be “minimal, no scrollback”, switch includeScrollback to false (or ensure the policy conditions match that goal).

🐢 Proposed change if scrollback should be excluded on the relaunch path
         didPersistUpdateRelaunchSnapshot = saveSessionSnapshotIncludingProcessDetectedIndexes(
-            includeScrollback: true,
+            includeScrollback: false,
             removeWhenEmpty: false,
             forceSynchronousWrite: true
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
didPersistUpdateRelaunchSnapshot = saveSessionSnapshotIncludingProcessDetectedIndexes(
includeScrollback: true,
removeWhenEmpty: false,
forceSynchronousWrite: true
)
didPersistUpdateRelaunchSnapshot = saveSessionSnapshotIncludingProcessDetectedIndexes(
includeScrollback: false,
removeWhenEmpty: false,
forceSynchronousWrite: true
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 1797 - 1801,
persistSessionForUpdateRelaunch() currently calls
saveSessionSnapshotIncludingProcessDetectedIndexes(includeScrollback: true,
forceSynchronousWrite: true) which causes
TerminalController.shared.readTerminalTextForSnapshot(..., includeScrollback:
true, ...) to capture scrollback synchronously during
updaterWillRelaunchApplication(_:). To make the relaunch snapshot minimal (no
scrollback), change the includeScrollback argument to false in the
saveSessionSnapshotIncludingProcessDetectedIndexes call inside
persistSessionForUpdateRelaunch(), or alternatively adjust the session
persistence policy checks (shouldPersistScrollback /
shouldReplaySessionScrollback / hibernationState) so that
readTerminalTextForSnapshot is not invoked with includeScrollback=true on the
update-relaunch path.

Comment on lines +10051 to +10052
lineLimit: lineLimit,
byteLimit: SessionPersistencePolicy.maxScrollbackCharactersPerTerminal
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check SessionPersistencePolicy definition for maxScrollbackCharactersPerTerminal

# Find the policy definition
ast-grep --pattern 'maxScrollbackCharactersPerTerminal'

# Check for any documentation or comments explaining the unit
rg -B3 -A3 'maxScrollbackCharactersPerTerminal'

Repository: manaflow-ai/cmux

Length of output: 6297


Fix scrollback truncation unit mismatch (characters vs bytes)

SessionPersistencePolicy.maxScrollbackCharactersPerTerminal is defined and enforced as a character-count limit (text.count) in Sources/SessionPersistence.swift, but Sources/TerminalController.swift passes it to readTerminalTextTail as byteLimit. This mixes units (characters vs bytes) and will truncate fewer characters than intended for UTF-8; align the policy/unit with the parameter (maxScrollbackBytesPerTerminal vs character-bounded limit) so persistence/snapshot truncation match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/TerminalController.swift` around lines 10051 - 10052, The code passes
SessionPersistencePolicy.maxScrollbackCharactersPerTerminal (a character-count
limit) into readTerminalTextTail as byteLimit, causing character/byte unit
mismatch; change the argument to the matching byte-based policy
(SessionPersistencePolicy.maxScrollbackBytesPerTerminal) or convert the
character limit to bytes before calling readTerminalTextTail so the units align;
update the call site of readTerminalTextTail in TerminalController (the
byteLimit parameter) to use the byte-based constant (or a utf8.count conversion
from the character limit) so persistence/snapshot truncation behavior matches
SessionPersistencePolicy.

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.

1 participant