Skip to content

fix(agent_health): detect profile-scoped gateway.pid to fix false "Gateway not configured" - #2927

Merged
1 commit merged into
nesquena:masterfrom
Carry00:fix/gateway-not-configured-profile-scoped-pid
May 25, 2026
Merged

1 commit merged into
nesquena:masterfrom
Carry00:fix/gateway-not-configured-profile-scoped-pid

Conversation

@Carry00

@Carry00 Carry00 commented May 25, 2026

Copy link
Copy Markdown
Contributor

Problem

When a Hermes gateway runs under a named profile — either via gateway run --profile <name> or because active_profile is set — it writes its runtime files under:

<hermes_root>/profiles/<name>/gateway.pid
<hermes_root>/profiles/<name>/gateway_state.json

_gateway_root_pid_path() in api/agent_health.py unconditionally returned <hermes_root>/gateway.pid (the root-level path), which is never created in profile-scoped deployments. As a result, build_agent_health_payload() always received a non-existent pid_path, fell through to reading the stale root-level gateway_state.json (which retains the last recorded state, often "stopped" from a previous default-profile run), and returned alive=None.

The /api/gateway/status route maps alive=None to configured=false, so the cron/scheduled-jobs page permanently displayed:

Gateway not configuredscheduled ticks need a gateway container or hermes gateway running outside the WebUI.

…even when a profile-scoped gateway was actively running.

Root cause

# Before
def _gateway_root_pid_path() -> Path | None:
    try:
        from hermes_constants import get_default_hermes_root
        return get_default_hermes_root() / _GATEWAY_PID_FILE  # always root-level
    except Exception:
        return None

The root-level gateway.pid is only written when the gateway runs without a named profile. Profile-scoped gateways skip it.

Fix

After failing to find a root-level gateway.pid, fall back to the active profile's directory via get_active_hermes_home():

root_pid = get_default_hermes_root() / _GATEWAY_PID_FILE
if root_pid.exists():
    return root_pid          # root-level wins when present (no behaviour change)
# Fall back to the active profile's directory
profile_pid = Path(get_active_hermes_home()) / _GATEWAY_PID_FILE
if profile_pid.exists():
    return profile_pid
return root_pid              # neither exists — return root path as before

Errors from get_active_hermes_home() are caught and silently ignored so the function retains its previous safe default.

Tests

Five new unit tests in tests/test_agent_health_pid_path_fallback.py:

Test Scenario
test_returns_root_pid_when_root_level_file_exists Root pid present → root path returned (no regression)
test_falls_back_to_profile_pid_when_root_absent Root absent, profile pid present → profile path returned
test_returns_root_path_when_neither_pid_exists Neither exists → root path returned (graceful)
test_returns_root_path_when_profile_lookup_raises get_active_hermes_home() raises → root path returned silently
test_root_takes_priority_over_profile_when_both_exist Both present → root wins

All 15 tests in test_gateway_status_agent_health.py and test_agent_health_pid_path_fallback.py pass.

_gateway_root_pid_path() unconditionally returned <hermes_root>/gateway.pid.
Profile-scoped gateways (started with --profile <name> or via active_profile)
write their runtime files under <hermes_root>/profiles/<name>/ instead of the
root, so the root-level path never existed.

build_agent_health_payload() therefore always received a non-existent pid_path,
fell through to the stale root-level gateway_state.json, and returned alive=None.
This caused the cron/scheduled-jobs page to display "Gateway not configured" even
when a gateway was actively running.

Fix: after failing to find a root-level gateway.pid, fall back to the active
profile directory via get_active_hermes_home(). Root-level wins when it exists,
so deployments that do write there are unaffected. Errors from profile lookup are
swallowed and the root path is returned, preserving the previous safe default.

Adds five focused unit tests covering the new fallback, the priority rule, and
the error-handling path.
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Summary

Reading api/agent_health.py:171-194 on this branch + the existing test pattern in tests/test_issue716_agent_heartbeat.py:78-105, the fix itself is on the right track but the new test module fails in CI for a fixable reason — and the change misses a sibling cause that issue #2935 calls out separately.

Code reference — production change is sound

The branch's _gateway_root_pid_path() now does the right thing:

root_pid = get_default_hermes_root() / _GATEWAY_PID_FILE
if root_pid.exists():
    return root_pid
try:
    from api.profiles import get_active_hermes_home
    profile_pid = Path(get_active_hermes_home()) / _GATEWAY_PID_FILE
    if profile_pid.exists():
        return profile_pid
except Exception:
    pass
return root_pid

Order-preserved (root wins when present, no regression for default-profile setups), failure caught, returns the root path when neither exists so downstream _read_gateway_runtime_status() still hits the same fallback file probe it always did. Confirmed the agent side at ~/.hermes/hermes-agent/gateway/status.py:42-47 writes gateway.pid under get_hermes_home(), which is profile-aware on the agent — so for a profile-scoped gateway the pid genuinely lives at <root>/profiles/<name>/gateway.pid, matching this fallback.

The CI failures are about test scaffolding, not the fix

All five tests fail with ModuleNotFoundError: No module named 'hermes_constants' at tests/test_agent_health_pid_path_fallback.py:24. The agent module isn't installable as a top-level package in the WebUI CI environment — the existing test that touches the exact same code path works around this by injecting a fake module into sys.modules instead of importing it:

# tests/test_issue716_agent_heartbeat.py:89-93
monkeypatch.setitem(
    sys.modules,
    "hermes_constants",
    types.SimpleNamespace(get_default_hermes_root=lambda: root_home),
)

Switching the new _call helper to that pattern (and the same trick for api.profiles.get_active_hermes_home via monkeypatch.setattr on the already-imported module, which you're doing correctly today) should make all five tests pass without changing the production fix.

Sibling cause — see #2935

The handler at api/routes.py:4754 maps alive is None to configured = False unconditionally. PR #2927 fixes one cause of alive=None (wrong pid path resolution). Issue #2935 documents a different cause: in multi-container Docker setups, _gateway_status_module() raises ImportError because gateway.status isn't in the WebUI container's Python path, the except branch at agent_health.py:298-307 returns alive=None, and identity_map from sessions.json (43 active Slack sessions in the example) is then ignored.

Even after this PR lands, that scenario will still show "Gateway not configured" because the gateway code isn't importable at all. A small companion change in api/routes.py:4754-4756configured = bool(identity_map) instead of unconditional False — would close the loop. Worth mentioning in the PR description so reviewers don't assume this PR covers both.

Verification

After updating the test scaffolding, the existing test_agent_health_uses_root_gateway_state_when_hermes_home_is_profile test (which already exercises the fallback path with a sys.modules-injected hermes_constants) should keep passing, and the five new tests should each be greenable individually. The production change needs no further edits.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Merged in Release DG / v0.51.135 (stage-batch17, batch with PRs #2906 #2912 #2917 #2919 #2921 #2922 #2927 #2936 #2940).

Thanks @Carry00! 🚢

huoli4844 pushed a commit to huoli4844/hermes-webui that referenced this pull request May 25, 2026
@Carry00
Carry00 deleted the fix/gateway-not-configured-profile-scoped-pid branch May 25, 2026 18:58
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants