Voice Overlay Lifecycle for macOS: Wake-Word and Push-to-Talk Overlap
This page explains how the voice overlay behaves when both wake-word activation and push-to-talk are used together. It is intended for macOS app contributors who need to ensure predictable overlay behavior.
Read this when
- Adjusting voice overlay behavior
Voice Overlay Lifecycle (macOS)
Intended for macOS app contributors. The objective is to ensure the voice overlay behaves predictably when both wake-word activation and push-to-talk are used together.
Behavior
- When the overlay is already displayed due to a wake-word trigger and the user activates the hotkey, the push-to-talk session takes over the current text rather than clearing it. The overlay remains visible as long as the hotkey is pressed. Releasing the key sends the text if it contains non-whitespace characters; otherwise, the overlay is closed.
- A wake-word activation continues to transmit automatically after a period of silence, while push-to-talk transmits immediately upon releasing the key.
Implementation
VoiceSessionCoordinator(apps/macos/Sources/OpenClaw/VoiceSessionCoordinator.swift) is the sole manager of the active voice session. It operates as a@MainActor @Observablesingleton rather than an actor. Its public interface includesstartSession,updatePartial,finalize,sendNow,dismiss,updateLevel, andsnapshot. Every session is assigned aUUIDtoken; any call that provides an outdated or mismatched token is ignored.VoiceWakeOverlayController(VoiceWakeOverlayController+Session.swift) is responsible for displaying the overlay and relaying user interactions (requestSend,dismiss) back to the coordinator using the session token. It does not maintain any session state on its own.- When push-to-talk (
VoicePushToTalk.begin()) is used, it takes the text already shown in the overlay and treats it asadoptedPrefix(by callingVoiceSessionCoordinator.shared.snapshot()). This means pressing the hotkey while the wake-word overlay is active preserves the existing text and adds any new speech to it. After the key is released, the system waits up to 1.5 seconds for a final transcription result, then falls back to whatever text is currently available. - On
dismiss, the overlay invokesVoiceSessionCoordinator.overlayDidDismiss, which in turn callsVoiceWakeRuntime.refresh(state:). This ensures that closing the overlay manually, dismissing it because the text is empty, or dismissing it after sending all resume wake-word listening. - The unified send logic works as follows: if the trimmed text is empty, the overlay is dismissed. Otherwise,
sendNowplays the send chime once, the text is forwarded viaVoiceWakeForwarder, and then the overlay is dismissed.
Logging
The voice subsystem is ai.openclaw. Each component logs messages under its own category:
| Category | Component |
|---|---|
voicewake.coordinator | VoiceSessionCoordinator |
voicewake.overlay | VoiceWakeOverlayController/VoiceWakeOverlay |
voicewake.ptt | Push-to-talk hotkey and capture |
voicewake.runtime | Wake-word runtime |
voicewake.chime | Chime playback |
voicewake.sync | Global settings sync |
voicewake.forward | Transcript forwarding |
voicewake.meter | Mic level monitor |
Debugging checklist
-
Stream logs while reproducing a persistent overlay:
sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact -
Confirm that only one active session token exists at a time. The coordinator drops any stale callbacks.
-
Verify that releasing push-to-talk always calls
end()with the current active token. If the text is empty, expect the overlay to be dismissed without playing the chime or sending.