From f33a1ed660d1c19d0012c18a18ce1fe1288e0e8b Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 4 Oct 2026 18:02:18 +0200 Subject: [PATCH] hive-agent: key the boot-announce todo so it dedups across boots --- hive-agent/src/boot_announce.rs | 55 +++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/hive-agent/src/boot_announce.rs b/hive-agent/src/boot_announce.rs index 65a8f9ff..53d64f85 100644 --- a/hive-agent/src/boot_announce.rs +++ b/hive-agent/src/boot_announce.rs @@ -18,11 +18,16 @@ //! (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. +//! 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; @@ -31,6 +36,10 @@ 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) { @@ -40,8 +49,42 @@ pub fn announce(label: &str, todos: &Todos, wake: &Notify) { off — claude's `--continue` session is intact, so prior context is \ still in your window." ); - match todos.upsert(SUBSYSTEM, None, &summary, None, false) { + match todos.upsert(SUBSYSTEM, Some(KEY), &summary, None, true) { Ok(_) => wake.notify_one(), Err(e) => tracing::warn!(error = ?e, "boot_announce: todo upsert failed"), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[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); + } +}