diff --git a/hive-c0re/src/broker.rs b/hive-c0re/src/broker.rs index 0d16b1a6..e28a698e 100644 --- a/hive-c0re/src/broker.rs +++ b/hive-c0re/src/broker.rs @@ -263,6 +263,27 @@ impl Broker { Ok(u64::try_from(n.max(0)).unwrap_or(0)) } + /// Returns true when the recipient already has at least one + /// undelivered message from `sender` with exactly `body` in the + /// broker. Used by the scheduler to skip re-delivery of the same + /// scheduled prompt without blocking distinct schedules whose + /// bodies differ. + pub fn has_pending_with_body( + &self, + recipient: &str, + sender: &str, + body: &str, + ) -> Result { + let conn = self.conn.lock().unwrap(); + let n: i64 = conn.query_row( + "SELECT COUNT(*) FROM messages + WHERE recipient = ?1 AND sender = ?2 AND body = ?3 AND delivered_at IS NULL", + params![recipient, sender, body], + |row| row.get(0), + )?; + Ok(n > 0) + } + /// Long-poll variant of `recv_batch`: returns immediately if any /// row is pending (popping up to `max`); otherwise waits up to /// `timeout` for the broker to emit a `Sent { to: recipient }` @@ -1254,3 +1275,4 @@ mod tests { assert!(pop_one(broker, "bob").is_none()); } } + diff --git a/hive-c0re/src/scheduled_prompts_worker.rs b/hive-c0re/src/scheduled_prompts_worker.rs index 611a57d4..a37f363a 100644 --- a/hive-c0re/src/scheduled_prompts_worker.rs +++ b/hive-c0re/src/scheduled_prompts_worker.rs @@ -96,6 +96,31 @@ fn fire_schedule(coord: &Arc, schedule: &Schedule, now: i64) { notify_operator_missing_target(coord, schedule, target); continue; } + // Skip delivery if there is already an unread message from + // "scheduled" waiting in this target's inbox. Prevents the same + // scheduled prompt from stacking up when an agent is slow or + // briefly offline, while still allowing distinct scheduled + // messages (different body) to enqueue independently. + match coord.broker.has_pending_with_body(target, "scheduled", &schedule.body) { + Ok(true) => { + tracing::debug!( + schedule = schedule.id, + %target, + "scheduled_prompts: skipping — same body already pending for target" + ); + let _ = coord.scheduled_prompts.record_target_result( + schedule.id, + target, + now, + "skipped: already pending", + ); + continue; + } + Err(e) => { + tracing::warn!(schedule = schedule.id, %target, error = ?e, "has_pending_with_body failed"); + } + Ok(false) => {} + } let msg = Message { from: "scheduled".to_owned(), to: target.clone(), @@ -359,3 +384,4 @@ async fn known_agents_async() -> std::collections::HashSet { } out } +