drop hive and agent response fields the frontend never reads
This commit is contained in:
parent
1875181d59
commit
a437f9ccd5
3 changed files with 347 additions and 598 deletions
|
|
@ -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",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue