From d9b1b41c12015cb0572ef9fa3b39e37ccdbbcab7 Mon Sep 17 00:00:00 2001 From: StuBehan Date: Fri, 10 Jul 2026 13:25:13 +0100 Subject: [PATCH] fix(update): restart the stackvox daemon after an app update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-app updater swaps the new bundle (with a fresh stackvox venv) into place but only kickstarted the panel agent — never the daemon. So the long-running `stackvox serve` kept executing the pre-update engine until the next login or crash, and engine fixes (streaming playback, pronunciation, ...) silently did not take effect after updating. Parameterize kickstartLaunchd(label:) and kickstart the daemon agent (com.stackonehq.stack-nudge-daemon) after the atomic swap. Non-fatal: launchd KeepAlive covers it on next login, and shell installs without the daemon agent just get a harmless no-op kickstart. Co-Authored-By: Claude Opus 4.8 (1M context) --- panel/Updater.swift | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/panel/Updater.swift b/panel/Updater.swift index 9cbcef3..61edc29 100644 --- a/panel/Updater.swift +++ b/panel/Updater.swift @@ -203,6 +203,7 @@ final class Updater { setPhase(.done) appendLog("Restarting…") try kickstartLaunchd() + restartDaemon() // pick up the stackvox engine the swap just installed relaunchInstalledBundle() scheduleAutoQuit() } @@ -480,12 +481,12 @@ final class Updater { // MARK: - Launchd - private func kickstartLaunchd() throws { + private func kickstartLaunchd(_ label: String = Bootstrap.appLabel) throws { let uid = getuid() let task = Process() task.executableURL = URL(fileURLWithPath: "/bin/launchctl") task.arguments = ["kickstart", "-k", - "gui/\(uid)/\(Bootstrap.appLabel)"] + "gui/\(uid)/\(label)"] task.standardOutput = Pipe() task.standardError = Pipe() try task.run() @@ -494,7 +495,23 @@ final class Updater { // Not fatal — the kickstart can fail if the agent isn't loaded // (e.g. fresh dev install). The new bundle is in place; user // can hit the hotkey to launch it manually next time. - appendLog("launchctl kickstart exited \(task.terminationStatus) (non-fatal)") + appendLog("launchctl kickstart \(label) exited \(task.terminationStatus) (non-fatal)") + } + } + + /// Restart the stackvox TTS daemon so it re-execs the engine the atomic + /// swap just placed on disk. The daemon is a long-running `stackvox serve` + /// launchd job; without an explicit kickstart it keeps running the + /// pre-update engine until the next login or crash, so engine fixes + /// (streaming playback, pronunciation, …) silently don't take effect after + /// an app update. Non-fatal: launchd KeepAlive brings it back on next login + /// regardless, and shell installs that never registered the daemon agent + /// simply get a harmless "not loaded" kickstart. + private func restartDaemon() { + do { + try kickstartLaunchd(Bootstrap.daemonLabel) + } catch { + appendLog("daemon kickstart failed: \(error) (non-fatal)") } }