From caa495aeda50f5c74cb81bbdfbba4dff609271dc Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 02:28:21 +0200 Subject: [PATCH 1/7] mcp: wire extra server allowedTools into --allowedTools arg --- hive-ag3nt/src/mcp.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 14c23118..3220f37c 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -544,6 +544,8 @@ impl ManagerServer { )] impl ServerHandler for ManagerServer {} + + /// Name of the hyperhive MCP server inside claude's view. Claude prefixes /// tools as `mcp____` (e.g. `mcp__hyperhive__send`). pub const SERVER_NAME: &str = "hyperhive"; @@ -606,7 +608,9 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec { } /// Combined allow-list passed to `--allowedTools` (auto-approve) — covers -/// both the built-ins and the MCP surface. +/// the built-ins, the hyperhive MCP surface, and any extra MCP servers. +/// Extra server tools are read from the same `/etc/hyperhive/extra-mcp.json` +/// file that `render_claude_config` uses, so the two are always in sync. #[must_use] pub fn allowed_tools_arg(flavor: Flavor) -> String { let mut all: Vec = ALLOWED_BUILTIN_TOOLS @@ -614,6 +618,18 @@ pub fn allowed_tools_arg(flavor: Flavor) -> String { .map(|s| (*s).to_owned()) .collect(); all.extend(allowed_mcp_tools(flavor)); + for (name, spec) in load_extra_mcp() { + if name == SERVER_NAME { + continue; // already covered above + } + for tool in &spec.allowed_tools { + if tool == "*" { + all.push(format!("mcp__{name}__*")); + } else { + all.push(format!("mcp__{name}__{tool}")); + } + } + } all.join(",") } From 286da8980eeddfea73749c8aab6dafa528c4ff98 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 02:31:26 +0200 Subject: [PATCH 2/7] Revert "mcp: wire extra server allowedTools into --allowedTools arg" This reverts commit e0b18ff3c2ec5a7f771ab9a1a247ff4a24a8c475. --- hive-ag3nt/src/mcp.rs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 3220f37c..14c23118 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -544,8 +544,6 @@ impl ManagerServer { )] impl ServerHandler for ManagerServer {} - - /// Name of the hyperhive MCP server inside claude's view. Claude prefixes /// tools as `mcp____` (e.g. `mcp__hyperhive__send`). pub const SERVER_NAME: &str = "hyperhive"; @@ -608,9 +606,7 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec { } /// Combined allow-list passed to `--allowedTools` (auto-approve) — covers -/// the built-ins, the hyperhive MCP surface, and any extra MCP servers. -/// Extra server tools are read from the same `/etc/hyperhive/extra-mcp.json` -/// file that `render_claude_config` uses, so the two are always in sync. +/// both the built-ins and the MCP surface. #[must_use] pub fn allowed_tools_arg(flavor: Flavor) -> String { let mut all: Vec = ALLOWED_BUILTIN_TOOLS @@ -618,18 +614,6 @@ pub fn allowed_tools_arg(flavor: Flavor) -> String { .map(|s| (*s).to_owned()) .collect(); all.extend(allowed_mcp_tools(flavor)); - for (name, spec) in load_extra_mcp() { - if name == SERVER_NAME { - continue; // already covered above - } - for tool in &spec.allowed_tools { - if tool == "*" { - all.push(format!("mcp__{name}__*")); - } else { - all.push(format!("mcp__{name}__{tool}")); - } - } - } all.join(",") } From f0e87f0bc59faea4ce41fb21e00d941fa18f1034 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 03:36:16 +0200 Subject: [PATCH 3/7] agent: add Wake command - co-process self-wake via agent socket --- hive-c0re/src/agent_server.rs | 15 +++++++++++++++ hive-sh4re/src/lib.rs | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 564b150b..3e6d574c 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -188,5 +188,20 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> }, } } + AgentRequest::Wake { from, body } => { + // Deliver a message to this agent from a co-process (e.g. a + // Matrix daemon). Identical to `Send { to: agent }` but the + // recipient is always the owning agent — no routing needed. + match broker.send(&Message { + from: from.clone(), + to: agent.to_owned(), + body: body.clone(), + }) { + Ok(()) => AgentResponse::Ok, + Err(e) => AgentResponse::Err { + message: format!("{e:#}"), + }, + } + } } } diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 2035fe4c..f0b330bb 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -218,6 +218,13 @@ pub enum AgentRequest { #[serde(default)] ttl_seconds: Option, }, + /// Deliver a message TO this agent from a co-process (e.g. a Matrix + /// daemon running in the same container). `from` identifies the sender + /// (e.g. `"matrix"`); `body` is a short summary. Unlike `Send`, the + /// recipient is always the agent that owns this socket — no `to` field. + /// Wakes whatever `Recv` the harness is parked on, triggering the next + /// claude turn. + Wake { from: String, body: String }, } /// Responses on a per-agent socket. From 862bc1de44afbf1acfd8d7904cfd90d3df04017d Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 03:37:45 +0200 Subject: [PATCH 4/7] Revert "agent: add Wake command - co-process self-wake via agent socket" This reverts commit 68a9b8575b1647643c87bd753767acabf96528c3. --- hive-c0re/src/agent_server.rs | 15 --------------- hive-sh4re/src/lib.rs | 7 ------- 2 files changed, 22 deletions(-) diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 3e6d574c..564b150b 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -188,20 +188,5 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> }, } } - AgentRequest::Wake { from, body } => { - // Deliver a message to this agent from a co-process (e.g. a - // Matrix daemon). Identical to `Send { to: agent }` but the - // recipient is always the owning agent — no routing needed. - match broker.send(&Message { - from: from.clone(), - to: agent.to_owned(), - body: body.clone(), - }) { - Ok(()) => AgentResponse::Ok, - Err(e) => AgentResponse::Err { - message: format!("{e:#}"), - }, - } - } } } diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index f0b330bb..2035fe4c 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -218,13 +218,6 @@ pub enum AgentRequest { #[serde(default)] ttl_seconds: Option, }, - /// Deliver a message TO this agent from a co-process (e.g. a Matrix - /// daemon running in the same container). `from` identifies the sender - /// (e.g. `"matrix"`); `body` is a short summary. Unlike `Send`, the - /// recipient is always the agent that owns this socket — no `to` field. - /// Wakes whatever `Recv` the harness is parked on, triggering the next - /// claude turn. - Wake { from: String, body: String }, } /// Responses on a per-agent socket. From 7e9fd8e978ce69a2c79c0a6a310be137f250c651 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 12:39:35 +0200 Subject: [PATCH 5/7] agent: add Remind request + ReminderTiming enum (stub implementation) --- hive-c0re/src/agent_server.rs | 11 +++++++++++ hive-sh4re/src/lib.rs | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 564b150b..f77f9538 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -188,5 +188,16 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> }, } } + AgentRequest::Remind { + message, + timing, + file_path, + } => { + // TODO: submit to reminder scheduler + // For now, return a stub response + AgentResponse::Err { + message: "remind not yet implemented".to_owned(), + } + } } } diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 2035fe4c..ed810516 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -169,6 +169,17 @@ pub struct InboxRow { pub at: 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 }, +} + /// Requests on a per-agent socket. The agent's identity is the socket /// it came in on; `Send.from` is filled in by the server, not the client. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -218,6 +229,18 @@ pub enum AgentRequest { #[serde(default)] ttl_seconds: Option, }, + /// Schedule a reminder message to be delivered to this agent at a + /// future time. The reminder lands in the agent's inbox as an auto-sent + /// message from `"reminder"`. Use for agent follow-ups (e.g. check task + /// status, retry failed operation). Message length is limited; pass + /// `file_path` to store in a file and get a path-reference message + /// instead. + Remind { + message: String, + timing: ReminderTiming, + #[serde(default)] + file_path: Option, + }, } /// Responses on a per-agent socket. From 4fc9c029341dabe735eb70c6231c7b8075c2c3fb Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 12:40:38 +0200 Subject: [PATCH 6/7] reminder: add sqlite storage + broker methods + dispatch --- hive-c0re/src/agent_server.rs | 26 +++++++++++++--- hive-c0re/src/broker.rs | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index f77f9538..a4cad214 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -193,10 +193,28 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> timing, file_path, } => { - // TODO: submit to reminder scheduler - // For now, return a stub response - AgentResponse::Err { - message: "remind not yet implemented".to_owned(), + use hive_sh4re::ReminderTiming; + let due_at = match timing { + ReminderTiming::InSeconds { seconds } => { + std::time::SystemTime::now() + .checked_add(std::time::Duration::from_secs(*seconds)) + .and_then(|t| { + t.duration_since(std::time::UNIX_EPOCH) + .ok() + .and_then(|d| i64::try_from(d.as_secs()).ok()) + }) + .unwrap_or(0) + } + ReminderTiming::At { unix_timestamp } => *unix_timestamp, + }; + match broker.store_reminder(agent, message, file_path.as_deref(), due_at) { + Ok(id) => { + tracing::info!(%id, %agent, %due_at, "reminder scheduled"); + AgentResponse::Ok + } + Err(e) => AgentResponse::Err { + message: format!("failed to store reminder: {e:#}"), + }, } } } diff --git a/hive-c0re/src/broker.rs b/hive-c0re/src/broker.rs index 72486bad..20773263 100644 --- a/hive-c0re/src/broker.rs +++ b/hive-c0re/src/broker.rs @@ -22,6 +22,18 @@ CREATE TABLE IF NOT EXISTS messages ( ); CREATE INDEX IF NOT EXISTS idx_messages_undelivered ON messages (recipient, id) WHERE delivered_at IS NULL; + +CREATE TABLE IF NOT EXISTS reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + agent TEXT NOT NULL, + message TEXT NOT NULL, + file_path TEXT, + due_at INTEGER NOT NULL, + created_at INTEGER NOT NULL, + sent_at INTEGER +); +CREATE INDEX IF NOT EXISTS idx_reminders_due + ON reminders (agent, due_at) WHERE sent_at IS NULL; "; /// Capacity of the live event channel. Slow subscribers (e.g. an idle browser) @@ -205,6 +217,51 @@ impl Broker { }); Ok(Some(Message { from, to, body })) } + + /// Store a new reminder. Returns the reminder id. + pub fn store_reminder( + &self, + agent: &str, + message: &str, + file_path: Option<&str>, + due_at: i64, + ) -> Result { + let conn = self.conn.lock().unwrap(); + conn.execute( + "INSERT INTO reminders (agent, message, file_path, due_at, created_at) VALUES (?1, ?2, ?3, ?4, ?5)", + params![agent, message, file_path, due_at, now_unix()], + )?; + let id = conn.last_insert_rowid(); + Ok(id) + } + + /// Get all reminders for an agent that are due now or in the past. + /// Returns (id, message, file_path) tuples. + pub fn get_due_reminders(&self, agent: &str) -> Result)>> { + let conn = self.conn.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT id, message, file_path FROM reminders WHERE agent = ?1 AND due_at <= ?2 AND sent_at IS NULL ORDER BY due_at ASC" + )?; + let rows = stmt.query_map(params![agent, now_unix()], |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, String>(1)?, + row.get::<_, Option>(2)?, + )) + })?; + rows.collect::>>() + .context("query reminders") + } + + /// Mark a reminder as sent (delivered). + pub fn mark_reminder_sent(&self, id: i64) -> Result<()> { + let conn = self.conn.lock().unwrap(); + conn.execute( + "UPDATE reminders SET sent_at = ?1 WHERE id = ?2", + params![now_unix(), id], + )?; + Ok(()) + } } fn now_unix() -> i64 { From f38510930a77425d804c66bef71e833c5e77dbcb Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 16 May 2026 12:42:05 +0200 Subject: [PATCH 7/7] reminder: add background scheduler loop - checks & delivers due reminders every 5s --- hive-c0re/src/coordinator.rs | 4 ++++ hive-c0re/src/main.rs | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index ad151a88..6f348cff 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -118,6 +118,10 @@ impl Coordinator { } } + 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) { self.transient.lock().unwrap().insert( diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 663f0ad4..5908b7b6 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -157,6 +157,52 @@ async fn main() -> Result<()> { // when a previously-running container goes away without an // operator-initiated transient state. crash_watch::spawn(coord.clone()); + // Reminder scheduler: checks for due reminders every 5 seconds, + // delivers them as inbox messages from "reminder". + let reminder_coord = coord.clone(); + tokio::spawn(async move { + use hive_sh4re::Message; + loop { + // Get all agents currently registered + let agents = reminder_coord.list_agents(); + for agent in agents { + match reminder_coord.broker.get_due_reminders(&agent) { + Ok(reminders) => { + for (id, message, _file_path) in reminders { + // Deliver as inbox message from "reminder" + if let Err(e) = reminder_coord.broker.send(&Message { + from: "reminder".to_owned(), + to: agent.clone(), + body: message.clone(), + }) { + tracing::warn!( + reminder_id = id, + %agent, + error = ?e, + "failed to deliver reminder" + ); + continue; + } + // Mark as sent + if let Err(e) = reminder_coord.broker.mark_reminder_sent(id) { + tracing::warn!( + reminder_id = id, + error = ?e, + "failed to mark reminder sent" + ); + } + } + } + Err(e) => tracing::warn!( + %agent, + error = ?e, + "failed to query due reminders" + ), + } + } + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + }); let dash_coord = coord.clone(); tokio::spawn(async move { if let Err(e) = dashboard::serve(dashboard_port, dash_coord).await {