use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use uuid::Uuid;
use super::session_routes::{Delivery, deliver_to_session, turn_probe};
use crate::brain::agent::QueuedUserMessage;
pub const POLL_INTERVAL: Duration = Duration::from_millis(500);
pub struct DeferredNotify {
pub msg: QueuedUserMessage,
pub quiet_for: Duration,
pub max_delay: Duration,
pub created_at: Instant,
}
struct TargetState {
last_busy: Instant,
entries: HashMap<Uuid, DeferredNotify>,
}
fn registry() -> &'static Mutex<HashMap<Uuid, TargetState>> {
static CELL: OnceLock<Mutex<HashMap<Uuid, TargetState>>> = OnceLock::new();
CELL.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn defer_quiet(
target: Uuid,
msg: QueuedUserMessage,
quiet_for: Duration,
max_delay: Duration,
) -> Uuid {
let id = Uuid::new_v4();
let entry = DeferredNotify {
msg,
quiet_for,
max_delay,
created_at: Instant::now(),
};
if let Ok(mut guard) = registry().lock() {
let state = guard.entry(target).or_insert_with(|| TargetState {
last_busy: Instant::now(),
entries: HashMap::new(),
});
state.entries.insert(id, entry);
}
tokio::spawn(watch(id, target, POLL_INTERVAL));
id
}
#[cfg_attr(not(test), expect(dead_code))]
pub fn cancel_deferred(id: Uuid) -> bool {
let Ok(mut guard) = registry().lock() else {
return false;
};
for state in guard.values_mut() {
if state.entries.remove(&id).is_some() {
guard.retain(|_, s| !s.entries.is_empty());
return true;
}
}
false
}
pub fn is_due(
mid_turn: bool,
quiet_elapsed: Duration,
total_elapsed: Duration,
quiet_for: Duration,
max_delay: Duration,
) -> bool {
if total_elapsed >= max_delay {
return true;
}
!mid_turn && quiet_elapsed >= quiet_for
}
async fn watch(id: Uuid, target: Uuid, poll: Duration) {
loop {
tokio::time::sleep(poll).await;
let now = Instant::now();
let mid_turn = turn_probe(target).is_some_and(|probe| probe());
let Some(batch) = sweep(id, target, now, mid_turn) else {
continue; };
release_batch(target, batch, mid_turn);
return;
}
}
fn sweep(
firing_id: Uuid,
target: Uuid,
now: Instant,
mid_turn: bool,
) -> Option<Vec<(Uuid, DeferredNotify)>> {
let mut batch: Vec<(Uuid, DeferredNotify)> = Vec::new();
let fired = {
let Ok(mut guard) = registry().lock() else {
return None;
};
let Some(state) = guard.get_mut(&target) else {
return Some(Vec::new()); };
if mid_turn {
state.last_busy = now;
}
let quiet_elapsed = now.duration_since(state.last_busy);
let due = |e: &DeferredNotify| {
is_due(
mid_turn,
quiet_elapsed,
now.duration_since(e.created_at),
e.quiet_for,
e.max_delay,
)
};
if let Some(entry) = state.entries.get(&firing_id)
&& !due(entry)
{
return None;
}
let ids: Vec<Uuid> = state
.entries
.iter()
.filter(|(eid, e)| *eid == &firing_id || due(e))
.map(|(eid, _)| *eid)
.collect();
for eid in ids {
if let Some(entry) = state.entries.remove(&eid) {
batch.push((eid, entry));
}
}
if state.entries.is_empty() {
guard.remove(&target);
}
!batch.is_empty()
};
fired.then_some(batch)
}
fn release_batch(target: Uuid, batch: Vec<(Uuid, DeferredNotify)>, mid_turn: bool) {
for (idx, (id, entry)) in batch.into_iter().enumerate() {
let interrupt = mid_turn || idx > 0;
let outcome = deliver_to_session(target, entry.msg, interrupt);
tracing::info!(
target: "quiet_delivery",
id = %id,
session = %target,
position = idx,
interrupt,
quiet_for_secs = entry.quiet_for.as_secs_f64(),
max_delay_secs = entry.max_delay.as_secs_f64(),
waited_secs = entry.created_at.elapsed().as_secs_f64(),
"quiet batch release: {}",
outcome_state(&outcome)
);
}
}
fn outcome_state(delivery: &Delivery) -> &'static str {
match delivery {
Delivery::Delivered => "delivered",
Delivery::Parked => "queued",
Delivery::Redirected { .. } => "redirected",
Delivery::RefusedInFlight { .. } | Delivery::NoRoute => "refused",
}
}