Watch
0
0
Fork
You've already forked hyperhive
0

harness metrics for tool calls, bash commands, reminders and first-turn context, on the agents board

The per-agent /stats page read these from its sqlite; the swarm board had
none of them. hive-agent now counts the tools a turn called, the commands
bash tasks ran (from the bash daemon's own table, newer rows only),
reminders scheduled and delivered, and records the first turn of a fresh
session; the turns tab shows them, with fresh sessions per agent. Claude's
own export has no tool-call counter, so the docs no longer say it does.
This commit is contained in:
iris 2026-11-03 16:05:30 +01:00
commit 23c2527199

View file

@ -449,6 +449,32 @@ fn read_bash_breakdown(conn: &Connection, from: i64) -> rusqlite::Result<Vec<Key
Ok(top_n(totals, 10))
}
/// The shell-command heads recorded after `since` (unix seconds, exclusive),
/// with how often each ran, for the OTEL bash-command counter. Reads the live
/// database read-only; a missing database or `bash_commands` table (no bash
/// task has run yet) is an empty list, not an error.
#[must_use]
pub fn bash_heads_since(since: i64) -> Vec<(String, u64)> {
let Ok(conn) = Connection::open_with_flags(default_path(), OpenFlags::SQLITE_OPEN_READ_ONLY)
else {
return Vec::new();
};
let _ = conn.busy_timeout(std::time::Duration::from_millis(500));
read_bash_heads_since(&conn, since).unwrap_or_default()
}
fn read_bash_heads_since(conn: &Connection, since: i64) -> rusqlite::Result<Vec<(String, u64)>> {
let mut stmt =
conn.prepare("SELECT head, COUNT(*) FROM bash_commands WHERE ts > ?1 GROUP BY head")?;
let rows = stmt.query_map([since], |row| {
Ok((
row.get::<_, String>(0)?,
u64_from_i64(row.get::<_, i64>(1)?),
))
})?;
rows.collect()
}
struct Row {
started_at: i64,
duration_ms: i64,
@ -782,6 +808,29 @@ mod tests {
assert!(s.bash_breakdown.is_empty());
}
/// The OTEL bash-command counter reads only the heads recorded after its
/// watermark, and a database without the table yields nothing.
#[test]
fn bash_heads_since_counts_only_newer_rows() {
let db = tmp_db();
let _ = std::fs::remove_file(&db);
let conn = Connection::open(&db).unwrap();
assert!(read_bash_heads_since(&conn, 0).is_err(), "no table yet");
conn.execute_batch("CREATE TABLE bash_commands (ts INTEGER NOT NULL, head TEXT NOT NULL);")
.unwrap();
for (ts, head) in [(10, "cargo"), (20, "cargo"), (30, "git"), (30, "cargo")] {
conn.execute(
"INSERT INTO bash_commands (ts, head) VALUES (?1, ?2)",
params![ts, head],
)
.unwrap();
}
let mut got = read_bash_heads_since(&conn, 10).unwrap();
got.sort();
assert_eq!(got, vec![("cargo".to_owned(), 2), ("git".to_owned(), 1)]);
assert!(read_bash_heads_since(&conn, 30).unwrap().is_empty());
}
/// With a populated `bash_commands` table, `bash_breakdown` rolls up
/// per-head counts (busiest first) and respects the window cutoff.
#[test]