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