hyperhive/hive-sh4re/src/inbox.rs
damocles 80f16094f1 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).
2026-08-10 23:26:15 +02:00

210 lines
9 KiB
Rust

//! 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,
}