From 3b8cdc7e20463b05ba4790a9956a6732c634f35c Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:07:31 +0200 Subject: [PATCH 1/9] todo: add broadcast messaging feature --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 53016100..118dd77b 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,7 @@ - Shared space for all agents to access documents/files without manager routing - Private git forge agents can push to and create new repos in - Move bind mounts in agents to `/agents//state` so path for agent = path for manager +- **Broadcast messaging**: allow sending messages with recipient "*" to all agents; deliver with hint "this was a broadcast and may not need any action from you" ## Reminder Tool From a57e500f48c2796528609f732b8ff753089a846f Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:14:17 +0200 Subject: [PATCH 2/9] todo: add multi-agent restart coordination item --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 118dd77b..d70c6f5c 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,7 @@ - Private git forge agents can push to and create new repos in - Move bind mounts in agents to `/agents//state` so path for agent = path for manager - **Broadcast messaging**: allow sending messages with recipient "*" to all agents; deliver with hint "this was a broadcast and may not need any action from you" +- **Multi-agent restart coordination**: when rebuilding all agents, manager should start first so it can coordinate post-restart confusion (notify agents, suppress unnecessary retries, etc) ## Reminder Tool From abcf7a0c417e871683437808baa9f081bf3b4ce5 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:16:13 +0200 Subject: [PATCH 3/9] implement broadcast messaging: send to '*' reaches all agents with hint --- hive-ag3nt/prompts/agent.md | 2 +- hive-ag3nt/prompts/manager.md | 2 +- hive-c0re/src/agent_server.rs | 45 ++++++++++++++++++++++++++++------- hive-c0re/src/coordinator.rs | 3 +++ 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/hive-ag3nt/prompts/agent.md b/hive-ag3nt/prompts/agent.md index 094b30d6..5ac52b39 100644 --- a/hive-ag3nt/prompts/agent.md +++ b/hive-ag3nt/prompts/agent.md @@ -3,7 +3,7 @@ You are hyperhive agent `{label}` in a multi-agent system. The operator (recipie Tools (hyperhive surface): - `mcp__hyperhive__recv(wait_seconds?)` — drain one more message from your inbox (returns `(empty)` if nothing pending). Without `wait_seconds` (or with `0`) it returns immediately — a cheap "anything pending?" peek you can sprinkle between tool calls. To **wait** for work when you have nothing else useful to do this turn, call with a long wait (e.g. `wait_seconds: 180`, the max) — incoming messages wake you instantly, otherwise the call returns empty at the timeout. That's strictly better than a fixed `sleep` shell command: lower latency on new work, no busy-loop. -- `mcp__hyperhive__send(to, body)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). Some agents have a per-agent allow-list (`hyperhive.allowedRecipients` in their `agent.nix`) — if so the tool refuses recipients outside the list with a clear error; route through the manager (`send(to: "manager", …)`) which is always reachable. +- `mcp__hyperhive__send(to, body)` — message a peer (by their name) or the operator (recipient `operator`, surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). Some agents have a per-agent allow-list (`hyperhive.allowedRecipients` in their `agent.nix`) — if so the tool refuses recipients outside the list with a clear error; route through the manager (`send(to: "manager", …)`) which is always reachable. - (some agents only) **extra MCP tools** surfaced as `mcp____` — these are agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. Treat them as first-class tools alongside the hyperhive surface; the operator already auto-approved them at deploy time. - `mcp__hyperhive__ask_operator(question, options?, multi?, ttl_seconds?)` — surface a question to the human operator on the dashboard. Returns immediately with a question id — do NOT wait inline. When the operator answers, a system message with event `operator_answered { id, question, answer }` lands in your inbox; handle it on a future turn. Use this for clarifications, permission for risky actions, or choice between options. `options` is advisory: a short fixed-choice list when applicable, otherwise leave empty for free text. `multi: true` lets the operator pick multiple (checkboxes), answer comes back comma-joined. `ttl_seconds` auto-cancels with answer `[expired]` when the decision becomes moot. diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md index 84f0b526..448a6717 100644 --- a/hive-ag3nt/prompts/manager.md +++ b/hive-ag3nt/prompts/manager.md @@ -3,7 +3,7 @@ You are the hyperhive manager `{label}` in a multi-agent system. You coordinate Tools (hyperhive surface): - `mcp__hyperhive__recv(wait_seconds?)` — drain one more message from your inbox. Without `wait_seconds` (or with `0`) it returns immediately — a cheap inbox peek you can drop between actions. To **wait** when you have nothing else to do, call with a long wait (e.g. `wait_seconds: 180`, the max) — you'll wake instantly on new work, otherwise return after the timeout. Use that instead of ending the turn or sleeping in a Bash command. -- `mcp__hyperhive__send(to, body)` — message an agent (by name), another peer, or the operator (`operator` surfaces in the dashboard). +- `mcp__hyperhive__send(to, body)` — message an agent (by name), another peer, or the operator (`operator` surfaces in the dashboard). Use `to: "*"` to broadcast to all agents (they receive a hint that it's a broadcast and may not need action). - `mcp__hyperhive__request_spawn(name)` — queue a brand-new sub-agent for operator approval (≤9 char name). - `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent. No approval required. - `mcp__hyperhive__start(name)` — start a stopped sub-agent. No approval required. diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 961de512..6a8197e9 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -102,15 +102,42 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> let broker = &coord.broker; match req { AgentRequest::Send { to, body } => { - match broker.send(&Message { - from: agent.to_owned(), - to: to.clone(), - body: body.clone(), - }) { - Ok(()) => AgentResponse::Ok, - Err(e) => AgentResponse::Err { - message: format!("{e:#}"), - }, + // Handle broadcast sends (recipient = "*") + if to == "*" { + let agents = coord.list_agents(); + let broadcast_hint = "\n\n⚠️ _hint: this was a broadcast and may not need any action from you_"; + let broadcast_body = format!("{}{}", body, broadcast_hint); + let mut errors = Vec::new(); + + for agent_name in agents { + if let Err(e) = broker.send(&Message { + from: agent.to_owned(), + to: agent_name.clone(), + body: broadcast_body.clone(), + }) { + errors.push(format!("{}: {e}", agent_name)); + } + } + + if errors.is_empty() { + AgentResponse::Ok + } else { + AgentResponse::Err { + message: format!("broadcast failed for agents: {}", errors.join(", ")), + } + } + } else { + // Normal unicast send + match broker.send(&Message { + from: agent.to_owned(), + to: to.clone(), + body: body.clone(), + }) { + Ok(()) => AgentResponse::Ok, + Err(e) => AgentResponse::Err { + message: format!("{e:#}"), + }, + } } } AgentRequest::Recv { wait_seconds } => match broker diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index ad151a88..2d347dba 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -117,6 +117,9 @@ impl Coordinator { let _ = std::fs::remove_file(&socket.path); } } + pub fn list_agents(&self) -> Vec { + self.agents.lock().unwrap().keys().cloned().collect() + } /// Mark an agent as in-progress (only one state per agent for now). pub fn set_transient(&self, name: &str, kind: TransientKind) { From 3642ae1a61dcb63390048782d3f7f4c9c02c10d7 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:40:15 +0200 Subject: [PATCH 4/9] todo: add dashboard ui for pending reminders --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index d70c6f5c..4f6aff7c 100644 --- a/TODO.md +++ b/TODO.md @@ -20,6 +20,7 @@ ## Dashboard +- **UI for pending reminders**: show pending/queued reminders in dashboard, allow operator to view/debug/cancel - Per-agent reminder status (pending, delivered) - Reminder query interface for debugging - Display reminder delivery errors (failed sends, mark failures) From 37e56af6ba35651e9489cf30abb582b2c5389e5d Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:42:41 +0200 Subject: [PATCH 5/9] add /shared mount: new shared directory accessible to all agents --- hive-c0re/src/lifecycle.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 005bb7e2..2b9656da 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -31,6 +31,11 @@ pub const CONTAINER_CLAUDE_MOUNT: &str = "/root/.claude"; /// state here; persists across destroy/recreate. pub const CONTAINER_NOTES_MOUNT: &str = "/state"; +/// Mount point of the shared directory accessible to all agents. +/// All agents can read/write here; agents should only put things they're +/// willing to lose (other agents may delete them). +pub const CONTAINER_SHARED_MOUNT: &str = "/shared"; + const GIT_NAME: &str = "c0re"; const GIT_EMAIL: &str = "c0re@hyperhive"; @@ -722,19 +727,27 @@ const HOST_APPLIED_ROOT: &str = "/var/lib/hyperhive/applied"; /// `meta::meta_dir()` but duplicated here so lifecycle stays a leaf. const HOST_META_ROOT: &str = "/var/lib/hyperhive/meta"; +/// Shared directory accessible to all agents. All agents bind-mount this RW. +const HOST_SHARED_ROOT: &str = "/var/lib/hyperhive/shared"; + fn set_nspawn_flags( container: &str, runtime_dir: &Path, claude_dir: &Path, notes_dir: &Path, ) -> Result<()> { + // Ensure /shared directory exists before binding. systemd-nspawn requires the bind source to exist. + std::fs::create_dir_all(HOST_SHARED_ROOT) + .with_context(|| format!("create {HOST_SHARED_ROOT}"))?; + let path = format!("/etc/nixos-containers/{container}.conf"); let original = std::fs::read_to_string(&path).with_context(|| format!("read {path}"))?; let mut binds = format!( - "--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{CONTAINER_CLAUDE_MOUNT} --bind={notes}:{CONTAINER_NOTES_MOUNT}", + "--bind={runtime}:{CONTAINER_RUNTIME_MOUNT} --bind={claude}:{CONTAINER_CLAUDE_MOUNT} --bind={notes}:{CONTAINER_NOTES_MOUNT} --bind={shared}:{CONTAINER_SHARED_MOUNT}", runtime = runtime_dir.display(), claude = claude_dir.display(), notes = notes_dir.display(), + shared = HOST_SHARED_ROOT, ); if container == MANAGER_NAME { use std::fmt::Write as _; From ab0df71068e77e7ebe935df65845e2cee9b090ee Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:43:05 +0200 Subject: [PATCH 6/9] docs: update system prompts to document /shared directory --- hive-ag3nt/prompts/agent.md | 2 ++ hive-ag3nt/prompts/manager.md | 1 + 2 files changed, 3 insertions(+) diff --git a/hive-ag3nt/prompts/agent.md b/hive-ag3nt/prompts/agent.md index 5ac52b39..88f48d03 100644 --- a/hive-ag3nt/prompts/agent.md +++ b/hive-ag3nt/prompts/agent.md @@ -11,6 +11,8 @@ Need new packages, env vars, or other NixOS config for yourself? You can't edit Durable knowledge: write to `/state/notes.md` (free-form) or any other path under `/state/`. That directory is bind-mounted from the host and persists across container destroy/recreate — claude's `--continue` session only carries short-term context, but `/state/` is forever. Read it back at the start of relevant turns to remember things across resets. +**Shared space**: `/shared` is accessible to all agents (read/write). Only put things here you're willing to lose — other agents may delete them. Use for explicit cross-agent communication or shared artifacts when appropriate. + Keep messages short — a few sentences each. For anything big (file listings, long diffs, transcripts, analysis): write the payload to `/state/` and `send` a short pointer ("dropped the cluster audit in /state/cluster-audit-2026-05.md, headline: 3 nodes over 80% mem"). The manager + operator can read your `/state/` from the host as `/agents/{label}/state/`. Sub-agent peers can't read each other's `/state/` directly — go through the manager if a payload needs to reach another sub-agent. When your inbox has a message, handle it and stop. Don't narrate intent — act. diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md index 448a6717..8a4d9452 100644 --- a/hive-ag3nt/prompts/manager.md +++ b/hive-ag3nt/prompts/manager.md @@ -81,6 +81,7 @@ Keep messages short — a few sentences each. For anything big (digests, agent r - To a sub-agent X: write to `/agents/X/state/` and tell them "see /state/". - To the operator: write to your own `/state/` (host path `/var/lib/hyperhive/agents/hm1nd/state/`) and tell them where to look. +- For shared artifacts (coordination, common reference data): write to `/shared/`. Only put things here you're willing to lose — other agents may delete them. A one-line headline + the file path beats a wall-of-text every time — it survives context compaction and the operator can read it in their own time. From 4ceae6cf674460eb5f7d196b3e7939a98cb9d31b Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:43:41 +0200 Subject: [PATCH 7/9] todo: add bug - pending message wake-up issue --- TODO.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TODO.md b/TODO.md index 4f6aff7c..3634237b 100644 --- a/TODO.md +++ b/TODO.md @@ -24,3 +24,7 @@ - Per-agent reminder status (pending, delivered) - Reminder query interface for debugging - Display reminder delivery errors (failed sends, mark failures) + +## Bugs + +- **Pending message wake-up**: when a message is pending and an agent turn ends without recv(), session doesn't immediately wake up again. Requires another message to trigger wake-up. (inbox/recv logic issue) From d99e0812d09ff9e2140e442ffb6cb705137ee7c9 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:48:04 +0200 Subject: [PATCH 8/9] fix: move sleep to only occur when recv returns empty, avoid message delivery delay --- hive-ag3nt/src/bin/hive-ag3nt.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 23f3c9b1..2a8d8d89 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -166,7 +166,13 @@ async fn serve( turn::emit_turn_end(&bus, &outcome); bus.set_state(TurnState::Idle); } - Ok(AgentResponse::Empty) => {} + Ok(AgentResponse::Empty) => { + // Idle: brief sleep before next poll to avoid busy-looping + // on consecutive Empty responses. The recv() call already + // waits up to 180s for messages, so this is just for + // responsiveness if recv() times out. + tokio::time::sleep(interval).await; + } Ok(AgentResponse::Ok | AgentResponse::Status { .. } | AgentResponse::Recent { .. } @@ -180,7 +186,6 @@ async fn serve( tracing::warn!(error = ?e, "recv failed; retrying"); } } - tokio::time::sleep(interval).await; } } From 3d2a7ffec7cf927677620973a42741d25b070650 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 13:50:11 +0200 Subject: [PATCH 9/9] fix: auto-wake after turn if pending messages exist, don't block on recv --- hive-ag3nt/src/bin/hive-ag3nt.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 2a8d8d89..65dd0a18 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -165,6 +165,15 @@ async fn serve( let outcome = turn::drive_turn(&prompt, files, &bus).await; turn::emit_turn_end(&bus, &outcome); bus.set_state(TurnState::Idle); + + // After turn completes, check if there are pending messages waiting. + // If so, immediately process them instead of blocking on recv(). + // This ensures messages queued during the turn are processed ASAP. + let pending = inbox_unread(socket).await; + if pending > 0 { + tracing::info!(%pending, "pending messages after turn; fetching next"); + continue; // Loop back to recv() immediately instead of sleeping + } } Ok(AgentResponse::Empty) => { // Idle: brief sleep before next poll to avoid busy-looping