Files
hermes-agent/gateway/run_idle_gates.py
kshitijk4poor ffab36579a refactor(gateway): idle gates live in run_idle_gates; row semantics live with loops/heartbeat
The probe helpers had been appended to the gateway/run.py facade and the three watchers each
carried their own copy of the "probe → fail open → offload" shell, with gateway code parsing
loop/heartbeat rows itself. One sibling now owns the gate shape (_gate + off_loop_gate), and
hermes_cli.loops.store_has_active_loop / hermes_cli.heartbeat.store_has_active_heartbeat own
what an ACTIVE row is (heartbeat gains the _META_PREFIX loops already had). One fail-open layer
per read: has_pending_handoffs is a bare bounded query, the gate catches. The loop watcher calls
its executor hop directly — it already did so unconditionally for the scan, so the
"runner without a hop" fallback there was dead.
2026-09-17 20:56:27 +05:30

67 lines
2.5 KiB
Python

"""Idle gates for the gateway's per-profile pollers (heartbeat restore, handoff watcher, loop wakeup).
Entering ``_profile_runtime_scope`` costs a config.yaml load, a ``.env`` parse, secret hydration and a
terminal-policy build; on a multiplex gateway the pollers paid that per profile per tick with nothing
to do. Each gate answers "does this profile's store hold work?" from the goals-cached SessionDB with
ONLY the HERMES_HOME contextvar installed. Every gate fails OPEN: an unavailable store, a failing
read or a corrupt row is "cannot prove emptiness", never "idle".
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any, Callable, Optional
logger = logging.getLogger("gateway.run")
def _profile_session_db_probe(profile_home: Path) -> Optional[Any]:
"""The goals-cached SessionDB for *profile_home*; None when unavailable."""
from hermes_cli.goals import _get_session_db
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
token = set_hermes_home_override(str(profile_home))
try:
return _get_session_db()
except Exception:
logger.debug("session-db probe failed for %s", profile_home, exc_info=True)
return None
finally:
reset_hermes_home_override(token)
def _gate(profile_home: Path, store_has_work: Callable[[Any], bool]) -> bool:
db = _profile_session_db_probe(profile_home)
if db is None:
return True
try:
return store_has_work(db)
except Exception:
logger.debug("idle probe failed for %s; keeping the full sweep", profile_home, exc_info=True)
return True
def profile_has_active_heartbeat(profile_home: Path) -> bool:
from hermes_cli.heartbeat import store_has_active_heartbeat
return _gate(profile_home, store_has_active_heartbeat)
def profile_has_active_loop(profile_home: Path) -> bool:
from hermes_cli.loops import store_has_active_loop
return _gate(profile_home, store_has_active_loop)
def profile_has_pending_handoff(profile_home: Path) -> bool:
return _gate(profile_home, lambda db: db.has_pending_handoffs())
async def off_loop_gate(runner: object, probe: Callable[[], bool]) -> bool:
"""Run a sync gate through the runner's executor hop. Runners without one (bare test stand-ins
for the handoff watcher) keep the historical always-enter behaviour."""
offload = getattr(runner, "_run_in_executor_with_context", None)
if not callable(offload):
return True
return bool(await offload(probe))