From d529736597fd94b0c79ec02c240caf32bb9605c3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 28 Dec 2025 10:17:30 +0000 Subject: [PATCH] fix(macos): fully stop Voice Wake runtime when disabled --- CHANGELOG.md | 5 +++++ .../Sources/Clawdis/VoiceWakeRuntime.swift | 22 +++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1fd72d59..fd23d5b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.0.0-beta5 — Unreleased + +### Fixes +- macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background. + ## 2.0.0-beta4 — 2025-12-27 ### Fixes diff --git a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift index a91630d23..230b5e505 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeRuntime.swift @@ -37,6 +37,7 @@ actor VoiceWakeRuntime { private var listeningState: ListeningState = .idle private var overlayToken: UUID? private var activeTriggerEndTime: TimeInterval? + private var scheduledRestartTask: Task? // Tunables // Silence threshold once we've captured user speech (post-trigger). @@ -182,21 +183,19 @@ actor VoiceWakeRuntime { } } - private func stop(dismissOverlay: Bool = true) { + private func stop(dismissOverlay: Bool = true, cancelScheduledRestart: Bool = true) { + if cancelScheduledRestart { + self.scheduledRestartTask?.cancel() + self.scheduledRestartTask = nil + } self.captureTask?.cancel() self.captureTask = nil self.isCapturing = false self.capturedTranscript = "" self.captureStartedAt = nil self.triggerChimePlayed = false - self.recognitionTask?.cancel() - self.recognitionTask = nil - self.recognitionRequest?.endAudio() - self.recognitionRequest = nil - self.audioEngine?.inputNode.removeTap(onBus: 0) - self.audioEngine?.stop() - // Release the engine so we also release any audio session/resources when Voice Wake is disabled/stopped. - self.audioEngine = nil + self.haltRecognitionPipeline() + self.recognizer = nil self.currentConfig = nil self.listeningState = .idle self.activeTriggerEndTime = nil @@ -425,7 +424,7 @@ actor VoiceWakeRuntime { private func restartRecognizer() { // Restart the recognizer so we listen for the next trigger with a clean buffer. let current = self.currentConfig - self.stop(dismissOverlay: false) + self.stop(dismissOverlay: false, cancelScheduledRestart: false) if let current { Task { await self.start(with: current) } } @@ -437,7 +436,8 @@ actor VoiceWakeRuntime { } private func scheduleRestartRecognizer(delay: TimeInterval = 0.7) { - Task { [weak self] in + self.scheduledRestartTask?.cancel() + self.scheduledRestartTask = Task { [weak self] in let nanos = UInt64(max(0, delay) * 1_000_000_000) try? await Task.sleep(nanoseconds: nanos) guard let self else { return }