fix(tui): give the stdio TUI discovery a retry-after-zero-connected path

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.
This commit is contained in:
Teknium 2026-07-21 05:50:40 -07:00
parent 8b6e92acf5
commit c54b568a38
2 changed files with 71 additions and 1 deletions

View file

@ -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 == []

View file

@ -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