Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6639fe093 | ||
|
|
c580d721fb | ||
|
|
4375ab6246 |
5 changed files with 77 additions and 11 deletions
|
|
@ -85,6 +85,11 @@ code {
|
||||||
color: var(--purple); border-color: var(--purple);
|
color: var(--purple); border-color: var(--purple);
|
||||||
text-shadow: 0 0 6px color-mix(in srgb, var(--purple) 40%, transparent);
|
text-shadow: 0 0 6px color-mix(in srgb, var(--purple) 40%, transparent);
|
||||||
}
|
}
|
||||||
|
/* Active Claude model badge on dashboard container rows. */
|
||||||
|
.badge-model {
|
||||||
|
color: var(--blue); border-color: var(--blue);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
/* Context-window usage badges on dashboard container rows. */
|
/* Context-window usage badges on dashboard container rows. */
|
||||||
.badge-ctx-ok {
|
.badge-ctx-ok {
|
||||||
color: var(--green); border-color: var(--green);
|
color: var(--green); border-color: var(--green);
|
||||||
|
|
|
||||||
|
|
@ -521,6 +521,7 @@ window.marked = marked;
|
||||||
needs_login: c.needs_login,
|
needs_login: c.needs_login,
|
||||||
needs_update: c.needs_update,
|
needs_update: c.needs_update,
|
||||||
pending_reminders: c.pending_reminders,
|
pending_reminders: c.pending_reminders,
|
||||||
|
active_model: c.active_model,
|
||||||
port: c.port,
|
port: c.port,
|
||||||
pending,
|
pending,
|
||||||
opRunning,
|
opRunning,
|
||||||
|
|
@ -726,6 +727,12 @@ window.marked = marked;
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (c.active_model) {
|
||||||
|
head.append(el('span',
|
||||||
|
{ class: 'badge badge-model', title: `active claude model: ${c.active_model}` },
|
||||||
|
c.active_model));
|
||||||
|
}
|
||||||
|
|
||||||
if (c.pending_reminders && c.pending_reminders > 0) {
|
if (c.pending_reminders && c.pending_reminders > 0) {
|
||||||
head.append(el('span',
|
head.append(el('span',
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ fn harness_json_path() -> PathBuf {
|
||||||
crate::paths::state_dir().join(HARNESS_JSON)
|
crate::paths::state_dir().join(HARNESS_JSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_harness_state() -> (bool, bool) {
|
fn read_harness_state() -> (bool, bool, Option<String>) {
|
||||||
// Try the new consolidated file first.
|
// Try the new consolidated file first.
|
||||||
if let Ok(raw) = std::fs::read_to_string(harness_json_path())
|
if let Ok(raw) = std::fs::read_to_string(harness_json_path())
|
||||||
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
|
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
|
||||||
|
|
@ -119,24 +119,34 @@ fn read_harness_state() -> (bool, bool) {
|
||||||
.get("needs_login")
|
.get("needs_login")
|
||||||
.and_then(serde_json::Value::as_bool)
|
.and_then(serde_json::Value::as_bool)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
return (rate_limited, needs_login);
|
let active_model = v
|
||||||
|
.get("active_model")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(str::to_owned);
|
||||||
|
return (rate_limited, needs_login, active_model);
|
||||||
}
|
}
|
||||||
// Fall back to legacy sentinel files written by older harness builds.
|
// Fall back to legacy sentinel files written by older harness builds.
|
||||||
let state_dir = crate::paths::state_dir();
|
let state_dir = crate::paths::state_dir();
|
||||||
let rate_limited = state_dir.join("hyperhive-rate-limited").exists();
|
let rate_limited = state_dir.join("hyperhive-rate-limited").exists();
|
||||||
let needs_login = state_dir.join("hyperhive-needs-login").exists();
|
let needs_login = state_dir.join("hyperhive-needs-login").exists();
|
||||||
(rate_limited, needs_login)
|
(rate_limited, needs_login, None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write harness state atomically via a `.tmp` + `rename` pair so
|
/// Write harness state atomically via a `.tmp` + `rename` pair so
|
||||||
/// hive-c0re never reads a partial file.
|
/// hive-c0re never reads a partial file. Pass `active_model: Some(s)`
|
||||||
fn write_harness_state(rate_limited: bool, needs_login: bool) {
|
/// to include the resolved model (as surfaced in the dashboard badge);
|
||||||
|
/// `None` omits the field, which hive-c0re treats as "not yet known".
|
||||||
|
fn write_harness_state(rate_limited: bool, needs_login: bool, active_model: Option<&str>) {
|
||||||
let path = harness_json_path();
|
let path = harness_json_path();
|
||||||
let body = serde_json::json!({
|
let mut json = serde_json::json!({
|
||||||
"rate_limited": rate_limited,
|
"rate_limited": rate_limited,
|
||||||
"needs_login": needs_login,
|
"needs_login": needs_login,
|
||||||
})
|
});
|
||||||
.to_string();
|
if let Some(model) = active_model {
|
||||||
|
json["active_model"] = serde_json::Value::String(model.to_string());
|
||||||
|
}
|
||||||
|
let body = json.to_string();
|
||||||
let tmp = path.with_extension("json.tmp");
|
let tmp = path.with_extension("json.tmp");
|
||||||
if std::fs::write(&tmp, &body).is_ok() {
|
if std::fs::write(&tmp, &body).is_ok() {
|
||||||
let _ = std::fs::rename(&tmp, &path);
|
let _ = std::fs::rename(&tmp, &path);
|
||||||
|
|
@ -688,7 +698,12 @@ impl Bus {
|
||||||
// Restore rate_limited (and needs_login) from the consolidated
|
// Restore rate_limited (and needs_login) from the consolidated
|
||||||
// harness state file so the dashboard shows the correct status
|
// harness state file so the dashboard shows the correct status
|
||||||
// on cold load if the harness crashed while parked.
|
// on cold load if the harness crashed while parked.
|
||||||
let (was_rate_limited, _was_needs_login) = read_harness_state();
|
let (was_rate_limited, was_needs_login, _) = read_harness_state();
|
||||||
|
// Write the resolved active model to hyperhive-harness.json on
|
||||||
|
// startup so hive-c0re can surface the model badge without reading
|
||||||
|
// hyperhive-model directly. Written once here; updated on every
|
||||||
|
// set_model() call (runtime override) and emit_status() call.
|
||||||
|
write_harness_state(was_rate_limited, was_needs_login, Some(&initial_model));
|
||||||
Self {
|
Self {
|
||||||
tx: Arc::new(tx),
|
tx: Arc::new(tx),
|
||||||
event_seq: Arc::new(AtomicU64::new(0)),
|
event_seq: Arc::new(AtomicU64::new(0)),
|
||||||
|
|
@ -797,6 +812,11 @@ impl Bus {
|
||||||
if let Err(e) = persist_model(&value) {
|
if let Err(e) = persist_model(&value) {
|
||||||
tracing::warn!(error = ?e, "model: persist failed");
|
tracing::warn!(error = ?e, "model: persist failed");
|
||||||
}
|
}
|
||||||
|
// Mirror the resolved model into hyperhive-harness.json so
|
||||||
|
// hive-c0re can surface it on the dashboard badge without reading
|
||||||
|
// the hyperhive-model override file directly.
|
||||||
|
let (rate_limited, needs_login, _) = read_harness_state();
|
||||||
|
write_harness_state(rate_limited, needs_login, Some(&value));
|
||||||
self.emit(LiveEvent::ModelChanged { model: value });
|
self.emit(LiveEvent::ModelChanged { model: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1037,7 +1057,7 @@ impl Bus {
|
||||||
// turns, so the two callers never overlap. Documented rather than
|
// turns, so the two callers never overlap. Documented rather than
|
||||||
// locked because adding a Mutex here would be overkill for the
|
// locked because adding a Mutex here would be overkill for the
|
||||||
// actual call pattern.
|
// actual call pattern.
|
||||||
let (_, current_needs_login) = read_harness_state();
|
let (_, current_needs_login, _) = read_harness_state();
|
||||||
let new_needs_login = if status == "needs_login_idle" {
|
let new_needs_login = if status == "needs_login_idle" {
|
||||||
true
|
true
|
||||||
} else if status == "online" {
|
} else if status == "online" {
|
||||||
|
|
@ -1045,7 +1065,8 @@ impl Bus {
|
||||||
} else {
|
} else {
|
||||||
current_needs_login
|
current_needs_login
|
||||||
};
|
};
|
||||||
write_harness_state(new_rate_limited, new_needs_login);
|
let current_model = self.model.lock().unwrap().clone();
|
||||||
|
write_harness_state(new_rate_limited, new_needs_login, Some(¤t_model));
|
||||||
self.emit(LiveEvent::StatusChanged { status });
|
self.emit(LiveEvent::StatusChanged { status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,13 @@ pub struct ContainerView {
|
||||||
/// in the tree. See `docs/agent-hierarchy.md::Current state`.
|
/// in the tree. See `docs/agent-hierarchy.md::Current state`.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub parent: Option<String>,
|
pub parent: Option<String>,
|
||||||
|
/// The Claude model the agent's harness is currently using, read from
|
||||||
|
/// `state/hyperhive-harness.json["active_model"]`. `None` when the
|
||||||
|
/// agent has never started a turn or the field is absent. Only
|
||||||
|
/// meaningful when `running` is true; the dashboard skips the badge
|
||||||
|
/// for stopped containers.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub active_model: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the full container list. Wraps `lifecycle::list()` and
|
/// Build the full container list. Wraps `lifecycle::list()` and
|
||||||
|
|
@ -78,6 +85,14 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
||||||
let needs_login = running
|
let needs_login = running
|
||||||
&& (!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
&& (!claude_has_session(&Coordinator::agent_claude_dir(&logical))
|
||||||
|| auth_failed_sentinel(&logical));
|
|| auth_failed_sentinel(&logical));
|
||||||
|
// Read the active model from the harness state file. Only surfaced
|
||||||
|
// when the container is running — stale model info from a stopped
|
||||||
|
// agent is misleading (the model may change on next boot).
|
||||||
|
let active_model = if running {
|
||||||
|
read_active_model(&logical)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
out.push(ContainerView {
|
out.push(ContainerView {
|
||||||
port: lifecycle::agent_web_port(&logical),
|
port: lifecycle::agent_web_port(&logical),
|
||||||
running,
|
running,
|
||||||
|
|
@ -88,6 +103,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
||||||
deployed_sha,
|
deployed_sha,
|
||||||
pending_reminders,
|
pending_reminders,
|
||||||
parent,
|
parent,
|
||||||
|
active_model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
@ -190,6 +206,22 @@ pub async fn read_agent_status_live(name: &str) -> (Option<String>, Option<i64>,
|
||||||
(text, set_at, true)
|
(text, set_at, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the active Claude model from `hyperhive-harness.json` (the
|
||||||
|
/// consolidated harness state file in the agent's state dir). Written by
|
||||||
|
/// the harness on startup and on every `set_model` / `emit_status` call,
|
||||||
|
/// so it always reflects the resolved priority (nix config > runtime
|
||||||
|
/// override > default). Returns `None` when the field is absent or the
|
||||||
|
/// harness has not yet started a turn.
|
||||||
|
fn read_active_model(name: &str) -> Option<String> {
|
||||||
|
let path = Coordinator::agent_notes_dir(name).join("hyperhive-harness.json");
|
||||||
|
let raw = std::fs::read_to_string(path).ok()?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&raw).ok()?;
|
||||||
|
v.get("active_model")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(str::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
/// Host-side hive + swarm display names, read from the c0re service's
|
/// Host-side hive + swarm display names, read from the c0re service's
|
||||||
/// own process env. The `hive-c0re.nix` module sets these from
|
/// own process env. The `hive-c0re.nix` module sets these from
|
||||||
/// `services.hyperhive.{hiveName, swarmName}`. The agent-side
|
/// `services.hyperhive.{hiveName, swarmName}`. The agent-side
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,7 @@ mod tests {
|
||||||
deployed_sha: None,
|
deployed_sha: None,
|
||||||
pending_reminders: 0,
|
pending_reminders: 0,
|
||||||
parent: None,
|
parent: None,
|
||||||
|
active_model: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue