fix(status): dashboard keeps a watchdog-exited gateway degraded with its reason

`/api/status` mapped a not-running gateway's retained record through
`retained_gateway_state`, which only kept `startup_failed`; a watchdog-stamped
`degraded` + `exit_reason` of a dead PID became a bare `stopped` with the
reason nulled, so the sidebar strip and System page read "Stopped" while
`hermes gateway status` said "exited degraded: event loop stopped dispatching".

Retain `degraded` under the same rule as `startup_failed` (only while
`desired_state` still wants the gateway running, and only for the watchdog
reasons in the new `gateway.status.WATCHDOG_EXIT_REASONS`); the resolver
already keeps `gateway_exit_reason` for any non-`stopped` verdict. The sidebar
strip gains the `degraded` label (warning while live, destructive when the
process is gone) and the System page describes a dead `degraded` record as a
watchdog exit. `/api/messaging/platforms` keeps yielding `gateway_stopped` for
it — the channels really are down.

Invariant tests: retained_gateway_state keeps/drops the verdict by
desired_state and exit_reason; /api/status carries degraded + exit_reason for
the dead PID and stopped/null after `hermes gateway stop`.
This commit is contained in:
teknium1
2026-09-18 04:17:37 -07:00
committed by Teknium
parent 8bfc66b252
commit 993d1b9891
9 changed files with 95 additions and 8 deletions

View File

@@ -57,6 +57,12 @@ export function gatewayLine(
running: { label: g.running, tone: "text-success" },
starting: { label: g.starting, tone: "text-warning" },
startup_failed: { label: g.failed, tone: "text-destructive" },
// Live: some channels offline. Retained on a dead PID: a watchdog hard-exited a wedged
// process (gateway_exit_reason names it) — same verdict `hermes gateway status` prints.
degraded: {
label: g.degraded,
tone: status.gateway_running ? "text-warning" : "text-destructive",
},
stopped: { label: g.stopped, tone: "text-muted-foreground" },
};
if (status.gateway_state && byState[status.gateway_state]) {

View File

@@ -65,6 +65,7 @@ export const en: Translations = {
activeSessionsLabel: "Active Sessions:",
gatewayStatusLabel: "Gateway Status:",
gatewayStrip: {
degraded: "Degraded",
failed: "Start failed",
off: "Off",
running: "Running",

View File

@@ -84,6 +84,7 @@ export interface Translations {
activeSessionsLabel: string;
gatewayStatusLabel: string;
gatewayStrip: {
degraded: string;
failed: string;
off: string;
running: string;

View File

@@ -33,11 +33,15 @@ const GATEWAY_STATE_COPY: Record<string, string> = {
startup_failed: "Failed to start — see Logs",
};
/** A `degraded` record of a dead process is a watchdog exit, not a live gateway with channels down. */
const GATEWAY_EXITED_DEGRADED_COPY = "Exited: a watchdog stopped a wedged gateway — see Logs";
/** Plain description of the gateway's state; null/unknown falls back to running/stopped. */
export function gatewayStateDescription(
state: string | null | undefined,
running: boolean | undefined,
): string {
if (state === "degraded" && running === false) return GATEWAY_EXITED_DEGRADED_COPY;
if (state && GATEWAY_STATE_COPY[state]) return GATEWAY_STATE_COPY[state];
return running ? GATEWAY_STATE_COPY.running : GATEWAY_STATE_COPY.stopped;
}