set_status: add status_set_at timestamp (mtime of status file)

This commit is contained in:
damocles 2026-05-23 01:08:50 +02:00 committed by Mara
parent fe2933b213
commit 8e8e8a771f
7 changed files with 75 additions and 14 deletions

View file

@ -221,12 +221,13 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
}
}
AgentRequest::Whoami => {
let status_text = crate::container_view::read_agent_status_text(agent);
let (status_text, status_set_at) = crate::container_view::read_agent_status(agent);
AgentResponse::Whoami {
name: agent.to_owned(),
role: "agent".to_owned(),
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
status_text,
status_set_at,
}
}
AgentRequest::SetStatus { text } => {

View file

@ -82,6 +82,11 @@ pub struct ContainerView {
/// is absent or empty — the agent hasn't set one yet.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_text: Option<String>,
/// Unix timestamp (seconds since epoch) when the status was last written.
/// Derived from the `hyperhive-status` file's mtime. `None` when no
/// status is set.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_set_at: Option<i64>,
}
/// Build the full container list. Wraps `lifecycle::list()` and
@ -124,7 +129,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
.and_then(|(_, model)| resolve_ctx_window(model, &coord.context_window_tokens));
let rate_limited = is_rate_limited(&logical);
let extra_links = read_dashboard_links(&logical);
let status_text = read_status_text(&logical);
let (status_text, status_set_at) = read_status(&logical);
out.push(ContainerView {
port: lifecycle::agent_web_port(&logical),
running: lifecycle::is_running(&logical).await,
@ -140,6 +145,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
rate_limited,
extra_links,
status_text,
status_set_at,
});
}
out
@ -179,18 +185,25 @@ fn is_rate_limited(name: &str) -> bool {
.exists()
}
/// Read the agent's free-text status set via `mcp__hyperhive__set_status`.
/// Returns `None` when the file is absent or empty — best-effort, never panics.
/// `pub` so `agent_server` and `manager_server` can include it in `Whoami` responses.
pub fn read_agent_status_text(name: &str) -> Option<String> {
/// Read the agent's free-text status and the Unix timestamp when it was last set
/// (derived from the file's mtime). Returns `(None, None)` when the file is absent
/// or empty. `pub` so `agent_server` and `manager_server` can populate `Whoami`.
pub fn read_agent_status(name: &str) -> (Option<String>, Option<i64>) {
let path = Coordinator::agent_notes_dir(name).join("hyperhive-status");
let s = std::fs::read_to_string(path).ok()?;
let trimmed = s.trim();
if trimmed.is_empty() { None } else { Some(trimmed.to_owned()) }
let meta = std::fs::metadata(&path).ok();
let s = std::fs::read_to_string(&path).ok();
let text = s.as_deref().map(str::trim).filter(|t| !t.is_empty()).map(str::to_owned);
let mtime = meta.and_then(|m| {
m.modified().ok().and_then(|t| {
t.duration_since(std::time::UNIX_EPOCH).ok()
.and_then(|d| i64::try_from(d.as_secs()).ok())
})
});
if text.is_none() { (None, None) } else { (text, mtime) }
}
fn read_status_text(name: &str) -> Option<String> {
read_agent_status_text(name)
fn read_status(name: &str) -> (Option<String>, Option<i64>) {
read_agent_status(name)
}
/// Read the agent's most recent completed turn from its turn-stats

View file

@ -481,12 +481,14 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
}
}
ManagerRequest::Whoami => {
let status_text = crate::container_view::read_agent_status_text(MANAGER_AGENT);
let (status_text, status_set_at) =
crate::container_view::read_agent_status(MANAGER_AGENT);
ManagerResponse::Whoami {
name: MANAGER_AGENT.to_owned(),
role: "manager".to_owned(),
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
status_text,
status_set_at,
}
}
ManagerRequest::SetStatus { text } => {