From 9d58cafec94befc659fcc83054ed97bb06279f4b Mon Sep 17 00:00:00 2001 From: 0xbyt4 <35742124+0xbyt4@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:31:50 +0300 Subject: [PATCH] fix: move process_loop voice restart to daemon thread, use _cprint consistently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - process_loop's continuous mode restart called _voice_start_recording() directly, blocking the loop if play_beep/sd.wait hangs — queued user input would stall silently. Dispatch to daemon thread like Ctrl+B handler. - Replace print() with _cprint() in _handle_voice_command for consistency with the rest of the voice mode code. --- cli.py | 28 +++++++++++++---------- tests/tools/test_voice_cli_integration.py | 7 +++--- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/cli.py b/cli.py index e2ee9a267..98476a427 100755 --- a/cli.py +++ b/cli.py @@ -3772,8 +3772,8 @@ class HermesCLI: else: self._enable_voice_mode() else: - print(f"Unknown voice subcommand: {subcommand}") - print("Usage: /voice [on|off|tts|status]") + _cprint(f"Unknown voice subcommand: {subcommand}") + _cprint("Usage: /voice [on|off|tts|status]") def _enable_voice_mode(self): """Enable voice mode after checking requirements.""" @@ -5602,17 +5602,21 @@ class HermesCLI: self._spinner_text = "" app.invalidate() # Refresh status line - # Continuous voice: auto-restart recording after agent responds + # Continuous voice: auto-restart recording after agent responds. + # Dispatch to a daemon thread so play_beep (sd.wait) and + # AudioRecorder.start (lock acquire) never block process_loop — + # otherwise queued user input would stall silently. if self._voice_mode and self._voice_continuous and not self._voice_recording: - try: - # Wait for TTS to finish so we don't record the speaker - if self._voice_tts: - self._voice_tts_done.wait(timeout=60) - time.sleep(0.3) # Brief pause after TTS ends - self._voice_start_recording() - app.invalidate() - except Exception as e: - _cprint(f"{_DIM}Voice auto-restart failed: {e}{_RST}") + def _restart_recording(): + try: + if self._voice_tts: + self._voice_tts_done.wait(timeout=60) + time.sleep(0.3) + self._voice_start_recording() + app.invalidate() + except Exception as e: + _cprint(f"{_DIM}Voice auto-restart failed: {e}{_RST}") + threading.Thread(target=_restart_recording, daemon=True).start() except Exception as e: print(f"Error: {e}") diff --git a/tests/tools/test_voice_cli_integration.py b/tests/tools/test_voice_cli_integration.py index e7be698d3..105b27fc4 100644 --- a/tests/tools/test_voice_cli_integration.py +++ b/tests/tools/test_voice_cli_integration.py @@ -875,16 +875,15 @@ class TestHandleVoiceCommandReal: cli._handle_voice_command("/voice") cli._enable_voice_mode.assert_called_once() - @patch("builtins.print") @patch("cli._cprint") - def test_unknown_subcommand(self, _cp, mock_print): + def test_unknown_subcommand(self, mock_cp): cli = self._cli() cli._handle_voice_command("/voice foobar") cli._enable_voice_mode.assert_not_called() cli._disable_voice_mode.assert_not_called() - # Should print usage via print() (not _cprint) + # Should print usage via _cprint assert any("Unknown" in str(c) or "unknown" in str(c) - for c in mock_print.call_args_list) + for c in mock_cp.call_args_list) class TestEnableVoiceModeReal: