From 2413d664a192beaeddd932040a1f68180bbb5d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Fri, 15 May 2026 20:19:36 +0200 Subject: [PATCH] agents get a kickoff inbox message on start/restart/rebuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new Coordinator::kick_agent(name, reason) drops a system message into the agent's inbox so the next turn picks it up with a 'you were just (re)started, check /state/ for notes, --continue session is intact' hint. wakes the turn loop without any harness-side handling needed — it's just another inbox message with sender = 'system'. wired from: - dashboard /start /restart /rebuild handlers (via lifecycle_action's on-success tail) - manager mcp_hyperhive_start / restart dashboard: pending approvals + tombstones + questions now refresh on a 5s heartbeat when nothing else is happening. previously refresh only fired on async-form submit or on broker traffic addressed to operator — manager-queued approvals went through neither, so the operator had to reload to see them. 5s is the slow-path; 2s remains for in-flight transients. --- hive-c0re/assets/app.js | 10 +++++++--- hive-c0re/src/coordinator.rs | 22 ++++++++++++++++++++++ hive-c0re/src/dashboard.rs | 9 +++++---- hive-c0re/src/manager_server.rs | 10 ++++++++-- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 40e1590..889aedd 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -397,10 +397,14 @@ renderQuestions(s); renderInbox(s); renderApprovals(s); - // Auto-refresh while a spawn is in flight OR while any container - // has a pending lifecycle action; otherwise back off. + // Auto-refresh: fast (2s) while a spawn or a per-container + // action is in flight, otherwise heartbeat (5s) so newly-queued + // approvals from the manager show up without the operator + // having to reload the page. Broker SSE already triggers a + // refresh on operator-bound messages; this catches the rest + // (approvals, tombstones, questions). const anyPending = s.containers.some((c) => c.pending); - const next = (s.transients.length || anyPending) ? 2000 : 0; + const next = (s.transients.length || anyPending) ? 2000 : 5000; if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } if (next) pollTimer = setTimeout(refreshState, next); } catch (err) { diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index b6df8ac..93ef7f6 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -121,6 +121,28 @@ impl Coordinator { self.transient.lock().unwrap().clone() } + /// Drop a system message into the given agent's inbox. Wakes the + /// turn loop with a "you were just (re)started" hint — operator + /// caused the transition, agent picks up where it left off + /// (notes are in /state/, last turn is in --continue's session). + /// Best-effort; broker errors are logged but don't propagate. + pub fn kick_agent(&self, name: &str, reason: &str) { + let body = format!( + "{reason}\n\nYou were just (re)started by the operator. \ + If you were mid-task, check `/state/` for your notes \ + and pick up where you left off. claude's `--continue` \ + session is intact, so prior context is still in your \ + window." + ); + if let Err(e) = self.broker.send(&hive_sh4re::Message { + from: hive_sh4re::SYSTEM_SENDER.to_owned(), + to: name.to_owned(), + body, + }) { + tracing::warn!(error = ?e, %name, "kick_agent: broker.send failed"); + } + } + /// Push a `HelperEvent` into the manager's inbox. Encoded as JSON in /// `Message::body`; sender = `SYSTEM_SENDER`. The manager harness /// recognises the sender and parses the body. Best-effort: a serde or diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index cc7cef6..eed42cd 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -225,7 +225,8 @@ async fn build_container_views( if needs_update { any_stale = true; } - let needs_login = !is_manager && !claude_has_session(&Coordinator::agent_claude_dir(&logical)); + let needs_login = + !is_manager && !claude_has_session(&Coordinator::agent_claude_dir(&logical)); let pending = transient_snapshot .get(&logical) .map(|st| transient_label(st.kind)); @@ -497,7 +498,7 @@ async fn post_rebuild(State(state): State, AxumPath(name): AxumPath, AxumPath(name): AxumPath, AxumPath(name): AxumPath ManagerResponse }; } match lifecycle::start(name).await { - Ok(()) => ManagerResponse::Ok, + Ok(()) => { + coord.kick_agent(name, "container started"); + ManagerResponse::Ok + } Err(e) => ManagerResponse::Err { message: format!("{e:#}"), }, @@ -176,7 +179,10 @@ async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse }; } match lifecycle::restart(name).await { - Ok(()) => ManagerResponse::Ok, + Ok(()) => { + coord.kick_agent(name, "container restarted"); + ManagerResponse::Ok + } Err(e) => ManagerResponse::Err { message: format!("{e:#}"), },