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

@ -517,14 +517,20 @@ pub enum AgentResponse {
/// `GetAgentMeta` result: identity + status metadata for an agent.
/// `role` is `"agent"` for sub-agents and `"manager"` for the
/// manager. `hyperhive_rev` is `None` only when the configured
/// flake URL has no canonical path. `status_text` is the last value
/// written via `SetStatus`, or `None` when none has been set or the
/// agent name is unknown. `status_set_at` is a Unix timestamp
/// (seconds since epoch) of when the status was last written;
/// `None` when no status is set.
/// flake URL has no canonical path. `running` reflects whether the
/// target's container is currently up (#432); when it's false,
/// `status_text` / `status_set_at` are intentionally cleared by the
/// host because the on-disk values are stale snapshots from before
/// the stop. `status_text` is the last value written via
/// `SetStatus`, or `None` when none has been set or the agent name
/// is unknown. `status_set_at` is a Unix timestamp (seconds since
/// epoch) of when the status was last written; `None` when no
/// status is set.
AgentMeta {
name: String,
role: String,
#[serde(default = "default_true")]
running: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
hyperhive_rev: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@ -534,6 +540,14 @@ pub enum AgentResponse {
},
}
/// Serde default for the `running` field on legacy wire payloads that
/// predate #432 — older harnesses never serialised it, and `true`
/// matches the historical assumption (the host only knew how to ask
/// about live containers).
fn default_true() -> bool {
true
}
// -----------------------------------------------------------------------------
// Manager socket — /run/hyperhive/manager/mcp.sock on the host, bind-mounted
// into the manager container at /run/hive/mcp.sock.
@ -944,10 +958,15 @@ pub enum ManagerResponse {
ReminderRollup(ReminderStats),
/// Mirror of `AgentResponse::AgentMeta` on the manager surface.
/// `role` is `"manager"` for the manager and `"agent"` for any
/// sub-agent looked up by name.
/// sub-agent looked up by name. `running` is false when the
/// target's container is stopped (#432) — in that case
/// `status_text` / `status_set_at` are cleared by the host so
/// stale pre-stop values don't leak through.
AgentMeta {
name: String,
role: String,
#[serde(default = "default_true")]
running: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
hyperhive_rev: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]