diff --git a/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift b/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift index 4baff760057..44bba6c72c0 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift @@ -144,8 +144,16 @@ extension AppDelegate { } @objc func performRestart() { + isRestarting = true let bundleURL = Bundle.main.bundleURL + // Disconnect SSE and health checks *before* the CLI kills the + // daemon/gateway. Otherwise the health check detects the daemon + // dying, triggers autoWakeIfAssistantDied(), and wakes the daemon + // right back up — fighting with the shutdown. (Same pattern as + // performRetireAsync().) + connectionManager.disconnect() + // Write a timestamped sentinel so the new instance's single-instance // guard knows this is an intentional restart, not a duplicate launch. // The sentinel contains the current Unix timestamp; the new instance @@ -165,6 +173,13 @@ extension AppDelegate { // Clean up the sentinel so a failed restart doesn't leave // a file that could bypass the guard on the next launch. try? FileManager.default.removeItem(at: sentinelPath) + self?.isRestarting = false + // Reconnect SSE and health checks so the app doesn't stay + // in a disconnected state after a failed relaunch attempt. + // (Same pattern as performRetireAsync()'s cancel path.) + Task { @MainActor [weak self] in + try? await self?.connectionManager.connect() + } return } Task { @MainActor [weak self] in diff --git a/clients/macos/vellum-assistant/App/AppDelegate.swift b/clients/macos/vellum-assistant/App/AppDelegate.swift index 8165d236e73..2f0756b72a6 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate.swift @@ -38,6 +38,7 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { var lastRegisteredQuickInputHotkey: String? var globalHotkeyObserver: AnyCancellable? var escapeMonitor: Any? + var isRestarting = false var hasSetupHotKey = false var fnVGlobalMonitor: Any? var fnVLocalMonitor: Any? @@ -745,6 +746,14 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { /// /// Reference: https://developer.apple.com/documentation/appkit/nsapplicationdelegate/applicationshouldterminate(_:) public func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + // During a restart, performRestart() already stopped the CLI and + // disconnected the connection manager. Skip the async + // .terminateLater path whose MainActor.run dispatch is fragile + // during AppKit shutdown and can leave the process as a zombie. + if isRestarting { + return .terminateNow + } + let cli = vellumCli Task.detached { await cli.stop()