refactor(#838): consolidate harness state files into hyperhive-harness.json
This commit is contained in:
parent
f6b3145349
commit
fce1f49f6a
7 changed files with 166 additions and 78 deletions
|
|
@ -4,8 +4,8 @@
|
|||
//! excluded from the map (manager UI routes via the dashboard
|
||||
//! upstream, not per-agent `/agent/<name>/`).
|
||||
//!
|
||||
//! Full mechanism — per-agent subdir bind-mount, `.bound` marker
|
||||
//! gate, gateway UDS upstream, transition vs `agent-ports.json`,
|
||||
//! Full mechanism — per-agent subdir bind-mount, `hyperhive-socket-bound`
|
||||
//! marker gate, gateway UDS upstream, transition vs `agent-ports.json`,
|
||||
//! 10s poll loop: `docs/gateway.md::Per-agent unix-socket upstream`.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
|
@ -36,7 +36,13 @@ pub const SOCKET_FILENAME: &str = "web.sock";
|
|||
/// publish the unix upstream for this agent yet". Without this gate
|
||||
/// the gateway would `proxy_pass` to a non-existent socket for every
|
||||
/// sub-agent that hasn't flipped the option yet.
|
||||
pub const READY_MARKER: &str = ".bound";
|
||||
///
|
||||
/// Renamed from `.bound` (legacy) to match the `hyperhive-` prefix
|
||||
/// convention for all harness-written state files (#838). `build_map`
|
||||
/// checks both names during the transition window so existing containers
|
||||
/// don't lose gateway routing before their next rebuild.
|
||||
pub const READY_MARKER: &str = "hyperhive-socket-bound";
|
||||
const READY_MARKER_LEGACY: &str = ".bound";
|
||||
|
||||
#[must_use]
|
||||
pub fn host_sockets_path() -> PathBuf {
|
||||
|
|
@ -72,18 +78,27 @@ pub fn socket_path_for(name: &str) -> PathBuf {
|
|||
/// `proxy_pass` to a non-existent socket for every sub-agent that
|
||||
/// hasn't yet flipped `hyperhive.web.useUnixSocket = true`.
|
||||
///
|
||||
/// Accepts either the new `hyperhive-socket-bound` marker or the legacy
|
||||
/// `.bound` marker so existing containers keep their gateway routing
|
||||
/// through the transition window (before their next rebuild writes the
|
||||
/// new marker name).
|
||||
///
|
||||
/// `BTreeMap` keeps the JSON output sorted by key so a re-emit
|
||||
/// without churn produces byte-identical output — same idempotency
|
||||
/// shape `agent_ports::write` relies on.
|
||||
#[must_use]
|
||||
pub fn build_map(names: &[String]) -> BTreeMap<String, PathBuf> {
|
||||
build_map_with(names, |name| ready_marker_for(name).exists())
|
||||
build_map_with(names, |name| {
|
||||
ready_marker_for(name).exists()
|
||||
|| agent_dir_for(name).join(READY_MARKER_LEGACY).exists()
|
||||
})
|
||||
}
|
||||
|
||||
/// Body of `build_map` with the ready-check parameterised. Tests
|
||||
/// pass a predicate they control (no real filesystem access).
|
||||
/// Production callers go through `build_map` which wires the
|
||||
/// predicate to the on-disk `.bound` marker check.
|
||||
/// predicate to the on-disk `hyperhive-socket-bound` (or legacy
|
||||
/// `.bound`) marker check.
|
||||
fn build_map_with<F>(names: &[String], is_ready: F) -> BTreeMap<String, PathBuf>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
|
|
@ -96,7 +111,7 @@ where
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Path to the per-agent `.bound` marker file the harness writes
|
||||
/// Path to the `hyperhive-socket-bound` marker file the harness writes
|
||||
/// after a successful `bind_unix`. Lives next to `web.sock` in the
|
||||
/// per-agent subdir so it's covered by the same bind-mount and same
|
||||
/// per-agent isolation as the socket itself.
|
||||
|
|
@ -271,10 +286,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn build_map_filters_by_ready_predicate() {
|
||||
// The new gate: only ready agents (with `.bound` marker) get
|
||||
// published. Pin the behaviour so a future refactor that
|
||||
// drops the filter surfaces here, not as a 502-spew in the
|
||||
// gateway.
|
||||
// Only ready agents (with `hyperhive-socket-bound` marker) get
|
||||
// published. Pin the behaviour so a future refactor that drops
|
||||
// the filter surfaces here, not as a 502-spew in the gateway.
|
||||
let names: Vec<String> = ["iris", "argus", "atlas"]
|
||||
.iter()
|
||||
.map(|s| (*s).to_owned())
|
||||
|
|
@ -296,7 +310,7 @@ mod tests {
|
|||
let marker = ready_marker_for("iris");
|
||||
let socket = socket_path_for("iris");
|
||||
assert_eq!(marker.parent(), socket.parent());
|
||||
assert_eq!(marker, Path::new("/run/hive-agent/iris/.bound"));
|
||||
assert_eq!(marker, Path::new("/run/hive-agent/iris/hyperhive-socket-bound"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -211,24 +211,31 @@ fn read_dashboard_links(name: &str) -> Vec<DashboardLink> {
|
|||
serde_json::from_str::<Vec<DashboardLink>>(&text).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns true if the agent's harness is currently parked after an API
|
||||
/// rate-limit response. Detected via the sentinel file written by
|
||||
/// `hive_ag3nt::events::Bus::emit_status("rate_limited")`.
|
||||
/// Read `rate_limited` + `needs_login` from the consolidated
|
||||
/// `hyperhive-harness.json`. Falls back to the legacy individual
|
||||
/// sentinel files written by older harness builds so in-place upgrades
|
||||
/// don't lose state during the transition window.
|
||||
fn read_harness_flags(name: &str) -> (bool, bool) {
|
||||
let dir = Coordinator::agent_notes_dir(name);
|
||||
if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json")) {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) {
|
||||
let rl = v.get("rate_limited").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
let nl = v.get("needs_login").and_then(|x| x.as_bool()).unwrap_or(false);
|
||||
return (rl, nl);
|
||||
}
|
||||
}
|
||||
// Legacy fallback: presence of individual sentinel files.
|
||||
let rate_limited = dir.join("hyperhive-rate-limited").exists();
|
||||
let needs_login = dir.join("hyperhive-needs-login").exists();
|
||||
(rate_limited, needs_login)
|
||||
}
|
||||
|
||||
fn is_rate_limited(name: &str) -> bool {
|
||||
Coordinator::agent_notes_dir(name)
|
||||
.join("hyperhive-rate-limited")
|
||||
.exists()
|
||||
read_harness_flags(name).0
|
||||
}
|
||||
|
||||
/// True when the harness wrote `{state_dir}/hyperhive-needs-login`
|
||||
/// after a 401 mid-turn. Lets the dashboard surface `needs_login` for
|
||||
/// agents whose `/root/.claude/` dir still exists (so
|
||||
/// `claude_has_session` returns true) but whose OAuth credentials
|
||||
/// inside it have actually expired.
|
||||
fn auth_failed_sentinel(name: &str) -> bool {
|
||||
Coordinator::agent_notes_dir(name)
|
||||
.join("hyperhive-needs-login")
|
||||
.exists()
|
||||
read_harness_flags(name).1
|
||||
}
|
||||
|
||||
/// Read the agent's free-text status and the Unix timestamp when it was last set
|
||||
|
|
|
|||
Loading…
Reference in a new issue