Compare commits

...
2 changed files with 48 additions and 0 deletions

View file

@ -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<bool> {
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());
}
}

View file

@ -96,6 +96,31 @@ fn fire_schedule(coord: &Arc<Coordinator>, 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<String> {
}
out
}