From 56408321165eb165e2b419d2dcb23a086e5946f4 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 7 Sep 2026 12:46:54 +0200 Subject: [PATCH] AgentStatusRow: carry active_model + set_status text/timestamp --- hive-c0re/src/container_view.rs | 22 ++++++++++++++++++++++ hive-c0re/src/dashboard/state_snapshot.rs | 2 ++ hive-c0re/src/server.rs | 3 +++ hive-c0re/src/stats/host_stats.rs | 2 ++ hive-sh4re/src/container.rs | 17 +++++++++++++++++ 5 files changed, 46 insertions(+) diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index f6a84b2c..592382e1 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -63,6 +63,16 @@ pub struct ContainerView { /// for stopped containers. #[serde(default, skip_serializing_if = "Option::is_none")] pub active_model: Option, + /// The agent's current free-text status (`set_status`). `None` when + /// unset, or when the container isn't running — see + /// [`read_agent_status_live`] for why a stopped agent's on-disk + /// status is treated as stale rather than surfaced. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status_text: Option, + /// Unix timestamp (seconds) `status_text` was last set. `None` + /// exactly when `status_text` is `None`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status_set_at: Option, /// The agent's turn loop is parked: the container is up and still /// serving its web UI / MCP daemons, but it drives no turns and its /// inbox messages queue unacked until it resumes. Sourced from the @@ -155,6 +165,16 @@ pub async fn build_all(hive: &crate::coordinator::HiveEnv) -> Vec } else { None }; + // Same running-gate as `active_model` just above, and the same + // rule `read_agent_status_live` applies for its other callers — + // duplicated inline rather than calling that helper because it + // would re-derive `running` with its own `lifecycle::is_running` + // shell-out when this loop already has it for free. + let (status_text, status_set_at) = if running { + read_agent_status(&logical) + } else { + (None, None) + }; let paused = Coordinator::is_paused(&logical); let (cpu_quota, memory_max) = crate::resource_limits::effective_from( &limits, @@ -173,6 +193,8 @@ pub async fn build_all(hive: &crate::coordinator::HiveEnv) -> Vec deployed_sha, parent, active_model, + status_text, + status_set_at, paused, cpu_quota, memory_max, diff --git a/hive-c0re/src/dashboard/state_snapshot.rs b/hive-c0re/src/dashboard/state_snapshot.rs index da62d746..5b7fc68b 100644 --- a/hive-c0re/src/dashboard/state_snapshot.rs +++ b/hive-c0re/src/dashboard/state_snapshot.rs @@ -817,6 +817,8 @@ mod tests { deployed_sha: None, parent: None, active_model: None, + status_text: None, + status_set_at: None, paused: false, cpu_quota: "200%".to_owned(), memory_max: "4G".to_owned(), diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 85a80cde..9eaba30e 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -384,6 +384,9 @@ async fn handle_agent_status(coord: &Arc) -> HostResponse { pending_reminders: 0, parent: v.parent, paused: v.paused, + active_model: v.active_model, + status_text: v.status_text, + status_set_at: v.status_set_at, }) .collect(); HostResponse::agent_statuses(rows) diff --git a/hive-c0re/src/stats/host_stats.rs b/hive-c0re/src/stats/host_stats.rs index 2047e25c..56459f0f 100644 --- a/hive-c0re/src/stats/host_stats.rs +++ b/hive-c0re/src/stats/host_stats.rs @@ -254,6 +254,8 @@ mod tests { deployed_sha: None, parent: None, active_model: None, + status_text: None, + status_set_at: None, paused: false, cpu_quota: "200%".to_owned(), memory_max: "4G".to_owned(), diff --git a/hive-sh4re/src/container.rs b/hive-sh4re/src/container.rs index 59755a14..24fcbeaa 100644 --- a/hive-sh4re/src/container.rs +++ b/hive-sh4re/src/container.rs @@ -64,6 +64,23 @@ pub struct AgentStatusRow { /// Parent in the topology tree. `None` marks a root-level agent. #[serde(default, skip_serializing_if = "Option::is_none")] pub parent: Option, + /// The Claude model the agent's harness is currently using. Mirrors + /// `container_view::ContainerView::active_model`: `None` when the + /// agent has never started a turn, the field is absent from its + /// harness state file, or the container isn't running. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub active_model: Option, + /// The agent's current free-text status, set via the `set_status` + /// tool. `None` when unset or the container isn't running — a + /// stopped agent's on-disk status is a stale snapshot from before + /// the stop (same rule `container_view::read_agent_status_live` + /// applies for every other reader of this data). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status_text: Option, + /// Unix timestamp (seconds) `status_text` was last set. `None` + /// exactly when `status_text` is `None`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub status_set_at: Option, } /// One matrix identity an agent can act as, surfaced in `GetAgentMeta`'s