diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index ae452a7d..260bfec5 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -334,7 +334,7 @@ pub struct GetHostJournalArgs { pub lines: Option, /// Minimum syslog priority level. #[serde(default)] - pub priority: Option, + pub priority: Option, /// Regex to match against log message fields (journalctl --grep). #[serde(default)] pub grep: Option, diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index 2b158426..96ecf1bd 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -373,7 +373,7 @@ impl AgentServer { let summary = format_matrix_summary(&unread_rooms); loose_ends.insert( 0, - hive_sh4re::LooseEnd::UnreadMatrix { + hive_sh4re::inbox::LooseEnd::UnreadMatrix { rooms: total, summary, }, @@ -507,7 +507,7 @@ impl AgentServer { // Reminders are harness-local — dial the in-agent socket // directly instead of the broker; every other kind // (question/approval) still lives in c0re. - if kind == hive_sh4re::CancelLooseEndKind::Reminder { + if kind == hive_sh4re::inbox::CancelLooseEndKind::Reminder { return match dial_agent_socket(&hive_agent_sock::Request::CancelReminder { id }) .await { @@ -527,7 +527,7 @@ impl AgentServer { let (resp, retries) = self .dispatch(hive_core_agent_sock::Request::CancelLooseEnd { kind, id }) .await; - if resp.is_ok() && kind == hive_sh4re::CancelLooseEndKind::Question { + if resp.is_ok() && kind == hive_sh4re::inbox::CancelLooseEndKind::Question { // Best-effort — cancel is ownership-gated to the asker on // the c0re side, so a successful cancel here always means // *this* agent's own `asked` mirror row for `id`. Known gap @@ -627,8 +627,8 @@ impl AgentServer { `at_unix_timestamp`" .to_string(); } - (Some(s), None) => hive_sh4re::ReminderTiming::InSeconds { seconds: s }, - (None, Some(t)) => hive_sh4re::ReminderTiming::At { unix_timestamp: t }, + (Some(s), None) => hive_sh4re::inbox::ReminderTiming::InSeconds { seconds: s }, + (None, Some(t)) => hive_sh4re::inbox::ReminderTiming::At { unix_timestamp: t }, }; // Reminders are harness-local — dial the in-agent socket // directly instead of the broker. diff --git a/hive-agent-mcp/src/mcp/render.rs b/hive-agent-mcp/src/mcp/render.rs index c093e569..435ef834 100644 --- a/hive-agent-mcp/src/mcp/render.rs +++ b/hive-agent-mcp/src/mcp/render.rs @@ -66,8 +66,8 @@ pub fn format_recv(resp: Result) /// The synthetic single-message directive rendered for a fenced (graceful-stop) /// inbox — see the `GracefulStop` arm of [`format_recv`]. -fn graceful_stop_message() -> hive_sh4re::DeliveredMessage { - hive_sh4re::DeliveredMessage { +fn graceful_stop_message() -> hive_sh4re::inbox::DeliveredMessage { + hive_sh4re::inbox::DeliveredMessage { from: "graceful-stop".into(), body: "⛔ GRACEFUL STOP IN PROGRESS — the container shuts down as soon as this turn \ ends. Flush anything worth keeping to your durable /state files, then END \ @@ -85,7 +85,10 @@ fn graceful_stop_message() -> hive_sh4re::DeliveredMessage { /// depth; when non-zero a shared "(N more pending …)" hint (identical to the /// wake prompt's) is appended so an in-turn drain knows more is queued. The /// empty path never carries the hint (nothing was popped). -fn render_recv_messages(messages: &[hive_sh4re::DeliveredMessage], remaining: u64) -> String { +fn render_recv_messages( + messages: &[hive_sh4re::inbox::DeliveredMessage], + remaining: u64, +) -> String { use std::fmt::Write as _; if messages.is_empty() { return "(empty)".to_owned(); @@ -93,7 +96,7 @@ fn render_recv_messages(messages: &[hive_sh4re::DeliveredMessage], remaining: u6 let mut out = if messages.len() == 1 { let m = &messages[0]; let banner = if m.redelivered { - hive_sh4re::REDELIVERY_HINT + hive_sh4re::inbox::REDELIVERY_HINT } else { "" }; @@ -106,7 +109,7 @@ fn render_recv_messages(messages: &[hive_sh4re::DeliveredMessage], remaining: u6 out.push_str("\n---\n\n"); } let banner = if m.redelivered { - hive_sh4re::REDELIVERY_HINT + hive_sh4re::inbox::REDELIVERY_HINT } else { "" }; @@ -120,7 +123,7 @@ fn render_recv_messages(messages: &[hive_sh4re::DeliveredMessage], remaining: u6 } out }; - out.push_str(&hive_sh4re::pending_hint(remaining)); + out.push_str(&hive_sh4re::inbox::pending_hint(remaining)); out } @@ -156,14 +159,14 @@ fn msg_id_tag(id: i64) -> String { /// inbox messages are). const MAX_RENDERED_TODOS: usize = 40; -/// Render one non-`Todo` [`hive_sh4re::LooseEnd`] variant onto `out`. Split +/// Render one non-`Todo` [`hive_sh4re::inbox::LooseEnd`] variant onto `out`. Split /// out of `render_loose_ends` to keep that function under clippy's /// `too_many_lines` limit — `Todo` stays inline there since it also needs /// the shared `shown_todos` counter. -fn render_one_loose_end(out: &mut String, t: &hive_sh4re::LooseEnd) { +fn render_one_loose_end(out: &mut String, t: &hive_sh4re::inbox::LooseEnd) { use std::fmt::Write as _; match t { - hive_sh4re::LooseEnd::Approval { + hive_sh4re::inbox::LooseEnd::Approval { id, agent, commit_ref, @@ -179,7 +182,7 @@ fn render_one_loose_end(out: &mut String, t: &hive_sh4re::LooseEnd) { "- approval #{id} ({agent} @ {commit_ref}, {age_seconds}s old){desc}" ); } - hive_sh4re::LooseEnd::Question { + hive_sh4re::inbox::LooseEnd::Question { id, asker, target, @@ -192,7 +195,7 @@ fn render_one_loose_end(out: &mut String, t: &hive_sh4re::LooseEnd) { "- question #{id} ({asker} → {to}, {age_seconds}s old): {question}" ); } - hive_sh4re::LooseEnd::Reminder { + hive_sh4re::inbox::LooseEnd::Reminder { id, owner, message, @@ -204,13 +207,13 @@ fn render_one_loose_end(out: &mut String, t: &hive_sh4re::LooseEnd) { "- reminder #{id} ({owner}, scheduled {age_seconds}s ago, due_at={due_at}): {message}" ); } - hive_sh4re::LooseEnd::PendingMessages { count } => { + hive_sh4re::inbox::LooseEnd::PendingMessages { count } => { let _ = writeln!( out, "- {count} pending inbox message(s) — drain with recv (recv(max: {count}) to batch)" ); } - hive_sh4re::LooseEnd::UnreadMatrix { rooms, summary } => { + hive_sh4re::inbox::LooseEnd::UnreadMatrix { rooms, summary } => { let _ = write!(out, "- unread matrix messages in {rooms} room(s)"); if summary.is_empty() { let _ = writeln!( @@ -228,13 +231,13 @@ fn render_one_loose_end(out: &mut String, t: &hive_sh4re::LooseEnd) { ); } } - hive_sh4re::LooseEnd::Todo { .. } => { + hive_sh4re::inbox::LooseEnd::Todo { .. } => { // Handled inline by the caller (needs the shared shown-count). } } } -/// Render one `Todo` [`hive_sh4re::LooseEnd`] onto `out`. Split out for the +/// Render one `Todo` [`hive_sh4re::inbox::LooseEnd`] onto `out`. Split out for the /// same `too_many_lines` reason as [`render_one_loose_end`]. fn render_todo_loose_end( out: &mut String, @@ -258,7 +261,7 @@ fn render_todo_loose_end( /// Inner renderer for a `Vec` already extracted from the socket /// reply. Called by the `get_loose_ends` handler, which injects the /// `UnreadMatrix` entry before formatting. -pub(super) fn render_loose_ends(loose_ends: &[hive_sh4re::LooseEnd]) -> String { +pub(super) fn render_loose_ends(loose_ends: &[hive_sh4re::inbox::LooseEnd]) -> String { use std::fmt::Write as _; if loose_ends.is_empty() { return "(no loose ends)".to_owned(); @@ -267,7 +270,7 @@ pub(super) fn render_loose_ends(loose_ends: &[hive_sh4re::LooseEnd]) -> String { let mut shown_todos = 0usize; let mut hidden_todos = 0usize; for t in loose_ends { - let hive_sh4re::LooseEnd::Todo { + let hive_sh4re::inbox::LooseEnd::Todo { id, subsystem, subsystem_key, @@ -376,7 +379,7 @@ pub(super) async fn dial_agent_socket( /// (loose-ends v2). Returns `None` when `HIVE_AGENT_SOCKET` is unset / /// absent or the query fails — best-effort, like [`matrix_unread_summary`], /// so an agent without the socket is not penalised. -pub(super) async fn local_todos() -> Option> { +pub(super) async fn local_todos() -> Option> { match dial_agent_socket(&hive_agent_sock::Request::ListTodos { subsystem: None }).await? { hive_agent_sock::Response::LooseEnds { loose_ends } => Some(loose_ends), _ => None, @@ -386,7 +389,7 @@ pub(super) async fn local_todos() -> Option> { /// Query the harness's in-agent socket for this agent's local pending /// reminders — was a broker query before reminders moved in-container. /// Same best-effort contract as [`local_todos`]. -pub(super) async fn local_reminders() -> Option> { +pub(super) async fn local_reminders() -> Option> { match dial_agent_socket(&hive_agent_sock::Request::ListReminders).await? { hive_agent_sock::Response::LooseEnds { loose_ends } => Some(loose_ends), _ => None, @@ -397,7 +400,7 @@ pub(super) async fn local_reminders() -> Option> { /// (both roles — asked and answering). Same best-effort /// contract as [`local_reminders`]; c0re stays the actual `Ask`/`Answer` /// routing, this only mirrors the durable "still owed a reply" view. -pub(super) async fn local_questions() -> Option> { +pub(super) async fn local_questions() -> Option> { match dial_agent_socket(&hive_agent_sock::Request::ListQuestions).await? { hive_agent_sock::Response::LooseEnds { loose_ends } => Some(loose_ends), _ => None, @@ -451,11 +454,13 @@ pub(super) fn format_matrix_summary(rooms: &[MatrixRoomUnread]) -> String { /// wire enum. Accepts a small alias set so claude doesn't have to /// remember the exact spelling (`"q"` / `"r"` shorthand falls out /// for free). -pub(super) fn parse_loose_end_kind(raw: &str) -> Result { +pub(super) fn parse_loose_end_kind( + raw: &str, +) -> Result { match raw.trim().to_ascii_lowercase().as_str() { - "question" | "q" => Ok(hive_sh4re::CancelLooseEndKind::Question), - "reminder" | "r" => Ok(hive_sh4re::CancelLooseEndKind::Reminder), - "approval" | "a" => Ok(hive_sh4re::CancelLooseEndKind::Approval), + "question" | "q" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Question), + "reminder" | "r" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Reminder), + "approval" | "a" => Ok(hive_sh4re::inbox::CancelLooseEndKind::Approval), other => Err(format!( "cancel_loose_end: unknown kind '{other}' \ (expected \"question\", \"reminder\", \"approval\", or \"todo\")" @@ -467,11 +472,11 @@ pub(super) fn parse_loose_end_kind(raw: &str) -> Result &'static str { +pub(super) fn loose_end_kind_label(kind: hive_sh4re::inbox::CancelLooseEndKind) -> &'static str { match kind { - hive_sh4re::CancelLooseEndKind::Question => "question", - hive_sh4re::CancelLooseEndKind::Reminder => "reminder", - hive_sh4re::CancelLooseEndKind::Approval => "approval", + hive_sh4re::inbox::CancelLooseEndKind::Question => "question", + hive_sh4re::inbox::CancelLooseEndKind::Reminder => "reminder", + hive_sh4re::inbox::CancelLooseEndKind::Approval => "approval", } } @@ -595,8 +600,8 @@ pub fn annotate_retries(mut s: String, retries: u32) -> String { mod tests { use super::format_recv; - fn msg(id: i64, from: &str, body: &str) -> hive_sh4re::DeliveredMessage { - hive_sh4re::DeliveredMessage { + fn msg(id: i64, from: &str, body: &str) -> hive_sh4re::inbox::DeliveredMessage { + hive_sh4re::inbox::DeliveredMessage { from: from.to_owned(), body: body.to_owned(), id, @@ -643,7 +648,7 @@ mod tests { assert!(out.starts_with("popped 2 message(s):")); assert_eq!(out.matches("more message(s) pending").count(), 1); // `max` suggestion is clamped to the server-side recv cap. - let batch = 9u64.min(u64::from(hive_sh4re::RECV_BATCH_MAX)); + let batch = 9u64.min(u64::from(hive_sh4re::inbox::RECV_BATCH_MAX)); assert!(out.contains(&format!("max: {batch}"))); } } diff --git a/hive-agent-sock/src/lib.rs b/hive-agent-sock/src/lib.rs index 0045c9d7..480c668d 100644 --- a/hive-agent-sock/src/lib.rs +++ b/hive-agent-sock/src/lib.rs @@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize}; -use hive_sh4re::LooseEnd; +use hive_sh4re::inbox::LooseEnd; pub mod paths; @@ -80,7 +80,7 @@ pub enum Request { /// harness persists an over-cap body instead of inlining it. StoreReminder { message: String, - timing: hive_sh4re::ReminderTiming, + timing: hive_sh4re::inbox::ReminderTiming, #[serde(default, skip_serializing_if = "Option::is_none")] file_path: Option, }, diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index 53da488c..bc16baf4 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -222,7 +222,7 @@ struct TurnControl { /// set when this is the last todo wake before the miss-streak would hit /// `TODO_MISS_PAUSE_THRESHOLD`, so the agent gets one unambiguous chance to /// avoid being auto-paused. -fn synthetic_todo_message(stern: bool) -> hive_sh4re::DeliveredMessage { +fn synthetic_todo_message(stern: bool) -> hive_sh4re::inbox::DeliveredMessage { let body = if stern { "you have todos — call get_loose_ends NOW. you've skipped it on recent todo \ wakes in a row; if this turn doesn't call it, the harness will pause your \ @@ -231,7 +231,7 @@ fn synthetic_todo_message(stern: bool) -> hive_sh4re::DeliveredMessage { } else { "you have todos — call get_loose_ends to see them".to_owned() }; - hive_sh4re::DeliveredMessage { + hive_sh4re::inbox::DeliveredMessage { from: "todo".into(), body, id: 0, @@ -243,8 +243,8 @@ fn synthetic_todo_message(stern: bool) -> hive_sh4re::DeliveredMessage { /// Synthetic message that drives the single stop-checkpoint turn when c0re /// signals a graceful stop. The agent gets one final turn to flush durable /// `/state` before the container is stopped; new inbound is already fenced. -fn graceful_stop_message() -> hive_sh4re::DeliveredMessage { - hive_sh4re::DeliveredMessage { +fn graceful_stop_message() -> hive_sh4re::inbox::DeliveredMessage { + hive_sh4re::inbox::DeliveredMessage { from: "graceful-stop".into(), body: "You are being gracefully stopped — the container will shut down after this turn, \ and new inbound messages are already fenced. This is your one checkpoint turn: \ @@ -272,7 +272,7 @@ enum RecvOutcome { /// body/id is per-row data the producer already resolved, so the /// select arm wraps it straight into this variant — no dedicated /// `LocalReminder` variant needed. - Message(hive_sh4re::DeliveredMessage), + Message(hive_sh4re::inbox::DeliveredMessage), /// Long-poll timed out cleanly (empty `Messages` response). Caller /// sleeps then retries. Empty, @@ -529,7 +529,7 @@ async fn serve_main(socket: &Path, poll_ms: u64) -> Result<()> { let bus = Bus::new(); // Set by the web UI's `/api/cancel` on a successful SIGINT, read-and- // cleared by `handle_turn` before building the next wake prompt — see - // `hive_sh4re::INTERRUPTED_HINT`. Shared between the web server task and + // `hive_sh4re::inbox::INTERRUPTED_HINT`. Shared between the web server task and // the serve loop the same way `bus`/`todo_wake` are. let interrupted = Arc::new(std::sync::atomic::AtomicBool::new(false)); let stats = TurnStats::open_default(); @@ -654,7 +654,7 @@ async fn serve_loop( files: &turn::TurnFiles, todo_wake: Arc, todos_store: Option>, - mut reminder_rx: tokio::sync::mpsc::UnboundedReceiver, + mut reminder_rx: tokio::sync::mpsc::UnboundedReceiver, interrupted: Arc, ) -> Result<()> { tracing::info!(socket = %socket.display(), "harness serve"); @@ -849,7 +849,7 @@ async fn handle_turn( stats: Option<&TurnStats>, files: &turn::TurnFiles, session: &turn::AgentSession, - first: hive_sh4re::DeliveredMessage, + first: hive_sh4re::inbox::DeliveredMessage, interrupted: &std::sync::atomic::AtomicBool, ) -> TurnControl { let from = first.from; diff --git a/hive-agent/src/reminder_timer.rs b/hive-agent/src/reminder_timer.rs index 8d857343..f0f56819 100644 --- a/hive-agent/src/reminder_timer.rs +++ b/hive-agent/src/reminder_timer.rs @@ -1,6 +1,6 @@ //! Delivery half of the harness-local reminders store. Polls [`Reminders`] //! for due rows and pushes each as a -//! [`hive_sh4re::DeliveredMessage`] down an mpsc channel the serve loop +//! [`hive_sh4re::inbox::DeliveredMessage`] down an mpsc channel the serve loop //! races against the broker long-poll — mirrors `todo_server`'s `Notify` //! wake, but a reminder fire carries real per-row data (message/id), so a //! bare `Notify` doesn't fit; the channel carries the finished message @@ -60,7 +60,7 @@ fn remind_max_pending() -> u64 { /// select), while cleanly disabling delivery. pub async fn run( store: Option>, - tx: mpsc::UnboundedSender, + tx: mpsc::UnboundedSender, ) { let Some(store) = store else { tracing::error!("reminders db unavailable — reminder delivery disabled"); @@ -73,7 +73,7 @@ pub async fn run( } } -fn tick(store: &Reminders, tx: &mpsc::UnboundedSender) { +fn tick(store: &Reminders, tx: &mpsc::UnboundedSender) { let now = chrono::Utc::now().timestamp(); let due = match store.due(now, REMINDER_BATCH_LIMIT) { Ok(rows) => rows, @@ -84,7 +84,7 @@ fn tick(store: &Reminders, tx: &mpsc::UnboundedSender, ) -> Result { let max = remind_max_pending(); @@ -237,8 +237,8 @@ fn write_payload(path: &Path, message: &str) -> Result<(), String> { } /// Resolve the `due_at` unix timestamp for a `StoreReminder` request. -fn resolve_due_at(timing: &hive_sh4re::ReminderTiming) -> anyhow::Result { - use hive_sh4re::ReminderTiming; +fn resolve_due_at(timing: &hive_sh4re::inbox::ReminderTiming) -> anyhow::Result { + use hive_sh4re::inbox::ReminderTiming; match timing { ReminderTiming::InSeconds { seconds } => { let now = std::time::SystemTime::now(); @@ -268,14 +268,15 @@ mod tests { #[test] fn resolve_due_at_in_seconds_is_close_to_now_plus_n() { - let due = resolve_due_at(&hive_sh4re::ReminderTiming::InSeconds { seconds: 60 }).unwrap(); + let due = + resolve_due_at(&hive_sh4re::inbox::ReminderTiming::InSeconds { seconds: 60 }).unwrap(); let now = chrono::Utc::now().timestamp(); assert!((due - now - 60).abs() <= 2, "due={due} now={now}"); } #[test] fn resolve_due_at_at_passes_through() { - let due = resolve_due_at(&hive_sh4re::ReminderTiming::At { + let due = resolve_due_at(&hive_sh4re::inbox::ReminderTiming::At { unix_timestamp: 123_456, }) .unwrap(); diff --git a/hive-agent/src/serve_common.rs b/hive-agent/src/serve_common.rs index a0b14e26..bd02813e 100644 --- a/hive-agent/src/serve_common.rs +++ b/hive-agent/src/serve_common.rs @@ -27,9 +27,9 @@ pub fn format_wake_prompt( interrupted: bool, ) -> String { let banner = if redelivered { - hive_sh4re::REDELIVERY_HINT + hive_sh4re::inbox::REDELIVERY_HINT } else if interrupted { - hive_sh4re::INTERRUPTED_HINT + hive_sh4re::inbox::INTERRUPTED_HINT } else { "" }; @@ -38,7 +38,7 @@ pub fn format_wake_prompt( } else { String::new() }; - let pending = hive_sh4re::pending_hint(unread); + let pending = hive_sh4re::inbox::pending_hint(unread); format!("{banner}{tag}Incoming message from `{from}`:\n---\n{body}\n---{pending}") } diff --git a/hive-agent/src/todo_server.rs b/hive-agent/src/todo_server.rs index 3b628a76..00409d69 100644 --- a/hive-agent/src/todo_server.rs +++ b/hive-agent/src/todo_server.rs @@ -30,7 +30,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use hive_agent_sock::{Request, Response}; -use hive_sh4re::LooseEnd; +use hive_sh4re::inbox::LooseEnd; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; use tokio::sync::Notify; @@ -248,7 +248,7 @@ fn dispatch( fn store_reminder( reminders: Option<&Reminders>, message: &str, - timing: &hive_sh4re::ReminderTiming, + timing: &hive_sh4re::inbox::ReminderTiming, file_path: Option<&str>, ) -> Response { let Some(r) = reminders else { diff --git a/hive-agent/src/web_ui/mod.rs b/hive-agent/src/web_ui/mod.rs index a419fb54..13d820be 100644 --- a/hive-agent/src/web_ui/mod.rs +++ b/hive-agent/src/web_ui/mod.rs @@ -60,7 +60,7 @@ struct AppState { gui_vnc_port: Option, /// Set by `post_cancel_turn` on a successful SIGINT; read-and-cleared /// by the serve loop's next `handle_turn` to prepend - /// `hive_sh4re::INTERRUPTED_HINT` to that turn's wake prompt. Shared + /// `hive_sh4re::inbox::INTERRUPTED_HINT` to that turn's wake prompt. Shared /// with the serve loop via the same `Arc` (see `serve_main`) — an /// in-memory flag, not a marker file, since a `/cancel` from a prior /// process lifetime isn't meaningful once the harness restarts. diff --git a/hive-agent/src/web_ui/state.rs b/hive-agent/src/web_ui/state.rs index c2296477..9c84e8d0 100644 --- a/hive-agent/src/web_ui/state.rs +++ b/hive-agent/src/web_ui/state.rs @@ -124,7 +124,7 @@ pub(super) struct StateSnapshot { /// Last N messages addressed to this agent, newest-first. Pulled /// from the broker via the per-agent socket on each render. /// Empty on transport failure. - inbox: Vec, + inbox: Vec, /// Authoritative turn-loop state from the harness and the unix /// timestamp the state was entered. The JS computes the age /// client-side off this rather than tracking it from SSE events. @@ -363,7 +363,7 @@ struct ExtraLink { /// Best-effort: pull the last 30 messages addressed to us via the /// per-agent / manager socket. Empty list on any transport / decode /// failure — the inbox section is decorative, not authoritative. -async fn recent_inbox(socket: &std::path::Path) -> Vec { +async fn recent_inbox(socket: &std::path::Path) -> Vec { const LIMIT: u64 = 30; // Deadline-bounded (via `broker_request`): `/api/state` must render even // when hive-c0re is busy — an empty inbox section beats a hung snapshot. diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 6f4ffec3..78bfaa42 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -654,16 +654,17 @@ impl Coordinator { /// after each tick that fires or rearms a row) so the dashboard's /// scheduled-prompts tab updates live without polling. pub fn emit_schedules_snapshot(self: &Arc) { - let mut schedules: Vec = match self.scheduled_prompts.list() { - Ok(rows) => rows - .into_iter() - .map(crate::socket_server::schedule_to_wire_public) - .collect(), - Err(e) => { - tracing::warn!(error = ?e, "emit_schedules_snapshot: list failed"); - return; - } - }; + let mut schedules: Vec = + match self.scheduled_prompts.list() { + Ok(rows) => rows + .into_iter() + .map(crate::socket_server::schedule_to_wire_public) + .collect(), + Err(e) => { + tracing::warn!(error = ?e, "emit_schedules_snapshot: list failed"); + return; + } + }; // Strip ghost targets (destroyed agents) so the dashboard doesn't // render dead columns. Best-effort: if the roster cache is // momentarily contended we emit unfiltered rather than block this @@ -1055,7 +1056,7 @@ impl Coordinator { let old_label = old_parent.as_deref().unwrap_or(""); let new_label = new_parent.unwrap_or(""); if let Some(op) = old_parent.as_deref() { - let _ = self.broker.send(&hive_sh4re::Message { + let _ = self.broker.send(&hive_sh4re::inbox::Message { from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), to: op.to_owned(), body: format!("{child} moved out of your subtree to {new_label}"), @@ -1063,7 +1064,7 @@ impl Coordinator { }); } if let Some(np) = new_parent { - let _ = self.broker.send(&hive_sh4re::Message { + let _ = self.broker.send(&hive_sh4re::inbox::Message { from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), to: np.to_owned(), body: format!( @@ -1344,7 +1345,7 @@ impl Coordinator { `--continue` session is intact, so prior context is \ still in your window." ); - if let Err(e) = self.broker.send(&hive_sh4re::Message { + if let Err(e) = self.broker.send(&hive_sh4re::inbox::Message { from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::SYSTEM_SENDER), to: name.to_owned(), body, @@ -1492,7 +1493,7 @@ impl Coordinator { return; } }; - if let Err(e) = self.broker.send(&hive_sh4re::Message { + if let Err(e) = self.broker.send(&hive_sh4re::inbox::Message { from: hive_sh4re::manager::trusted_sender(from), to: agent.to_owned(), body, @@ -1514,7 +1515,7 @@ impl Coordinator { if agent_name == from { continue; } - if let Err(e) = self.broker.send(&hive_sh4re::Message { + if let Err(e) = self.broker.send(&hive_sh4re::inbox::Message { from: hive_sh4re::manager::trusted_sender(from), to: agent_name.clone(), body: broadcast_body.clone(), diff --git a/hive-c0re/src/dashboard/misc_api.rs b/hive-c0re/src/dashboard/misc_api.rs index f7987c64..c335cae4 100644 --- a/hive-c0re/src/dashboard/misc_api.rs +++ b/hive-c0re/src/dashboard/misc_api.rs @@ -227,7 +227,7 @@ pub(super) async fn post_op_send( errors.join("; ") )); } - } else if let Err(e) = state.coord.broker.send(&hive_sh4re::Message { + } else if let Err(e) = state.coord.broker.send(&hive_sh4re::inbox::Message { from: hive_sh4re::manager::trusted_sender(hive_sh4re::manager::OPERATOR_RECIPIENT), to: to.clone(), body, diff --git a/hive-c0re/src/dashboard/schedules.rs b/hive-c0re/src/dashboard/schedules.rs index 321a31aa..6ea9ea30 100644 --- a/hive-c0re/src/dashboard/schedules.rs +++ b/hive-c0re/src/dashboard/schedules.rs @@ -23,7 +23,7 @@ use super::{AppState, error_problem, error_response}; /// /// Returns the wire shape directly so the frontend can render /// without an extra translation layer. -// `hive_sh4re::WireSchedule` (the actual body) has no `ToSchema` — adding +// `hive_sh4re::schedule::WireSchedule` (the actual body) has no `ToSchema` — adding // one would pull `utoipa` into the wire-types crate for a single dashboard // endpoint. `serde_json::Value` placeholder; see the batch report. #[utoipa::path( @@ -38,7 +38,7 @@ use super::{AppState, error_problem, error_response}; pub(super) async fn api_schedules(State(state): State) -> Response { match state.coord.scheduled_prompts.list() { Ok(rows) => { - let mut wire: Vec = rows + let mut wire: Vec = rows .into_iter() .map(crate::socket_server::schedule_to_wire_public) .collect(); diff --git a/hive-c0re/src/dashboard_events.rs b/hive-c0re/src/dashboard_events.rs index b1874d6c..04a6686f 100644 --- a/hive-c0re/src/dashboard_events.rs +++ b/hive-c0re/src/dashboard_events.rs @@ -239,7 +239,7 @@ pub enum DashboardEvent { /// naturally re-derived from the full list. SchedulesChanged { seq: u64, - schedules: Vec, + schedules: Vec, }, /// Full snapshot of capability grants (per-agent `Vec`). /// Emitted from the rebuild-queue worker after a `PermChange` diff --git a/hive-c0re/src/loose_ends.rs b/hive-c0re/src/loose_ends.rs index b20ef567..93d9d9a7 100644 --- a/hive-c0re/src/loose_ends.rs +++ b/hive-c0re/src/loose_ends.rs @@ -21,7 +21,7 @@ use anyhow::Result; use chrono::Utc; -use hive_sh4re::LooseEnd; +use hive_sh4re::inbox::LooseEnd; use crate::coordinator::Coordinator; diff --git a/hive-c0re/src/questions.rs b/hive-c0re/src/questions.rs index b924b8be..0977f4c1 100644 --- a/hive-c0re/src/questions.rs +++ b/hive-c0re/src/questions.rs @@ -152,11 +152,11 @@ pub fn handle_answer( pub fn handle_cancel_loose_end( coord: &Arc, canceller: &str, - kind: hive_sh4re::CancelLooseEndKind, + kind: hive_sh4re::inbox::CancelLooseEndKind, id: i64, ) -> Result<(), String> { match kind { - hive_sh4re::CancelLooseEndKind::Question => { + hive_sh4re::inbox::CancelLooseEndKind::Question => { // Agent-socket path: never privileged — an agent may only cancel // its own question (ownership). The operator's cancel-anything // path goes through a separate handler with `privileged = true`. @@ -185,7 +185,7 @@ pub fn handle_cancel_loose_end( coord.emit_question_resolved(id, &sentinel, canceller, true, target.as_deref()); Ok(()) } - hive_sh4re::CancelLooseEndKind::Reminder => { + hive_sh4re::inbox::CancelLooseEndKind::Reminder => { // Reminders are now agent-local (in-container store) — the // agent-mcp `cancel_loose_end` tool branches on this kind and // dials the agent's own socket directly, never forwarding to @@ -196,7 +196,7 @@ pub fn handle_cancel_loose_end( not by hive-c0re" )) } - hive_sh4re::CancelLooseEndKind::Approval => { + hive_sh4re::inbox::CancelLooseEndKind::Approval => { // Withdrawing an approval needs the grantable `approvals` // tool-group (held by any approval-submitting orchestrator) // AND ownership: only the agent that submitted the approval diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 799a3bf1..35e7cd5c 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -354,7 +354,7 @@ async fn handle_agent_status(coord: &Arc) -> HostResponse { let rows = crate::container_view::build_all(&coord.hive_env()) .await .into_iter() - .map(|v| hive_sh4re::AgentStatusRow { + .map(|v| hive_sh4re::container::AgentStatusRow { name: v.name, running: v.running, needs_update: v.needs_update, diff --git a/hive-c0re/src/socket_server/lifecycle_handlers.rs b/hive-c0re/src/socket_server/lifecycle_handlers.rs index c1421edf..2f6fecd2 100644 --- a/hive-c0re/src/socket_server/lifecycle_handlers.rs +++ b/hive-c0re/src/socket_server/lifecycle_handlers.rs @@ -189,7 +189,7 @@ pub(super) async fn handle_list_descendants(coord: &Arc, agent: &st // rather than erroring — matches the old membership-check's // default-false behavior for an unknown name. let running = running_by_name.get(name.as_str()).copied().unwrap_or(false); - hive_sh4re::ContainerInfo { name, running } + hive_sh4re::container::ContainerInfo { name, running } }) .collect(); Response::Containers { containers } diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index a166cfc2..452da227 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -14,7 +14,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use hive_core_agent_sock::{Request, Response}; -use hive_sh4re::Message; +use hive_sh4re::inbox::Message; use hive_sh4re::manager::MANAGER_AGENT; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; @@ -169,9 +169,9 @@ async fn serve(stream: UnixStream, agent: String, coord: Arc) -> Re pub(crate) const RECV_LONG_POLL_MAX: std::time::Duration = std::time::Duration::from_mins(3); /// Server-side hard cap on `Recv.max` — canonical value lives in -/// `hive_sh4re::RECV_BATCH_MAX` so the harness's wake-prompt hint and +/// `hive_sh4re::inbox::RECV_BATCH_MAX` so the harness's wake-prompt hint and /// this enforcement site can't drift apart. -pub(crate) const RECV_BATCH_MAX: u32 = hive_sh4re::RECV_BATCH_MAX; +pub(crate) const RECV_BATCH_MAX: u32 = hive_sh4re::inbox::RECV_BATCH_MAX; pub(crate) fn recv_timeout(wait_seconds: Option) -> std::time::Duration { match wait_seconds { @@ -314,7 +314,7 @@ async fn handle_recv( hive_core_agent_sock::Response::Messages { messages: deliveries .into_iter() - .map(|d| hive_sh4re::DeliveredMessage { + .map(|d| hive_sh4re::inbox::DeliveredMessage { from: d.message.from.to_string(), body: d.message.body, id: d.id, @@ -458,11 +458,13 @@ async fn handle_get_agent_meta( /// or the daemon not up yet) yields an empty list. The `MatrixIdentity` /// serde shape matches the snapshot entries; the snapshot's `live` field is /// ignored (only live accounts are written). -fn read_agent_matrix_identities(agent: &hive_types::Ident) -> Vec { +fn read_agent_matrix_identities( + agent: &hive_types::Ident, +) -> Vec { let path = Coordinator::agent_notes_dir(agent).join("matrix-accounts.json"); std::fs::read_to_string(&path) .ok() - .and_then(|s| serde_json::from_str::>(&s).ok()) + .and_then(|s| serde_json::from_str::>(&s).ok()) .unwrap_or_default() } @@ -825,7 +827,7 @@ pub struct HostJournalArgs<'a> { pub unit: &'a Option, pub container: &'a Option, pub lines: &'a Option, - pub priority: &'a Option, + pub priority: &'a Option, pub grep: &'a Option, pub since: &'a Option, pub until: &'a Option, diff --git a/hive-c0re/src/socket_server/schedules.rs b/hive-c0re/src/socket_server/schedules.rs index 3e8c96d0..41d015fb 100644 --- a/hive-c0re/src/socket_server/schedules.rs +++ b/hive-c0re/src/socket_server/schedules.rs @@ -290,7 +290,9 @@ fn cancel_authorized(requester: &str, owner: &str) -> bool { /// Public alias `schedule_to_wire_public` re-exports for /// `dashboard.rs::api_schedules` without crossing the module /// boundary into the socket-server file. -pub fn schedule_to_wire_public(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSchedule { +pub fn schedule_to_wire_public( + s: crate::scheduled_prompts::Schedule, +) -> hive_sh4re::schedule::WireSchedule { schedule_to_wire(s) } @@ -306,7 +308,7 @@ pub fn schedule_to_wire_public(s: crate::scheduled_prompts::Schedule) -> hive_sh /// schedule rows keep every target, so a re-spawned agent's targets /// reappear on their own. pub(crate) fn filter_ghost_schedule_targets( - schedules: &mut [hive_sh4re::WireSchedule], + schedules: &mut [hive_sh4re::schedule::WireSchedule], live: &std::collections::HashSet, ) { for s in schedules.iter_mut() { @@ -316,8 +318,8 @@ pub(crate) fn filter_ghost_schedule_targets( } } -fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSchedule { - hive_sh4re::WireSchedule { +fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::schedule::WireSchedule { + hive_sh4re::schedule::WireSchedule { id: s.id, owner: s.owner, body: s.body, @@ -326,10 +328,10 @@ fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSc created_at_unix: s.created_at_unix, source: match s.source { crate::scheduled_prompts::ScheduleSource::Operator => { - hive_sh4re::WireScheduleSource::Operator + hive_sh4re::schedule::WireScheduleSource::Operator } crate::scheduled_prompts::ScheduleSource::Approval { id } => { - hive_sh4re::WireScheduleSource::Approval { id } + hive_sh4re::schedule::WireScheduleSource::Approval { id } } }, cancelled_at_unix: s.cancelled_at_unix, @@ -338,7 +340,7 @@ fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSc targets: s .targets .into_iter() - .map(|t| hive_sh4re::WireScheduleTarget { + .map(|t| hive_sh4re::schedule::WireScheduleTarget { target: t.target, cancelled_at_unix: t.cancelled_at_unix, last_fired_at_unix: t.last_fired_at_unix, @@ -352,8 +354,8 @@ fn schedule_to_wire(s: crate::scheduled_prompts::Schedule) -> hive_sh4re::WireSc mod tests { use super::*; - fn target(name: &str) -> hive_sh4re::WireScheduleTarget { - hive_sh4re::WireScheduleTarget { + fn target(name: &str) -> hive_sh4re::schedule::WireScheduleTarget { + hive_sh4re::schedule::WireScheduleTarget { target: name.to_owned(), cancelled_at_unix: None, last_fired_at_unix: None, @@ -361,15 +363,15 @@ mod tests { } } - fn schedule(targets: &[&str]) -> hive_sh4re::WireSchedule { - hive_sh4re::WireSchedule { + fn schedule(targets: &[&str]) -> hive_sh4re::schedule::WireSchedule { + hive_sh4re::schedule::WireSchedule { id: 1, owner: "operator".to_owned(), body: "ping".to_owned(), interval_seconds: None, next_fire_at_unix: hive_sh4re::wire_time::from_secs(0), created_at_unix: hive_sh4re::wire_time::from_secs(0), - source: hive_sh4re::WireScheduleSource::Operator, + source: hive_sh4re::schedule::WireScheduleSource::Operator, cancelled_at_unix: None, paused_at_unix: None, description: None, diff --git a/hive-c0re/src/stores/broker.rs b/hive-c0re/src/stores/broker.rs index 3d4e500a..7a4f54a0 100644 --- a/hive-c0re/src/stores/broker.rs +++ b/hive-c0re/src/stores/broker.rs @@ -8,7 +8,7 @@ use std::sync::Mutex; use anyhow::{Context, Result}; use chrono::Utc; -use hive_sh4re::{InboxRow, Message}; +use hive_sh4re::inbox::{InboxRow, Message}; use crate::db::Migration; use rusqlite::{Connection, OptionalExtension, params}; diff --git a/hive-c0re/src/workers/scheduled_prompts_worker.rs b/hive-c0re/src/workers/scheduled_prompts_worker.rs index 315916da..51d7bc5f 100644 --- a/hive-c0re/src/workers/scheduled_prompts_worker.rs +++ b/hive-c0re/src/workers/scheduled_prompts_worker.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use std::time::Duration; use chrono::Utc; -use hive_sh4re::Message; +use hive_sh4re::inbox::Message; use crate::coordinator::Coordinator; use crate::scheduled_prompts::Schedule; diff --git a/hive-core-agent-sock/src/lib.rs b/hive-core-agent-sock/src/lib.rs index bf2ac37c..f5aaf3d6 100644 --- a/hive-core-agent-sock/src/lib.rs +++ b/hive-core-agent-sock/src/lib.rs @@ -7,11 +7,11 @@ //! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …) //! stay in `hive-sh4re`, which this crate depends on. +use hive_sh4re::container::{ContainerInfo, MatrixIdentity}; +use hive_sh4re::inbox::{CancelLooseEndKind, DeliveredMessage, InboxRow, LooseEnd}; +use hive_sh4re::journal::JournalPriority; use hive_sh4re::manager::SchedulePromptPayload; -use hive_sh4re::{ - CancelLooseEndKind, ContainerInfo, DeliveredMessage, InboxRow, JournalPriority, LooseEnd, - MatrixIdentity, WireSchedule, -}; +use hive_sh4re::schedule::WireSchedule; use hive_types::Ident; use serde::{Deserialize, Serialize}; diff --git a/hive-host-sock/src/lib.rs b/hive-host-sock/src/lib.rs index cfb2c748..ea993df3 100644 --- a/hive-host-sock/src/lib.rs +++ b/hive-host-sock/src/lib.rs @@ -12,8 +12,8 @@ use std::path::PathBuf; -use hive_sh4re::AgentStatusRow; use hive_sh4re::approvals::Approval; +use hive_sh4re::container::AgentStatusRow; use hive_types::Ident; use serde::{Deserialize, Serialize}; diff --git a/hive-sh4re/src/container.rs b/hive-sh4re/src/container.rs new file mode 100644 index 00000000..5f8c0c84 --- /dev/null +++ b/hive-sh4re/src/container.rs @@ -0,0 +1,78 @@ +//! Container/agent-roster wire shapes: what `ListDescendants` and +//! `HostRequest::AgentStatus` return, plus the per-account matrix +//! identity shape surfaced by `GetAgentMeta`. + +use serde::{Deserialize, Serialize}; + +/// One entry in a `ListDescendants` result. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContainerInfo { + /// Logical agent name (no `h-` prefix). + pub name: String, + /// Whether the container is currently running. + pub running: bool, +} + +/// One row in a `HostRequest::AgentStatus` result — the operator-CLI +/// projection of the dashboard's per-agent `ContainerView`. Carries the +/// agent's running/health flags plus the technical state an operator +/// wants in a roster overview (`hivectl list-agents`). +// +// Four orthogonal, independently-observed facts about one agent, each +// rendered as its own column/token by `hivectl list-agents` and read +// individually by `--json` consumers. Any combination is meaningful +// (a stopped agent can be paused and need an update), so folding them +// into a state machine or nested flag structs would only add +// `serde(flatten)` indirection to preserve the same flat JSON. Same +// rationale as `LifecycleScope` in hive-host-sock. +#[allow( + clippy::struct_excessive_bools, + reason = "flat wire projection of independent per-agent flags" +)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentStatusRow { + /// Logical agent name (no `h-` prefix). + pub name: String, + /// Whether the container is currently running. + pub running: bool, + /// Config commit is pending — the locked rev differs from the + /// agent's proposed/applied config (a rebuild would change it). + pub needs_update: bool, + /// The agent has no live claude session and is parked waiting for + /// the operator's re-auth flow. + pub needs_login: bool, + /// First 12 chars of the sha the meta flake currently has locked for + /// this agent's input. `None` when the agent has no locked rev yet. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deployed_sha: Option, + /// Count of this agent's pending reminders. + #[serde(default)] + pub pending_reminders: u64, + /// The agent's turn loop is parked (pause marker present in its + /// harness dir): the container may well be up and serving, it just + /// drives no turns. Orthogonal to `running` — an agent can be + /// paused while stopped, and pause survives a restart. + #[serde(default)] + pub paused: bool, + /// Parent in the topology tree. `None` marks a root-level agent. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub parent: Option, +} + +/// One matrix identity an agent can act as, surfaced in `GetAgentMeta`'s +/// `matrix_accounts`. Field names match the daemon's `matrix-accounts.json` +/// snapshot (written by `hive-matrix-mcp`'s account registry) so hive-c0re +/// deserializes the snapshot straight into `Vec`; the +/// snapshot's `live` / `is_primary` fields are ignored here (only live +/// accounts are listed). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MatrixIdentity { + /// Logical account name (the `account` arg on the matrix MCP tools). + pub name: String, + /// Matrix user id (`@user:server`). `None` if the session restored + /// without a known user id yet. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub user_id: Option, + /// Homeserver base URL this account is on. + pub homeserver: String, +} diff --git a/hive-sh4re/src/inbox.rs b/hive-sh4re/src/inbox.rs new file mode 100644 index 00000000..e186f7d4 --- /dev/null +++ b/hive-sh4re/src/inbox.rs @@ -0,0 +1,210 @@ +//! Per-agent socket — `/run/hyperhive/agents//mcp.sock` on the +//! host, bind-mounted into the container at `/run/hive/mcp.sock`. The +//! inbox/messaging wire shapes: message envelopes, the loose-ends +//! response types, and the shared wake-prompt/`recv`-result hint +//! constants + builder. + +use chrono::{DateTime, Utc}; +use hive_types::Ident; +use serde::{Deserialize, Serialize}; + +/// Server-side hard cap on `Recv.max` (see the `Recv` request). Bounds +/// the size of a single round-trip so a confused caller can't drain the +/// entire inbox in one go and blow past wire-buffer sizes; everything +/// above the cap silently clamps. 5 keeps individual turns small — a big +/// backlog is drained over several recv calls instead of one giant pop. +/// Lives here so both the enforcing side (hive-c0re's `socket_server`) and +/// the hinting side (hive-agent's wake prompt + tool docs) reference one +/// constant instead of a scattered magic value. +pub const RECV_BATCH_MAX: u32 = 5; + +/// Banner prepended to a wake prompt / `recv` result when the message was +/// redelivered after a harness restart (the turn that first drove it never +/// acked). Shared between the harness serve loop (wake prompt) and the MCP +/// server (`recv` tool result) so both surfaces phrase it identically. +pub const REDELIVERY_HINT: &str = "[redelivered after harness restart — may already be handled]\n"; + +/// Banner prepended to a wake prompt when the previous turn was cut off by +/// an explicit operator `/cancel` (SIGINT) rather than ending normally. Set +/// once, read-and-cleared by the next turn's wake-prompt build — see +/// `hive-agent`'s `post_cancel_turn` (sets it) and `handle_turn` (clears +/// it). Lives here for the same reason as `REDELIVERY_HINT`: a single +/// phrasing, not duplicated between call sites. +pub const INTERRUPTED_HINT: &str = "[your previous turn was interrupted by the operator (/cancel) \ + before it finished — check for new messages before resuming prior work]\n"; + +/// Shared "(N more message(s) pending …)" advisory appended after both the +/// wake prompt body and the `recv` tool result whenever the inbox still has +/// queued messages once the current message/batch is popped. Returns an empty +/// string when `remaining == 0`. The leading `\n\n` separates it from the +/// preceding body/message block, and the suggested `max` is clamped to the +/// server-side recv cap so the hint never asks for more than one round-trip +/// can deliver. One builder so the wake prompt (harness serve loop) and the +/// in-turn recv result (MCP server) stay identical. +#[must_use] +pub fn pending_hint(remaining: u64) -> String { + if remaining == 0 { + return String::new(); + } + let batch = remaining.min(u64::from(RECV_BATCH_MAX)); + format!( + "\n\n({remaining} more message(s) pending in your inbox — call `mcp__hyperhive__recv` \ + with `max: {batch}` to drain the next batch before acting. If the \ + backlog is stale/already handled, `ack_until(up_to: )` \ + clears everything up to that id in one call instead.)" + ) +} + +/// A logical message between agents. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub from: Ident, + pub to: String, + pub body: String, + /// Optional broker row-id of the message this is a reply to. + /// Stored in the DB and echoed back on `Recv` so the dashboard can + /// render conversation threads. `None` for messages that start a + /// new thread. Ignored if the referenced id is unknown or out of + /// retention — purely advisory. + pub in_reply_to: Option, +} + +/// One row of a broker inbox query — what the dashboard renders in +/// its operator-inbox section and what a per-agent web UI returns +/// from a `Recent` request. Lives in `hive_sh4re` so it can travel +/// over both the dashboard's `/api/state` and the agent socket +/// without an internal-to-wire conversion. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InboxRow { + pub id: i64, + pub from: String, + pub body: String, + pub at: i64, + /// Row-id of the message this is a reply to, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub in_reply_to: Option, +} + +/// One delivered message in a `Recv` response. +/// See `docs/conventions.md::Broker delivery + ack cycle` for the +/// full delivery/ack/requeue story. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeliveredMessage { + pub from: String, + pub body: String, + /// Broker row id, tracked by the harness for `AckTurn`. Opaque to + /// claude. `default` for wire backwards-compat. + #[serde(default)] + pub id: i64, + /// `true` if this row was resurfaced by `RequeueInflight` (previously + /// popped, never acked). Formatter prepends a "may already be handled" + /// hint when set. + #[serde(default)] + pub redelivered: bool, + /// Row-id of the message this is a reply to, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub in_reply_to: Option, +} + +/// Reminder timing: either relative (wait N seconds) or absolute (at unix +/// timestamp). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "timing_type", rename_all = "snake_case")] +pub enum ReminderTiming { + /// Remind after this many seconds from now. + InSeconds { seconds: u64 }, + /// Remind at this unix timestamp (seconds since epoch). + At { unix_timestamp: i64 }, +} + +/// One row in the response to `GetLooseEnds`. Tagged enum so new +/// thread kinds can land without breaking existing handlers. +/// Per-flavour scoping + per-variant fields + clock-anomaly +/// saturation behaviour live in +/// `docs/conventions.md::Loose-ends wire shape`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum LooseEnd { + /// A pending approval row. + Approval { + id: i64, + agent: String, + commit_ref: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + description: Option, + age_seconds: u64, + }, + /// An unanswered question row. + Question { + id: i64, + asker: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + target: Option, + question: String, + age_seconds: u64, + }, + /// A scheduled but un-delivered reminder row. + Reminder { + id: i64, + owner: String, + message: String, + due_at: DateTime, + age_seconds: u64, + }, + /// Undelivered inbox messages waiting to be `recv`'d by this agent. + /// Not cancellable — drain them with `recv`. Surfaced so an agent + /// doing a between-turns `get_loose_ends` sweep sees it still owes + /// itself a `recv` without having to poll the inbox separately. Only + /// emitted when `count > 0`. + PendingMessages { + /// Number of undelivered messages queued for this agent. + count: u64, + }, + /// Unread matrix notifications in one or more rooms. Not cancellable — + /// use `mark_read` via the matrix MCP to clear. Injected by the + /// in-container harness (not hive-c0re) because the matrix daemon + /// runs inside the agent container. + UnreadMatrix { + /// Number of rooms with at least one unread notification. + rooms: u32, + /// Per-room summary: one line per room with truncated last-message + /// body when count is 1, or just the unread count otherwise. Empty + /// when the daemon returned no per-room detail. + #[serde(default)] + summary: String, + }, + /// A dynamic, subsystem-pushed todo (loose-ends v2). Produced by an + /// in-container subsystem via `UpsertTodo` — matrix/bash/forge are the + /// *built-in* producers that ship today, but `subsystem` is a plain + /// string, not a closed set: any user-configured MCP server declared + /// in an agent's `agent.nix` can dial the in-agent socket and push its + /// own todos the same way. Cleared by that subsystem (`ClearTodo`) or + /// by the agent itself (`MarkTodoDone`, by `id`). + Todo { + id: i64, + /// Producing subsystem marker (`"matrix"`, `"forge"`, `"bash"`, … + /// — built-in producers; a user-configured MCP server can push + /// its own arbitrary marker here too, nothing enforces the set). + subsystem: String, + /// Optional subsystem-specific key (matrix room id, bash task id). + #[serde(default, skip_serializing_if = "Option::is_none")] + subsystem_key: Option, + summary: String, + /// Optional free-text provenance (room name / task label). + #[serde(default, skip_serializing_if = "Option::is_none")] + source: Option, + age_seconds: u64, + }, +} + +/// Kind discriminator for `CancelLooseEnd`. Per-kind store + +/// authorisation rules live in +/// `docs/conventions.md::Loose-ends wire shape`. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum CancelLooseEndKind { + Question, + Reminder, + /// Withdraw a pending approval (manager surface only). + Approval, +} diff --git a/hive-sh4re/src/journal.rs b/hive-sh4re/src/journal.rs new file mode 100644 index 00000000..e140cf8a --- /dev/null +++ b/hive-sh4re/src/journal.rs @@ -0,0 +1,41 @@ +//! Journal-priority encoding for `GetHostJournal` (`read_host_journal` +//! capability). One small enum, own topic module rather than a +//! catch-all — see `hive-sh4re/README.md`'s module list. + +use serde::{Deserialize, Serialize}; + +/// Syslog priority levels for `GetHostJournal`. Serialised as lowercase +/// strings matching journalctl `-p` accepted values. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum JournalPriority { + Emerg, + Alert, + Crit, + Err, + Warning, + Notice, + Info, + Debug, +} + +impl JournalPriority { + /// Returns the lowercase string journalctl expects for `-p`. Named + /// `as_journald_str` (not `as_str`) because this is a specific + /// external-tool encoding, not the enum's general wire/db string — + /// distinct call sites shouldn't reach for this by accident when they + /// actually want the serde wire representation. + #[must_use] + pub fn as_journald_str(self) -> &'static str { + match self { + Self::Emerg => "emerg", + Self::Alert => "alert", + Self::Crit => "crit", + Self::Err => "err", + Self::Warning => "warning", + Self::Notice => "notice", + Self::Info => "info", + Self::Debug => "debug", + } + } +} diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index b076e00c..2e4d2ddf 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -1,378 +1,13 @@ //! Wire types shared between `hive-c0re` and the in-container harness. -use chrono::{DateTime, Utc}; -use hive_types::Ident; -use serde::{Deserialize, Serialize}; - pub mod approvals; pub mod assets; pub mod bash_task; +pub mod container; +pub mod inbox; +pub mod journal; pub mod manager; pub mod paths; pub mod permissions; +pub mod schedule; pub mod wire_time; - -/// Server-side hard cap on `Recv.max` (see the `Recv` request). Bounds -/// the size of a single round-trip so a confused caller can't drain the -/// entire inbox in one go and blow past wire-buffer sizes; everything -/// above the cap silently clamps. 5 keeps individual turns small — a big -/// backlog is drained over several recv calls instead of one giant pop. -/// Lives here so both the enforcing side (hive-c0re's `socket_server`) and -/// the hinting side (hive-agent's wake prompt + tool docs) reference one -/// constant instead of a scattered magic value. -pub const RECV_BATCH_MAX: u32 = 5; - -/// Banner prepended to a wake prompt / `recv` result when the message was -/// redelivered after a harness restart (the turn that first drove it never -/// acked). Shared between the harness serve loop (wake prompt) and the MCP -/// server (`recv` tool result) so both surfaces phrase it identically. -pub const REDELIVERY_HINT: &str = "[redelivered after harness restart — may already be handled]\n"; - -/// Banner prepended to a wake prompt when the previous turn was cut off by -/// an explicit operator `/cancel` (SIGINT) rather than ending normally. Set -/// once, read-and-cleared by the next turn's wake-prompt build — see -/// `hive-agent`'s `post_cancel_turn` (sets it) and `handle_turn` (clears -/// it). Lives here for the same reason as `REDELIVERY_HINT`: a single -/// phrasing, not duplicated between call sites. -pub const INTERRUPTED_HINT: &str = "[your previous turn was interrupted by the operator (/cancel) \ - before it finished — check for new messages before resuming prior work]\n"; - -/// Shared "(N more message(s) pending …)" advisory appended after both the -/// wake prompt body and the `recv` tool result whenever the inbox still has -/// queued messages once the current message/batch is popped. Returns an empty -/// string when `remaining == 0`. The leading `\n\n` separates it from the -/// preceding body/message block, and the suggested `max` is clamped to the -/// server-side recv cap so the hint never asks for more than one round-trip -/// can deliver. One builder so the wake prompt (harness serve loop) and the -/// in-turn recv result (MCP server) stay identical. -#[must_use] -pub fn pending_hint(remaining: u64) -> String { - if remaining == 0 { - return String::new(); - } - let batch = remaining.min(u64::from(RECV_BATCH_MAX)); - format!( - "\n\n({remaining} more message(s) pending in your inbox — call `mcp__hyperhive__recv` \ - with `max: {batch}` to drain the next batch before acting. If the \ - backlog is stale/already handled, `ack_until(up_to: )` \ - clears everything up to that id in one call instead.)" - ) -} - -// ----------------------------------------------------------------------------- -// Per-agent socket — /run/hyperhive/agents//mcp.sock on the host, -// bind-mounted into the container at /run/hive/mcp.sock. -// ----------------------------------------------------------------------------- - -/// A logical message between agents. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Message { - pub from: Ident, - pub to: String, - pub body: String, - /// Optional broker row-id of the message this is a reply to. - /// Stored in the DB and echoed back on `Recv` so the dashboard can - /// render conversation threads. `None` for messages that start a - /// new thread. Ignored if the referenced id is unknown or out of - /// retention — purely advisory. - pub in_reply_to: Option, -} - -/// One row of a broker inbox query — what the dashboard renders in -/// its operator-inbox section and what a per-agent web UI returns -/// from a `Recent` request. Lives in `hive_sh4re` so it can travel -/// over both the dashboard's `/api/state` and the agent socket -/// without an internal-to-wire conversion. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InboxRow { - pub id: i64, - pub from: String, - pub body: String, - pub at: i64, - /// Row-id of the message this is a reply to, if any. - #[serde(skip_serializing_if = "Option::is_none")] - pub in_reply_to: Option, -} - -/// One delivered message in a `Recv` response. -/// See `docs/conventions.md::Broker delivery + ack cycle` for the -/// full delivery/ack/requeue story. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeliveredMessage { - pub from: String, - pub body: String, - /// Broker row id, tracked by the harness for `AckTurn`. Opaque to - /// claude. `default` for wire backwards-compat. - #[serde(default)] - pub id: i64, - /// `true` if this row was resurfaced by `RequeueInflight` (previously - /// popped, never acked). Formatter prepends a "may already be handled" - /// hint when set. - #[serde(default)] - pub redelivered: bool, - /// Row-id of the message this is a reply to, if any. - #[serde(skip_serializing_if = "Option::is_none")] - pub in_reply_to: Option, -} - -/// Reminder timing: either relative (wait N seconds) or absolute (at unix -/// timestamp). -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "timing_type", rename_all = "snake_case")] -pub enum ReminderTiming { - /// Remind after this many seconds from now. - InSeconds { seconds: u64 }, - /// Remind at this unix timestamp (seconds since epoch). - At { unix_timestamp: i64 }, -} - -/// One row in the response to `GetLooseEnds`. Tagged enum so new -/// thread kinds can land without breaking existing handlers. -/// Per-flavour scoping + per-variant fields + clock-anomaly -/// saturation behaviour live in -/// `docs/conventions.md::Loose-ends wire shape`. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum LooseEnd { - /// A pending approval row. - Approval { - id: i64, - agent: String, - commit_ref: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - description: Option, - age_seconds: u64, - }, - /// An unanswered question row. - Question { - id: i64, - asker: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - target: Option, - question: String, - age_seconds: u64, - }, - /// A scheduled but un-delivered reminder row. - Reminder { - id: i64, - owner: String, - message: String, - due_at: DateTime, - age_seconds: u64, - }, - /// Undelivered inbox messages waiting to be `recv`'d by this agent. - /// Not cancellable — drain them with `recv`. Surfaced so an agent - /// doing a between-turns `get_loose_ends` sweep sees it still owes - /// itself a `recv` without having to poll the inbox separately. Only - /// emitted when `count > 0`. - PendingMessages { - /// Number of undelivered messages queued for this agent. - count: u64, - }, - /// Unread matrix notifications in one or more rooms. Not cancellable — - /// use `mark_read` via the matrix MCP to clear. Injected by the - /// in-container harness (not hive-c0re) because the matrix daemon - /// runs inside the agent container. - UnreadMatrix { - /// Number of rooms with at least one unread notification. - rooms: u32, - /// Per-room summary: one line per room with truncated last-message - /// body when count is 1, or just the unread count otherwise. Empty - /// when the daemon returned no per-room detail. - #[serde(default)] - summary: String, - }, - /// A dynamic, subsystem-pushed todo (loose-ends v2). Produced by an - /// in-container subsystem via `UpsertTodo` — matrix/bash/forge are the - /// *built-in* producers that ship today, but `subsystem` is a plain - /// string, not a closed set: any user-configured MCP server declared - /// in an agent's `agent.nix` can dial the in-agent socket and push its - /// own todos the same way. Cleared by that subsystem (`ClearTodo`) or - /// by the agent itself (`MarkTodoDone`, by `id`). - Todo { - id: i64, - /// Producing subsystem marker (`"matrix"`, `"forge"`, `"bash"`, … - /// — built-in producers; a user-configured MCP server can push - /// its own arbitrary marker here too, nothing enforces the set). - subsystem: String, - /// Optional subsystem-specific key (matrix room id, bash task id). - #[serde(default, skip_serializing_if = "Option::is_none")] - subsystem_key: Option, - summary: String, - /// Optional free-text provenance (room name / task label). - #[serde(default, skip_serializing_if = "Option::is_none")] - source: Option, - age_seconds: u64, - }, -} - -/// Kind discriminator for `CancelLooseEnd`. Per-kind store + -/// authorisation rules live in -/// `docs/conventions.md::Loose-ends wire shape`. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum CancelLooseEndKind { - Question, - Reminder, - /// Withdraw a pending approval (manager surface only). - Approval, -} - -/// One entry in a `ListDescendants` result. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ContainerInfo { - /// Logical agent name (no `h-` prefix). - pub name: String, - /// Whether the container is currently running. - pub running: bool, -} - -/// One row in a `HostRequest::AgentStatus` result — the operator-CLI -/// projection of the dashboard's per-agent `ContainerView`. Carries the -/// agent's running/health flags plus the technical state an operator -/// wants in a roster overview (`hivectl list-agents`). -// -// Four orthogonal, independently-observed facts about one agent, each -// rendered as its own column/token by `hivectl list-agents` and read -// individually by `--json` consumers. Any combination is meaningful -// (a stopped agent can be paused and need an update), so folding them -// into a state machine or nested flag structs would only add -// `serde(flatten)` indirection to preserve the same flat JSON. Same -// rationale as `LifecycleScope` in hive-host-sock. -#[allow( - clippy::struct_excessive_bools, - reason = "flat wire projection of independent per-agent flags" -)] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AgentStatusRow { - /// Logical agent name (no `h-` prefix). - pub name: String, - /// Whether the container is currently running. - pub running: bool, - /// Config commit is pending — the locked rev differs from the - /// agent's proposed/applied config (a rebuild would change it). - pub needs_update: bool, - /// The agent has no live claude session and is parked waiting for - /// the operator's re-auth flow. - pub needs_login: bool, - /// First 12 chars of the sha the meta flake currently has locked for - /// this agent's input. `None` when the agent has no locked rev yet. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub deployed_sha: Option, - /// Count of this agent's pending reminders. - #[serde(default)] - pub pending_reminders: u64, - /// The agent's turn loop is parked (pause marker present in its - /// harness dir): the container may well be up and serving, it just - /// drives no turns. Orthogonal to `running` — an agent can be - /// paused while stopped, and pause survives a restart. - #[serde(default)] - pub paused: bool, - /// Parent in the topology tree. `None` marks a root-level agent. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parent: Option, -} - -/// One matrix identity an agent can act as, surfaced in `GetAgentMeta`'s -/// `matrix_accounts`. Field names match the daemon's `matrix-accounts.json` -/// snapshot (written by `hive-matrix-mcp`'s account registry) so hive-c0re -/// deserializes the snapshot straight into `Vec`; the -/// snapshot's `live` / `is_primary` fields are ignored here (only live -/// accounts are listed). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MatrixIdentity { - /// Logical account name (the `account` arg on the matrix MCP tools). - pub name: String, - /// Matrix user id (`@user:server`). `None` if the session restored - /// without a known user id yet. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub user_id: Option, - /// Homeserver base URL this account is on. - pub homeserver: String, -} - -/// Per-agent capability grants. Stored in `meta/capabilities.json` -/// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`). -/// Capabilities control system-level access that hive-c0re enforces -/// Syslog priority levels for `GetHostJournal`. Serialised as lowercase -/// strings matching journalctl `-p` accepted values. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum JournalPriority { - Emerg, - Alert, - Crit, - Err, - Warning, - Notice, - Info, - Debug, -} - -impl JournalPriority { - /// Returns the lowercase string journalctl expects for `-p`. Named - /// `as_journald_str` (not `as_str`) because this is a specific - /// external-tool encoding, not the enum's general wire/db string — - /// distinct call sites shouldn't reach for this by accident when they - /// actually want the serde wire representation. - #[must_use] - pub fn as_journald_str(self) -> &'static str { - match self { - Self::Emerg => "emerg", - Self::Alert => "alert", - Self::Crit => "crit", - Self::Err => "err", - Self::Warning => "warning", - Self::Notice => "notice", - Self::Info => "info", - Self::Debug => "debug", - } - } -} - -/// Schedule row shape on the wire — mirror of -/// `scheduled_prompts::Schedule` but in the public crate so the -/// dashboard and agent surfaces can deserialize without depending -/// on hive-c0re-internal types. Kept structurally identical to the -/// in-process type; the conversion is field-by-field in -/// `manager_server` / `dashboard`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WireSchedule { - pub id: i64, - pub owner: String, - pub body: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub interval_seconds: Option, - pub next_fire_at_unix: DateTime, - pub created_at_unix: DateTime, - pub source: WireScheduleSource, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cancelled_at_unix: Option>, - /// Set while the schedule is paused. Worker skips paused rows; - /// they keep their `next_fire_at_unix` so resuming at any time - /// fires at the next intended instant (no catch-up clamp needed - /// — a paused schedule simply slips its next fire). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub paused_at_unix: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option, - pub targets: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum WireScheduleSource { - Operator, - Approval { id: i64 }, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WireScheduleTarget { - pub target: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cancelled_at_unix: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub last_fired_at_unix: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub last_result: Option, -} diff --git a/hive-sh4re/src/schedule.rs b/hive-sh4re/src/schedule.rs new file mode 100644 index 00000000..7ba71128 --- /dev/null +++ b/hive-sh4re/src/schedule.rs @@ -0,0 +1,52 @@ +//! Scheduled-prompt row shape on the wire, shared by the dashboard and +//! agent surfaces (mirrors `hive-c0re`'s internal `scheduled_prompts::Schedule`). + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Schedule row shape on the wire — mirror of +/// `scheduled_prompts::Schedule` but in the public crate so the +/// dashboard and agent surfaces can deserialize without depending +/// on hive-c0re-internal types. Kept structurally identical to the +/// in-process type; the conversion is field-by-field in +/// `manager_server` / `dashboard`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WireSchedule { + pub id: i64, + pub owner: String, + pub body: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub interval_seconds: Option, + pub next_fire_at_unix: DateTime, + pub created_at_unix: DateTime, + pub source: WireScheduleSource, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cancelled_at_unix: Option>, + /// Set while the schedule is paused. Worker skips paused rows; + /// they keep their `next_fire_at_unix` so resuming at any time + /// fires at the next intended instant (no catch-up clamp needed + /// — a paused schedule simply slips its next fire). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub paused_at_unix: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + pub targets: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum WireScheduleSource { + Operator, + Approval { id: i64 }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WireScheduleTarget { + pub target: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cancelled_at_unix: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_fired_at_unix: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub last_result: Option, +} diff --git a/hivectl/src/agents.rs b/hivectl/src/agents.rs index af9e9305..c20dcd78 100644 --- a/hivectl/src/agents.rs +++ b/hivectl/src/agents.rs @@ -147,7 +147,7 @@ pub(crate) async fn agents_list(socket: &Path, json: bool) -> Result<()> { } // STATUS collapses the health flags into one space-separated token so // the common case (`running`) stays short and anomalies stand out. - let status_of = |r: &hive_sh4re::AgentStatusRow| -> String { + let status_of = |r: &hive_sh4re::container::AgentStatusRow| -> String { let mut s = if r.running { "running" } else { "stopped" }.to_owned(); if r.paused { s.push_str(" paused");