Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b4917b256 | ||
|
|
683e59c757 |
6 changed files with 171 additions and 56 deletions
|
|
@ -499,14 +499,23 @@ window.marked = marked;
|
||||||
// answering) the error handler falls it back to the dimmed
|
// answering) the error handler falls it back to the dimmed
|
||||||
// hyperhive mark (`/favicon.svg`, served by the dashboard
|
// hyperhive mark (`/favicon.svg`, served by the dashboard
|
||||||
// itself, always reachable). (issues #195, #202)
|
// itself, always reachable). (issues #195, #202)
|
||||||
const iconImg = el('img', { class: 'container-icon-img', src: `${url}icon`, alt: '' });
|
const iconImg = el('img', { class: 'container-icon-img', alt: '' });
|
||||||
const icon = el('div', { class: 'container-icon' }, iconImg);
|
const icon = el('div', { class: 'container-icon' }, iconImg);
|
||||||
|
if (c.running) {
|
||||||
|
iconImg.src = `${url}icon`;
|
||||||
iconImg.addEventListener('error', () => {
|
iconImg.addEventListener('error', () => {
|
||||||
if (iconImg.dataset.fallback) return; // guard: don't loop if the favicon itself 404s
|
if (iconImg.dataset.fallback) return; // guard: don't loop if the favicon itself 404s
|
||||||
iconImg.dataset.fallback = '1';
|
iconImg.dataset.fallback = '1';
|
||||||
icon.classList.add('icon-unreachable');
|
icon.classList.add('icon-unreachable');
|
||||||
iconImg.src = '/favicon.svg';
|
iconImg.src = '/favicon.svg';
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Container stopped (#432) — skip the doomed `${url}icon` fetch
|
||||||
|
// and go straight to the dimmed hyperhive mark. Avoids a noisy
|
||||||
|
// failed request in the console + the brief broken-image flash.
|
||||||
|
icon.classList.add('icon-unreachable');
|
||||||
|
iconImg.src = '/favicon.svg';
|
||||||
|
}
|
||||||
// Card body: the three stacked content lines, right of the icon.
|
// Card body: the three stacked content lines, right of the icon.
|
||||||
const body = el('div', { class: 'card-body' });
|
const body = el('div', { class: 'card-body' });
|
||||||
|
|
||||||
|
|
@ -527,6 +536,7 @@ window.marked = marked;
|
||||||
head.append(navStrip);
|
head.append(navStrip);
|
||||||
const forgeBase = `http://${hostname}:3000`;
|
const forgeBase = `http://${hostname}:3000`;
|
||||||
const containerBase = `http://${hostname}:${c.port}`;
|
const containerBase = `http://${hostname}:${c.port}`;
|
||||||
|
if (c.running) {
|
||||||
fetch(`/api/agent/${encodeURIComponent(c.name)}/links`)
|
fetch(`/api/agent/${encodeURIComponent(c.name)}/links`)
|
||||||
.then((r) => (r.ok ? r.json() : []))
|
.then((r) => (r.ok ? r.json() : []))
|
||||||
.then((links) => {
|
.then((links) => {
|
||||||
|
|
@ -548,9 +558,21 @@ window.marked = marked;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => { /* graceful: agent down → no strip */ });
|
.catch(() => { /* graceful: agent down → no strip */ });
|
||||||
|
}
|
||||||
|
// Status / runtime badges. Pending transients always win
|
||||||
|
// (start / stop / restart / rebuild is in progress). Otherwise,
|
||||||
|
// when the container is stopped, surface a single `■ not
|
||||||
|
// running` badge; the backend has already cleared rate_limited /
|
||||||
|
// needs_login / ctx_tokens / status_text in that case (#432) so
|
||||||
|
// the rest of the chain is a no-op for stopped containers — but
|
||||||
|
// we still want SOME badge there so the row doesn't look empty.
|
||||||
if (pending) {
|
if (pending) {
|
||||||
head.append(el('span', { class: 'pending-state' },
|
head.append(el('span', { class: 'pending-state' },
|
||||||
el('span', { class: 'spinner' }, '◐'), ' ', pending + '…'));
|
el('span', { class: 'spinner' }, '◐'), ' ', pending + '…'));
|
||||||
|
} else if (!c.running) {
|
||||||
|
head.append(el('span',
|
||||||
|
{ class: 'badge badge-muted', title: 'container is shut down — start it to bring the harness back up' },
|
||||||
|
'■ not running'));
|
||||||
} else if (c.rate_limited) {
|
} else if (c.rate_limited) {
|
||||||
head.append(el('span',
|
head.append(el('span',
|
||||||
{ class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
|
{ class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
|
||||||
|
|
@ -602,6 +624,11 @@ window.marked = marked;
|
||||||
body.append(head);
|
body.append(head);
|
||||||
|
|
||||||
// ── agent status text ─────────────────────────────────────────
|
// ── agent status text ─────────────────────────────────────────
|
||||||
|
// Self-reported status (via set_status MCP tool) — only fresh
|
||||||
|
// while the harness is up. The backend already clears
|
||||||
|
// `status_text` on stopped containers (#432) so we can render
|
||||||
|
// unconditionally here: a stopped container simply has no
|
||||||
|
// `status_text` and skips this block naturally.
|
||||||
if (c.status_text) {
|
if (c.status_text) {
|
||||||
const nowUnix = Math.floor(Date.now() / 1000);
|
const nowUnix = Math.floor(Date.now() / 1000);
|
||||||
const ageStr = c.status_set_at != null
|
const ageStr = c.status_set_at != null
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ pub enum SocketReply {
|
||||||
AgentMeta {
|
AgentMeta {
|
||||||
name: String,
|
name: String,
|
||||||
role: String,
|
role: String,
|
||||||
|
running: bool,
|
||||||
hyperhive_rev: Option<String>,
|
hyperhive_rev: Option<String>,
|
||||||
status_text: Option<String>,
|
status_text: Option<String>,
|
||||||
status_set_at: Option<i64>,
|
status_set_at: Option<i64>,
|
||||||
|
|
@ -75,12 +76,14 @@ impl From<hive_sh4re::AgentResponse> for SocketReply {
|
||||||
hive_sh4re::AgentResponse::AgentMeta {
|
hive_sh4re::AgentResponse::AgentMeta {
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
|
running,
|
||||||
hyperhive_rev,
|
hyperhive_rev,
|
||||||
status_text,
|
status_text,
|
||||||
status_set_at,
|
status_set_at,
|
||||||
} => Self::AgentMeta {
|
} => Self::AgentMeta {
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
|
running,
|
||||||
hyperhive_rev,
|
hyperhive_rev,
|
||||||
status_text,
|
status_text,
|
||||||
status_set_at,
|
status_set_at,
|
||||||
|
|
@ -107,12 +110,14 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
|
||||||
hive_sh4re::ManagerResponse::AgentMeta {
|
hive_sh4re::ManagerResponse::AgentMeta {
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
|
running,
|
||||||
hyperhive_rev,
|
hyperhive_rev,
|
||||||
status_text,
|
status_text,
|
||||||
status_set_at,
|
status_set_at,
|
||||||
} => Self::AgentMeta {
|
} => Self::AgentMeta {
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
|
running,
|
||||||
hyperhive_rev,
|
hyperhive_rev,
|
||||||
status_text,
|
status_text,
|
||||||
status_set_at,
|
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 +
|
/// Format helper for `get_agent_meta`: renders an agent's identity +
|
||||||
/// current status as a short human-readable block. `name`, `role`, and
|
/// current status as a short human-readable block. `name`, `role`,
|
||||||
/// `hyperhive_rev` are always shown; `status` only appears when one is
|
/// `hyperhive_rev`, and `running` are always shown; `status` only
|
||||||
/// set, otherwise the line reads `status: <none>`.
|
/// 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]
|
#[must_use]
|
||||||
pub fn format_agent_meta(resp: Result<SocketReply, anyhow::Error>) -> String {
|
pub fn format_agent_meta(resp: Result<SocketReply, anyhow::Error>) -> String {
|
||||||
match resp {
|
match resp {
|
||||||
Ok(SocketReply::AgentMeta {
|
Ok(SocketReply::AgentMeta {
|
||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
|
running,
|
||||||
hyperhive_rev,
|
hyperhive_rev,
|
||||||
status_text,
|
status_text,
|
||||||
status_set_at,
|
status_set_at,
|
||||||
}) => {
|
}) => {
|
||||||
let rev = hyperhive_rev.as_deref().unwrap_or("<unknown>");
|
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 {
|
match status_text {
|
||||||
None => out.push_str("\nstatus: <none>"),
|
None => out.push_str("\nstatus: <none>"),
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
|
|
|
||||||
|
|
@ -246,8 +246,12 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
||||||
}
|
}
|
||||||
AgentRequest::GetAgentMeta { name } => {
|
AgentRequest::GetAgentMeta { name } => {
|
||||||
let target = name.as_deref().unwrap_or(agent);
|
let target = name.as_deref().unwrap_or(agent);
|
||||||
let (status_text, status_set_at) =
|
// #432: gate status on the target's running state so a
|
||||||
crate::container_view::read_agent_status(target);
|
// stopped container's stale on-disk status doesn't leak
|
||||||
|
// through. Also surface `running` itself so callers can
|
||||||
|
// tell (e.g. "iris is down" vs "iris has no status set").
|
||||||
|
let (status_text, status_set_at, running) =
|
||||||
|
crate::container_view::read_agent_status_live(target).await;
|
||||||
let role = if target == hive_sh4re::MANAGER_AGENT {
|
let role = if target == hive_sh4re::MANAGER_AGENT {
|
||||||
"manager"
|
"manager"
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -257,6 +261,7 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
||||||
AgentResponse::AgentMeta {
|
AgentResponse::AgentMeta {
|
||||||
name: target.to_owned(),
|
name: target.to_owned(),
|
||||||
role,
|
role,
|
||||||
|
running,
|
||||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||||
status_text,
|
status_text,
|
||||||
status_set_at,
|
status_set_at,
|
||||||
|
|
|
||||||
|
|
@ -117,14 +117,6 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
||||||
};
|
};
|
||||||
let deployed_full = locked.get(&format!("agent-{logical}")).map(std::string::String::as_str);
|
let deployed_full = locked.get(&format!("agent-{logical}")).map(std::string::String::as_str);
|
||||||
let needs_update = crate::auto_update::agent_config_pending(&logical, deployed_full);
|
let needs_update = crate::auto_update::agent_config_pending(&logical, deployed_full);
|
||||||
// needs_login fires when EITHER the claude session dir is
|
|
||||||
// missing (boot-time / fresh container) OR the harness wrote
|
|
||||||
// the auth-failed sentinel because a turn hit 401 (#419). The
|
|
||||||
// manager has its own session lifecycle and never participates
|
|
||||||
// in needs_login.
|
|
||||||
let needs_login = !is_manager
|
|
||||||
&& (!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
|
||||||
|| auth_failed_sentinel(&logical));
|
|
||||||
let deployed_sha = deployed_full.map(|s| s[..s.len().min(12)].to_owned());
|
let deployed_sha = deployed_full.map(|s| s[..s.len().min(12)].to_owned());
|
||||||
// Recipient name the broker uses for this agent — sub-agents
|
// Recipient name the broker uses for this agent — sub-agents
|
||||||
// are addressed by logical name, the manager by the
|
// are addressed by logical name, the manager by the
|
||||||
|
|
@ -140,18 +132,41 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
||||||
.broker
|
.broker
|
||||||
.count_pending_reminders_for(reminder_recipient)
|
.count_pending_reminders_for(reminder_recipient)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
let extra_links = read_dashboard_links(&logical);
|
||||||
|
let parent = topology.get(&logical).cloned().flatten();
|
||||||
|
let running = lifecycle::is_running(&logical).await;
|
||||||
|
// Live-only fields (#432) — only meaningful while the harness
|
||||||
|
// is up. When the container is stopped, sentinel files +
|
||||||
|
// turn-stats rows + the on-disk status blob are all stale
|
||||||
|
// snapshots from before the stop, so we clear them here
|
||||||
|
// rather than letting the dashboard / `get_agent_meta` surface
|
||||||
|
// misleading values. Static / declared fields (extra_links,
|
||||||
|
// deployed_sha, pending_reminders, needs_update, parent) stay
|
||||||
|
// populated regardless of run state.
|
||||||
|
let (needs_login, ctx_tokens, context_window_tokens, rate_limited, status_text, status_set_at) =
|
||||||
|
if running {
|
||||||
|
// needs_login fires when EITHER the claude session dir is
|
||||||
|
// missing (boot-time / fresh container) OR the harness wrote
|
||||||
|
// the auth-failed sentinel because a turn hit 401 (#419). The
|
||||||
|
// manager has its own session lifecycle and never participates
|
||||||
|
// in needs_login.
|
||||||
|
let needs_login = !is_manager
|
||||||
|
&& (!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
||||||
|
|| auth_failed_sentinel(&logical));
|
||||||
let last_turn = read_last_turn(&logical);
|
let last_turn = read_last_turn(&logical);
|
||||||
let ctx_tokens = last_turn.as_ref().map(|(toks, _)| *toks);
|
let ctx_tokens = last_turn.as_ref().map(|(toks, _)| *toks);
|
||||||
let context_window_tokens = last_turn
|
let context_window_tokens = last_turn
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|(_, model)| resolve_ctx_window(model, &coord.context_window_tokens));
|
.and_then(|(_, model)| resolve_ctx_window(model, &coord.context_window_tokens));
|
||||||
let rate_limited = is_rate_limited(&logical);
|
let rate_limited = is_rate_limited(&logical);
|
||||||
let extra_links = read_dashboard_links(&logical);
|
|
||||||
let (status_text, status_set_at) = read_status(&logical);
|
let (status_text, status_set_at) = read_status(&logical);
|
||||||
let parent = topology.get(&logical).cloned().flatten();
|
(needs_login, ctx_tokens, context_window_tokens, rate_limited, status_text, status_set_at)
|
||||||
|
} else {
|
||||||
|
(false, None, None, false, None, None)
|
||||||
|
};
|
||||||
out.push(ContainerView {
|
out.push(ContainerView {
|
||||||
port: lifecycle::agent_web_port(&logical),
|
port: lifecycle::agent_web_port(&logical),
|
||||||
running: lifecycle::is_running(&logical).await,
|
running,
|
||||||
container: c.clone(),
|
container: c.clone(),
|
||||||
name: logical,
|
name: logical,
|
||||||
is_manager,
|
is_manager,
|
||||||
|
|
@ -219,6 +234,10 @@ fn auth_failed_sentinel(name: &str) -> bool {
|
||||||
/// Read the agent's free-text status and the Unix timestamp when it was last set
|
/// 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
|
/// (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 `AgentMeta`.
|
/// or empty. `pub` so `agent_server` and `manager_server` can populate `AgentMeta`.
|
||||||
|
///
|
||||||
|
/// NB: callers building `AgentMeta` for a *stopped* container should
|
||||||
|
/// clear the result — the on-disk status is a stale snapshot from
|
||||||
|
/// before the stop (#432). Use `read_agent_status_live` for that.
|
||||||
pub fn read_agent_status(name: &str) -> (Option<String>, Option<i64>) {
|
pub fn read_agent_status(name: &str) -> (Option<String>, Option<i64>) {
|
||||||
let path = Coordinator::agent_notes_dir(name).join("hyperhive-status");
|
let path = Coordinator::agent_notes_dir(name).join("hyperhive-status");
|
||||||
let meta = std::fs::metadata(&path).ok();
|
let meta = std::fs::metadata(&path).ok();
|
||||||
|
|
@ -237,6 +256,34 @@ fn read_status(name: &str) -> (Option<String>, Option<i64>) {
|
||||||
read_agent_status(name)
|
read_agent_status(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wraps `read_agent_status` with the same "stopped containers have
|
||||||
|
/// stale state" gate `build_all` uses (#432). Returns
|
||||||
|
/// `(None, None, false)` when the container isn't running so callers
|
||||||
|
/// don't have to know about the sentinel rules — they just hand back
|
||||||
|
/// what we give them.
|
||||||
|
///
|
||||||
|
/// Returned tuple is `(status_text, status_set_at, running)`. The
|
||||||
|
/// `name` argument is the broker-side recipient — `MANAGER_AGENT` for
|
||||||
|
/// the manager, the logical agent name otherwise — so callers can
|
||||||
|
/// reuse the same string they used to look the agent up.
|
||||||
|
pub async fn read_agent_status_live(name: &str) -> (Option<String>, Option<i64>, bool) {
|
||||||
|
// The lifecycle helper wants the on-disk name (`hm1nd` for the
|
||||||
|
// manager, the bare logical name for sub-agents) and internally
|
||||||
|
// adds the `h-` prefix. Map the broker-side `MANAGER_AGENT`
|
||||||
|
// sentinel back to the lifecycle name here so callers don't have
|
||||||
|
// to bother.
|
||||||
|
let lifecycle_name = if name == hive_sh4re::MANAGER_AGENT {
|
||||||
|
lifecycle::MANAGER_NAME
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
if !lifecycle::is_running(lifecycle_name).await {
|
||||||
|
return (None, None, false);
|
||||||
|
}
|
||||||
|
let (text, set_at) = read_agent_status(name);
|
||||||
|
(text, set_at, true)
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the agent's most recent completed turn from its turn-stats
|
/// Read the agent's most recent completed turn from its turn-stats
|
||||||
/// `SQLite`: the context-window size (prompt tokens) and the model name.
|
/// `SQLite`: the context-window size (prompt tokens) and the model name.
|
||||||
/// Returns `None` when the file is absent or has no rows. Best-effort
|
/// Returns `None` when the file is absent or has no rows. Best-effort
|
||||||
|
|
|
||||||
|
|
@ -494,12 +494,17 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
||||||
}
|
}
|
||||||
ManagerRequest::GetAgentMeta { name } => {
|
ManagerRequest::GetAgentMeta { name } => {
|
||||||
let target = name.as_deref().unwrap_or(MANAGER_AGENT);
|
let target = name.as_deref().unwrap_or(MANAGER_AGENT);
|
||||||
let (status_text, status_set_at) =
|
// #432: gate status on the target's running state so a
|
||||||
crate::container_view::read_agent_status(target);
|
// stopped container's stale on-disk status doesn't leak
|
||||||
|
// through. Also surface `running` itself so callers can
|
||||||
|
// tell (e.g. "iris is down" vs "iris has no status set").
|
||||||
|
let (status_text, status_set_at, running) =
|
||||||
|
crate::container_view::read_agent_status_live(target).await;
|
||||||
let role = if target == MANAGER_AGENT { "manager" } else { "agent" }.to_owned();
|
let role = if target == MANAGER_AGENT { "manager" } else { "agent" }.to_owned();
|
||||||
ManagerResponse::AgentMeta {
|
ManagerResponse::AgentMeta {
|
||||||
name: target.to_owned(),
|
name: target.to_owned(),
|
||||||
role,
|
role,
|
||||||
|
running,
|
||||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||||
status_text,
|
status_text,
|
||||||
status_set_at,
|
status_set_at,
|
||||||
|
|
|
||||||
|
|
@ -517,14 +517,20 @@ pub enum AgentResponse {
|
||||||
/// `GetAgentMeta` result: identity + status metadata for an agent.
|
/// `GetAgentMeta` result: identity + status metadata for an agent.
|
||||||
/// `role` is `"agent"` for sub-agents and `"manager"` for the
|
/// `role` is `"agent"` for sub-agents and `"manager"` for the
|
||||||
/// manager. `hyperhive_rev` is `None` only when the configured
|
/// manager. `hyperhive_rev` is `None` only when the configured
|
||||||
/// flake URL has no canonical path. `status_text` is the last value
|
/// flake URL has no canonical path. `running` reflects whether the
|
||||||
/// written via `SetStatus`, or `None` when none has been set or the
|
/// target's container is currently up (#432); when it's false,
|
||||||
/// agent name is unknown. `status_set_at` is a Unix timestamp
|
/// `status_text` / `status_set_at` are intentionally cleared by the
|
||||||
/// (seconds since epoch) of when the status was last written;
|
/// host because the on-disk values are stale snapshots from before
|
||||||
/// `None` when no status is set.
|
/// the stop. `status_text` is the last value written via
|
||||||
|
/// `SetStatus`, or `None` when none has been set or the agent name
|
||||||
|
/// is unknown. `status_set_at` is a Unix timestamp (seconds since
|
||||||
|
/// epoch) of when the status was last written; `None` when no
|
||||||
|
/// status is set.
|
||||||
AgentMeta {
|
AgentMeta {
|
||||||
name: String,
|
name: String,
|
||||||
role: String,
|
role: String,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
running: bool,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
hyperhive_rev: Option<String>,
|
hyperhive_rev: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
|
@ -534,6 +540,14 @@ pub enum AgentResponse {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serde default for the `running` field on legacy wire payloads that
|
||||||
|
/// predate #432 — older harnesses never serialised it, and `true`
|
||||||
|
/// matches the historical assumption (the host only knew how to ask
|
||||||
|
/// about live containers).
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Manager socket — /run/hyperhive/manager/mcp.sock on the host, bind-mounted
|
// Manager socket — /run/hyperhive/manager/mcp.sock on the host, bind-mounted
|
||||||
// into the manager container at /run/hive/mcp.sock.
|
// into the manager container at /run/hive/mcp.sock.
|
||||||
|
|
@ -944,10 +958,15 @@ pub enum ManagerResponse {
|
||||||
ReminderRollup(ReminderStats),
|
ReminderRollup(ReminderStats),
|
||||||
/// Mirror of `AgentResponse::AgentMeta` on the manager surface.
|
/// Mirror of `AgentResponse::AgentMeta` on the manager surface.
|
||||||
/// `role` is `"manager"` for the manager and `"agent"` for any
|
/// `role` is `"manager"` for the manager and `"agent"` for any
|
||||||
/// sub-agent looked up by name.
|
/// sub-agent looked up by name. `running` is false when the
|
||||||
|
/// target's container is stopped (#432) — in that case
|
||||||
|
/// `status_text` / `status_set_at` are cleared by the host so
|
||||||
|
/// stale pre-stop values don't leak through.
|
||||||
AgentMeta {
|
AgentMeta {
|
||||||
name: String,
|
name: String,
|
||||||
role: String,
|
role: String,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
running: bool,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
hyperhive_rev: Option<String>,
|
hyperhive_rev: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue