hive-agent: self-announce boot instead of host-side kick_agent
This commit is contained in:
parent
55a67e38fa
commit
cd61ea4a37
4 changed files with 79 additions and 9 deletions
|
|
@ -222,11 +222,19 @@ to ack), plus an inline `({unread} more pending — drain via …)` hint
|
||||||
when `unread > 0`. Claude drives any further `recv`/`send` itself via
|
when `unread > 0`. Claude drives any further `recv`/`send` itself via
|
||||||
the embedded MCP server.
|
the embedded MCP server.
|
||||||
|
|
||||||
Whenever hive-c0re starts / restarts / rebuilds a container, it
|
`hive-agent` self-announces once on every boot:
|
||||||
also drops a `system` message into the agent's inbox via
|
`boot_announce::announce` pushes a one-line "you were just
|
||||||
`Coordinator::kick_agent` — a one-line "you were just (re)started,
|
(re)started, check /state/ for your notes, your session is intact"
|
||||||
check /state/ for your notes, your session is intact." The
|
todo the moment the in-agent todo store opens, before the serve loop
|
||||||
next turn picks it up like any other inbox message.
|
starts. This replaced `hive-c0re`'s old `Coordinator::kick_agent`
|
||||||
|
calls at the tail of the rebuild and start DAG nodes — those fired a
|
||||||
|
broker message per DAG node, so one rebuild (always a real
|
||||||
|
`StopForUpdate` → `Swap` → `RebuildBookkeeping` → `Reconcile` →
|
||||||
|
`Start` cycle) queued two separate restart messages for a single
|
||||||
|
actual boot. The harness itself is the one side that knows for
|
||||||
|
certain whether a boot happened, so it dedups for free: exactly one
|
||||||
|
todo per real process start, surfaced the normal todo-wake way
|
||||||
|
rather than as an inbox message.
|
||||||
|
|
||||||
## On-boot files
|
## On-boot files
|
||||||
|
|
||||||
|
|
|
||||||
47
hive-agent/src/boot_announce.rs
Normal file
47
hive-agent/src/boot_announce.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
//! One-shot startup self-announce: pushes a todo on every harness boot so
|
||||||
|
//! the agent gets a "you were just (re)started" notice without the host
|
||||||
|
//! having to send one.
|
||||||
|
//!
|
||||||
|
//! Replaces `hive-c0re`'s old `Coordinator::kick_agent` calls at the tail
|
||||||
|
//! of the rebuild and start DAG nodes, which an operator reported as
|
||||||
|
//! duplicate restart messages: those fired one broker message per DAG
|
||||||
|
//! node, so a single rebuild — always a real
|
||||||
|
//! `StopForUpdate` → `Swap` → `RebuildBookkeeping` → `Reconcile` → `Start`
|
||||||
|
//! cycle — queued *two* separate restart messages for one actual boot, and
|
||||||
|
//! an agent paused for days could rack up dozens more from repeated
|
||||||
|
//! host-side sweeps. The harness itself has perfect knowledge of "did a
|
||||||
|
//! boot actually happen": it only runs this once, at the top of
|
||||||
|
//! `serve_main`. Self-announcing here dedups for free — one todo per real
|
||||||
|
//! process start, no host-side bookkeeping needed.
|
||||||
|
//!
|
||||||
|
//! No first-spawn-vs-restart distinction: every boot gets the same notice
|
||||||
|
//! (the old first-spawn message didn't reliably fire anyway, so unifying
|
||||||
|
//! the two loses nothing).
|
||||||
|
//!
|
||||||
|
//! Keyless on purpose: each boot is a genuinely new occurrence, not a
|
||||||
|
//! reconciled condition like `disk_watch`'s, so there's nothing to dedup
|
||||||
|
//! *against* within one store — `Todos::upsert`'s keyless path always
|
||||||
|
//! inserts a fresh row and always reports `changed`, which is exactly
|
||||||
|
//! "wake me, this happened" with no extra bookkeeping.
|
||||||
|
|
||||||
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
|
use crate::todos::Todos;
|
||||||
|
|
||||||
|
/// Producing subsystem marker for the boot-announce todo row.
|
||||||
|
const SUBSYSTEM: &str = "harness";
|
||||||
|
|
||||||
|
/// Push the "you were just (re)started" todo and wake the turn loop.
|
||||||
|
/// Best-effort: a store error is logged, never fatal to boot.
|
||||||
|
pub fn announce(label: &str, todos: &Todos, wake: &Notify) {
|
||||||
|
let summary = format!(
|
||||||
|
"You were just (re)started. If you were mid-task, check \
|
||||||
|
`/agents/{label}/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."
|
||||||
|
);
|
||||||
|
match todos.upsert(SUBSYSTEM, None, &summary, None, false) {
|
||||||
|
Ok(_) => wake.notify_one(),
|
||||||
|
Err(e) => tracing::warn!(error = ?e, "boot_announce: todo upsert failed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
//! Single bin crate: the module tree below (formerly this crate's `lib.rs`,
|
//! Single bin crate: the module tree below (formerly this crate's `lib.rs`,
|
||||||
//! before lib + bin were collapsed into one) plus the serve loop.
|
//! before lib + bin were collapsed into one) plus the serve loop.
|
||||||
|
|
||||||
|
mod boot_announce;
|
||||||
mod claude_md_watch;
|
mod claude_md_watch;
|
||||||
mod claude_usage_watch;
|
mod claude_usage_watch;
|
||||||
mod db_migrate;
|
mod db_migrate;
|
||||||
|
|
@ -613,6 +614,12 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
|
||||||
};
|
};
|
||||||
tokio::spawn(reminder_timer::run(reminder_store.clone(), local_msg_tx));
|
tokio::spawn(reminder_timer::run(reminder_store.clone(), local_msg_tx));
|
||||||
let (todo_wake, todos_store) = spawn_todo_socket(reminder_store.clone(), &bus);
|
let (todo_wake, todos_store) = spawn_todo_socket(reminder_store.clone(), &bus);
|
||||||
|
// Self-announce this boot as a todo instead of relying on hive-c0re's
|
||||||
|
// old `kick_agent` broker messages — see `boot_announce`'s module doc
|
||||||
|
// for why this is strictly better than a host-side kick.
|
||||||
|
if let Some(store) = &todos_store {
|
||||||
|
boot_announce::announce(&label, store, &todo_wake);
|
||||||
|
}
|
||||||
if matches!(initial, LoginState::NeedsLogin) {
|
if matches!(initial, LoginState::NeedsLogin) {
|
||||||
login::wait_for_login(
|
login::wait_for_login(
|
||||||
&claude_dir,
|
&claude_dir,
|
||||||
|
|
|
||||||
|
|
@ -513,10 +513,16 @@ async fn run_rebuild_bookkeeping(coord: &Arc<Coordinator>, name: &str) -> Result
|
||||||
// access all recover without a hive-c0re restart. (No matrix step: the
|
// access all recover without a hive-c0re restart. (No matrix step: the
|
||||||
// swarm mints an agent's matrix account, not this hive.)
|
// swarm mints an agent's matrix account, not this hive.)
|
||||||
crate::forge::sync_agent(name, crate::forge::core_token().as_deref()).await;
|
crate::forge::sync_agent(name, crate::forge::core_token().as_deref()).await;
|
||||||
// Wake the agent on its next turn so claude sees a "you were rebuilt"
|
// No `kick_agent` here any more — reporting from `hive-agent`'s own
|
||||||
// hint; rescan so dashboards drop the "needs update" chip; lock bump →
|
// boot instead avoids the duplicate-restart-message problem this used
|
||||||
|
// to cause: the container is still stopped at this point (`swap_update` requires it stopped and
|
||||||
|
// never restarts it — the DAG's tail `Reconcile`/`Start` does), so a
|
||||||
|
// host-side "you were rebuilt" broker message would just queue
|
||||||
|
// alongside `run_start`'s own kick for the same boot. `hive-agent` now
|
||||||
|
// self-announces once on its own startup instead — one message per
|
||||||
|
// actual boot, not one per DAG node that happens to run during it.
|
||||||
|
// Rescan so dashboards drop the "needs update" chip; lock bump →
|
||||||
// meta-inputs re-render.
|
// meta-inputs re-render.
|
||||||
coord.kick_agent(name, "container rebuilt");
|
|
||||||
coord.rescan_containers_and_emit().await;
|
coord.rescan_containers_and_emit().await;
|
||||||
crate::dashboard::emit_meta_inputs_snapshot(coord);
|
crate::dashboard::emit_meta_inputs_snapshot(coord);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -674,7 +680,9 @@ async fn run_start(coord: &Arc<Coordinator>, name: &str) -> Result<()> {
|
||||||
// (no background poll) — c0re owns the listener lifecycle, so
|
// (no background poll) — c0re owns the listener lifecycle, so
|
||||||
// register here rather than waiting for a sweep.
|
// register here rather than waiting for a sweep.
|
||||||
coord.register_agent(name)?;
|
coord.register_agent(name)?;
|
||||||
coord.kick_agent(name, "container started");
|
// No `kick_agent` here any more — `hive-agent` pushes its own "you
|
||||||
|
// were just (re)started" todo on boot now, so this node doesn't need
|
||||||
|
// to tell it. See the matching comment in `run_rebuild_bookkeeping`.
|
||||||
coord.rescan_containers_and_emit().await;
|
coord.rescan_containers_and_emit().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue