Watch
0
0
Fork
You've already forked hyperhive
0

drop hive and agent response fields the frontend never reads

This commit is contained in:
atlas 2026-11-13 03:25:41 +01:00 • committed by mara
commit a437f9ccd5
3 changed files with 347 additions and 598 deletions

View file

@ -109,8 +109,6 @@ fn adaptive_bucket_secs(span_secs: i64) -> i64 {
pub struct Snapshot {
pub window: &'static str,
pub bucket_seconds: i64,
pub now: i64,
pub from: i64,
/// Total turns in the window.
pub turn_count: u64,
/// Time-bucketed trend series, oldest first. Always covers the
@ -231,8 +229,6 @@ fn empty_snapshot(window: Window) -> Snapshot {
Snapshot {
window: window.label(),
bucket_seconds: window.bucket_secs(),
now,
from,
turn_count: 0,
buckets,
tool_breakdown: Vec::new(),
@ -360,8 +356,6 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
Ok(Snapshot {
window: window.label(),
bucket_seconds: bucket_secs,
now,
from,
turn_count: acc.turn_count,
buckets,
tool_breakdown: top_n(acc.tool_totals, 10),
@ -873,4 +867,32 @@ mod tests {
Some("cargo")
);
}
/// `GET /api/stats` carries exactly the fields the stats page reads.
#[test]
fn the_stats_snapshot_serialises_exactly_its_read_fields() {
let value = serde_json::to_value(empty_snapshot(Window::Hour)).unwrap();
let mut keys: Vec<&str> = value
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
keys.sort_unstable();
assert_eq!(
keys,
[
"bash_breakdown",
"bucket_seconds",
"buckets",
"duration_summary",
"models",
"result_mix",
"tool_breakdown",
"turn_count",
"wake_mix",
"window",
]
);
}
}

View file

@ -6,7 +6,7 @@ use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use std::future::Future;
use super::{AppState, SOCKET_FETCH_TIMEOUT, error_response};
@ -53,9 +53,8 @@ pub(super) struct MarkTodosDoneForm {
/// `hive-agent-sock`: the todos list is small (single-digit rows most of the
/// time), so N same-host socket round-trips isn't a real cost, and it keeps
/// the wire protocol's `Request` enum — already used by the `cancel_loose_end`
/// MCP tool — unchanged. Unknown/already-acked ids just don't add to the
/// `acked` count (same "acking twice is not a new action" semantics as the
/// single-id path); a request with no ids or where every id fails to parse
/// MCP tool — unchanged. Unknown/already-acked ids succeed too (acking twice is
/// not a new action); a request with no ids or where every id fails to parse
/// is rejected as a client error rather than silently acking nothing. An id
/// whose store call errors or gets no answer fails the request with a 500
/// naming how many failed. More than [`MAX_MARK_DONE_IDS`] ids is a client
@ -102,15 +101,14 @@ where
);
}
match settle_mark_done(answers) {
Ok(acked) => axum::Json(MarkTodosDoneBody { acked }).into_response(),
Ok(()) => StatusCode::OK.into_response(),
Err(message) => error_response(StatusCode::INTERNAL_SERVER_ERROR, &message),
}
}
/// Folds the per-id answers into the acked total, or the operator-facing
/// failure. `None` is a dial that got no answer; `Response::Err` and any
/// Folds the per-id answers into success, or the operator-facing failure. `None` is a dial that got no answer; `Response::Err` and any
/// other variant are failures. `Acked { count: 0 }` is a success.
fn settle_mark_done(answers: Vec<Option<hive_agent_sock::Response>>) -> Result<u64, String> {
fn settle_mark_done(answers: Vec<Option<hive_agent_sock::Response>>) -> Result<(), String> {
let total = answers.len();
let mut acked = 0u64;
let mut failed = 0usize;
@ -133,18 +131,13 @@ fn settle_mark_done(answers: Vec<Option<hive_agent_sock::Response>>) -> Result<u
}
}
match first_error {
None => Ok(acked),
None => Ok(()),
Some(error) => Err(format!(
"mark-done: {failed} of {total} failed ({acked} marked done): {error}"
)),
}
}
#[derive(Serialize)]
struct MarkTodosDoneBody {
acked: u64,
}
#[cfg(test)]
mod tests {
use super::{mark_ids_done, settle_mark_done};
@ -187,13 +180,13 @@ mod tests {
}
#[tokio::test]
async fn mark_done_handler_answers_200_with_the_acked_total() {
async fn mark_done_handler_answers_200_with_an_empty_body() {
let response = mark_ids_done(vec![1, 2], std::time::Duration::from_secs(3), |_| async {
Some(hive_agent_sock::Response::Acked { count: 1 })
})
.await;
assert_eq!(response.status(), axum::http::StatusCode::OK);
assert_eq!(body_text(response).await, r#"{"acked":2}"#);
assert_eq!(body_text(response).await, "");
}
#[tokio::test(start_paused = true)]
@ -256,6 +249,6 @@ mod tests {
Some(hive_agent_sock::Response::Acked { count: 0 }),
Some(hive_agent_sock::Response::Acked { count: 2 }),
];
assert_eq!(settle_mark_done(answers).unwrap(), 2);
assert!(settle_mark_done(answers).is_ok());
}
}

View file

@ -9,9 +9,6 @@ use crate::login::LoginState;
use super::AppState;
pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<StateSnapshot> {
// Capture seq *before* any reads so the dedupe contract is
// "events with seq > snapshot.seq are post-snapshot, never missed."
let seq = state.bus.current_seq();
let login = *state.login.lock().unwrap();
let status = status_of(login, state.bus.is_rate_limited());
let dashboard_port = std::env::var("HIVE_DASHBOARD_PORT")
@ -20,7 +17,6 @@ pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<State
.unwrap_or(7000);
let inbox = recent_inbox(&state.socket).await;
let (turn_state, turn_state_since) = state.bus.state_snapshot();
let model = state.bus.model();
// The alias the operator selected (e.g. "sonnet") vs. what the last
// turn actually ran on (e.g. "claude-sonnet-4-5-20260805") — mara:
// "i cannot see if sonnet became claude-sonnet-5 or something else".
@ -28,12 +24,10 @@ pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<State
// resolve from yet); same source `serve_common.rs`'s turn-stats
// rollup already prefers over the requested alias.
let resolved_model = state.bus.last_resolved_model();
let context_window_tokens = state.bus.effective_context_window(&model);
let ctx_usage = state.bus.last_ctx_usage();
let cost_usage = state.bus.last_cost_usage();
let shown = shown(&state.bus);
axum::Json(StateSnapshot {
seq,
label: state.label.clone(),
qualified_label: crate::identity::qualify(&state.label),
dashboard_port,
@ -44,7 +38,6 @@ pub(super) async fn api_state(State(state): State<AppState>) -> axum::Json<State
model: shown.model,
model_source: state.bus.model_source(),
resolved_model,
context_window_tokens,
ctx_usage,
cost_usage,
links: agent_links(state.gui_vnc_port.is_some()),
@ -70,19 +63,9 @@ pub(super) async fn api_dashboard_state(
) -> axum::Json<DashboardState> {
let (status_text, status_set_at) = read_own_status();
let rate_limited = state.bus.is_rate_limited();
let model = state.bus.model();
let context_window_tokens = state.bus.effective_context_window(&model);
// Full context-window size = input + cache-read + cache-creation. Using
// raw `input_tokens` here reported only the *uncached* sliver, which is
// ~0 once prompt caching kicks in — so every card showed `ctx·0k`. Match
// the agent page (which ships the whole `ctx_usage` and sums it) and the
// cache-TTL logic in turn.rs, both of which use `context_tokens()`.
let ctx_tokens = state.bus.last_ctx_usage().map(|u| u.context_tokens());
axum::Json(DashboardState {
status_text,
status_set_at,
ctx_tokens,
context_window_tokens,
rate_limited,
links: agent_links(state.gui_vnc_port.is_some()),
})
@ -90,12 +73,6 @@ pub(super) async fn api_dashboard_state(
#[derive(Serialize)]
pub(super) struct StateSnapshot {
/// Bus seq at the moment this snapshot was assembled. Clients dedupe
/// their buffered SSE traffic against this value: events with
/// `seq <= snapshot.seq` are already reflected (or pre-date the
/// snapshot); `seq > snapshot.seq` is post-snapshot. Reset to 0 on
/// harness restart — clients treat reconnect as a fresh world.
seq: u64,
label: String,
/// Hive-qualified long name (`${label}@${hyperhive.domain}`) when
/// the host has been configured for a multi-hive swarm; falls back
@ -126,12 +103,6 @@ pub(super) struct StateSnapshot {
/// operator picked, which claude itself maps to a specific dated
/// snapshot. `None` before any turn has completed.
resolved_model: Option<String>,
/// Effective context-window token budget for the current model.
/// Primary source: API-reported `modelUsage.*.contextWindow` from
/// the last result event (authoritative per-inference active window).
/// Falls back to `harness_state::context_window_tokens(model)`.
/// Consumers (e.g. dashboard badge) use this to render ctx-usage %.
context_window_tokens: u64,
/// Last-inference token usage from the most recent completed
/// turn — represents the current context-window size at turn-end.
/// `null` until the first turn finishes.
@ -214,14 +185,6 @@ pub(super) struct DashboardState {
/// `None` when no status is set.
#[serde(skip_serializing_if = "Option::is_none")]
status_set_at: Option<i64>,
/// Full context-window size from the most recent completed turn
/// (`ctx_usage.context_tokens()` = input + cache-read + cache-creation).
/// `None` until the first turn finishes. Drives the `ctx·Nk` card badge.
#[serde(skip_serializing_if = "Option::is_none")]
ctx_tokens: Option<u64>,
/// Effective context-window budget for the current model. Same
/// derivation as `StateSnapshot::context_window_tokens`.
context_window_tokens: u64,
/// True while the harness is parked after a rate-limit response.
rate_limited: bool,
/// Navigation links for the dashboard card's icon strip. This is
@ -487,4 +450,76 @@ mod tests {
shown("haiku", None)
);
}
fn sorted_keys(v: &serde_json::Value) -> Vec<&str> {
let mut keys: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
keys.sort_unstable();
keys
}
/// `GET /api/state` carries exactly the fields the agent page and stats
/// page read; a field added back needs a reader.
#[test]
fn the_state_snapshot_serialises_exactly_its_read_fields() {
let snap = super::StateSnapshot {
label: "a".to_owned(),
qualified_label: "a".to_owned(),
dashboard_port: 7000,
status: "online",
inbox: Vec::new(),
turn_state: crate::events::TurnState::Idle,
turn_state_since: 0,
model: "haiku".to_owned(),
model_source: swarm_wire::agent_inference::Source::Default,
resolved_model: None,
ctx_usage: None,
cost_usage: None,
links: Vec::new(),
hive_name: None,
swarm_name: None,
effort: None,
effort_source: swarm_wire::agent_inference::Source::Default,
paused: false,
};
let value = serde_json::to_value(&snap).unwrap();
assert_eq!(
sorted_keys(&value),
[
"cost_usage",
"ctx_usage",
"dashboard_port",
"effort",
"effort_source",
"hive_name",
"inbox",
"label",
"links",
"model",
"model_source",
"paused",
"qualified_label",
"resolved_model",
"status",
"swarm_name",
"turn_state",
"turn_state_since",
]
);
}
/// `GET /api/dashboard-state` carries exactly what the dashboard card reads.
#[test]
fn the_dashboard_state_serialises_exactly_its_read_fields() {
let state = super::DashboardState {
status_text: Some("busy".to_owned()),
status_set_at: Some(1),
rate_limited: false,
links: Vec::new(),
};
let value = serde_json::to_value(&state).unwrap();
assert_eq!(
sorted_keys(&value),
["links", "rate_limited", "status_set_at", "status_text"]
);
}
}