diff --git a/hermes_cli/main.py b/hermes_cli/main.py index f09cde9ec1..dda9825002 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -11166,22 +11166,25 @@ def _prepare_agent_startup(args) -> None: return _accept_hooks = bool(getattr(args, "accept_hooks", False)) - try: - from hermes_cli.plugins import start_background_plugin_discovery + if not _is_tui_chat_launch(args): + # The TUI backend process does its own plugin discovery; the launcher + # only spawns Node, so discovery here would be thrown-away work. + try: + from hermes_cli.plugins import start_background_plugin_discovery - # Discovery runs in a daemon thread so its ~150ms of manifest - # scanning + plugin imports overlaps the rest of startup (cli / - # prompt_toolkit imports, worktree git calls). Correctness is - # unchanged: every synchronous reader goes through - # discover_plugins(), which joins this thread first — including - # the discover_plugins() call model_tools makes at import time, - # which happens before any tool list is built. - start_background_plugin_discovery() - except Exception: - logger.warning( - "plugin discovery failed at CLI startup", - exc_info=True, - ) + # Discovery runs in a daemon thread so its ~150ms of manifest + # scanning + plugin imports overlaps the rest of startup (cli / + # prompt_toolkit imports, worktree git calls). Correctness is + # unchanged: every synchronous reader goes through + # discover_plugins(), which joins this thread first — including + # the discover_plugins() call model_tools makes at import time, + # which happens before any tool list is built. + start_background_plugin_discovery() + except Exception: + logger.warning( + "plugin discovery failed at CLI startup", + exc_info=True, + ) _run_inline_mcp_discovery = True if _is_tui_chat_launch(args): # The TUI launcher hands off to a dedicated startup path that already diff --git a/tests/hermes_cli/test_tui_launcher_skips_plugin_discovery.py b/tests/hermes_cli/test_tui_launcher_skips_plugin_discovery.py new file mode 100644 index 0000000000..e029cbce85 --- /dev/null +++ b/tests/hermes_cli/test_tui_launcher_skips_plugin_discovery.py @@ -0,0 +1,63 @@ + +"""Regression test: the TUI launcher must not spend time on plugin discovery. + +`hermes --tui` just spawns a Node process; the spawned tui_gateway backend +performs its own plugin discovery. Running discover_plugins() in the +launcher added ~0.5s to every `hermes --tui` startup for work the backend +then redoes. Plain chat must still discover plugins. +""" + +from __future__ import annotations + +from argparse import Namespace +import sys +import types + +from hermes_cli import main as main_mod + + +def _install_discover_spy(monkeypatch): + calls = [] + + def _discover(): + calls.append("discover") + + monkeypatch.setitem( + sys.modules, + "hermes_cli.plugins", + types.SimpleNamespace( + discover_plugins=_discover, + # main.py now kicks discovery off in a background thread; both + # entry points count as "discovery work happened in the launcher". + start_background_plugin_discovery=_discover, + ), + ) + return calls + + +def _args(**overrides): + base = { + "accept_hooks": False, + "yolo": False, + "safe_mode": False, + "command": None, + "query": None, + "image": None, + } + base.update(overrides) + return Namespace(**base) + + +def test_plugin_discovery_skipped_for_tui_launch(monkeypatch): + calls = _install_discover_spy(monkeypatch) + main_mod._prepare_agent_startup(_args(tui=True)) + assert calls == [], ( + "Plugin discovery must not run in the TUI launcher: the spawned " + "tui_gateway backend discovers plugins itself." + ) + + +def test_plugin_discovery_runs_for_plain_chat(monkeypatch): + calls = _install_discover_spy(monkeypatch) + main_mod._prepare_agent_startup(_args(tui=False, command="chat")) + assert calls == ["discover"]