//! 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). //! //! Keyed on a constant `"boot"` key, not keyless: the store (`Todos`, //! backed by `paths::state_db()`, a sqlite file) outlives the process, so //! a keyless insert would never dedup across boots — N harness starts //! while the agent hasn't acked the previous row would leave N identical //! rows, exactly the restart-storm this module replaces. With a constant //! key and the summary text constant too, `Todos::upsert`'s content- //! unchanged path makes a boot while the previous notice is still pending //! a no-op (no second wake), and `reopen_if_acked = true` makes a boot //! after the agent acked it re-open the same row and wake again. One row, //! ever. use tokio::sync::Notify; use crate::todos::Todos; /// Producing subsystem marker for the boot-announce todo row. const SUBSYSTEM: &str = "harness"; /// Dedup key for the boot-announce row — constant, so every boot targets /// the same row instead of inserting a fresh one. See module doc. const KEY: &str = "boot"; /// 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, Some(KEY), &summary, None, true) { Ok((_, true)) => wake.notify_one(), Ok((_, false)) => {} Err(e) => tracing::warn!(error = ?e, "boot_announce: todo upsert failed"), } } #[cfg(test)] mod tests { use super::*; /// Drains any pending permit so the next check starts from "no wake /// armed" — `notify_one` coalesces multiple calls into a single /// stored permit, so an un-drained leftover from a prior step would /// make this step's check pass regardless of whether `announce` wove /// in a fresh one. fn wake_fired(wake: &Notify) -> bool { use futures_util::FutureExt as _; wake.notified().now_or_never().is_some() } #[test] fn two_boots_without_an_ack_leave_one_row() { let dir = tempfile::tempdir().expect("tempdir"); let todos = Todos::open(&dir.path().join("state.sqlite")).expect("open"); let wake = Notify::new(); announce("a", &todos, &wake); let rows = todos.list(Some(SUBSYSTEM)).expect("list"); assert_eq!(rows.len(), 1); let id = rows[0].id; // A second boot before the first notice is acked: same row, no // duplicate. announce("a", &todos, &wake); let rows = todos.list(Some(SUBSYSTEM)).expect("list"); assert_eq!(rows.len(), 1); assert_eq!(rows[0].id, id); // Ack it, then boot again: the row re-opens (still the same row, // un-acked again), not a second insert. todos.mark_done(id).expect("mark_done"); assert!(todos.list(Some(SUBSYSTEM)).expect("list").is_empty()); announce("a", &todos, &wake); let rows = todos.list(Some(SUBSYSTEM)).expect("list"); assert_eq!(rows.len(), 1); assert_eq!(rows[0].id, id); } #[test] fn only_a_changed_upsert_wakes() { let dir = tempfile::tempdir().expect("tempdir"); let todos = Todos::open(&dir.path().join("state.sqlite")).expect("open"); let wake = Notify::new(); // First boot: fresh row, must wake. announce("a", &todos, &wake); assert!(wake_fired(&wake), "first boot wakes"); // Second boot before any ack: same row, unchanged summary — must // NOT wake. This is the restart-storm case: a boot while the // notice is still pending costs nothing extra. announce("a", &todos, &wake); assert!( !wake_fired(&wake), "repeat boot with the row still open must not wake" ); // Ack it, then boot again: the row reopens — must wake again. let id = todos.list(Some(SUBSYSTEM)).expect("list")[0].id; todos.mark_done(id).expect("mark_done"); announce("a", &todos, &wake); assert!( wake_fired(&wake), "boot after an ack reopens the row and wakes" ); } }