fix(cli): prevent process_loop freeze from MCP reload join and voice flag leak

Two narrow fixes that contribute to the TUI input black hole reported in
issue #16803, where the CLI keeps rendering but stops consuming user input.

1. MCP reload no longer blocks process_loop. _check_config_mcp_changes()
   runs in process_loop's idle branch; the prior _reload_thread.join(timeout=30)
   froze input consumption for up to 30s (longer if an MCP server hung). The
   reload daemon already reports its own status via print(), so the join is
   removed and the reload runs purely in the background.

2. Voice recording flag can no longer leak. _voice_recording was set True
   before create_audio_recorder(), which runs outside any try/except. A
   recorder-creation failure (no input device, PortAudio init error) left the
   flag stuck True, so every future voice start was silently skipped by the
   double-start guard. Recorder creation is now wrapped to reset the flag and
   re-raise on failure, matching the existing start() handler.

Closes #16803
This commit is contained in:
HiddenPuppy 2026-06-30 16:55:54 -07:00 committed by Teknium
parent 36bfe3a449
commit 972aa33d37

18
cli.py
View file

@ -10107,9 +10107,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
target=self._reload_mcp, daemon=True
)
_reload_thread.start()
_reload_thread.join(timeout=30)
if _reload_thread.is_alive():
print(" ⚠️ MCP reload timed out (30s). Some servers may not have reconnected.")
# Do NOT join here — process_loop calls this from its idle branch, so a
# blocking join would freeze input consumption for up to 30s (and a hung
# MCP server could block far longer). The reload runs purely in the
# background daemon thread, which reports its own progress/completion
# status via print() inside _reload_mcp().
# Inline-skip tokens that bypass the destructive-slash confirmation modal.
# A general escape hatch for non-interactive use (scripting/automation) and
@ -10737,8 +10739,16 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
except Exception:
pass
# Recorder creation can fail (no input device, PortAudio init error).
# Reset the flag on failure or _voice_recording stays True forever and
# every future voice start is silently skipped by the guard above.
if self._voice_recorder is None:
self._voice_recorder = create_audio_recorder()
try:
self._voice_recorder = create_audio_recorder()
except Exception:
with self._voice_lock:
self._voice_recording = False
raise
# Apply config-driven silence params (numeric-guarded so YAML
# scalar corruption doesn't break recording start-up).