← Back to blog

Hermes Gateway Runtime Readiness Checks Replace Binary Liveness

hermesgatewayobservabilityhealth-checksdevops
Hermes Gateway Runtime Readiness Checks Replace Binary Liveness

Before PR #62650, the Hermes gateway had one health check: GET /health returned {"status": "ok"} if the process was alive. That was it. A server could be running with a corrupt state database, a full disk, or a misconfigured model - and the liveness probe would still report ok.

PR #62645 by teknium1 (merged July 11) adds authenticated runtime readiness checks that report real subsystem state without mutating anything. The public /health endpoint stays unchanged - cheap, no authentication, binary answer. A new /health/detailed endpoint requires authentication and returns the state of eight subsystems.

What gets checked

The new readiness module lives in gateway/readiness.py (117 lines) with a matching test suite in tests/gateway/test_readiness.py (103 lines). The API server integration is 36 lines in gateway/platforms/api_server.py. Total: 316 lines added across 6 files, 5 lines removed.

The collect_runtime_readiness() function probes eight subsystems. Each returns a status (ok or degraded) with structured detail. Here is the breakdown:

Check What it probes Degraded when
state_db SQLite read-only schema query File missing, unreadable, or corrupt
config YAML parse of config.yaml Invalid YAML or non-mapping top level
model Configured model name Empty or missing model string
disk Disk usage on Hermes home directory Usage at or above 90%
gateway Gateway state and platform connectivity State is not "running" or "draining"
active_api_runs Count of in-flight API runs Informational only (count returned)
process_completions Pending background process queue Informational only (count returned)
active_delegations Active delegation count Informational only (count returned)

The background queue metrics (active_api_runs, process_completions, active_delegations) are informational - they return counts without a degraded/ok judgment. The other five checks produce a binary status.

SQLite probe: read-only by design

The state database check uses a read-only connection to avoid competing with normal writers:

uri = f"file:{path.as_posix()}?mode=ro"
with sqlite3.connect(uri, uri=True, timeout=1.0) as conn:
    conn.execute("PRAGMA query_only = ON")
    conn.execute("SELECT name FROM sqlite_master LIMIT 1").fetchone()

This matters for production deployments. A health endpoint polled every few seconds must never take a write lock. The mode=ro URI parameter and PRAGMA query_only are defense-in-depth - either alone would block writes, but both together guard against a misconfigured connection string. The timeout=1.0 prevents the probe from blocking indefinitely if something else holds a lock.

If the database file does not exist yet (a fresh install), the probe returns ok with detail "not initialized" rather than degraded. A missing file is expected state for a new deployment; only an unreadable or corrupt file that does exist triggers degraded status.

What the endpoint does not expose

The readiness contract explicitly limits what gets returned. The collect_runtime_readiness() docstring states:

probes expose status and counts only: never config values,
credentials, paths, commands, queue payloads, or exception messages.

The config check confirms the YAML parses - it does not return any key values. The model check returns ok or degraded - it does not return the model name. Exception messages are replaced with type names (type(exc).__name__). Platform credential states are never exposed.

This is in contrast to health endpoints that return full stack traces or environment details. The design treats /health/detailed as an authenticated but still external-facing surface.

Gateway platform connectivity

The gateway check inspects the runtime status object's platform list and reports how many platforms are connected:

connected = sum(
    1 for value in platforms.values()
    if isinstance(value, dict)
    and str(value.get("state") or value.get("status") or "").lower()
    in {"connected", "running", "ok"}
)

A platform in any other state (disconnected, error, reconnecting) does not count as connected. The response includes connected_platforms and total platforms so operators can see 3/5 connected without SSHing in.

Active API run retention correctness

The active run count uses lifecycle states rather than a simple counter. Retained completed streams - runs that finished but whose output has not been collected - are excluded from the active count. This prevents a long-running Hermes instance from reporting hundreds of "active" runs when most are retained but idle.

Test coverage

The PR's validation table reports 201 targeted gateway tests passing, plus live HTTP end-to-end verification:

Check Result
Targeted gateway tests 201 passed
Public health endpoint ok
Unauthenticated detailed health 401
Authenticated readiness ok
Retained completed run excluded verified

Why this matters for VPS deployments

Hermes Agent is often run on a VPS with no SSH access during routine operation. Before this PR, the only operational signal was process liveness - a binary yes/no. A full disk, a corrupt state database, or a gateway stuck in a degraded state were all invisible to the operator.

The readiness endpoint provides enough information to decide whether a restart resolved an issue without opening a terminal. It does not replace monitoring - 90% disk usage is a coarse threshold, and queue depth counts without rate-of-change data tell you very little. But it replaces the worst case: a server reporting ok while silently failing to do work.

The module is self-contained (117 lines of Python with no framework dependency) and the read-only SQLite pattern is reusable for any health check that touches the state database.

[^1]: teknium1. "feat(gateway): add truthful runtime readiness checks (PR #62645)." NousResearch/hermes-agent. July 11, 2026.

Termagotchi
_

Ryan Underdown

Autodidact. Rarely listens to advice.

Follow on X @catamarammed or GitHub @underdown