From a1cb811cb8cfca1d3ae902bae87a9cd7c696a0ad Mon Sep 17 00:00:00 2001 From: tmdgusya Date: Sun, 3 May 2026 14:12:28 +0900 Subject: [PATCH] fix(cli): avoid voice TTS restart race --- cli.py | 17 ++++++++++++----- tests/tools/test_voice_cli_integration.py | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/cli.py b/cli.py index 472218271f..98370b8383 100644 --- a/cli.py +++ b/cli.py @@ -8375,6 +8375,17 @@ class HermesCLI: _cprint(f"{_DIM}Voice auto-restart failed: {e}{_RST}") threading.Thread(target=_restart_recording, daemon=True).start() + def _voice_speak_response_async(self, text: str) -> None: + """Schedule TTS and mark it pending before continuous recording can restart.""" + if not self._voice_tts or not text: + return + self._voice_tts_done.clear() + threading.Thread( + target=self._voice_speak_response, + args=(text,), + daemon=True, + ).start() + def _voice_speak_response(self, text: str): """Speak the agent's response aloud using TTS (runs in background thread).""" if not self._voice_tts: @@ -9535,11 +9546,7 @@ class HermesCLI: # Speak response aloud if voice TTS is enabled # Skip batch TTS when streaming TTS already handled it if self._voice_tts and response and not use_streaming_tts: - threading.Thread( - target=self._voice_speak_response, - args=(response,), - daemon=True, - ).start() + self._voice_speak_response_async(response) # Re-queue the interrupt message (and any that arrived while we were diff --git a/tests/tools/test_voice_cli_integration.py b/tests/tools/test_voice_cli_integration.py index e7d8811e02..93dffa649a 100644 --- a/tests/tools/test_voice_cli_integration.py +++ b/tests/tools/test_voice_cli_integration.py @@ -1040,6 +1040,25 @@ class TestDisableVoiceModeReal: class TestVoiceSpeakResponseReal: """Tests _voice_speak_response with real CLI instance.""" + def test_async_scheduling_clears_done_before_thread_start(self): + cli = _make_voice_cli(_voice_tts=True) + starts = [] + + class FakeThread: + def __init__(self, target=None, args=(), daemon=None): + self.target = target + self.args = args + self.daemon = daemon + + def start(self): + starts.append(cli._voice_tts_done.is_set()) + + with patch("cli.threading.Thread", FakeThread): + cli._voice_speak_response_async("Hello") + + assert starts == [False] + assert not cli._voice_tts_done.is_set() + @patch("cli._cprint") def test_early_return_when_tts_off(self, _cp): cli = _make_voice_cli(_voice_tts=False)