diff --git a/docs/2-handbook/agents/web-page.md b/docs/2-handbook/agents/web-page.md index bf37ec21..56d20921 100644 --- a/docs/2-handbook/agents/web-page.md +++ b/docs/2-handbook/agents/web-page.md @@ -191,8 +191,8 @@ the full row taxonomy and dispatch logic): (`-` lines from `input.old_string`, `+` lines from `input.new_string` or every line of `input.content`). Summary carries the path + line counts. - - others (`Read /path`, `Bash $ cmd`, `mcp__hyperhive__send → - operator: "..."`, etc.): flat one-line per-tool format with + - others (`Read /path`, `Bash $ cmd`, `mcp__matrix__send_dm → + iris: "..."`, etc.): flat one-line per-tool format with per-tool salient-arg extraction (`fmtToolUse`). - `Stream` `tool_result` short → flat `← ...`; long → collapsed `
` `▸ ← Nl · headline` (select to expand full body). diff --git a/docs/4-implementation/turn-loop/claude-invocation.md b/docs/4-implementation/turn-loop/claude-invocation.md index ff8893d6..238ea6d1 100644 --- a/docs/4-implementation/turn-loop/claude-invocation.md +++ b/docs/4-implementation/turn-loop/claude-invocation.md @@ -220,12 +220,8 @@ the `CLAUDE.md` (see `docs/2-handbook/agents/persistence.md` for the boot-time rename). The wake prompt is intentionally minimal: the popped message's -`from`/`body`, prefixed with a `[msg #]` broker-row-id marker -(so the agent knows what id to pass to `ack_until` for bulk triage; -transient pings show no marker because their sentinel id 0 has nothing -to ack), plus an inline `({unread} more pending — drain via …)` hint -when `unread > 0`. Claude drives any further `recv`/`send` itself via -the embedded MCP server. +`from` and `body`, behind a redelivery or interruption banner when one applies. +The harness drives one turn per inbox message. `hive-agent` self-announces once on every boot: `boot_announce::announce` pushes a one-line "you were just diff --git a/docs/4-implementation/turn-loop/mcp.md b/docs/4-implementation/turn-loop/mcp.md index 03b81918..c8c400fd 100644 --- a/docs/4-implementation/turn-loop/mcp.md +++ b/docs/4-implementation/turn-loop/mcp.md @@ -36,41 +36,25 @@ runs on the change. ## Core tools (always available) -**Messaging** (`messaging` group): `send(to, body, in_reply_to?)`, -`recv(max?)`, `ack_until(up_to)`. +**Messaging.** There is no hyperhive messaging tool. Agents talk to peers and the +operator over matrix ([matrix](../agent-tools/matrix.md)); `send_dm` takes a peer's agent +name (`iris`, meaning that agent on the same homeserver) or a full user id. An agent +without matrix tools is cut off from peers on purpose, and the operator reads its +`set_status` line, its turn output on the dashboard, and its logs. The `messaging` +permission remains an accepted value for stored permission sets, but grants no tool. -- `send` — hive-local broker message to a peer on this hive (logical name). With - the matrix MCP tools, talk to peers over matrix instead (a DM to the `user_id` in - `get_agent_meta`'s `matrix_accounts`); `send` is for peers without a matrix - account and agents without matrix tools. Optional - `in_reply_to: i64` links the message to a prior id for thread - rendering. The broker refuses `to: "operator"`. The error - points an agent that has the matrix MCP tools at matrix - ([matrix](../agent-tools/matrix.md)), and tells one without them that the - operator reads its `set_status` line, its turn output on the dashboard, - and its logs. -- `recv` — drain inbox. Always an immediate peek, never blocks. `max` - (default 1, cap 5) drains up to N rows. `recv` prefixes each returned row - with `[msg #]` (broker row id; note the highest id seen, then pass - it to `ack_until` to bulk-triage the batch). **Graceful shutdown**: when the harness - receives a stop signal, the inbox becomes fenced and `recv` returns an - explicit `from: "graceful-stop"` message instead of an empty inbox. - This unmissably directs the agent to flush durable state (`/state` - files) and end the turn — the container exits when the turn completes. - The graceful-stop turn takes the same post-turn compaction path as any - other turn: if the context crossed the watermark the harness runs a - notes-checkpoint turn and then `/compact`. Compacting before shutdown - keeps a later cold start cheap instead of re-uploading a large transcript. -- `ack_until(up_to)` — bulk-mark inbox rows handled: it stamps every row - with broker id `<= up_to` as acked in a single UPDATE. - Recipient-scoped (agents can only ack their own rows). Use when a - restart redelivers a large backlog of already-handled messages: read - the highest `[msg #N]` from the set you've actually processed, then - `ack_until(N)` to prevent re-pop. Acked rows never redeliver. - Transient pings (sentinel id 0) have nothing to ack and show no marker. +The inbox itself is the harness's: it pops one message per turn and acks it when the +turn ends, so there is no agent-facing `recv` or `ack_until`. **Graceful shutdown**: +when the harness receives a stop signal, the inbox becomes fenced and the harness +delivers an explicit `from: "graceful-stop"` message as the turn's prompt. It +directs the agent to flush durable state (`/state` files) and end the turn; the +container exits when the turn completes. The graceful-stop turn takes the same +post-turn compaction path as any other turn: if the context crossed the watermark the +harness runs a notes-checkpoint turn and then `/compact`. Compacting before shutdown +keeps a later cold start cheap instead of re-uploading a large transcript. **System messages** (from sender `system`): the broker delivers them as regular inbox -messages (same `recv` path). A container crash never reaches an +messages (the same path as every other inbox message). A container crash never reaches an agent — it only produces a log line + swarm-level grafana alert, no agent turn at all (`docs/4-implementation/scheduler/observability.md`). Lifecycle transitions the job-queue scheduler or crash watcher drive directly — diff --git a/docs/5-project/process/conventions.md b/docs/5-project/process/conventions.md index 0bf53dc3..4b677957 100644 --- a/docs/5-project/process/conventions.md +++ b/docs/5-project/process/conventions.md @@ -123,8 +123,7 @@ request still carries an optional `wait_seconds` (long-poll the first message, once one arrives — or one is already pending — the call drains up to `max` in total): the harness's own turn-driving loop uses it internally (`hive-agent`'s `recv_next`, 180s). The -agent-facing MCP `recv` tool doesn't expose this parameter — it -always passes `wait_seconds: None`, an immediate peek. +agent-facing MCP surface has no `recv` tool at all. Per-row bookkeeping inside the broker: @@ -188,8 +187,8 @@ Per-variant fields: 3339 on the wire, see *Timestamps on the wire* below); clients compute time-until-fire against it. - `PendingMessages { count }` — undelivered inbox messages the - agent still owes itself a `recv` for. Informational + not - cancellable (drain with `recv`); only emitted when `count > 0`, + agent has not handled yet. Informational + not + cancellable; only emitted when `count > 0`, and surfaced first in the list as the most actionable signal. Counted host-side from the broker (`count_pending`), so it reflects what's genuinely still queued — @@ -283,7 +282,7 @@ host access it has. | Permission | Grants | |---|---| -| `messaging` | `send`, `recv`, `ack_until` | +| `messaging` | nothing (kept as an accepted value; agents message over matrix) | | `meta` | `get_agent_meta` (`set_status` is always-on, see below) | | `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` | | `execution` | the `bash` MCP server (`mcp__bash__run`/`status`/`kill`), rendered into the agent's MCP config only with it; a subagent gets Claude's own `Bash` instead. See `docs/4-implementation/agent-tools/bash.md`. | diff --git a/hive-agent-mcp/README.md b/hive-agent-mcp/README.md index e84576ea..7f894bae 100644 --- a/hive-agent-mcp/README.md +++ b/hive-agent-mcp/README.md @@ -8,8 +8,7 @@ would hit. HTTP is the sole transport; there is no stdio mode here. ## When to use it -This is where the core hyperhive tool surface lives: `send`, `recv`, -`remind`, `get_loose_ends`, `set_status` +This is where the core hyperhive tool surface lives: `remind`, `get_loose_ends`, `set_status` and `get_agent_meta`. Reach for this crate when you're adding or changing a built-in tool rather than an `extraMcpServers` add-on — those are separate stdio bridges diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index a8f30f25..b1ef9db9 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -4,44 +4,6 @@ use rmcp::schemars; -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct SendArgs { - /// Logical agent name to deliver the message to (e.g. `"bob"`, - /// `"alice"`). `"operator"` is refused: reach the operator over matrix - /// if you have it. - pub to: String, - /// Message body. Plain text; the broker doesn't parse it. - pub body: String, - /// Optional broker row-id of the message this is a reply to. Lets - /// the dashboard render conversation threads. Pass the `id` from the - /// `DeliveredMessage` you're responding to; omit for new threads. - /// Silently ignored if the id is unknown or out of retention. - #[serde(default)] - pub in_reply_to: Option, -} - -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct RecvArgs { - /// Maximum number of messages to pop in this round-trip. Default - /// (None) is 1 (single-message behaviour — exactly what you want - /// when you're called to drive a turn off the first wake). Pass - /// a higher value (capped at 5 server-side) when you've been - /// told the inbox has more queued (the wake prompt mentions - /// pending count) and want to drain everything in one tool call. - #[serde(default)] - pub max: Option, -} - -/// MCP tool args for `ack_until`. -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct AckUntilArgs { - /// Highest broker message id to mark handled: every inbox message - /// with `id <= up_to` (ids show as `[msg #]` in recv output) - /// is acked in one sweep. Pass the highest id you've actually - /// seen/triaged — anything above it stays queued for later turns. - pub up_to: i64, -} - /// MCP tool args for `mark_todos_done`. #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct MarkTodosDoneArgs { diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index 3c3fd26c..23f1af78 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -24,10 +24,9 @@ mod args; mod render; pub use args::{ - AckUntilArgs, CancelLooseEndArgs, CompactArgs, GetAgentMetaArgs, MarkTodosDoneArgs, RecvArgs, - RemindArgs, SendArgs, SetStatusArgs, + CancelLooseEndArgs, CompactArgs, GetAgentMetaArgs, MarkTodosDoneArgs, RemindArgs, SetStatusArgs, }; -pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv}; +pub use render::{annotate_retries, format_ack, format_agent_meta}; use render::{ dial_agent_socket, format_matrix_summary, local_reminders, local_todos, mark_local_todo_done, @@ -140,92 +139,6 @@ impl AgentServer { // non-interactive `--print` mode with "permissions not granted yet". #[tool_router] impl AgentServer { - #[tool( - description = "Send a message through the hive-local broker to another hyperhive \ - agent on this hive. To talk to a peer agent, prefer matrix (a DM to the peer's \ - `user_id` from `get_agent_meta`) when you have the matrix MCP tools; use this \ - when you have none or the peer has no matrix account. Not for the operator: reach \ - them over matrix if you have the matrix MCP tools." - )] - async fn send(&self, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - let to = args.to.clone(); - run_tool_envelope("send", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::Send { - to: args.to, - body: args.body, - in_reply_to: args.in_reply_to, - }) - .await; - annotate_retries(format_ack(resp, "send", format!("sent to {to}")), retries) - }) - .await - } - - #[tool( - description = "Pop messages from this agent's inbox. Returns one or more messages, or \ - an empty marker if nothing is waiting. Always an immediate 'anything pending?' peek \ - — never blocks. \n\n\ - **Single-message default**: with no args (or `max: 1`) you get the next message — \ - same behaviour the harness uses to drive a turn. \n\n\ - **When idle, prefer ending the turn over polling here.** Ending the turn is the \ - ONLY path that observes an in-container todo wake (bash-task completions, matrix \ - unread, forge activity) — that signal reaches the harness loop between turns, never \ - a live `recv` call, so looping on `recv` while idle means sitting blind until \ - something else wakes you. Ending the turn also checkpoints your session, and costs \ - no latency vs. polling for a real inbox message either way — the broker wakes the \ - next turn just as fast. \n\n\ - **Batch drain**: pass `max: N` (capped at 5) to drain up to N messages in one \ - round-trip. Use this when the wake prompt told you the inbox has more queued, or \ - any time you expect a burst — one tool call beats N consecutive single recvs. Empty \ - result reported the same way regardless of `max`. \n\n\ - After popping, the result appends a `(N more message(s) pending …)` line whenever the \ - inbox still has queued messages — so you know whether to drain again (or `ack_until`) \ - without a separate status check. No line means the inbox is empty." - )] - async fn recv(&self, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - run_tool_envelope("recv", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::Recv { - wait_seconds: None, - max: args.max, - }) - .await; - annotate_retries(format_recv(resp), retries) - }) - .await - } - - #[tool( - description = "Bulk-mark inbox messages handled: every message with broker id \ - <= `up_to` (ids show as `[msg #]` in recv output and wake prompts) is \ - acked in one call — pending and already-delivered rows alike. Use this to \ - clear a backlog you've already triaged (e.g. a redelivered flood after a \ - container restart, or a pile of stale notifications) instead of draining it \ - one recv at a time: note the highest `[msg #N]` you've seen, then \ - `ack_until(up_to: N)`. Acked messages never redeliver. Only affects YOUR \ - inbox rows; messages newer than `up_to` stay queued. Returns how many rows \ - were newly acked." - )] - async fn ack_until(&self, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - run_tool_envelope("ack_until", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::AckUntil { up_to: args.up_to }) - .await; - let rendered = match resp { - Ok(hive_core_agent_sock::Response::Acked { count }) => { - format!("acked {count} message(s) up to id {}", args.up_to) - } - other => reply_err(other, "ack_until"), - }; - annotate_retries(rendered, retries) - }) - .await - } - #[tool( description = "List loose ends pending against this agent: pending reminders \ you scheduled. Also lists active \ diff --git a/hive-agent-mcp/src/mcp/render.rs b/hive-agent-mcp/src/mcp/render.rs index 4f7ed92a..38895436 100644 --- a/hive-agent-mcp/src/mcp/render.rs +++ b/hive-agent-mcp/src/mcp/render.rs @@ -49,108 +49,6 @@ pub fn format_ack( } } -/// Format helper for `recv`: renders zero, one, or many popped -/// messages. Empty list collapses to "(empty)" so claude doesn't go -/// hunting for content. A single message renders as the historical -/// `from: X\n\nbody` block (banner first if `redelivered`). A -/// multi-message batch renders with a `popped N message(s):` header -/// and `---` separators between bodies so the model can tell where -/// one ends and the next begins; per-message redelivery banners -/// included. -#[must_use] -pub fn format_recv(resp: Result) -> String { - match resp { - Ok(hive_core_agent_sock::Response::Messages { - messages, - remaining, - }) => render_recv_messages(&messages, remaining), - // A graceful stop is pending — the inbox is fenced. Render a single - // explicit directive (not an empty inbox, which claude might just - // recv again) so every recv during the stop unmissably tells claude - // to flush + end. `remaining` is forced to 0 — the inbox is fenced, - // so a "N more pending" hint would be misleading. - Ok(hive_core_agent_sock::Response::GracefulStop) => { - render_recv_messages(&[graceful_stop_message()], 0) - } - other => reply_err(other, "recv"), - } -} - -/// 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::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 \ - YOUR TURN now. Do NOT call recv again: the inbox is fenced and recv will \ - only keep returning this same notice." - .into(), - id: 0, - redelivered: false, - in_reply_to: None, - } -} - -/// Render the popped-message payload of a successful `recv` (see `format_recv` -/// for the empty/single/batch shapes). `remaining` is the post-pop inbox -/// 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::inbox::DeliveredMessage], - remaining: u64, -) -> String { - use std::fmt::Write as _; - if messages.is_empty() { - return "(empty)".to_owned(); - } - let mut out = if messages.len() == 1 { - let m = &messages[0]; - let banner = if m.redelivered { - hive_sh4re::inbox::REDELIVERY_HINT - } else { - "" - }; - format!("{banner}{}from: {}\n\n{}", msg_id_tag(m.id), m.from, m.body) - } else { - let n = messages.len(); - let mut out = format!("popped {n} message(s):\n\n"); - for (i, m) in messages.iter().enumerate() { - if i > 0 { - out.push_str("\n---\n\n"); - } - let banner = if m.redelivered { - hive_sh4re::inbox::REDELIVERY_HINT - } else { - "" - }; - let _ = write!( - out, - "{banner}{}from: {}\n\n{}", - msg_id_tag(m.id), - m.from, - m.body - ); - } - out - }; - out.push_str(&hive_sh4re::inbox::pending_hint(remaining)); - out -} - -/// `[msg #] ` marker prefixed to each recv row so the agent knows -/// what to pass to `ack_until` when bulk-triaging a backlog. Transient -/// pings carry the sentinel id 0 (in-memory only, nothing in the -/// broker to ack) and render without the marker. -fn msg_id_tag(id: i64) -> String { - if id > 0 { - format!("[msg #{id}] ") - } else { - String::new() - } -} - /// Hard cap on how many individual `Todo` lines `render_loose_ends` will /// emit. Reminders stay naturally bounded (they're triaged interactively /// and don't self-multiply), but todos are pushed by @@ -559,7 +457,7 @@ pub fn annotate_retries(mut s: String, retries: u32) -> String { #[cfg(test)] mod tests { - use super::{dial_socket, format_ack, format_recv}; + use super::{dial_socket, format_ack}; #[tokio::test] async fn a_socket_that_accepts_and_never_answers_is_given_up_on() { @@ -590,70 +488,18 @@ mod tests { assert!(resp.is_none()); } - 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, - redelivered: false, - in_reply_to: None, - } - } - - #[test] - fn empty_recv_renders_bare_empty_marker() { - let out = format_recv(Ok(hive_core_agent_sock::Response::Messages { - messages: vec![], - remaining: 0, - })); - assert_eq!(out, "(empty)"); - } - - #[test] - fn single_recv_with_remaining_appends_pending_hint() { - let out = format_recv(Ok(hive_core_agent_sock::Response::Messages { - messages: vec![msg(7, "alice", "hi")], - remaining: 3, - })); - assert!(out.starts_with("[msg #7] from: alice")); - assert!(out.contains("3 more message(s) pending")); - assert!(out.contains("max: 3")); - } - - #[test] - fn single_recv_no_remaining_has_no_pending_hint() { - let out = format_recv(Ok(hive_core_agent_sock::Response::Messages { - messages: vec![msg(7, "alice", "hi")], - remaining: 0, - })); - assert!(!out.contains("more message(s) pending")); - } - - #[test] - fn batch_recv_with_remaining_appends_pending_hint_once() { - let out = format_recv(Ok(hive_core_agent_sock::Response::Messages { - messages: vec![msg(7, "alice", "hi"), msg(8, "bob", "yo")], - remaining: 9, - })); - 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::inbox::RECV_BATCH_MAX)); - assert!(out.contains(&format!("max: {batch}"))); - } - #[test] fn ok_warn_keeps_the_success_line_and_appends_each_warning() { let out = format_ack( Ok(hive_core_agent_sock::Response::OkWarn { warnings: vec!["name is reserved".to_owned(), "second thing".to_owned()], }), - "send", - "sent to iris".to_owned(), + "remind", + "reminder set".to_owned(), ); // The operation HAPPENED — dropping the success line would read as a - // failure and invite a retry that sends a second message. - assert!(out.starts_with("sent to iris")); + // failure and invite a retry that repeats it. + assert!(out.starts_with("reminder set")); assert!(out.contains("⚠️ name is reserved")); assert!(out.contains("⚠️ second thing")); } @@ -664,7 +510,7 @@ mod tests { // warning marker would pass the test above. let out = format_ack( Ok(hive_core_agent_sock::Response::Ok), - "send", + "remind", "sent".to_owned(), ); assert_eq!(out, "sent"); diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index b245b37b..d893b8a3 100644 --- a/hive-agent/prompts/system.md +++ b/hive-agent/prompts/system.md @@ -1,17 +1,15 @@ You are hyperhive agent `{label}` (qualified: `{qualified_label}`){hive_identity}{swarm_identity} in a multi-agent system. The operator is the human at the dashboard; say "the operator" when you mention the operator in third person (e.g. when relaying to a peer or the manager). -When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) in text, never as a `send` target, so the operator + peers can disambiguate. +When you're talking to or about a peer on a different hive, use the qualified form (`name@hive`) in text, never as a `send_dm` recipient, so the operator + peers can disambiguate. Within your own hive the short form is fine.{operators} Tools: the hyperhive MCP tools and any extra MCP servers describe themselves in their own tool schema, so this prompt does not repeat them. What the schema doesn't tell you: -- `send` is the hive-local broker: system notices, and peers on this hive. -- **Talk to peers and the operator over matrix** if you have the matrix MCP tools (`mcp__matrix__send_dm` or `mcp__matrix__send_message`). -- A peer's matrix user is the `user_id` in `matrix_accounts` from `get_agent_meta(name)`; a peer with no matrix account is reached with `send`. -- Without matrix tools, use `send` for peers; the operator reads your `set_status` line and turn output on the dashboard, and your logs. -- `send(to: "operator")` is refused. -- Prefer ending the turn over repeatedly polling `recv` when idle. +- **Talk to peers and the operator over matrix** (`mcp__matrix__send_dm`, or `mcp__matrix__send_message` for a room). +- `send_dm` takes a peer's agent name (`iris`) or a full user id (`@iris:server`); a bare name means that agent on your own homeserver. +- An agent without matrix tools is cut off from peers on purpose; the operator reads your `set_status` line and turn output on the dashboard, and your logs. +- Prefer ending the turn over repeatedly polling for new messages when idle. Only turn-boundaries observe in-container todo wakes (bash-task completions, matrix unread, forge activity), and ending the turn is also your checkpoint. - For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time. There's no blind range-clear, only ids you've actually looked at. @@ -93,7 +91,7 @@ Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis), don't hand anyone a filesystem path — not your state dir; the operator can't open it and peers can't read your state. If it's about an issue or PR, attach it there (`hive-forge attach-issue` / `attach-comment`) or inline it in the comment. If it's a non-forge matter for the operator, send it over matrix (inline it; `send_file` for attachments). -For a peer, inline it in the matrix message (the `send` if you have no matrix tools) or put it on the relevant forge thread, then keep the message to a short headline ("cluster audit attached to the PR, headline: 3 nodes over 80% mem"). +For a peer, inline it in the matrix message or put it on the relevant forge thread, then keep the message to a short headline ("cluster audit attached to the PR, headline: 3 nodes over 80% mem"). Name any issue or PR to the operator as a full clickable URL, never a bare number. When your inbox has a message, handle it and stop. @@ -107,5 +105,5 @@ A long single turn that does step after step widens the window where a restart l For multi-step work (long builds, sequential edits) that spans more than one turn: end the turn and let an external wake drive the next one — a new inbox message, a `remind` you scheduled, or a backgrounded bash task's completion. Ending the turn is never a same-turn continuation: it's the checkpoint itself, and the only place an in-container todo wake (bash-task completion, matrix unread, forge activity) can reach you. -Polling `recv` in a loop happens _within_ the current turn instead and misses exactly those wakes, so it isn't a substitute for ending the turn. +Polling for messages in a loop happens _within_ the current turn instead and misses exactly those wakes, so it isn't a substitute for ending the turn. **Keep `set_status` current whenever the work changes** — a stale status is what actually loses context across a restart, not turn length; a status set at the start of a task and never touched again is a bug, not a shortcut. diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index a20786b4..2d88b3e8 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -994,7 +994,6 @@ async fn handle_turn( let from = first.from; let body = first.body; let redelivered = first.redelivered; - let msg_id = first.id; log_system_event(bus, &from, &body); tracing::info!(%from, %body, %redelivered, "inbox"); let unread = S::inbox_unread(socket).await; @@ -1011,14 +1010,7 @@ async fn handle_turn( // the "you were interrupted" banner, then the flag resets so a later // ordinary turn doesn't repeat a stale notice. let was_interrupted = interrupted.swap(false, std::sync::atomic::Ordering::Relaxed); - let prompt = serve_common::format_wake_prompt( - msg_id, - &from, - &body, - unread, - redelivered, - was_interrupted, - ); + let prompt = serve_common::format_wake_prompt(&from, &body, redelivered, was_interrupted); let outcome = turn::drive_turn(&prompt, files, bus, session).await; turn::emit_turn_end(bus, &outcome); bus.set_state(TurnState::Idle); diff --git a/hive-agent/src/serve_common.rs b/hive-agent/src/serve_common.rs index 7e869d39..c1d6fe8e 100644 --- a/hive-agent/src/serve_common.rs +++ b/hive-agent/src/serve_common.rs @@ -9,23 +9,13 @@ use crate::turn::{TurnError, TurnOutcome}; use crate::turn_stats::TurnStatRow; /// Assemble the per-turn wake prompt string. The role/tools/etc. live in the -/// system prompt; this is just the wake signal body. `id` is the broker row -/// id, rendered as a `[msg #]` marker so the agent can reference it in -/// `ack_until`. `unread` is the inbox depth after this message was popped. -/// `redelivered` prepends a "may already be handled" banner; `interrupted` +/// system prompt; this is just the wake signal body. `redelivered` prepends a "may already be handled" banner; `interrupted` /// prepends a "previous turn was /cancel'd" banner instead (the two are /// mutually exclusive in practice — a redelivered message means the harness /// itself restarted, which also clears the in-memory interrupted flag — so /// `redelivered` takes priority if both were somehow set). #[must_use] -pub fn format_wake_prompt( - id: i64, - from: &str, - body: &str, - unread: u64, - redelivered: bool, - interrupted: bool, -) -> String { +pub fn format_wake_prompt(from: &str, body: &str, redelivered: bool, interrupted: bool) -> String { let banner = if redelivered { hive_sh4re::inbox::REDELIVERY_HINT } else if interrupted { @@ -33,13 +23,7 @@ pub fn format_wake_prompt( } else { "" }; - let tag = if id > 0 { - format!("[msg #{id}] ") - } else { - String::new() - }; - let pending = hive_sh4re::inbox::pending_hint(unread); - format!("{banner}{tag}Incoming message from `{from}`:\n---\n{body}\n---{pending}") + format!("{banner}Incoming message from `{from}`:\n---\n{body}\n---") } /// Field-named args for [`build_row`]. Mirrors the turn-stats row diff --git a/hive-sh4re/src/inbox.rs b/hive-sh4re/src/inbox.rs index 11df71c1..2f3eeaa9 100644 --- a/hive-sh4re/src/inbox.rs +++ b/hive-sh4re/src/inbox.rs @@ -33,28 +33,6 @@ pub const REDELIVERY_HINT: &str = "[redelivered after harness restart — may al 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 { diff --git a/hive-types/src/permission.rs b/hive-types/src/permission.rs index eeb796a2..43523962 100644 --- a/hive-types/src/permission.rs +++ b/hive-types/src/permission.rs @@ -11,7 +11,10 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Permission { - /// `send`, `recv`, `ack_until` + /// Grants no tool any more: agents message each other over matrix, and + /// the hyperhive `send` / `recv` / `ack_until` tools are gone. Kept as an + /// accepted value because it is stored across the swarm (permission sets, + /// imports, agent configs). Messaging, /// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`) Meta, @@ -72,9 +75,9 @@ impl Permission { #[must_use] pub fn tools(self) -> &'static [&'static str] { match self { - Self::Messaging => &["send", "recv", "ack_until"], Self::Meta => &["get_agent_meta"], Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"], + // `Messaging` grants nothing now (agents message over matrix). // `Execution` grants the out-of-process `bash` MCP server, // gated at config-render time in `hive-agent/src/mcp_config.rs` // — an out-of-process server has no later enforcement point, so @@ -82,7 +85,8 @@ impl Permission { // Claude built-ins (`builtin_tools()`); `ManageRootAgent` grants // bind mounts; `Logs` and `Metrics` are checked by the store // gateways, not by a tool allowlist. - Self::Execution + Self::Messaging + | Self::Execution | Self::WebTools | Self::ManageRootAgent | Self::Logs