hive-sh4re: split inbox, container, journal, and schedule wire shapes into their own modules
Closes the #3110 split — lib.rs is now just the crate doc comment and the pub mod list. journal.rs's new doc comment fixes a pre-existing bug: the old JournalPriority doc text in lib.rs was actually half Capability's doc (a leftover from an earlier reorder that moved the code but not the comment above it).
This commit is contained in:
parent
b785f96d30
commit
80f16094f1
30 changed files with 513 additions and 486 deletions
|
|
@ -334,7 +334,7 @@ pub struct GetHostJournalArgs {
|
|||
pub lines: Option<u32>,
|
||||
/// Minimum syslog priority level.
|
||||
#[serde(default)]
|
||||
pub priority: Option<hive_sh4re::JournalPriority>,
|
||||
pub priority: Option<hive_sh4re::journal::JournalPriority>,
|
||||
/// Regex to match against log message fields (journalctl --grep).
|
||||
#[serde(default)]
|
||||
pub grep: Option<String>,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ pub fn format_recv(resp: Result<hive_core_agent_sock::Response, anyhow::Error>)
|
|||
|
||||
/// 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<LooseEnd>` 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<Vec<hive_sh4re::LooseEnd>> {
|
||||
pub(super) async fn local_todos() -> Option<Vec<hive_sh4re::inbox::LooseEnd>> {
|
||||
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<Vec<hive_sh4re::LooseEnd>> {
|
|||
/// 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<Vec<hive_sh4re::LooseEnd>> {
|
||||
pub(super) async fn local_reminders() -> Option<Vec<hive_sh4re::inbox::LooseEnd>> {
|
||||
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<Vec<hive_sh4re::LooseEnd>> {
|
|||
/// (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<Vec<hive_sh4re::LooseEnd>> {
|
||||
pub(super) async fn local_questions() -> Option<Vec<hive_sh4re::inbox::LooseEnd>> {
|
||||
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<hive_sh4re::CancelLooseEndKind, String> {
|
||||
pub(super) fn parse_loose_end_kind(
|
||||
raw: &str,
|
||||
) -> Result<hive_sh4re::inbox::CancelLooseEndKind, String> {
|
||||
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<hive_sh4re::CancelLooseE
|
|||
/// the success ack so the caller always sees `"question"` /
|
||||
/// `"reminder"` instead of whatever alias they passed in (`"q"` /
|
||||
/// `"r"`).
|
||||
pub(super) fn loose_end_kind_label(kind: hive_sh4re::CancelLooseEndKind) -> &'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}")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<S: Surface>(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<S: Surface>(
|
|||
files: &turn::TurnFiles,
|
||||
todo_wake: Arc<tokio::sync::Notify>,
|
||||
todos_store: Option<Arc<todos::Todos>>,
|
||||
mut reminder_rx: tokio::sync::mpsc::UnboundedReceiver<hive_sh4re::DeliveredMessage>,
|
||||
mut reminder_rx: tokio::sync::mpsc::UnboundedReceiver<hive_sh4re::inbox::DeliveredMessage>,
|
||||
interrupted: Arc<std::sync::atomic::AtomicBool>,
|
||||
) -> Result<()> {
|
||||
tracing::info!(socket = %socket.display(), "harness serve");
|
||||
|
|
@ -849,7 +849,7 @@ async fn handle_turn<S: Surface>(
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -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<Arc<Reminders>>,
|
||||
tx: mpsc::UnboundedSender<hive_sh4re::DeliveredMessage>,
|
||||
tx: mpsc::UnboundedSender<hive_sh4re::inbox::DeliveredMessage>,
|
||||
) {
|
||||
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<hive_sh4re::DeliveredMessage>) {
|
||||
fn tick(store: &Reminders, tx: &mpsc::UnboundedSender<hive_sh4re::inbox::DeliveredMessage>) {
|
||||
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<hive_sh4re::DeliveredMessa
|
|||
};
|
||||
for r in due {
|
||||
let body = prepare_body(&r.message, r.file_path.as_deref());
|
||||
let dm = hive_sh4re::DeliveredMessage {
|
||||
let dm = hive_sh4re::inbox::DeliveredMessage {
|
||||
from: "reminder".into(),
|
||||
body,
|
||||
id: 0,
|
||||
|
|
@ -109,7 +109,7 @@ fn tick(store: &Reminders, tx: &mpsc::UnboundedSender<hive_sh4re::DeliveredMessa
|
|||
pub fn store(
|
||||
store: &Reminders,
|
||||
message: &str,
|
||||
timing: &hive_sh4re::ReminderTiming,
|
||||
timing: &hive_sh4re::inbox::ReminderTiming,
|
||||
file_path: Option<&str>,
|
||||
) -> Result<i64, String> {
|
||||
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<i64> {
|
||||
use hive_sh4re::ReminderTiming;
|
||||
fn resolve_due_at(timing: &hive_sh4re::inbox::ReminderTiming) -> anyhow::Result<i64> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ struct AppState {
|
|||
gui_vnc_port: Option<u16>,
|
||||
/// 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.
|
||||
|
|
|
|||
|
|
@ -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<hive_sh4re::InboxRow>,
|
||||
inbox: Vec<hive_sh4re::inbox::InboxRow>,
|
||||
/// 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<hive_sh4re::InboxRow> {
|
||||
async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::inbox::InboxRow> {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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<Self>) {
|
||||
let mut schedules: Vec<hive_sh4re::WireSchedule> = 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<hive_sh4re::schedule::WireSchedule> =
|
||||
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("<root>");
|
||||
let new_label = new_parent.unwrap_or("<root>");
|
||||
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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<AppState>) -> Response {
|
||||
match state.coord.scheduled_prompts.list() {
|
||||
Ok(rows) => {
|
||||
let mut wire: Vec<hive_sh4re::WireSchedule> = rows
|
||||
let mut wire: Vec<hive_sh4re::schedule::WireSchedule> = rows
|
||||
.into_iter()
|
||||
.map(crate::socket_server::schedule_to_wire_public)
|
||||
.collect();
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ pub enum DashboardEvent {
|
|||
/// naturally re-derived from the full list.
|
||||
SchedulesChanged {
|
||||
seq: u64,
|
||||
schedules: Vec<hive_sh4re::WireSchedule>,
|
||||
schedules: Vec<hive_sh4re::schedule::WireSchedule>,
|
||||
},
|
||||
/// Full snapshot of capability grants (per-agent `Vec<cap_name>`).
|
||||
/// Emitted from the rebuild-queue worker after a `PermChange`
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use hive_sh4re::LooseEnd;
|
||||
use hive_sh4re::inbox::LooseEnd;
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
|
|
|
|||
|
|
@ -152,11 +152,11 @@ pub fn handle_answer(
|
|||
pub fn handle_cancel_loose_end(
|
||||
coord: &Arc<Coordinator>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ async fn handle_agent_status(coord: &Arc<Coordinator>) -> 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,
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ pub(super) async fn handle_list_descendants(coord: &Arc<Coordinator>, 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 }
|
||||
|
|
|
|||
|
|
@ -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<Coordinator>) -> 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<u64>) -> 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<hive_sh4re::MatrixIdentity> {
|
||||
fn read_agent_matrix_identities(
|
||||
agent: &hive_types::Ident,
|
||||
) -> Vec<hive_sh4re::container::MatrixIdentity> {
|
||||
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::<Vec<hive_sh4re::MatrixIdentity>>(&s).ok())
|
||||
.and_then(|s| serde_json::from_str::<Vec<hive_sh4re::container::MatrixIdentity>>(&s).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
|
|
@ -825,7 +827,7 @@ pub struct HostJournalArgs<'a> {
|
|||
pub unit: &'a Option<String>,
|
||||
pub container: &'a Option<String>,
|
||||
pub lines: &'a Option<u32>,
|
||||
pub priority: &'a Option<hive_sh4re::JournalPriority>,
|
||||
pub priority: &'a Option<hive_sh4re::journal::JournalPriority>,
|
||||
pub grep: &'a Option<String>,
|
||||
pub since: &'a Option<String>,
|
||||
pub until: &'a Option<String>,
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
) {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
78
hive-sh4re/src/container.rs
Normal file
78
hive-sh4re/src/container.rs
Normal file
|
|
@ -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<String>,
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// 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<MatrixIdentity>`; 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<String>,
|
||||
/// Homeserver base URL this account is on.
|
||||
pub homeserver: String,
|
||||
}
|
||||
210
hive-sh4re/src/inbox.rs
Normal file
210
hive-sh4re/src/inbox.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
//! Per-agent socket — `/run/hyperhive/agents/<name>/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: <highest [msg #N] seen>)` \
|
||||
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<i64>,
|
||||
}
|
||||
|
||||
/// 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<i64>,
|
||||
}
|
||||
|
||||
/// 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<i64>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
age_seconds: u64,
|
||||
},
|
||||
/// An unanswered question row.
|
||||
Question {
|
||||
id: i64,
|
||||
asker: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
target: Option<String>,
|
||||
question: String,
|
||||
age_seconds: u64,
|
||||
},
|
||||
/// A scheduled but un-delivered reminder row.
|
||||
Reminder {
|
||||
id: i64,
|
||||
owner: String,
|
||||
message: String,
|
||||
due_at: DateTime<Utc>,
|
||||
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<String>,
|
||||
summary: String,
|
||||
/// Optional free-text provenance (room name / task label).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
source: Option<String>,
|
||||
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,
|
||||
}
|
||||
41
hive-sh4re/src/journal.rs
Normal file
41
hive-sh4re/src/journal.rs
Normal file
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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: <highest [msg #N] seen>)` \
|
||||
clears everything up to that id in one call instead.)"
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Per-agent socket — /run/hyperhive/agents/<name>/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<i64>,
|
||||
}
|
||||
|
||||
/// 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<i64>,
|
||||
}
|
||||
|
||||
/// 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<i64>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
age_seconds: u64,
|
||||
},
|
||||
/// An unanswered question row.
|
||||
Question {
|
||||
id: i64,
|
||||
asker: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
target: Option<String>,
|
||||
question: String,
|
||||
age_seconds: u64,
|
||||
},
|
||||
/// A scheduled but un-delivered reminder row.
|
||||
Reminder {
|
||||
id: i64,
|
||||
owner: String,
|
||||
message: String,
|
||||
due_at: DateTime<Utc>,
|
||||
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<String>,
|
||||
summary: String,
|
||||
/// Optional free-text provenance (room name / task label).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
source: Option<String>,
|
||||
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<String>,
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// 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<MatrixIdentity>`; 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<String>,
|
||||
/// 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<u64>,
|
||||
pub next_fire_at_unix: DateTime<Utc>,
|
||||
pub created_at_unix: DateTime<Utc>,
|
||||
pub source: WireScheduleSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cancelled_at_unix: Option<DateTime<Utc>>,
|
||||
/// 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<DateTime<Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub targets: Vec<WireScheduleTarget>,
|
||||
}
|
||||
|
||||
#[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<DateTime<Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_fired_at_unix: Option<DateTime<Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_result: Option<String>,
|
||||
}
|
||||
|
|
|
|||
52
hive-sh4re/src/schedule.rs
Normal file
52
hive-sh4re/src/schedule.rs
Normal file
|
|
@ -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<u64>,
|
||||
pub next_fire_at_unix: DateTime<Utc>,
|
||||
pub created_at_unix: DateTime<Utc>,
|
||||
pub source: WireScheduleSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cancelled_at_unix: Option<DateTime<Utc>>,
|
||||
/// 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<DateTime<Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub targets: Vec<WireScheduleTarget>,
|
||||
}
|
||||
|
||||
#[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<DateTime<Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_fired_at_unix: Option<DateTime<Utc>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_result: Option<String>,
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue