host.sock: push a live agent-status stream instead of poll-only

This commit is contained in:
damocles 2026-09-07 18:24:22 +02:00 committed by mara
commit 3871749da9
3 changed files with 181 additions and 27 deletions

View file

@ -96,6 +96,38 @@ pub struct ContainerView {
pub memory_max: String,
}
impl From<ContainerView> for hive_sh4re::container::AgentStatusRow {
/// The one place a `ContainerView` becomes the wire row `host.sock`
/// serves it as — `handle_agent_status`'s poll path and the
/// `SubscribeAgentStatus` push path both go through this, so the two
/// can never quietly diverge on which fields make the cut.
///
/// `pending_reminders` has no source here: reminders are agent-local
/// now, and c0re has no cross-agent visibility into pending counts
/// anymore. Stubbed to `0` rather than dropping the wire field
/// outright — leaves `hivectl status`/the dashboard column intact
/// syntactically, just always empty, until iris's frontend follow-up
/// decides whether to drop the column entirely. `port`, `cpu_quota`,
/// `memory_max` have no equivalent on the row at all and are simply
/// not carried over.
fn from(v: ContainerView) -> Self {
Self {
name: v.name,
running: v.running,
failed: v.failed,
needs_update: v.needs_update,
needs_login: v.needs_login,
deployed_sha: v.deployed_sha,
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,
}
}
}
/// Build the full container list. Wraps `lifecycle::list()` and
/// resolves every per-agent attribute the dashboard surfaces.
///
@ -468,4 +500,53 @@ mod tests {
let ok = lock(r#""a":"a""#, r#""a":{"locked":{"rev":"abc"}}"#);
assert_eq!(parse_locked_revs(&ok).len(), 1);
}
/// The `From` impl both `handle_agent_status`'s poll path and
/// `stream_agent_status`'s push path go through — pins the field
/// mapping so the two can't quietly diverge, and that `port` /
/// `cpu_quota` / `memory_max` (no equivalent on the row) are dropped
/// on purpose rather than by omission.
#[test]
fn agent_status_row_carries_every_field_the_row_has_and_drops_the_rest() {
use super::ContainerView;
use chrono::{TimeZone, Utc};
use hive_sh4re::container::AgentStatusRow;
let view = ContainerView {
name: "alice".to_owned(),
container: "h-alice".to_owned(),
port: 7000,
running: true,
failed: false,
needs_update: true,
needs_login: false,
deployed_sha: Some("abc123def456".to_owned()),
parent: Some("bob".to_owned()),
active_model: Some("claude-opus".to_owned()),
status_text: Some("shipping".to_owned()),
status_set_at: Some(Utc.timestamp_opt(1_700_000_000, 0).unwrap()),
paused: true,
cpu_quota: "400%".to_owned(),
memory_max: "8G".to_owned(),
};
let row = AgentStatusRow::from(view);
assert_eq!(row.name, "alice");
assert!(row.running);
assert!(!row.failed);
assert!(row.needs_update);
assert!(!row.needs_login);
assert_eq!(row.deployed_sha.as_deref(), Some("abc123def456"));
assert_eq!(row.parent.as_deref(), Some("bob"));
assert!(row.paused);
assert_eq!(row.active_model.as_deref(), Some("claude-opus"));
assert_eq!(row.status_text.as_deref(), Some("shipping"));
assert_eq!(
row.status_set_at,
Some(Utc.timestamp_opt(1_700_000_000, 0).unwrap())
);
// No source for reminders on this side (see the `From` impl's doc
// comment) — always the stub, never left uninitialised.
assert_eq!(row.pending_reminders, 0);
}
}