diff --git a/tests/tui_gateway/test_prompt_accept_logging.py b/tests/tui_gateway/test_prompt_accept_logging.py index 4a482dc81b..c1ffcda592 100644 --- a/tests/tui_gateway/test_prompt_accept_logging.py +++ b/tests/tui_gateway/test_prompt_accept_logging.py @@ -107,8 +107,10 @@ def test_accepted_and_finished_records_on_success(turn_env, caplog): def test_turn_settles_before_post_turn_trim(monkeypatch, caplog): - """A blocked post-turn trim must not hold the session running or its bookend (#131740).""" + """A blocked post-turn trim must not hold the session running or its bookend (#131740); + turn audio still ends BEFORE settlement, so a next turn admitted during the trim keeps its own.""" import hermes_cli.mem_trim as mem_trim + import tools.voice_mode as voice_mode entered, release = threading.Event(), threading.Event() @@ -122,6 +124,10 @@ def test_turn_settles_before_post_turn_trim(monkeypatch, caplog): monkeypatch.setattr(server, "_sync_agent_model_with_config", lambda sid, session: None) monkeypatch.setattr(server, "_tts_stream_begin", lambda: None) monkeypatch.setattr(server, "_get_usage", lambda agent: {}) + audio_end = [] # (event, session running at that moment) + tts = types.SimpleNamespace(put=lambda x: x is None and audio_end.append(("tts", session["running"]))) + monkeypatch.setattr(server, "_start_turn_voice", lambda: (tts, True)) + monkeypatch.setattr(voice_mode, "stop_thinking_sound", lambda: audio_end.append(("thinking", session["running"]))) agent = types.SimpleNamespace( session_id="agent-sid-1", run_conversation=lambda *a, **k: {"final_response": "done"}, clear_interrupt=lambda: None) @@ -133,6 +139,7 @@ def test_turn_settles_before_post_turn_trim(monkeypatch, caplog): assert entered.wait(timeout=5) assert session["running"] is False assert len(_records(caplog, "tui turn finished")) == 1 + assert audio_end == [("thinking", True), ("tts", True)] finally: release.set() session["_run_thread"].join(timeout=5) diff --git a/tui_gateway/prompt_turn.py b/tui_gateway/prompt_turn.py index 90d4290b9f..7ded53b405 100644 --- a/tui_gateway/prompt_turn.py +++ b/tui_gateway/prompt_turn.py @@ -1028,13 +1028,19 @@ def _recover_turn_exception(sid: str, session: dict, st: _TurnRun, e: BaseExcept def _release_turn_scopes(sid: str, session: dict, st: _TurnRun) -> None: - """Finally-path, before settlement: drop snapshots, undo a one-turn model, reset scopes (home stays bound).""" + """Finally-path, before settlement: drop snapshots, end turn audio, undo a one-turn model, reset scopes (not home).""" # Drop both pre-turn history snapshots before asking glibc to return pages (a test # inspects these two locals by name). history, run_kwargs = st.history, st.run_kwargs history.clear() if isinstance(run_kwargs, dict): run_kwargs.clear() + if st.thinking_started: + with contextlib.suppress(Exception): + from tools.voice_mode import stop_thinking_sound + stop_thinking_sound() + if st.tts_queue is not None: + st.tts_queue.put(None) # end-of-text sentinel — flush + finish speaking if st.one_turn_restore: try: _restore_agent_model_runtime(st.agent, st.one_turn_restore) @@ -1057,7 +1063,7 @@ def _release_turn_scopes(sid: str, session: dict, st: _TurnRun) -> None: def _post_turn_housekeeping(sid: str, session: dict, st: _TurnRun) -> None: - """Best-effort tail AFTER the turn settled: a slow trim/TTS flush must not hold the bookend (#131740).""" + """Best-effort tail AFTER the turn settled: a slow trim must not hold the bookend (#131740).""" try: # while the profile HERMES_HOME override is still active (session's own config) from hermes_cli.mem_trim import trim_memory # Every OTHER session must be idle (#58576). @@ -1065,12 +1071,6 @@ def _post_turn_housekeeping(sid: str, session: dict, st: _TurnRun) -> None: trim_memory(reason="tui turn completion") except Exception: logger.debug("post-turn memory trim failed", exc_info=True) - if st.thinking_started: - with contextlib.suppress(Exception): - from tools.voice_mode import stop_thinking_sound - stop_thinking_sound() - if st.tts_queue is not None: - st.tts_queue.put(None) # end-of-text sentinel — flush + finish speaking if st.scopes.home is not None: reset_hermes_home_override(st.scopes.home)