container_view: clear live-only fields when stopped (#432)
per mara's review on #433, move the gating from the dashboard into the host so a stopped container's stale on-disk state (rate_limited sentinel, hyperhive-needs-login, last-turn-stats row, status blob) never reaches the wire in the first place. when build_all sees is_running == false: - needs_login → false - ctx_tokens / context_window_tokens → None - rate_limited → false - status_text / status_set_at → None static / declared fields (extra_links, deployed_sha, pending_reminders, needs_update, parent) stay populated regardless of run state. extend AgentMeta (both AgentResponse + ManagerResponse) with a `running: bool` field so get_agent_meta callers can tell whether the target is up — answers the second half of #432 ("agent meta should probably show the info that it is not running as well"). read_agent_status_live wraps the existing read_agent_status with the same is_running gate so the manager/agent socket handlers don't have to know about sentinel semantics. format_agent_meta now prints a `running: yes|no` line so claude sees the run state in plain text alongside hyperhive_rev. frontend follow-up in the same commit: drop the redundant `c.running &&` guards on ctx_tokens / status_text in renderContainers — the backend now guarantees those fields are absent when the container is stopped, so the existing truthy-check is sufficient. the `■ not running` badge + icon / links fetch short-circuits stay (those are pure presentation / network-noise wins the backend can't address).
This commit is contained in:
parent
683e59c757
commit
7b4917b256
6 changed files with 131 additions and 42 deletions
|
|
@ -246,8 +246,12 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
|||
}
|
||||
AgentRequest::GetAgentMeta { name } => {
|
||||
let target = name.as_deref().unwrap_or(agent);
|
||||
let (status_text, status_set_at) =
|
||||
crate::container_view::read_agent_status(target);
|
||||
// #432: gate status on the target's running state so a
|
||||
// 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 {
|
||||
"manager"
|
||||
} else {
|
||||
|
|
@ -257,6 +261,7 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
|||
AgentResponse::AgentMeta {
|
||||
name: target.to_owned(),
|
||||
role,
|
||||
running,
|
||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||
status_text,
|
||||
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 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());
|
||||
// Recipient name the broker uses for this agent — sub-agents
|
||||
// are addressed by logical name, the manager by the
|
||||
|
|
@ -140,18 +132,41 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
|||
.broker
|
||||
.count_pending_reminders_for(reminder_recipient)
|
||||
.unwrap_or(0);
|
||||
let last_turn = read_last_turn(&logical);
|
||||
let ctx_tokens = last_turn.as_ref().map(|(toks, _)| *toks);
|
||||
let context_window_tokens = last_turn
|
||||
.as_ref()
|
||||
.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, status_set_at) = read_status(&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 ctx_tokens = last_turn.as_ref().map(|(toks, _)| *toks);
|
||||
let context_window_tokens = last_turn
|
||||
.as_ref()
|
||||
.and_then(|(_, model)| resolve_ctx_window(model, &coord.context_window_tokens));
|
||||
let rate_limited = is_rate_limited(&logical);
|
||||
let (status_text, status_set_at) = read_status(&logical);
|
||||
(needs_login, ctx_tokens, context_window_tokens, rate_limited, status_text, status_set_at)
|
||||
} else {
|
||||
(false, None, None, false, None, None)
|
||||
};
|
||||
out.push(ContainerView {
|
||||
port: lifecycle::agent_web_port(&logical),
|
||||
running: lifecycle::is_running(&logical).await,
|
||||
running,
|
||||
container: c.clone(),
|
||||
name: logical,
|
||||
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
|
||||
/// (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`.
|
||||
///
|
||||
/// 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>) {
|
||||
let path = Coordinator::agent_notes_dir(name).join("hyperhive-status");
|
||||
let meta = std::fs::metadata(&path).ok();
|
||||
|
|
@ -237,6 +256,34 @@ fn read_status(name: &str) -> (Option<String>, Option<i64>) {
|
|||
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
|
||||
/// `SQLite`: the context-window size (prompt tokens) and the model name.
|
||||
/// 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 } => {
|
||||
let target = name.as_deref().unwrap_or(MANAGER_AGENT);
|
||||
let (status_text, status_set_at) =
|
||||
crate::container_view::read_agent_status(target);
|
||||
// #432: gate status on the target's running state so a
|
||||
// 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();
|
||||
ManagerResponse::AgentMeta {
|
||||
name: target.to_owned(),
|
||||
role,
|
||||
running,
|
||||
hyperhive_rev: crate::auto_update::current_flake_rev(&coord.hyperhive_flake),
|
||||
status_text,
|
||||
status_set_at,
|
||||
|
|
|
|||
Loading…
Reference in a new issue