docs: add missing #[must_use], # Errors, # Panics across public api

This commit is contained in:
damocles 2026-05-22 19:41:27 +02:00
parent 748536203b
commit 908cadb151
10 changed files with 157 additions and 0 deletions

View file

@ -19,6 +19,11 @@ const RETRY_BACKOFFS_MS: &[u64] = &[2_000, 4_000, 8_000, 16_000, 30_000];
/// the retry count. Use this from non-tool callers (the harness serve
/// loop, web UI, CLI subcommands) where we just want the socket-restart
/// resilience without surfacing the bookkeeping.
///
/// # Errors
///
/// Returns an error if the socket is unreachable after all retries, or if
/// serialization / deserialization of the request or response fails.
pub async fn request<Req, Resp>(socket: &Path, req: &Req) -> Result<Resp>
where
Req: Serialize + ?Sized,
@ -33,6 +38,16 @@ where
/// retries happened — that way claude knows the prior socket flake
/// wasn't a content error and shouldn't trigger an LLM-level retry of
/// its own.
///
/// # Errors
///
/// Returns an error if all retries are exhausted, or on a fatal protocol
/// error (serialization / deserialization failure).
///
/// # Panics
///
/// Panics if `RETRY_BACKOFFS_MS.len()` does not fit in a `u32`, which
/// cannot happen with the current compile-time constant.
pub async fn request_retried<Req, Resp>(socket: &Path, req: &Req) -> Result<(Resp, u32)>
where
Req: Serialize + ?Sized,

View file

@ -222,6 +222,7 @@ pub struct TokenUsage {
impl TokenUsage {
/// Total context consumed this turn (input + cache reads + cache writes).
#[must_use]
pub fn context_tokens(&self) -> u64 {
self.input_tokens + self.cache_read_input_tokens + self.cache_creation_input_tokens
}
@ -230,6 +231,7 @@ impl TokenUsage {
/// **cumulative** sum across every inference in the turn — useful as a
/// cost signal, but NOT the current context size (a tool-heavy turn
/// sums per-call cached prompts and easily exceeds the model window).
#[must_use]
pub fn from_stream_event(v: &serde_json::Value) -> Option<Self> {
if v.get("type").and_then(|t| t.as_str()) != Some("result") {
return None;
@ -241,6 +243,7 @@ impl TokenUsage {
/// `.message.usage` block. Each turn fires one of these for every
/// model call; tracking the LAST one over the turn gives the actual
/// conversation context size — the number to watch for compaction.
#[must_use]
pub fn from_assistant_event(v: &serde_json::Value) -> Option<Self> {
if v.get("type").and_then(|t| t.as_str()) != Some("assistant") {
return None;
@ -443,12 +446,17 @@ impl Bus {
/// Take + clear the one-shot. Returns true iff the caller should
/// run claude without `--continue` for this turn.
#[must_use]
pub fn take_skip_continue(&self) -> bool {
self.skip_continue_once.swap(false, Ordering::SeqCst)
}
/// Currently-selected claude model name. Read on every turn so a
/// `/model <name>` flip takes effect on the next turn.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn model(&self) -> String {
self.model.lock().unwrap().clone()
@ -459,6 +467,10 @@ impl Bus {
/// state dir (`hyperhive-model`) so the override survives harness
/// restart and container rebuild (gone on `--purge`, matching
/// every other piece of agent state).
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
pub fn set_model(&self, name: impl Into<String>) {
let value: String = name.into();
self.model.lock().unwrap().clone_from(&value);
@ -472,6 +484,10 @@ impl Bus {
/// emitting a SSE event. Used by the bin entrypoints to backfill
/// from the most recent `turn_stats` row so the per-agent web UI's
/// ctx + cost badges paint real numbers on cold load.
///
/// # Panics
///
/// Panics if an internal lock is poisoned.
pub fn seed_usage(&self, ctx: Option<TokenUsage>, cost: Option<TokenUsage>) {
if ctx.is_some() {
*self.last_ctx_usage.lock().unwrap() = ctx;
@ -485,6 +501,10 @@ impl Bus {
/// usage (current context size); `cost` is the cumulative across
/// every inference in the turn (cost signal). One SSE event fires
/// per turn carrying both.
///
/// # Panics
///
/// Panics if an internal lock is poisoned.
pub fn record_turn_usage(&self, ctx: TokenUsage, cost: TokenUsage) {
*self.last_ctx_usage.lock().unwrap() = Some(ctx);
*self.last_cost_usage.lock().unwrap() = Some(cost);
@ -503,6 +523,10 @@ impl Bus {
/// per-turn counter for each one we find. Called by the stdout
/// pump on every parsed line. Cheap when the line isn't an
/// assistant message — the field-check short-circuits.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
pub fn observe_stream(&self, v: &serde_json::Value) {
if v.get("type").and_then(|t| t.as_str()) != Some("assistant") {
return;
@ -531,6 +555,10 @@ impl Bus {
/// Snapshot + clear the per-turn tool-call counter. The harness
/// calls this between turns to fold the breakdown into a
/// `turn_stats` row, then start the next turn with an empty map.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn take_tool_calls(&self) -> std::collections::HashMap<String, u64> {
std::mem::take(&mut *self.tool_calls.lock().unwrap())
@ -538,6 +566,10 @@ impl Bus {
/// Last context-size snapshot (last inference of the most recent
/// turn), or `None` if no turn has completed yet.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn last_ctx_usage(&self) -> Option<TokenUsage> {
*self.last_ctx_usage.lock().unwrap()
@ -545,6 +577,10 @@ impl Bus {
/// Last cumulative cost snapshot (sum across the most recent turn's
/// inferences), or `None` if no turn has completed yet.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn last_cost_usage(&self) -> Option<TokenUsage> {
*self.last_cost_usage.lock().unwrap()
@ -552,6 +588,10 @@ impl Bus {
/// Update the harness's authoritative turn-loop state. Records
/// the transition time so `state_snapshot` can return a since-age.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
pub fn set_state(&self, next: TurnState) {
let since;
{
@ -598,6 +638,10 @@ impl Bus {
}
/// Current state + since-when (unix seconds). Snapshot copy, no lock held.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn state_snapshot(&self) -> (TurnState, i64) {
*self.state.lock().unwrap()
@ -617,6 +661,7 @@ impl Bus {
let _ = self.tx.send(envelope);
}
#[must_use]
pub fn subscribe(&self) -> broadcast::Receiver<BusEvent> {
self.tx.subscribe()
}

View file

@ -48,6 +48,11 @@ impl LoginSession {
/// `HYPERHIVE_LOGIN_CMD` (single string, shell-split into argv); by
/// default we run `claude auth login`. Failing to spawn returns an error
/// before any state is registered.
///
/// # Errors
///
/// Returns an error if spawning the login command fails, or if the child's
/// stdio handles cannot be acquired.
pub fn start() -> Result<Self> {
let (cmd, args) = resolve_command();
tracing::info!(%cmd, ?args, "spawning login session");
@ -82,6 +87,11 @@ impl LoginSession {
/// Write `code` (plus a newline) to the child's stdin. Returns an error
/// if the stdin has already been closed (e.g. after the child exited or
/// after a prior submission consumed it).
///
/// # Errors
///
/// Returns an error if the login stdin is already closed, or if writing
/// to or flushing the stdin pipe fails.
pub async fn submit_code(&self, code: &str) -> Result<()> {
let mut guard = self.stdin.lock().await;
let stdin = guard.as_mut().context("login stdin already closed")?;
@ -100,18 +110,34 @@ impl LoginSession {
let _ = self.stdin.lock().await.take();
}
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn output(&self) -> String {
self.state.lock().unwrap().output.clone()
}
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn url(&self) -> Option<String> {
self.state.lock().unwrap().url.clone()
}
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn finished(&self) -> bool {
self.state.lock().unwrap().finished
}
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn exit_note(&self) -> Option<String> {
self.state.lock().unwrap().exit_note.clone()
}
@ -119,6 +145,10 @@ impl LoginSession {
/// Best-effort: poll the child once and update `finished`/`exit_note`.
/// Called by the web UI on each render so the state stays fresh without
/// running a dedicated reaper task.
///
/// # Panics
///
/// Panics if an internal lock is poisoned.
pub fn poll(&self) {
let mut child = self.child.lock().unwrap();
match child.try_wait() {
@ -137,6 +167,10 @@ impl LoginSession {
}
/// Kill the child if it's still running. Idempotent.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
pub fn kill(&self) {
if let Err(e) = self.child.lock().unwrap().start_kill() {
tracing::warn!(error = ?e, "kill login child");
@ -217,6 +251,10 @@ fn extract_url(line: &str) -> Option<String> {
/// Helper used by the web UI to gate "is there a session running right now"
/// without holding both this module's mutex and the `AppState`'s at once.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
pub fn drop_if_finished(slot: &Mutex<Option<Arc<LoginSession>>>) {
let mut guard = slot.lock().unwrap();
if let Some(s) = guard.as_ref() {

View file

@ -114,6 +114,7 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
/// Format helper for "send-like" tools (anything that expects an `Ok`).
/// `tool` and `ok_msg` only appear in the result string; they don't change
/// behavior.
#[must_use]
pub fn format_ack(resp: Result<SocketReply, anyhow::Error>, tool: &str, ok_msg: String) -> String {
match resp {
Ok(SocketReply::Ok) => ok_msg,
@ -131,6 +132,7 @@ pub fn format_ack(resp: Result<SocketReply, anyhow::Error>, tool: &str, ok_msg:
/// and `---` separators between bodies so the model can tell where
/// one ends and the next begins; per-message redelivery banners
/// included.
#[must_use]
pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
use std::fmt::Write as _;
let messages = match resp {
@ -171,6 +173,7 @@ pub const REDELIVERY_HINT: &str =
/// of pending approvals + questions + reminders. Empty list collapses
/// to a clear marker so claude doesn't go hunting for a payload that
/// isn't there.
#[must_use]
pub fn format_loose_ends(resp: Result<SocketReply, anyhow::Error>) -> String {
use std::fmt::Write as _;
let loose_ends = match resp {
@ -259,6 +262,7 @@ fn loose_end_kind_label(kind: hive_sh4re::CancelLooseEndKind) -> &'static str {
/// Format helper for `whoami`: renders the identity block as a short
/// human-readable string. Skips fields that are `None` so the output
/// doesn't carry dead placeholders.
#[must_use]
pub fn format_whoami(resp: Result<SocketReply, anyhow::Error>) -> String {
match resp {
Ok(SocketReply::Whoami {
@ -294,6 +298,7 @@ where
/// from "c0re flickered and the harness rode it out" — without the
/// hint, a tool result that took 30s to come back looks identical to a
/// content failure and the model would burn a turn retrying it.
#[must_use]
pub fn annotate_retries(mut s: String, retries: u32) -> String {
if retries > 0 {
use std::fmt::Write as _;
@ -660,6 +665,11 @@ impl AgentServer {
impl ServerHandler for AgentServer {}
/// Run the agent MCP server over stdio. Returns when the client disconnects.
///
/// # Errors
///
/// Returns an error if the MCP server fails to initialize or the transport
/// encounters a fatal error.
pub async fn serve_agent_stdio(socket: PathBuf) -> Result<()> {
let server = AgentServer::new(socket);
let service = server.serve(stdio()).await?;
@ -668,6 +678,11 @@ pub async fn serve_agent_stdio(socket: PathBuf) -> Result<()> {
}
/// Run the manager MCP server over stdio. Same idea, different tool surface.
///
/// # Errors
///
/// Returns an error if the MCP server fails to initialize or the transport
/// encounters a fatal error.
pub async fn serve_manager_stdio(socket: PathBuf) -> Result<()> {
let server = ManagerServer::new(socket);
let service = server.serve(stdio()).await?;

View file

@ -12,6 +12,7 @@ use crate::turn_stats::TurnStatRow;
/// system prompt; this is just the wake signal body. `unread` is the inbox
/// depth after this message was popped. `redelivered` prepends a "may already
/// be handled" banner.
#[must_use]
pub fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool) -> String {
let banner = if redelivered { REDELIVERY_HINT } else { "" };
let pending = if unread == 0 {
@ -26,6 +27,7 @@ pub fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool
}
/// Current time as a Unix timestamp (seconds). Returns 0 on any error.
#[must_use]
pub fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@ -37,6 +39,7 @@ pub fn now_unix() -> i64 {
/// Assemble a `TurnStatRow` from the harness's per-turn state. Used by both
/// the agent and manager serve loops — the shape is identical, only the
/// post-turn count fetch helpers differ (and those stay in each binary).
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn build_row(
started_at: i64,

View file

@ -29,6 +29,7 @@ pub enum Window {
}
impl Window {
#[must_use]
pub fn parse(s: &str) -> Self {
match s {
"1h" => Self::Hour,
@ -51,6 +52,7 @@ impl Window {
}
}
#[must_use]
pub fn span_secs(self) -> i64 {
match self {
Self::Hour => 3600,

View file

@ -97,6 +97,10 @@ pub struct TurnFiles {
impl TurnFiles {
/// Write all three files into the per-agent runtime dir alongside
/// `socket`. Idempotent — overwrites whatever was there.
///
/// # Errors
///
/// Returns an error if any of the config files cannot be written to disk.
pub async fn prepare(socket: &Path, label: &str, flavor: mcp::Flavor) -> Result<Self> {
Ok(Self {
mcp_config: write_mcp_config(socket).await?,
@ -112,6 +116,10 @@ impl TurnFiles {
/// as `--socket <path>`); `binary_subcommand` is e.g. `"mcp"` for sub-agents
/// or `"mcp"` for the manager (both binaries name their MCP subcommand the
/// same — the differentiator is which binary `/proc/self/exe` resolves to).
///
/// # Errors
///
/// Returns an error if the config file cannot be written.
pub async fn write_mcp_config(socket: &Path) -> Result<PathBuf> {
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
tokio::fs::create_dir_all(parent).await.ok();
@ -128,6 +136,10 @@ pub async fn write_mcp_config(socket: &Path) -> Result<PathBuf> {
/// Drop the static `--settings` JSON next to the MCP config so we can
/// pass a path (`--settings <file>`) instead of an ever-growing inline
/// blob — the CLI argv has a finite length budget.
///
/// # Errors
///
/// Returns an error if the settings file cannot be written.
pub async fn write_settings(socket: &Path) -> Result<PathBuf> {
let parent = socket.parent().unwrap_or_else(|| Path::new("/run/hive"));
tokio::fs::create_dir_all(parent).await.ok();
@ -142,6 +154,10 @@ pub async fn write_settings(socket: &Path) -> Result<PathBuf> {
/// `--system-prompt-file`, replacing claude's default system prompt with
/// the role + tools instructions. Per-turn prompts become much smaller
/// (just the wake message body).
///
/// # Errors
///
/// Returns an error if the system prompt file cannot be written.
pub async fn write_system_prompt(
socket: &Path,
label: &str,
@ -399,6 +415,10 @@ pub fn emit_turn_end(bus: &Bus, outcome: &TurnOutcome) {
/// Block until the bound `~/.claude/` dir contains a session, polling
/// `claude_dir` on a `poll_ms` interval (min 2s). Flips `state` to
/// `Online` when login lands; caller resumes its serve loop.
///
/// # Panics
///
/// Panics if the internal login-state lock is poisoned.
pub async fn wait_for_login(
claude_dir: &Path,
state: Arc<Mutex<LoginState>>,
@ -442,6 +462,11 @@ pub async fn run_turn(prompt: &str, files: &TurnFiles, bus: &Bus) -> TurnOutcome
/// surface, same system prompt, same allowed-tools — so the post-
/// compact state matches a normal turn's. Only the prompt over stdin
/// differs (`/compact` vs the wake-up payload).
///
/// # Errors
///
/// Returns an error if the `claude --print /compact` invocation fails
/// (non-zero exit or I/O error).
pub async fn compact_session(files: &TurnFiles, bus: &Bus) -> Result<()> {
bus.emit(LiveEvent::Note {
text: "context overflow — running /compact on the persistent session".into(),

View file

@ -148,6 +148,10 @@ impl TurnStats {
/// Insert a row. Best-effort — logs + swallows errors so a sqlite
/// hiccup (locked db, full disk) doesn't crash the harness.
///
/// # Panics
///
/// Panics if the internal lock is poisoned.
pub fn record(&self, row: &TurnStatRow) {
let conn = self.inner.lock().unwrap();
let res = conn.execute(
@ -209,6 +213,9 @@ impl TurnStats {
/// have last-inference zeros — those rows yield `ctx = None` so the
/// badge stays empty until the next real turn rather than showing a
/// misleading 0.
/// # Panics
///
/// Panics if the internal lock is poisoned.
#[must_use]
pub fn last_usage(
&self,

View file

@ -75,6 +75,9 @@ impl AppState {
/// `post_compact`) the allowed-tools surface claude sees.
pub type Flavor = mcp::Flavor;
/// # Errors
///
/// Returns an error if the TCP listener cannot bind to the given port.
pub async fn serve(
label: String,
port: u16,

View file

@ -133,6 +133,7 @@ pub struct ReminderStats {
}
impl HostResponse {
#[must_use]
pub fn success() -> Self {
Self {
ok: true,
@ -142,6 +143,7 @@ impl HostResponse {
}
}
#[must_use]
pub fn error(message: impl Into<String>) -> Self {
Self {
ok: false,
@ -151,6 +153,7 @@ impl HostResponse {
}
}
#[must_use]
pub fn list(agents: Vec<String>) -> Self {
Self {
ok: true,
@ -160,6 +163,7 @@ impl HostResponse {
}
}
#[must_use]
pub fn pending(approvals: Vec<Approval>) -> Self {
Self {
ok: true,