refactor(dash): fold active_model into hyperhive-harness.json, not a separate file
hive-c0re was reading harness/hyperhive-model directly to surface the model badge on the dashboard. hyperhive-model is a runtime-override file (not the resolved priority) and adds to the marker-file count. Instead: mirror the fully-resolved model into hyperhive-harness.json (the consolidated state file that already replaced hyperhive-rate-limited / hyperhive-needs-login). Written by hive-ag3nt on: - Bus::new() startup (captures nix config > override > default) - set_model() runtime change (MCP set-model call) - emit_status() (keeps model current across rate-limit / auth flips) hive-c0re reads active_model from hyperhive-harness.json, same dir + same read path as rate_limited / needs_login. No new files.
This commit is contained in:
parent
4375ab6246
commit
c580d721fb
2 changed files with 45 additions and 22 deletions
|
|
@ -106,7 +106,7 @@ fn harness_json_path() -> PathBuf {
|
|||
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.
|
||||
if let Ok(raw) = std::fs::read_to_string(harness_json_path())
|
||||
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
|
||||
|
|
@ -119,24 +119,34 @@ fn read_harness_state() -> (bool, bool) {
|
|||
.get("needs_login")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.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.
|
||||
let state_dir = crate::paths::state_dir();
|
||||
let rate_limited = state_dir.join("hyperhive-rate-limited").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
|
||||
/// hive-c0re never reads a partial file.
|
||||
fn write_harness_state(rate_limited: bool, needs_login: bool) {
|
||||
/// hive-c0re never reads a partial file. Pass `active_model: Some(s)`
|
||||
/// 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 body = serde_json::json!({
|
||||
let mut json = serde_json::json!({
|
||||
"rate_limited": rate_limited,
|
||||
"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");
|
||||
if std::fs::write(&tmp, &body).is_ok() {
|
||||
let _ = std::fs::rename(&tmp, &path);
|
||||
|
|
@ -688,7 +698,12 @@ impl Bus {
|
|||
// Restore rate_limited (and needs_login) from the consolidated
|
||||
// harness state file so the dashboard shows the correct status
|
||||
// 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 {
|
||||
tx: Arc::new(tx),
|
||||
event_seq: Arc::new(AtomicU64::new(0)),
|
||||
|
|
@ -797,6 +812,11 @@ impl Bus {
|
|||
if let Err(e) = persist_model(&value) {
|
||||
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 });
|
||||
}
|
||||
|
||||
|
|
@ -1037,7 +1057,7 @@ impl Bus {
|
|||
// turns, so the two callers never overlap. Documented rather than
|
||||
// locked because adding a Mutex here would be overkill for the
|
||||
// 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" {
|
||||
true
|
||||
} else if status == "online" {
|
||||
|
|
@ -1045,7 +1065,8 @@ impl Bus {
|
|||
} else {
|
||||
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 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -205,18 +205,20 @@ pub async fn read_agent_status_live(name: &str) -> (Option<String>, Option<i64>,
|
|||
(text, set_at, true)
|
||||
}
|
||||
|
||||
/// Read the active Claude model from the agent's harness state file
|
||||
/// (`harness/hyperhive-model`). Returns `None` when the file is absent
|
||||
/// or empty — callers should treat `None` as "model not yet known".
|
||||
/// 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_harness_dir(name).join("hyperhive-model");
|
||||
let s = std::fs::read_to_string(path).ok()?;
|
||||
let trimmed = s.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_owned())
|
||||
}
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue