set_status: add status_set_at timestamp (mtime of status file)
This commit is contained in:
parent
fe2933b213
commit
8e8e8a771f
7 changed files with 75 additions and 14 deletions
|
|
@ -54,6 +54,7 @@ pub enum SocketReply {
|
|||
role: String,
|
||||
hyperhive_rev: Option<String>,
|
||||
status_text: Option<String>,
|
||||
status_set_at: Option<i64>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -76,11 +77,13 @@ impl From<hive_sh4re::AgentResponse> for SocketReply {
|
|||
role,
|
||||
hyperhive_rev,
|
||||
status_text,
|
||||
status_set_at,
|
||||
} => Self::Whoami {
|
||||
name,
|
||||
role,
|
||||
hyperhive_rev,
|
||||
status_text,
|
||||
status_set_at,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -106,11 +109,13 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
|
|||
role,
|
||||
hyperhive_rev,
|
||||
status_text,
|
||||
status_set_at,
|
||||
} => Self::Whoami {
|
||||
name,
|
||||
role,
|
||||
hyperhive_rev,
|
||||
status_text,
|
||||
status_set_at,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -275,11 +280,22 @@ pub fn format_whoami(resp: Result<SocketReply, anyhow::Error>) -> String {
|
|||
role,
|
||||
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}");
|
||||
if let Some(s) = status_text {
|
||||
out.push_str(&format!("\nstatus: {s}"));
|
||||
let age = status_set_at.and_then(|ts| {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs();
|
||||
let secs = now.saturating_sub(ts as u64);
|
||||
Some(format_age_secs(secs))
|
||||
});
|
||||
if let Some(a) = age {
|
||||
out.push_str(&format!("\nstatus: {s} (set {a} ago)"));
|
||||
} else {
|
||||
out.push_str(&format!("\nstatus: {s}"));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
|
@ -289,6 +305,19 @@ pub fn format_whoami(resp: Result<SocketReply, anyhow::Error>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Format a duration in seconds as a human-readable age string.
|
||||
fn format_age_secs(secs: u64) -> String {
|
||||
if secs < 60 {
|
||||
format!("{secs}s")
|
||||
} else if secs < 3600 {
|
||||
format!("{}m", secs / 60)
|
||||
} else if secs < 86400 {
|
||||
format!("{}h", secs / 3600)
|
||||
} else {
|
||||
format!("{}d", secs / 86400)
|
||||
}
|
||||
}
|
||||
|
||||
/// Common envelope around every MCP tool handler: pre-log → run →
|
||||
/// post-log. The inbox-status hint used to be appended to every tool
|
||||
/// result; that lives in the wake prompt + UI header now, so tool
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const fmtAgeSecs = (s) => s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s/60)}m`
|
||||
: s < 86400 ? `${Math.floor(s/3600)}h` : `${Math.floor(s/86400)}d`;
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
||||
({ '&':'&', '<':'<', '>':'>', '"':'"' }[c])
|
||||
);
|
||||
|
|
@ -729,9 +731,16 @@
|
|||
|
||||
// ── agent status text ─────────────────────────────────────────
|
||||
if (c.status_text) {
|
||||
body.append(el('div', { class: 'agent-status', title: 'agent self-reported status' },
|
||||
const nowUnix = Math.floor(Date.now() / 1000);
|
||||
const ageStr = c.status_set_at != null
|
||||
? ` (set ${fmtAgeSecs(nowUnix - c.status_set_at)} ago)` : '';
|
||||
body.append(el('div', {
|
||||
class: 'agent-status',
|
||||
title: `agent self-reported status${ageStr}`,
|
||||
},
|
||||
el('span', { class: 'status-icon' }, '◈ '),
|
||||
c.status_text,
|
||||
el('span', { class: 'status-age' }, ageStr),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ a:hover {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
.agent-status .status-icon { opacity: 0.65; }
|
||||
.agent-status .status-age { opacity: 0.5; font-size: 0.9em; margin-left: 0.2em; }
|
||||
|
||||
.container-row.tombstone {
|
||||
border-style: dashed;
|
||||
|
|
|
|||
|
|
@ -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 } => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 } => {
|
||||
|
|
|
|||
|
|
@ -513,6 +513,8 @@ pub enum AgentResponse {
|
|||
/// response). `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` if none has been set.
|
||||
/// `status_set_at` is a Unix timestamp (seconds since epoch) of
|
||||
/// when the status was last written; `None` when no status is set.
|
||||
Whoami {
|
||||
name: String,
|
||||
role: String,
|
||||
|
|
@ -520,6 +522,8 @@ pub enum AgentResponse {
|
|||
hyperhive_rev: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
status_text: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
status_set_at: Option<i64>,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -936,5 +940,7 @@ pub enum ManagerResponse {
|
|||
hyperhive_rev: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
status_text: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
status_set_at: Option<i64>,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue