container_view: clear live-only fields when stopped (#432)

per mara's review on #433, move the gating from the dashboard into
the host so a stopped container's stale on-disk state (rate_limited
sentinel, hyperhive-needs-login, last-turn-stats row, status blob)
never reaches the wire in the first place. when build_all sees
is_running == false:

  - needs_login → false
  - ctx_tokens / context_window_tokens → None
  - rate_limited → false
  - status_text / status_set_at → None

static / declared fields (extra_links, deployed_sha,
pending_reminders, needs_update, parent) stay populated regardless
of run state.

extend AgentMeta (both AgentResponse + ManagerResponse) with a
`running: bool` field so get_agent_meta callers can tell whether
the target is up — answers the second half of #432 ("agent meta
should probably show the info that it is not running as well").
read_agent_status_live wraps the existing read_agent_status with
the same is_running gate so the manager/agent socket handlers don't
have to know about sentinel semantics.

format_agent_meta now prints a `running: yes|no` line so claude
sees the run state in plain text alongside hyperhive_rev.

frontend follow-up in the same commit: drop the redundant
`c.running &&` guards on ctx_tokens / status_text in
renderContainers — the backend now guarantees those fields are
absent when the container is stopped, so the existing
truthy-check is sufficient. the `■ not running` badge + icon /
links fetch short-circuits stay (those are pure presentation /
network-noise wins the backend can't address).
This commit is contained in:
iris 2026-05-25 23:35:03 +02:00
commit 7b4917b256
6 changed files with 131 additions and 42 deletions

View file

@ -559,13 +559,13 @@ window.marked = marked;
})
.catch(() => { /* graceful: agent down → no strip */ });
}
// Status / runtime badges (#432). Pending transients always win
// (start / stop / restart / rebuild is in progress). Otherwise:
// when the container is stopped, surface a single `■ stopped`
// badge and skip everything that depends on a live harness
// (alive / rate-limited / needs-login / ctx / status text);
// those badges go stale the moment the harness goes away and
// just confuse the operator if we keep showing them.
// Status / runtime badges. Pending transients always win
// (start / stop / restart / rebuild is in progress). Otherwise,
// when the container is stopped, surface a single `■ not
// running` badge; the backend has already cleared rate_limited /
// needs_login / ctx_tokens / status_text in that case (#432) so
// the rest of the chain is a no-op for stopped containers — but
// we still want SOME badge there so the row doesn't look empty.
if (pending) {
head.append(el('span', { class: 'pending-state' },
el('span', { class: 'spinner' }, '◐'), ' ', pending + '…'));
@ -603,7 +603,7 @@ window.marked = marked;
},
`${c.pending_reminders}`));
}
if (c.running && c.ctx_tokens != null) {
if (c.ctx_tokens != null) {
const k = Math.round(c.ctx_tokens / 1000);
// Thresholds track the model's real context window when the
// backend supplies it; otherwise fall back to fixed constants.
@ -625,10 +625,11 @@ window.marked = marked;
// ── agent status text ─────────────────────────────────────────
// Self-reported status (via set_status MCP tool) — only fresh
// while the harness is up. Skip on stopped containers (#432);
// the text is from the last time the harness was running and
// just misleads now.
if (c.running && c.status_text) {
// while the harness is up. The backend already clears
// `status_text` on stopped containers (#432) so we can render
// unconditionally here: a stopped container simply has no
// `status_text` and skips this block naturally.
if (c.status_text) {
const nowUnix = Math.floor(Date.now() / 1000);
const ageStr = c.status_set_at != null
? ` (set ${fmtAgeSecs(nowUnix - c.status_set_at)} ago)` : '';