From c54b568a3889bb2a200b309501dce5a3ee294a2f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:50:40 -0700 Subject: [PATCH] fix(tui): give the stdio TUI discovery a retry-after-zero-connected path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on fazerluga-creator's #66981 (cherry-picked as the previous two commits). start_background_mcp_discovery()'s retry allowance only fires when the function is CALLED again, but tui_gateway/entry.py main() calls it exactly once at startup — so a first discovery run that connected nothing still latched the stdio TUI MCP-less for the whole session. Re-invoke the idempotent spawn from wait_for_mcp_discovery() (the per-agent-build wait) when the process is MCP-enabled, gated on a flag set in main() so non-MCP sessions never pay the MCP import on the wait path. Adds regression tests for both the retry re-invocation and the non-MCP skip. --- tests/test_tui_entry_mcp_owner.py | 42 +++++++++++++++++++++++++++++++ tui_gateway/entry.py | 30 +++++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/test_tui_entry_mcp_owner.py b/tests/test_tui_entry_mcp_owner.py index 7dbc96d65af3..1022fc4fbf5b 100644 --- a/tests/test_tui_entry_mcp_owner.py +++ b/tests/test_tui_entry_mcp_owner.py @@ -46,3 +46,45 @@ def test_wait_still_joins_entry_local_thread(monkeypatch): entry.wait_for_mcp_discovery(timeout=2.0) assert not thread.is_alive() + + +def test_wait_reinvokes_shared_spawn_when_discovery_enabled(monkeypatch): + """The TUI wait path must give the shared owner a retry opportunity. + + start_background_mcp_discovery() allows a retry after a run that + connected zero servers — but only when it is CALLED again. main() calls + it exactly once, so the per-agent-build wait must re-invoke the + idempotent spawn when this process is MCP-enabled. + """ + monkeypatch.setattr(entry, "_mcp_discovery_thread", None) + monkeypatch.setattr(entry, "_mcp_discovery_enabled", True) + + calls = [] + + def _fake_start(*, logger, thread_name): + calls.append(thread_name) + + monkeypatch.setattr(mcp_startup, "start_background_mcp_discovery", _fake_start) + monkeypatch.setattr(mcp_startup, "_mcp_discovery_thread", None) + + entry.wait_for_mcp_discovery(timeout=0.1) + + assert calls == ["tui-mcp-discovery"] + + +def test_wait_skips_spawn_when_discovery_not_enabled(monkeypatch): + """Non-MCP sessions must not import/spawn discovery on the wait path.""" + monkeypatch.setattr(entry, "_mcp_discovery_thread", None) + monkeypatch.setattr(entry, "_mcp_discovery_enabled", False) + + calls = [] + + def _fake_start(*, logger, thread_name): + calls.append(thread_name) + + monkeypatch.setattr(mcp_startup, "start_background_mcp_discovery", _fake_start) + monkeypatch.setattr(mcp_startup, "_mcp_discovery_thread", None) + + entry.wait_for_mcp_discovery(timeout=0.1) + + assert calls == [] diff --git a/tui_gateway/entry.py b/tui_gateway/entry.py index 089463f8aa44..a81c0238eb9d 100644 --- a/tui_gateway/entry.py +++ b/tui_gateway/entry.py @@ -29,6 +29,17 @@ logger = logging.getLogger(__name__) # the agent snapshots its tool list (see wait_for_mcp_discovery). _mcp_discovery_thread = None +# True once main() decided this TUI process has MCP servers configured and +# spawned discovery through the shared owner. Lets wait_for_mcp_discovery +# re-invoke the (idempotent) spawn on later agent builds so the +# retry-after-zero-connected allowance in +# hermes_cli.mcp_startup.start_background_mcp_discovery can actually fire for +# the stdio TUI — without this, main()'s single spawn is the only call and a +# first run that connected nothing latches the process MCP-less. Kept as a +# flag (rather than re-probing config) so non-MCP sessions never pay the +# tools.mcp_tool import on the per-agent-build wait path. +_mcp_discovery_enabled = False + def _install_sidecar_publisher() -> None: """Mirror every dispatcher emit to the dashboard sidebar via WS. @@ -233,7 +244,22 @@ def wait_for_mcp_discovery(timeout: "float | None" = None) -> None: thread.join(timeout=bound) return # The stdio TUI spawns discovery via the shared owner (see main()); wait - # on it so the first agent build still catches fast servers. + # on it so the first agent build still catches fast servers. Re-invoke + # the idempotent spawn first: if the previous run finished with zero + # connected servers, start_background_mcp_discovery's + # retry-after-zero-connected allowance kicks off a fresh discovery run + # here instead of leaving the TUI latched MCP-less for the session. + if _mcp_discovery_enabled: + try: + from hermes_cli.mcp_startup import start_background_mcp_discovery + + start_background_mcp_discovery( + logger=logger, thread_name="tui-mcp-discovery" + ) + except Exception: + logger.debug( + "TUI MCP discovery retry-spawn failed", exc_info=True + ) try: from hermes_cli.mcp_startup import ( wait_for_mcp_discovery as _startup_wait, @@ -345,6 +371,8 @@ def main(): # instead of latching the process into a no-MCP-tools state. # wait_for_mcp_discovery/mcp_discovery_in_flight/ # join_mcp_discovery below already consult that owner. + global _mcp_discovery_enabled + _mcp_discovery_enabled = True try: from hermes_cli.mcp_startup import start_background_mcp_discovery