Skip to content

Bound app termination with a force-exit watchdog (#6758) - #6837

Merged
austinywang merged 14 commits into
mainfrom
issue-6758-main-thread-hang-30s-on-cmd
Jun 29, 2026
Merged

austinywang merged 14 commits into
mainfrom
issue-6758-main-thread-hang-30s-on-cmd

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6758

Problem

cmux hangs the main thread for ~30s on Cmd+Q. The hang is AppKit's
will-terminate gauntlet running CFPasteboardResolveAllPromisedData, which
flushes promised (lazy) pasteboard data with a blocking mach round-trip to the
pasteboard server (pboard). When a clipboard-history manager (Paste, Raycast,
Maccy, Pastebot, …) is mid-read of cmux's promised clipboard data, that
round-trip wedges until the OS force-kills the app.

Reported stack (every sample identical):

-[NSApplication terminate:]
  → will-terminate notification
    → CFPasteboardResolveAllPromisedData
      → __CFRunLoopRun → __CFRunLoopServiceMachPort → mach_msg   ← blocked ~30s

This is the third "an observer blocks the main thread during quit" report
(cf. #6415 PostHog flush, #6381 ghostty lock). As the reporter notes, the
structural gap is that quit has no global "return within N seconds no matter
what" guard
— the blocking observer here is Apple-owned
(CFPasteboardResolveAllPromisedData), so cmux cannot prevent it from running
once terminate: posts the notification.

Why not "just write concrete data on copy"?

cmux already does. Its terminal/image clipboard writes are eager
(clearContents() + setString/setData, writeObjects with materialized
items), and every declareTypes/addTypes uses owner: nil (no lazy owner).
The promise being resolved is held by pboard on cmux's behalf for the
external reader; cmux can't clear it without the same blocking round-trip. So
the right fix is the missing structural guard.

Fix

Add TerminationWatchdog: a one-shot, idempotent hard deadline on the
committed-quit sequence.

  • Runs on a dedicated background thread (raw Thread + Thread.sleep, not
    GCD) with no run-loop, GCD-queue, or main-actor dependency, so it fires even
    while the main thread is parked in mach_msg.
  • Armed in prepareForConfirmedAppTermination() — after the critical
    session/state save and before AppKit posts will-terminate — and, as a
    backstop, at the start of applicationWillTerminate(). Arming is idempotent,
    so the two sites and repeated quit attempts never stack threads.
  • If the process hasn't exited within the deadline (8s — generous headroom over
    normal sub-second teardown, far under the OS's ~30s hang watchdog), it
    force-exits via an unconditional, lock-free _exit. The firing path does
    no Foundation/filesystem work before exiting (the termination it guards
    against may itself be wedged on exactly such a lock). The bytes that matter
    are already on disk because the save runs before the watchdog is armed.
  • Owned by AppDelegate (next to the existing terminateKillWatchdogTask), not
    a global singleton.

Result: a wedged Apple observer turns a ~30s freeze into a bounded quit. This
closes the same structural gap behind #6415 and #6381.

Testing

Two-commit red → green per the repo's regression policy:

  1. test: red regression … — adds TerminationWatchdog + tests with the
    watchdog deliberately inert (never starts the firing thread) → tests fail.
  2. Bound app termination … — implements arm and wires it into the terminate
    path → tests pass.

The tests cover the watchdog mechanism (fires onFire after the deadline
from a background thread; idempotent so it fires exactly once). The end-to-end
pasteboard deadlock itself is not unit-testable — reproducing it needs the
real pasteboard server plus a clipboard-history reader and would wedge the test
process — so the unit tests bound what can be tested deterministically.

Review iteration (commit Address review …)

  • Codex correctness P1: the watchdog's onFire originally logged a
    StartupBreadcrumbLog breadcrumb (flock + Foundation/file I/O) before
    _exit. If that stalled during an already-wedged termination, the watchdog
    itself could block and never exit — defeating the guarantee. Removed; the
    firing path is now a bare, lock-free _exit.
  • Greptile P1 (no-ambient-global-state): replaced TerminationWatchdog.shared
    with an AppDelegate-owned instance.
  • Greptile P2: documented the raw-Thread-vs-GCD choice inline.

Localization audit

No user-facing strings added or changed — code comments only. (The earlier
diagnostic breadcrumb string was removed in the review iteration above.) Nothing
to add to Resources/Localizable.xcstrings or the web message catalogs.

Summary by CodeRabbit

  • Bug Fixes

    • Improved app quit reliability by adding a hard termination watchdog that forces exit if the normal shutdown sequence stalls.
    • Added an additional termination entrypoint coverage path so the watchdog can start even when the confirmed-quit flow isn’t used.
  • Tests

    • Added deterministic tests for termination watchdog timing, including verification that arming is idempotent and the termination handler runs exactly once.

cmux and others added 2 commits June 26, 2026 01:22
cmux can hang the main thread for ~30s on Cmd+Q when a clipboard-history
manager (Paste, Raycast, Maccy, …) is mid-read of cmux's promised
pasteboard data: AppKit's will-terminate gauntlet runs
CFPasteboardResolveAllPromisedData, which blocks on a stuck mach
round-trip to the pasteboard server until the OS force-kills the app.

This is the third "an observer blocks the main thread during quit" report
(cf. #6415 PostHog flush, #6381 ghostty lock); the structural gap is that
quit has no global "return within N seconds no matter what" guard.

Add TerminationWatchdog plus its tests, with the watchdog deliberately
inert (it never starts the firing thread) so the tests go red. The end-to-
end pasteboard deadlock is not unit-testable — reproducing it requires the
real pasteboard server and would wedge the test process — so the tests
cover the watchdog mechanism that bounds it. The fix commit starts the
thread and arms the watchdog from the terminate path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement TerminationWatchdog.arm and arm it from the terminate path so a
committed quit always returns within a bounded time, even when AppKit's
will-terminate gauntlet wedges on an Apple-owned observer we don't control
(CFPasteboardResolveAllPromisedData blocking on a stuck pasteboard-server
round-trip while a clipboard-history manager reads cmux's promised data).

The watchdog runs on a dedicated background thread with no run-loop, GCD,
or main-actor dependency, so it fires even while the main thread is parked
in mach_msg. It is armed in prepareForConfirmedAppTermination() — after the
critical session/state save and before AppKit posts will-terminate — and,
as a backstop, at the start of applicationWillTerminate(). Arming is
idempotent, so the two sites and repeated quit attempts never stack
threads. If the process has not exited within the deadline it force-exits
cleanly, turning a ~30s hang into a bounded quit.

This closes the structural gap shared with #6415 and #6381: quit now has a
global "return within N seconds no matter what" guard.

Fixes #6758

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled Jun 29, 2026 5:51am
cmux-staging Building Building Preview, Comment Jun 29, 2026 5:51am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a termination watchdog that arms a hard quit deadline, integrates it into AppDelegate’s termination paths, and includes atomic support, tests, and Xcode project wiring.

Changes

Termination Watchdog

Layer / File(s) Summary
Watchdog core and tests
Sources/TerminationWatchdog.swift, cmuxTests/TerminationWatchdogTests.swift
TerminationWatchdog adds injectable deadline scheduling, one-shot arming, and a default _exit(EXIT_SUCCESS) fire path; tests verify idempotent arming and deadline firing with a capturing scheduler.
Atomic latch support
Sources/TerminationWatchdogAtomic.h, Sources/TerminationWatchdogAtomic.c, cmux-Bridging-Header.h
A C latch type backs the watchdog’s arm state, and the bridging header imports the new header for Swift access.
AppDelegate integration and project wiring
Sources/AppDelegate.swift, cmux.xcodeproj/project.pbxproj
AppDelegate stores a watchdog instance and arms it in both termination entrypoints, while the project file adds the new Swift, C, and test sources to the app and test targets.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • lawrencecchen

Poem

🐇 A watchdog hops on quit-time ground,
No frozen tail can keep us bound.
One deadline tick, one final flit,
And pasteboard stalls can’t trap the kit.
Hop, hop — the exit now is swift!


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error TerminationWatchdog is a new mutable reference type marked Sendable with nonisolated(unsafe) state, but no actor/MainActor isolation or @unchecked safety wrapper. Remove Sendable, or isolate the watchdog (@MainActor/actor) or switch to @unchecked Sendable with a documented atomic-safety justification.
Cmux Swift Blocking Runtime ❌ Error Sources/TerminationWatchdog.swift adds a production Thread.sleep(forTimeInterval:) watchdog thread, which the rule explicitly flags outside tests. Replace the sleep-based scheduler with a non-blocking, cancellation-aware signal/timer/explicit-completion mechanism, or confine it to test-only code.
Cmux Architecture Rethink ❌ Error FAIL: Sources/TerminationWatchdog.swift adds a Thread.sleep→_exit timeout, and AppDelegate arms it in both prepareForConfirmedAppTermination() and applicationWillTerminate(), matching the forbi... Make quit impossible to wedge by removing promised clipboard ownership from teardown-sensitive code or giving it a single immediate owner; avoid the sleep-based production backstop and duplicate arm sites.
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (21 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding a force-exit watchdog for app termination.
Description check ✅ Passed The description covers the problem, fix, testing, and review notes; missing template extras are non-critical.
Linked Issues check ✅ Passed The change adds an idempotent termination watchdog on app quit, matching the issue’s required timeout guard and hang fix.
Out of Scope Changes check ✅ Passed The added files and wiring are directly supporting the watchdog feature and tests; no unrelated changes stand out.
Cmux Browser Automation Off-Main ✅ Passed Touched files are quit-path/watchdog only; no browser socket automation routing, worker-policy, or WebKit-wait changes appear in the diff.
Cmux Expensive Synchronous Load ✅ Passed The only code change is a terminate-path refactor; it adds no new agent-history loads, JSON parsing, or other expensive synchronous main-actor work.
Cmux Cache Substitution Correctness ✅ Passed The PR only adds a termination watchdog; the snapshot path still calls ProcessDetectedResumeIndexes.loadSynchronously(), not a cached substitute.
Cmux No Hacky Sleeps ✅ Passed Only Swift/C/header/project changes were introduced; no non-Swift runtime sleeps, timers, polling, or delayed dispatch were added, so this rule doesn’t apply.
Cmux Algorithmic Complexity ✅ Passed No new scalable collection scans, repeated sorts/filters, or batch rescans were introduced; the change is constant-time termination watchdog logic plus test scaffolding.
Cmux Swift Concurrency ✅ Passed No new DispatchQueue/Combine/completion-handler/Task lifecycle pattern was added; the watchdog uses a raw thread for an OS termination boundary, which the rule allows.
Cmux Swift @Concurrent ✅ Passed No new @concurrent or nonisolated-async misuse: the watchdog and terminate-path edits are synchronous, and no UI-isolated async helper was added.
Cmux Swift File And Package Boundaries ✅ Passed PASS: the new 93-line watchdog is focused app-termination glue, with tests and tiny C atomic support; no oversized file growth or mixed responsibilities.
Cmux Swiftpm Lockfiles ✅ Passed Diff only adds source files in cmux.xcodeproj; no .gitignore or Package.resolved changes, and no SwiftPM package-reference edits.
Cmux Swift Logging ✅ Passed The PR adds no new print/debugPrint/NSLog/Logger-style logging; the watchdog change is control flow/comments only.
Cmux User-Facing Error Privacy ✅ Passed The diff only adds internal watchdog code/comments/tests and wiring; no user-facing errors, alerts, command output, or recovery copy were added or changed.
Cmux Full Internationalization ✅ Passed Touched files add only termination logic, tests, comments, and C atomics; no new user-facing text or locale resources were introduced.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state/layout changes were introduced; the PR only touches AppDelegate/AppKit teardown, watchdog, tests, and C atomic glue.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only adds termination-watchdog code and tests; no new user-visible NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes were introduced.
Cmux Source Artifacts ✅ Passed Changed paths are intentional source/test/project files; no logs, caches, build output, DerivedData, or temp/scratch artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new debug/test seam was added in production Sources; the watchdog uses ordinary DI and no seam-like names or accessors were introduced.
Cmux No Ambient Global State ✅ Passed The watchdog is an instance (private let terminationWatchdog = TerminationWatchdog()) owned by AppDelegate; no new global var, singleton, or static-only namespace was added.
✨ 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-6758-main-thread-hang-30s-on-cmd

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.

@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds TerminationWatchdog, a one-shot, idempotent hard deadline that calls _exit if the process hasn't exited within 8 seconds after the app commits to quitting — bounding the ~30s main-thread hang caused by CFPasteboardResolveAllPromisedData blocking in a will-terminate observer (#6758). The fix is structural, closing the same gap that produced #6415 and #6381.

  • TerminationWatchdog uses a C11 atomic latch for idempotent arming, a raw Thread + Thread.sleep (explicitly not GCD, with inline justification — the wedged path may itself be sitting on GCD/run-loop infrastructure), and a bare _exit firing path with no Foundation or filesystem work before exiting.
  • AppDelegate owns the instance (private let terminationWatchdog) next to the existing terminate-control state; arm() is called in prepareForConfirmedAppTermination (primary, after the critical state save) and again in applicationWillTerminate as a backstop — both arms are idempotent.
  • Tests inject a CapturingScheduler that captures deadline handlers instead of sleeping, keeping coverage deterministic without real-time dependencies.

Confidence Score: 5/5

Safe to merge. The change is well-scoped: a single AppDelegate-owned watchdog instance armed only after the critical state save, with a lock-free _exit firing path that avoids any Foundation or filesystem work while termination may already be wedged.

The watchdog fires after 8 seconds on a dedicated OS thread with no dependency on GCD, run loops, or Foundation — exactly the infrastructure that may be stalled. The C11 atomic latch correctly prevents double-scheduling. Critical state is saved before arm() is called on both the primary and backstop paths. The raw-Thread-over-GCD choice is explicitly justified inline. Tests cover the mechanism deterministically with an injected scheduler. No new global state, no test seams in production source, and no unconditional blocking on interactive paths.

No files require special attention.

Important Files Changed

Filename Overview
Sources/TerminationWatchdog.swift New TerminationWatchdog class: injectable dependency-injection design (no test seam in production source), C11 atomic latch for idempotent one-shot arming, raw Thread + sleep with explicit GCD-avoidance justification, lock-free _exit firing path.
Sources/TerminationWatchdogAtomic.c Minimal C11 atomic latch: CMUXTerminationWatchdogLatchMake + CMUXTerminationWatchdogLatchClaim using atomic_compare_exchange_strong_explicit with correct acq_rel/acquire memory ordering for a one-shot flag.
Sources/TerminationWatchdogAtomic.h C header declaring the atomic latch struct and its two functions; correct include guards and stdatomic.h / stdbool.h usage.
Sources/AppDelegate.swift Adds private let terminationWatchdog instance (AppDelegate-owned, not singleton), arms it after the critical state save in both prepareForConfirmedAppTermination (primary) and applicationWillTerminate (backstop). Save/flush moved before arm() in the backstop path so bytes are on disk before the clock starts.
cmuxTests/TerminationWatchdogTests.swift Two deterministic tests using an injected CapturingScheduler (virtual-time, no real sleep): idempotent arming schedules exactly once, elapsed deadline fires handler exactly once.
cmux-Bridging-Header.h Adds #import Sources/TerminationWatchdogAtomic.h to expose the C atomic latch to Swift.
cmux.xcodeproj/project.pbxproj Adds TerminationWatchdog.swift and TerminationWatchdogAtomic.c to the app target and TerminationWatchdogTests.swift to the test target.
.github/swift-file-length-budget.tsv Bumps AppDelegate.swift budget by 15 lines to reflect the new watchdog wiring.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant AppDelegate
    participant TerminationWatchdog
    participant WatchdogThread
    participant AppKit

    User->>AppDelegate: Cmd+Q
    AppDelegate->>AppDelegate: prepareForConfirmedAppTermination()
    AppDelegate->>AppDelegate: saveSessionSnapshot() + flushPendingSaves()
    AppDelegate->>TerminationWatchdog: arm() [latch: 0→1]
    TerminationWatchdog->>WatchdogThread: spawn raw Thread (sleep 8s)
    AppDelegate->>AppKit: allow termination

    AppKit->>AppDelegate: applicationWillTerminate()
    AppDelegate->>AppDelegate: saveSessionSnapshot() + flushPendingSaves()
    AppDelegate->>TerminationWatchdog: arm() [latch already 1 → no-op]
    AppKit-->>AppKit: CFPasteboardResolveAllPromisedData (may wedge main thread)

    alt "Process exits normally (< 8s)"
        AppKit-->>AppKit: terminates
        WatchdogThread-->>WatchdogThread: thread reclaimed on exit
    else "Wedged > 8s (deadline fires)"
        WatchdogThread->>WatchdogThread: sleep(8s) elapses
        WatchdogThread->>AppDelegate: _exit(EXIT_SUCCESS) [lock-free]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant AppDelegate
    participant TerminationWatchdog
    participant WatchdogThread
    participant AppKit

    User->>AppDelegate: Cmd+Q
    AppDelegate->>AppDelegate: prepareForConfirmedAppTermination()
    AppDelegate->>AppDelegate: saveSessionSnapshot() + flushPendingSaves()
    AppDelegate->>TerminationWatchdog: arm() [latch: 0→1]
    TerminationWatchdog->>WatchdogThread: spawn raw Thread (sleep 8s)
    AppDelegate->>AppKit: allow termination

    AppKit->>AppDelegate: applicationWillTerminate()
    AppDelegate->>AppDelegate: saveSessionSnapshot() + flushPendingSaves()
    AppDelegate->>TerminationWatchdog: arm() [latch already 1 → no-op]
    AppKit-->>AppKit: CFPasteboardResolveAllPromisedData (may wedge main thread)

    alt "Process exits normally (< 8s)"
        AppKit-->>AppKit: terminates
        WatchdogThread-->>WatchdogThread: thread reclaimed on exit
    else "Wedged > 8s (deadline fires)"
        WatchdogThread->>WatchdogThread: sleep(8s) elapses
        WatchdogThread->>AppDelegate: _exit(EXIT_SUCCESS) [lock-free]
    end
Loading

Reviews (12): Last reviewed commit: "Avoid growing AppDelegate termination pa..." | Re-trigger Greptile

Comment thread Sources/TerminationWatchdog.swift Outdated
Comment thread Sources/TerminationWatchdog.swift Outdated
cmux and others added 4 commits June 26, 2026 02:08
- Codex/autoreview P1 (correctness): the watchdog's onFire logged a
  StartupBreadcrumbLog entry (flock + Foundation/file I/O) before _exit. If
  that logging stalled or contended during an already-wedged termination, the
  watchdog thread could block before reaching _exit and the quit hang would
  stay unbounded — defeating the guarantee. Drop the breadcrumb: the firing
  path is now an unconditional, lock-free _exit (the default onFire), which
  does zero Foundation/filesystem work before exiting.

- Greptile P1 (no-ambient-global-state): replace the
  TerminationWatchdog.shared singleton with an AppDelegate-owned instance,
  next to the existing terminate-control state (terminateKillWatchdogTask).
  The type was already injectable, so this is a small wiring change.

- Greptile P2: document why the deadline uses a raw Thread + Thread.sleep
  rather than a GCD timer (the wedged termination can sit on GCD/run-loop
  infrastructure, so the firing path must not depend on it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cmux-policy (Aziz concurrency) prefers actor isolation over locks for new
runtime state. Rejected here with rationale recorded in-code: an actor would
force `arm()` async, but it is called synchronously from the terminate delegate
methods and the deadline fires on a raw Thread — and the watchdog must not
depend on the Swift concurrency runtime, which may itself be wedged during the
termination it guards against. This is the same sanctioned NSLock +
nonisolated(unsafe) shape TerminalPasteboardService uses for synchronous-
callback state. Comment-only change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d-hang-30s-on-cmd

# Conflicts:
#	.github/swift-file-length-budget.tsv
CI's test-determinism gate (scripts/check-test-determinism.py --strict) flagged
the prior tests for real sleeps / wall-clock timeouts (sleep-then-assert and
assert-on-duration). Invert the time dependency per the gate's contract instead
of allowlisting: extract the deadline scheduler as an injectable
`DeadlineScheduler`. Production keeps the raw background Thread
(`TerminationWatchdog.threadScheduler`); the tests inject a synchronous
capturing scheduler and advance the deadline by hand.

The tests now assert idempotency (three arms schedule the deadline exactly once)
and exactly-once firing with zero real sleeps, timeouts, or wall-clock reads, so
they are deterministic by construction. arm() is now a thin idempotent latch
over scheduleDeadline(deadline, onFire); behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@austinywang

Copy link
Copy Markdown
Contributor Author

CI status note

All substantive checks are green: workflow-guard-tests (budget + test-determinism gate), swift-package-tests, ui-regressions, tests-build-and-lag, release-build, app-host unit tests shards 1/3/4, plus Greptile / CodeRabbit / Socket. Codex autoreview is clean (patch is correct), and the two Greptile threads are resolved.

The only red is app-host unit tests (2/4) (and its aggregates tests / ci-status). This is a pre-existing, load-induced flake in that shard, not related to this change:

Re-running shard 2 once CI load subsides should clear it.

# Conflicts:
#	.github/swift-file-length-budget.tsv
@blacksmith-sh

This comment has been minimized.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 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 2001-2004: The termination flow in applicationWillTerminate(_:)
arms the backstop watchdog too late, leaving
saveSessionSnapshotIncludingProcessDetectedIndexes(includeScrollback:removeWhenEmpty:)
and ClosedItemHistoryStore.shared.flushPendingSaves() outside the deadline
window. Move terminationWatchdog.arm() to run before those I/O calls, keeping
the existing isTerminatingApp flag update and snapshot/flush sequence intact so
the backstop is active even when prepareForConfirmedAppTermination() was never
reached.
🪄 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: 16ca1104-cf3c-468c-b463-326b12138ca9

📥 Commits

Reviewing files that changed from the base of the PR and between 22af91a and 9739e5b.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (6)
  • Sources/AppDelegate.swift
  • Sources/TerminationWatchdog.swift
  • Sources/TerminationWatchdogAtomic.c
  • Sources/TerminationWatchdogAtomic.h
  • cmux-Bridging-Header.h
  • cmux.xcodeproj/project.pbxproj

Comment thread Sources/AppDelegate.swift

This branch was successfully deployed

1 active deployment
Preview – cmux — 9739e5b5 Deployed Jun 29, 2026 by vercel[bot]
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.

Main-thread hang (~30s) on Cmd+Q: CFPasteboardResolveAllPromisedData blocks in terminate: flushing promised clipboard data (0.64.17)

1 participant