fix(tui_gateway): end turn audio before settlement, defer only the trim
Gate finding (2c + quality, convergent): the settle-before-trim split moved the thinking-sound stop and the TTS end-of-text sentinel after session["running"] = False. Both are non-blocking (a lock + Event.set; an unbounded queue put), so deferring them gained nothing, and it opened a window where a prompt admitted on the same session during the trim had its thinking sound stopped by the old thread and the old stream's final chunk cut as a barge-in. They run in _release_turn_scopes again, before settlement; _post_turn_housekeeping keeps only the trim and the HERMES_HOME override reset. (The original commit subject says "trim/TTS"; only the trim is deferred now.) The settle-before-trim test now also asserts both audio teardown steps ran while the session was still running.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user