From 0dcc6e2e6cc3c26d68331f19e802c499633a3cb5 Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Mon, 6 Apr 2026 11:02:47 -0400 Subject: [PATCH 1/3] fix: prevent zombie instances on app restart Disconnect connectionManager before daemon stop in performRestart() to prevent autoWakeIfAssistantDied() from fighting with the shutdown. Add isRestarting flag so applicationShouldTerminate returns .terminateNow, skipping the redundant second cli.stop() and fragile async MainActor.run dispatch that could leave the process as a zombie. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../vellum-assistant/App/AppDelegate+AuthLifecycle.swift | 8 ++++++++ clients/macos/vellum-assistant/App/AppDelegate.swift | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift b/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift index 4baff760057..547632a476f 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 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() From 80716e2f218ddb2841b1d7068c04d0f4da0583af Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Mon, 6 Apr 2026 11:07:32 -0400 Subject: [PATCH 2/3] fix: reset isRestarting flag on relaunch failure Co-Authored-By: Claude Opus 4.6 (1M context) --- .../macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift b/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift index 547632a476f..887d19830c0 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift @@ -173,6 +173,7 @@ 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 return } Task { @MainActor [weak self] in From 4becbb776159c5271806a30e3ba3c8834f285de7 Mon Sep 17 00:00:00 2001 From: Vellum Assistant Date: Mon, 6 Apr 2026 11:15:04 -0400 Subject: [PATCH 3/3] fix: reconnect connectionManager on restart relaunch failure Co-Authored-By: Claude Opus 4.6 (1M context) --- .../vellum-assistant/App/AppDelegate+AuthLifecycle.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift b/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift index 887d19830c0..44bba6c72c0 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+AuthLifecycle.swift @@ -174,6 +174,12 @@ extension AppDelegate { // 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