From 1f1acc0e4ddd8a39ca10a6aaf832a8029232ecaf Mon Sep 17 00:00:00 2001 From: kshitij <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:41:01 +0530 Subject: [PATCH] fix(dashboard): warm cold check_fn verdicts with a background probe On dashboard-only sessions nothing else executes check_fn warmers (they live only in the tool-schema build), so the hub's read-only cache lookup would report auth_required=False forever. On a cache miss, schedule a deduplicated daemon-thread probe off the request path; the short hub TTL surfaces the verdict on the next fetch. --- hermes_cli/web_server.py | 46 ++++++++++++++++++ .../hermes_cli/test_plugins_hub_perf_guard.py | 47 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 8b8357e707998..3168d44209c3e 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -16646,6 +16646,44 @@ def _invalidate_plugins_hub_cache() -> None: _plugins_hub_cache_expires_at = 0.0 +_plugins_hub_probe_inflight: set = set() +_plugins_hub_probe_lock = threading.Lock() + + +def _schedule_check_fn_probe(fn) -> Optional[threading.Thread]: + """Warm a cold ``check_fn`` verdict off the request path. + + The hub read path only consumes cached availability (never probes + inline). But the only other warmer lives in the tool-schema build, which + a dashboard-only session never runs — so a cold cache would report + ``auth_required=False`` forever. Kick a daemon-thread probe on the miss; + the short hub TTL picks up the verdict on the next fetch. Deduplicates + concurrent probes per function. Returns the spawned thread (or ``None`` + when a probe for *fn* is already in flight). + """ + with _plugins_hub_probe_lock: + if fn in _plugins_hub_probe_inflight: + return None + _plugins_hub_probe_inflight.add(fn) + + def _probe(): + try: + from tools.registry import _check_fn_cached + + _check_fn_cached(fn) + except Exception: + pass + finally: + with _plugins_hub_probe_lock: + _plugins_hub_probe_inflight.discard(fn) + + thread = threading.Thread( + target=_probe, name="plugins-hub-checkfn-probe", daemon=True + ) + thread.start() + return thread + + def _merged_plugins_hub(force_refresh: bool = False) -> Dict[str, Any]: """Agent discovery + dashboard manifests + optional provider picker metadata. @@ -16731,6 +16769,14 @@ def _merged_plugins_hub(force_refresh: bool = False) -> Dict[str, Any]: if not entry or not entry.check_fn: continue cached_result = get_cached_check_fn_result(entry.check_fn) + if cached_result is None: + # Cold cache: nothing else warms check_fns on + # dashboard-only sessions, so kick a background + # probe; the short hub TTL surfaces the verdict on + # the next fetch instead of pinning auth_required + # to False forever. + _schedule_check_fn_probe(entry.check_fn) + continue if cached_result is False: auth_required = True auth_command = f"hermes auth {name}" diff --git a/tests/hermes_cli/test_plugins_hub_perf_guard.py b/tests/hermes_cli/test_plugins_hub_perf_guard.py index 7ef83c4afb8fb..370e425e90fbd 100644 --- a/tests/hermes_cli/test_plugins_hub_perf_guard.py +++ b/tests/hermes_cli/test_plugins_hub_perf_guard.py @@ -1,5 +1,6 @@ from __future__ import annotations +import threading from pathlib import Path from types import SimpleNamespace @@ -41,19 +42,61 @@ def test_plugins_hub_does_not_probe_cold_check_fns(monkeypatch): tools_registry.invalidate_check_fn_cache() web_server._invalidate_plugins_hub_cache() - calls = {"count": 0} + calls = {"count": 0, "threads": set()} def check_fn(): calls["count"] += 1 + calls["threads"].add(threading.current_thread()) return False _patch_minimal_hub_dependencies(monkeypatch, check_fn=check_fn) payload = web_server._merged_plugins_hub(force_refresh=True) - assert calls["count"] == 0 + # The request path itself must never execute the probe: the cold verdict + # is unknown, so the payload reports no auth requirement. Any probing + # happens on a background warmer thread, never inline. assert payload["plugins"][0]["auth_required"] is False assert payload["plugins"][0]["auth_command"] == "" + assert threading.current_thread() not in calls["threads"] + + +def test_plugins_hub_cold_cache_schedules_background_probe(monkeypatch): + tools_registry.invalidate_check_fn_cache() + web_server._invalidate_plugins_hub_cache() + + probe_ran = threading.Event() + + def check_fn(): + probe_ran.set() + return False + + _patch_minimal_hub_dependencies(monkeypatch, check_fn=check_fn) + + scheduled: list = [] + real_schedule = web_server._schedule_check_fn_probe + + def tracking_schedule(fn): + thread = real_schedule(fn) + scheduled.append(thread) + return thread + + monkeypatch.setattr(web_server, "_schedule_check_fn_probe", tracking_schedule) + + # Cold cache → the fetch schedules a background probe and reports the + # verdict as unknown (auth_required stays False for now). + payload = web_server._merged_plugins_hub(force_refresh=True) + assert payload["plugins"][0]["auth_required"] is False + assert scheduled and scheduled[0] is not None + + scheduled[0].join(timeout=5) + assert probe_ran.wait(timeout=5) + + # Once the TTL cache refreshes, the probed False verdict surfaces as an + # auth requirement. + refreshed = web_server._merged_plugins_hub(force_refresh=True) + assert refreshed["plugins"][0]["auth_required"] is True + assert refreshed["plugins"][0]["auth_command"] == "hermes auth demo"