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
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>,
|
||||
}
|
||||
Loading…
Reference in a new issue