Merge pull request 'fix(agent-health): gateway liveness reports a probe failure as a probe failure, never as an agent outage' (#93) from fix/agent-health-gateway-leg-deterministic-20260914 into master
This commit was merged in pull request #93.
This commit is contained in:
@@ -340,6 +340,36 @@ def check_gpu_ports():
|
|||||||
# CHECK 3: Agent Gateway Liveness + Streaming (now covers all agents)
|
# CHECK 3: Agent Gateway Liveness + Streaming (now covers all agents)
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _ssh_retry(host, cmd, user="root", timeout=15, retry_timeout=25, label=""):
|
||||||
|
"""SSH with one retry at a longer timeout.
|
||||||
|
|
||||||
|
Returns (stdout_or_None, probe_failed_bool, fail_kind).
|
||||||
|
When probe_failed is True, fail_kind is one of: timeout, ssh-failed.
|
||||||
|
"""
|
||||||
|
import subprocess as _sp
|
||||||
|
def _attempt(tmo, conn_tmo):
|
||||||
|
try:
|
||||||
|
r = _sp.run(
|
||||||
|
["ssh", "-o", "StrictHostKeyChecking=no", "-o", f"ConnectTimeout={conn_tmo}",
|
||||||
|
f"{user}@{host}", cmd],
|
||||||
|
capture_output=True, text=True, timeout=tmo)
|
||||||
|
return r.stdout.strip() if r.returncode == 0 else None
|
||||||
|
except _sp.TimeoutExpired:
|
||||||
|
return "__timeout__"
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
result = _attempt(timeout, 8)
|
||||||
|
if result is None or result == "__timeout__":
|
||||||
|
kind = "timeout" if result == "__timeout__" else "ssh-failed"
|
||||||
|
prefix = f"{label} " if label else ""
|
||||||
|
print(f" probe-failed: {prefix}ssh {user}@{host} — {kind} (retrying at {retry_timeout}s…)")
|
||||||
|
result = _attempt(retry_timeout, 15)
|
||||||
|
if result is None or result == "__timeout__":
|
||||||
|
kind = "timeout" if result == "__timeout__" else "ssh-failed"
|
||||||
|
return None, True, kind
|
||||||
|
return result, False, None
|
||||||
|
|
||||||
|
|
||||||
def check_agents():
|
def check_agents():
|
||||||
for name, agent in AGENTS.items():
|
for name, agent in AGENTS.items():
|
||||||
host = agent.get("host")
|
host = agent.get("host")
|
||||||
@@ -355,42 +385,49 @@ def check_agents():
|
|||||||
is_dsh = agent.get("runtime") == "dsh"
|
is_dsh = agent.get("runtime") == "dsh"
|
||||||
label = "DSH (DeepSeek Harness)" if is_dsh else "pi-only runtime"
|
label = "DSH (DeepSeek Harness)" if is_dsh else "pi-only runtime"
|
||||||
since = "since 2026-08-27" if is_dsh else "since the harness purge"
|
since = "since 2026-08-27" if is_dsh else "since the harness purge"
|
||||||
live = ssh(host, "true", user=user)
|
live, probe_failed, fail_kind = _ssh_retry(host, "true", user=user)
|
||||||
print(f" {'✅' if live is not None else '❌'} {name}: {label} — "
|
if probe_failed:
|
||||||
f"no Hermes gateway {since} (CT {ct}, SSH {'OK' if live is not None else 'FAIL'})")
|
print(f" ❌ {name}: {label} — probe-failed: ssh {user}@{host} {fail_kind} "
|
||||||
if live is None:
|
f"(retried at 25s: also {fail_kind}) [CT {ct}]")
|
||||||
_fail(f"unreachable:{name}", name)
|
_fail(f"probe-failed:{name}:{fail_kind}", name)
|
||||||
|
else:
|
||||||
|
print(f" ✅ {name}: {label} — no Hermes gateway {since} "
|
||||||
|
f"(ssh {user}@{host} OK, CT {ct})")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not host or not user:
|
if not host or not user:
|
||||||
print(f" ⬜ {name} (CT {ct}): cannot SSH — skip liveness check")
|
print(f" ⬜ {name} (CT {ct}): cannot SSH — skip liveness check")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Resolve the Hermes gateway PID once, before the report-only branch:
|
# Resolve the Hermes gateway PID with retry. The probe target is
|
||||||
# the summary line below renders `pid`, and it used to be bound only in
|
# explicit: ssh {user}@{host} pgrep -f hermes gateway.
|
||||||
# the report-only path — leaving it unbound on the abiba/koonimo path
|
pid, probe_failed, fail_kind = _ssh_retry(
|
||||||
# raised UnboundLocalError and crashed the whole check. Agents without
|
host, "pgrep -f '[h]ermes_cli.main gateway run' | grep -v infisical | head -1", user=user)
|
||||||
# a gateway get pid=?.
|
if not pid and not probe_failed:
|
||||||
pid = ssh(host, "pgrep -f '[h]ermes_cli.main gateway run' | grep -v infisical | head -1", user=user)
|
pid, probe_failed, fail_kind = _ssh_retry(
|
||||||
if not pid:
|
host, "pgrep -f '[h]ermes.*gateway' | grep -v infisical | grep -v bash | head -1", user=user)
|
||||||
pid = ssh(host, "pgrep -f '[h]ermes.*gateway' | grep -v infisical | grep -v bash | head -1", user=user)
|
if not pid and not probe_failed:
|
||||||
if not pid:
|
|
||||||
pid = "?"
|
pid = "?"
|
||||||
|
|
||||||
# ⛔ KOBY IS NEVER REPAIRED — diagnostic only
|
if probe_failed:
|
||||||
|
print(f" ❌ {name}: probe-failed: ssh {user}@{host} {fail_kind} "
|
||||||
|
f"(retried at 25s: also {fail_kind}) [CT {ct}] — gateway status UNDETERMINED")
|
||||||
|
_fail(f"probe-failed:{name}:{fail_kind}", name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ⛔ KOBY IS NEVER REPAIRED — diagnostic only (captain's 2026-08-17 ruling)
|
||||||
if report_only:
|
if report_only:
|
||||||
print(f" 🔍 {name}: REPORT-ONLY mode (diagnostic only, no repairs on .129)")
|
|
||||||
# Still check gateway status for reporting purposes
|
|
||||||
if pid == "?":
|
if pid == "?":
|
||||||
print(f" ⚠️ {name}: GATEWAY NOT RUNNING (reported only)")
|
print(f" 🔍 {name}: REPORT-ONLY — probe: ssh {user}@{host} pgrep hermes-gateway "
|
||||||
|
f"-> no process found (reported only, NOT counted) [CT {ct}]")
|
||||||
_fail(f"gateway-down:{name}", name)
|
_fail(f"gateway-down:{name}", name)
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
print(f" ✅ {name}: gateway running (pid={pid}, report-only mode)")
|
print(f" 🔍 {name}: REPORT-ONLY — probe: ssh {user}@{host} pgrep hermes-gateway "
|
||||||
continue # Skip the rest of the check for Koby
|
f"-> pid={pid} (running, reported only, NOT repaired) [CT {ct}]")
|
||||||
|
continue # Skip the rest of the check for Koby
|
||||||
|
|
||||||
# Gateway state file
|
# Gateway state file
|
||||||
state = ssh(host, "cat ~/.hermes/gateway_state.json 2>/dev/null", user=user)
|
state, _, _ = _ssh_retry(host, "cat ~/.hermes/gateway_state.json 2>/dev/null", user=user)
|
||||||
if state:
|
if state:
|
||||||
try:
|
try:
|
||||||
st = json.loads(state)
|
st = json.loads(state)
|
||||||
@@ -408,21 +445,22 @@ def check_agents():
|
|||||||
]
|
]
|
||||||
streaming = "no"
|
streaming = "no"
|
||||||
for p in adapter_paths:
|
for p in adapter_paths:
|
||||||
has_edit = ssh(host, f"grep -c 'async def edit_message' {p} 2>/dev/null", user=user)
|
has_edit, _, _ = _ssh_retry(host, f"grep -c 'async def edit_message' {p} 2>/dev/null", user=user)
|
||||||
if has_edit and has_edit != "0":
|
if has_edit and has_edit != "0":
|
||||||
streaming = "yes"
|
streaming = "yes"
|
||||||
break
|
break
|
||||||
|
|
||||||
# Recent errors
|
# Recent errors
|
||||||
recent_errors = ssh(host,
|
recent_errors, _, _ = _ssh_retry(
|
||||||
|
host,
|
||||||
r"journalctl --user -u hermes-gateway --since '10 min ago' -o cat --no-pager 2>/dev/null "
|
r"journalctl --user -u hermes-gateway --since '10 min ago' -o cat --no-pager 2>/dev/null "
|
||||||
r"| grep -ci 'error\|traceback\|exception\|401\|403\|500' || echo 0",
|
r"| grep -ci 'error\|traceback\|exception\|401\|403\|500' || echo 0",
|
||||||
user=user)
|
user=user)
|
||||||
recent_errors = (recent_errors or "0").strip().split("\n")[-1]
|
recent_errors = (recent_errors or "0").strip().split("\n")[-1]
|
||||||
|
|
||||||
print(f" {'✅' if gw_state == 'running' and zulip == 'connected' else '⚠️'} "
|
print(f" {'✅' if gw_state == 'running' and zulip == 'connected' else '⚠️'} "
|
||||||
f"{name}: gw={gw_state} zulip={zulip} streaming={streaming} "
|
f"{name}: probe: ssh {user}@{host} — gw={gw_state} zulip={zulip} "
|
||||||
f"errors_10m={recent_errors.strip() or '0'} pid={pid}")
|
f"streaming={streaming} errors_10m={recent_errors.strip() or '0'} pid={pid} [CT {ct}]")
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user