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

@ -52,6 +52,7 @@ pub enum SocketReply {
AgentMeta {
name: String,
role: String,
running: bool,
hyperhive_rev: Option<String>,
status_text: Option<String>,
status_set_at: Option<i64>,
@ -75,12 +76,14 @@ impl From<hive_sh4re::AgentResponse> for SocketReply {
hive_sh4re::AgentResponse::AgentMeta {
name,
role,
running,
hyperhive_rev,
status_text,
status_set_at,
} => Self::AgentMeta {
name,
role,
running,
hyperhive_rev,
status_text,
status_set_at,
@ -107,12 +110,14 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
hive_sh4re::ManagerResponse::AgentMeta {
name,
role,
running,
hyperhive_rev,
status_text,
status_set_at,
} => Self::AgentMeta {
name,
role,
running,
hyperhive_rev,
status_text,
status_set_at,
@ -270,21 +275,28 @@ fn loose_end_kind_label(kind: hive_sh4re::CancelLooseEndKind) -> &'static str {
}
/// Format helper for `get_agent_meta`: renders an agent's identity +
/// current status as a short human-readable block. `name`, `role`, and
/// `hyperhive_rev` are always shown; `status` only appears when one is
/// set, otherwise the line reads `status: <none>`.
/// current status as a short human-readable block. `name`, `role`,
/// `hyperhive_rev`, and `running` are always shown; `status` only
/// appears when one is set, otherwise the line reads `status: <none>`.
/// When `running` is false the host has already cleared `status_text`
/// (it would be stale from before the stop, #432) so the status line
/// is implicitly `<none>` in that case — but the explicit `running:
/// no` line tells the caller WHY.
#[must_use]
pub fn format_agent_meta(resp: Result<SocketReply, anyhow::Error>) -> String {
match resp {
Ok(SocketReply::AgentMeta {
name,
role,
running,
hyperhive_rev,
status_text,
status_set_at,
}) => {
let rev = hyperhive_rev.as_deref().unwrap_or("<unknown>");
let mut out = format!("name: {name}\nrole: {role}\nhyperhive_rev: {rev}");
let run = if running { "yes" } else { "no" };
let mut out =
format!("name: {name}\nrole: {role}\nhyperhive_rev: {rev}\nrunning: {run}");
match status_text {
None => out.push_str("\nstatus: <none>"),
Some(s) => {