From 43b096eedb89d74d64584b9767bcb5a906dbe8d9 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Wed, 10 Jun 2026 20:23:25 +0530 Subject: [PATCH] tui_gateway: blocking prompts wait for the human (drop _block timeouts) --- tests/test_tui_gateway_server.py | 84 ++++++++++++++++++++++++++++++++ tui_gateway/server.py | 8 ++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index dd93fda30e4..ee9b072f9c3 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -3580,6 +3580,90 @@ def test_respond_unpacks_sid_tuple_correctly(): server._answers.pop("rid-x", None) +# --------------------------------------------------------------------------- +# Blocking prompts wait for the human (v6 north-star #5): _block with +# timeout=None must never expire — interrupt/shutdown (_clear_pending) +# are the only releases. +# --------------------------------------------------------------------------- + + +def _run_block_in_thread(monkeypatch, sid): + """Start _block(timeout=None) on a background thread; return + (thread, results, get_rid) where get_rid polls for the pending rid.""" + monkeypatch.setattr(server, "_emit", lambda *a, **kw: None) + results: list[str] = [] + + def runner(): + results.append(server._block("clarify.request", sid, {"question": "q"})) + + t = threading.Thread(target=runner, daemon=True) + t.start() + + def get_rid(): + deadline = time.time() + 5 + while time.time() < deadline: + with server._prompt_lock: + for rid, (owner, _ev) in server._pending.items(): + if owner == sid: + return rid + time.sleep(0.01) + raise AssertionError("pending rid never appeared for sid=%s" % sid) + + return t, results, get_rid + + +def test_block_no_timeout_waits_for_delayed_answer(monkeypatch): + """_block(timeout=None) must keep blocking until the answer arrives — + no premature empty return.""" + t, results, get_rid = _run_block_in_thread(monkeypatch, "sid_block_wait") + rid = get_rid() + + # Answer after a short delay; _block must still be waiting. + time.sleep(0.3) + assert t.is_alive(), "_block returned before any answer was provided" + with server._prompt_lock: + server._answers[rid] = "green" + server._pending[rid][1].set() + + t.join(timeout=5) + assert not t.is_alive() + assert results == ["green"] + + +def test_clear_pending_releases_no_timeout_block(monkeypatch): + """_clear_pending(sid) must release a timeout=None _block with ''.""" + t, results, get_rid = _run_block_in_thread(monkeypatch, "sid_block_clear") + get_rid() + + server._clear_pending("sid_block_clear") + t.join(timeout=5) + assert not t.is_alive() + assert results == [""] + + +def test_clear_pending_other_sid_does_not_release_block(monkeypatch): + """_clear_pending on an unrelated session must NOT release a pending + timeout=None _block (session scoping).""" + t, results, get_rid = _run_block_in_thread(monkeypatch, "sid_block_scoped") + rid = get_rid() + + server._clear_pending("sid_some_other_session") + time.sleep(0.2) + assert t.is_alive(), ( + "_clear_pending on another sid released a prompt owned by " + "sid_block_scoped — session scoping is broken" + ) + assert not results + + # Clean up: release properly so the thread joins. + with server._prompt_lock: + server._answers[rid] = "done" + server._pending[rid][1].set() + t.join(timeout=5) + assert not t.is_alive() + assert results == ["done"] + + # --------------------------------------------------------------------------- # /model switch and other agent-mutating commands must reject while the # session is running. agent.switch_model() mutates self.model, self.provider, diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 19fe603b721..292b8b95c27 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1066,7 +1066,11 @@ def _enable_gateway_prompts() -> None: # ── Blocking prompt factory ────────────────────────────────────────── -def _block(event: str, sid: str, payload: dict, timeout: int = 300) -> str: +def _block(event: str, sid: str, payload: dict, timeout: int | None = None) -> str: + # Blocking prompts wait for the human; interrupt/shutdown (via + # _clear_pending) are the only releases — v6 north-star #5. A + # timeout would orphan the TUI prompt and silently feed the agent + # an empty answer, so callers default to waiting forever. rid = uuid.uuid4().hex[:8] ev = threading.Event() with _prompt_lock: @@ -2328,7 +2332,7 @@ def _wire_callbacks(sid: str): from tools.terminal_tool import set_sudo_password_callback from tools.skills_tool import set_secret_capture_callback - set_sudo_password_callback(lambda: _block("sudo.request", sid, {}, timeout=120)) + set_sudo_password_callback(lambda: _block("sudo.request", sid, {})) def secret_cb(env_var, prompt, metadata=None): pl = {"prompt": prompt, "env_var": env_var}