Ciclo de vida del overlay de voz (macOS)

Audiencia: contribuidores de la app macOS. Objetivo: mantener el overlay de voz predecible cuando la activación por voz y push-to-talk se superponen.

Intención actual

  • Si el overlay ya es visible por la activación por voz y el usuario presiona el hotkey, la sesión del hotkey adopta el texto existente en lugar de resetearlo. El overlay permanece visible mientras el hotkey está presionado. Cuando el usuario suelta: envía si hay texto con contenido, de lo contrario descarta.
  • La activación por voz sola sigue auto-enviando en silencio; push-to-talk envía inmediatamente al soltar.

Implementado (9 dic, 2025)

  • Las sesiones del overlay ahora llevan un token por captura (activación por voz o push-to-talk). Las actualizaciones partial/final/send/dismiss/level se descartan cuando el token no coincide, evitando callbacks obsoletos.
  • Push-to-talk adopta cualquier texto visible del overlay como prefijo (así que presionar el hotkey mientras el overlay de activación está visible mantiene el texto y añade nueva voz). Espera hasta 1.5s por una transcripción final antes de recurrir al texto actual.
  • El logging de chime/overlay se emite a nivel info en las categorías voicewake.overlay, voicewake.ptt y voicewake.chime (inicio de sesión, parcial, final, envío, descarte, razón del chime).

Próximos pasos

  1. VoiceSessionCoordinator (actor)
    • Gestiona exactamente una VoiceSession a la vez.
    • API (basada en tokens): beginWakeCapture, beginPushToTalk, updatePartial, endCapture, cancel, applyCooldown.
    • Descarta callbacks que lleven tokens obsoletos (evita que reconocedores antiguos reabran el overlay).
  2. VoiceSession (modelo)
    • Campos: token, source (wakeWord|pushToTalk), texto committed/volatile, flags de chime, timers (auto-send, idle), overlayMode (display|editing|sending), deadline de cooldown.
  3. Binding del overlay
    • VoiceSessionPublisher (ObservableObject) refleja la sesión activa en SwiftUI.
    • VoiceWakeOverlayView renderiza solo vía el publisher; nunca muta singletons globales directamente.
    • Las acciones del usuario en el overlay (sendNow, dismiss, edit) llaman de vuelta al coordinador con el token de sesión.
  4. Ruta de envío unificada
    • En endCapture: si el texto trimmed está vacío → descartar; si no performSend(session:) (reproduce chime de envío una vez, reenvía, descarta).
    • Push-to-talk: sin retraso; activación por voz: retraso opcional para auto-envío.
    • Aplica un cooldown corto al runtime de activación después de que push-to-talk termina para que la activación no se re-dispare inmediatamente.
  5. Logging
    • El coordinador emite logs .info en el subsistema ai.openclaw, categorías voicewake.overlay y voicewake.chime.
    • Eventos clave: session_started, adopted_by_push_to_talk, partial, finalized, send, dismiss, cancel, cooldown.

Lista de verificación de depuración

  • Transmite logs mientras reproduces un overlay atascado:

    sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact
  • Verifica que solo hay un token de sesión activo; los callbacks obsoletos deben ser descartados por el coordinador.

  • Asegúrate de que al soltar push-to-talk siempre se llama endCapture con el token activo; si el texto está vacío, espera dismiss sin chime ni envío.

Pasos de migración (sugeridos)

  1. Añade VoiceSessionCoordinator, VoiceSession y VoiceSessionPublisher.
  2. Refactoriza VoiceWakeRuntime para crear/actualizar/terminar sesiones en lugar de tocar VoiceWakeOverlayController directamente.
  3. Refactoriza VoicePushToTalk para adoptar sesiones existentes y llamar endCapture al soltar; aplica cooldown al runtime.
  4. Conecta VoiceWakeOverlayController al publisher; elimina llamadas directas desde runtime/PTT.
  5. Añade tests de integración para adopción de sesión, cooldown y descarte con texto vacío.