Skip to content

Commit daa159d

Browse files
AVADSA25Mikarina13claude
authored
fix(voice): L2 — VoicePipeline reliability (review sweep) (#172)
Nine findings from the codec_voice.py review. The voice pipeline is the live WebSocket loop, so each change is behavior-preserving on the happy path and guarded on the failure paths. 1. WebSocket send-after-close (CRITICAL) _safe_send_bytes / _safe_send_json + a self._ws_alive flag flipped False on disconnect (in _audio_receiver). _speak and the crew callback now no-op on a dead socket instead of raising — a client disconnect during a multi-minute crew no longer surfaces an exception out of the detached callback. 2. Fire-and-forget tasks (CRITICAL/HIGH) warmup_llm is now stored (self._warmup_task), gets an exception-logging done_callback, and is cancelled in run()'s finally (was orphaned → GC-able mid-flight + un-retrieved-future warnings). The two screen-overlay run_in_executor futures (orphaned + deprecated get_event_loop()) became a guarded _spawn_detached() — Popen is already non-blocking. 3. Double save_to_memory (HIGH) Both run()'s finally AND routes/websocket.py's finally call it → every voice turn was written to memory.db twice. Now idempotent via a _memory_saved flag. 4. _resolve_voice_option_choice longest-match (HIGH) The exact-substring loop returned the FIRST matching label, so "yes" beat "yes and notify" → mis-routed a non-strict multi-option answer. Now prefers the longest match. (Strict consent is unaffected — it bypasses this resolver.) 5. Echo-cooldown dropped barge-in audio (HIGH) feed_audio returned None unconditionally during the post-TTS cooldown, so a user already mid-utterance lost their leading words ("...end my email"). Now only a NEW speech start is suppressed; in-progress capture continues. 6. Unbounded audio_buffer (MEDIUM) Continuous mic noise above the VAD threshold kept last_speech_time fresh, so the silence gate never fired and the buffer grew without bound (~32 KB/s). Force-flush at MAX_UTTERANCE_BYTES (config: vad.max_utterance_seconds, 30s). 7. utterance_queue head-of-line block (MEDIUM) await queue.put() on the maxsize=3 queue blocked the receiver while the pipeline was slow — stalling interrupt/ping control frames. New _enqueue_utterance() is non-blocking and drops the oldest on overflow. 8. _stream_qwen error sentinel pollution (MEDIUM) "Sorry, I had a processing error." was appended to self.messages + saved to memory, so the LLM "saw" a fake apology it never reasoned. A _stream_error flag now skips persisting it (the user still HEARS it). 9. _announced_question_ids premature mark (HIGH) The poll marked a question announced BEFORE the announce ran; a failed/closed announce left the user answering an unheard question. Now the caller marks it only after a successful announce, so a failure retries next poll. Test surface: tests/test_voice_reliability_l2.py (10 tests) + updated test_voice_ask_user::test_poll_skips_already_announced_question for the new mark-on-success contract. Existing 13 voice-pipeline + 29 ask-user tests pass. Full suite: 2,065 passed / 77 skipped. ruff clean. Co-authored-by: Mickael Farina <farina.mickael@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 929c79c commit daa159d

3 files changed

Lines changed: 324 additions & 35 deletions

File tree

0 commit comments

Comments
 (0)