{
  "title": "Voice Overlay Lifecycle (macOS)",
  "content": "Audience: macOS app contributors. Goal: keep the voice overlay predictable when wake-word and push-to-talk overlap.\n\n* If the overlay is already visible from wake-word and the user presses the hotkey, the hotkey session *adopts* the existing text instead of resetting it. The overlay stays up while the hotkey is held. When the user releases: send if there is trimmed text, otherwise dismiss.\n* Wake-word alone still auto-sends on silence; push-to-talk sends immediately on release.\n\n### Implemented (Dec 9, 2025)\n\n* Overlay sessions now carry a token per capture (wake-word or push-to-talk). Partial/final/send/dismiss/level updates are dropped when the token doesn’t match, avoiding stale callbacks.\n* Push-to-talk adopts any visible overlay text as a prefix (so pressing the hotkey while the wake overlay is up keeps the text and appends new speech). It waits up to 1.5s for a final transcript before falling back to the current text.\n* Chime/overlay logging is emitted at `info` in categories `voicewake.overlay`, `voicewake.ptt`, and `voicewake.chime` (session start, partial, final, send, dismiss, chime reason).\n\n1. **VoiceSessionCoordinator (actor)**\n   * Owns exactly one `VoiceSession` at a time.\n   * API (token-based): `beginWakeCapture`, `beginPushToTalk`, `updatePartial`, `endCapture`, `cancel`, `applyCooldown`.\n   * Drops callbacks that carry stale tokens (prevents old recognizers from reopening the overlay).\n2. **VoiceSession (model)**\n   * Fields: `token`, `source` (wakeWord|pushToTalk), committed/volatile text, chime flags, timers (auto-send, idle), `overlayMode` (display|editing|sending), cooldown deadline.\n3. **Overlay binding**\n   * `VoiceSessionPublisher` (`ObservableObject`) mirrors the active session into SwiftUI.\n   * `VoiceWakeOverlayView` renders only via the publisher; it never mutates global singletons directly.\n   * Overlay user actions (`sendNow`, `dismiss`, `edit`) call back into the coordinator with the session token.\n4. **Unified send path**\n   * On `endCapture`: if trimmed text is empty → dismiss; else `performSend(session:)` (plays send chime once, forwards, dismisses).\n   * Push-to-talk: no delay; wake-word: optional delay for auto-send.\n   * Apply a short cooldown to the wake runtime after push-to-talk finishes so wake-word doesn’t immediately retrigger.\n5. **Logging**\n   * Coordinator emits `.info` logs in subsystem `bot.molt`, categories `voicewake.overlay` and `voicewake.chime`.\n   * Key events: `session_started`, `adopted_by_push_to_talk`, `partial`, `finalized`, `send`, `dismiss`, `cancel`, `cooldown`.\n\n### Debugging checklist\n\n* Stream logs while reproducing a sticky overlay:\n\n* Verify only one active session token; stale callbacks should be dropped by the coordinator.\n\n* Ensure push-to-talk release always calls `endCapture` with the active token; if text is empty, expect `dismiss` without chime or send.\n\n### Migration steps (suggested)\n\n1. Add `VoiceSessionCoordinator`, `VoiceSession`, and `VoiceSessionPublisher`.\n2. Refactor `VoiceWakeRuntime` to create/update/end sessions instead of touching `VoiceWakeOverlayController` directly.\n3. Refactor `VoicePushToTalk` to adopt existing sessions and call `endCapture` on release; apply runtime cooldown.\n4. Wire `VoiceWakeOverlayController` to the publisher; remove direct calls from runtime/PTT.\n5. Add integration tests for session adoption, cooldown, and empty-text dismissal.",
  "code_samples": [],
  "headings": [
    {
      "level": "h3",
      "text": "Current intent",
      "id": "current-intent"
    },
    {
      "level": "h3",
      "text": "Implemented (Dec 9, 2025)",
      "id": "implemented-(dec-9,-2025)"
    },
    {
      "level": "h3",
      "text": "Next steps",
      "id": "next-steps"
    },
    {
      "level": "h3",
      "text": "Debugging checklist",
      "id": "debugging-checklist"
    },
    {
      "level": "h3",
      "text": "Migration steps (suggested)",
      "id": "migration-steps-(suggested)"
    }
  ],
  "url": "llms-txt#voice-overlay-lifecycle-(macos)",
  "links": []
}