From 908cadb151e20561ed3fefbfdad808942958a3d8 Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 22 May 2026 19:41:27 +0200 Subject: [PATCH] docs: add missing #[must_use], # Errors, # Panics across public api --- hive-ag3nt/src/client.rs | 15 +++++++++++ hive-ag3nt/src/events.rs | 45 +++++++++++++++++++++++++++++++++ hive-ag3nt/src/login_session.rs | 38 ++++++++++++++++++++++++++++ hive-ag3nt/src/mcp.rs | 15 +++++++++++ hive-ag3nt/src/serve_common.rs | 3 +++ hive-ag3nt/src/stats.rs | 2 ++ hive-ag3nt/src/turn.rs | 25 ++++++++++++++++++ hive-ag3nt/src/turn_stats.rs | 7 +++++ hive-ag3nt/src/web_ui.rs | 3 +++ hive-sh4re/src/lib.rs | 4 +++ 10 files changed, 157 insertions(+) diff --git a/hive-ag3nt/src/client.rs b/hive-ag3nt/src/client.rs index 64e1293..5f4963b 100644 --- a/hive-ag3nt/src/client.rs +++ b/hive-ag3nt/src/client.rs @@ -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(socket: &Path, req: &Req) -> Result 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(socket: &Path, req: &Req) -> Result<(Resp, u32)> where Req: Serialize + ?Sized, diff --git a/hive-ag3nt/src/events.rs b/hive-ag3nt/src/events.rs index ac40950..7c0dff6 100644 --- a/hive-ag3nt/src/events.rs +++ b/hive-ag3nt/src/events.rs @@ -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 { 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 { 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 ` 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) { 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, cost: Option) { 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 { 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 { *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 { *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 { self.tx.subscribe() } diff --git a/hive-ag3nt/src/login_session.rs b/hive-ag3nt/src/login_session.rs index 68aca36..956375b 100644 --- a/hive-ag3nt/src/login_session.rs +++ b/hive-ag3nt/src/login_session.rs @@ -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 { 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 { 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 { 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 { /// 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>>) { let mut guard = slot.lock().unwrap(); if let Some(s) = guard.as_ref() { diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 1bd52d5..3ae9b5e 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -114,6 +114,7 @@ impl From 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, tool: &str, ok_msg: String) -> String { match resp { Ok(SocketReply::Ok) => ok_msg, @@ -131,6 +132,7 @@ pub fn format_ack(resp: Result, 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) -> 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) -> 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) -> 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?; diff --git a/hive-ag3nt/src/serve_common.rs b/hive-ag3nt/src/serve_common.rs index a1c9d48..a84dc9f 100644 --- a/hive-ag3nt/src/serve_common.rs +++ b/hive-ag3nt/src/serve_common.rs @@ -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, diff --git a/hive-ag3nt/src/stats.rs b/hive-ag3nt/src/stats.rs index f22165f..1731a20 100644 --- a/hive-ag3nt/src/stats.rs +++ b/hive-ag3nt/src/stats.rs @@ -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, diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 7aefc63..9301479 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -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 { Ok(Self { mcp_config: write_mcp_config(socket).await?, @@ -112,6 +116,10 @@ impl TurnFiles { /// as `--socket `); `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 { 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 { /// Drop the static `--settings` JSON next to the MCP config so we can /// pass a path (`--settings `) 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 { 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 { /// `--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>, @@ -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(), diff --git a/hive-ag3nt/src/turn_stats.rs b/hive-ag3nt/src/turn_stats.rs index ffae280..a17f81a 100644 --- a/hive-ag3nt/src/turn_stats.rs +++ b/hive-ag3nt/src/turn_stats.rs @@ -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, diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 993d97f..644844c 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -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, diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 199576e..c743b93 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -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) -> Self { Self { ok: false, @@ -151,6 +153,7 @@ impl HostResponse { } } + #[must_use] pub fn list(agents: Vec) -> Self { Self { ok: true, @@ -160,6 +163,7 @@ impl HostResponse { } } + #[must_use] pub fn pending(approvals: Vec) -> Self { Self { ok: true,