use std::collections::VecDeque;
use chrono::{DateTime, Utc};
use crate::core::agent::DelegationStatus;
use crate::core::session::SessionId;
pub(crate) const PENDING_STOP_CAP: usize = 256;
pub(crate) const PENDING_STOP_TTL_SECS: i64 = 15 * 60;
#[derive(Debug, Clone)]
struct PendingStop {
session: SessionId,
agent_id: String,
status: DelegationStatus,
seen_at: DateTime<Utc>,
}
#[derive(Debug, Default)]
pub struct PendingStops {
entries: parking_lot::Mutex<VecDeque<PendingStop>>,
}
impl PendingStops {
pub(crate) fn record(
&self,
session: SessionId,
agent_id: &str,
status: DelegationStatus,
now: DateTime<Utc>,
) {
let mut entries = self.entries.lock();
entries.retain(|e| !(e.session == session && e.agent_id == agent_id));
entries.push_back(PendingStop {
session,
agent_id: agent_id.to_string(),
status,
seen_at: now,
});
while entries.len() > PENDING_STOP_CAP {
if let Some(dropped) = entries.pop_front() {
tracing::warn!(
agent_id = dropped.agent_id,
cap = PENDING_STOP_CAP,
"delegation: dropped the oldest unresolved SubagentStop to keep the \
ledger bounded (#4142); its delegation now relies on the staleness \
sweep, exactly as it did before recovery existed"
);
}
}
}
pub(crate) fn peek(&self, session: SessionId, agent_id: &str) -> Option<DelegationStatus> {
self.entries
.lock()
.iter()
.find(|e| e.session == session && e.agent_id == agent_id)
.map(|e| e.status)
}
pub(crate) fn clear(&self, session: SessionId, agent_id: &str) {
self.entries
.lock()
.retain(|e| !(e.session == session && e.agent_id == agent_id));
}
pub(crate) fn prune_at(&self, now: DateTime<Utc>) -> usize {
let ttl = chrono::Duration::seconds(PENDING_STOP_TTL_SECS);
let mut entries = self.entries.lock();
let before = entries.len();
entries.retain(|e| now - e.seen_at <= ttl);
before - entries.len()
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.entries.lock().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sid() -> SessionId {
SessionId::new()
}
#[test]
fn a_recorded_stop_is_visible() {
let led = PendingStops::default();
let s = sid();
led.record(s, "aid1", DelegationStatus::Completed, Utc::now());
assert_eq!(led.peek(s, "aid1"), Some(DelegationStatus::Completed));
assert_eq!(led.peek(s, "aid2"), None, "another agent must not match");
assert_eq!(
led.peek(sid(), "aid1"),
None,
"another session must not match"
);
}
#[test]
fn a_failure_stop_keeps_its_status() {
let led = PendingStops::default();
let s = sid();
led.record(s, "aid1", DelegationStatus::Failed, Utc::now());
assert_eq!(led.peek(s, "aid1"), Some(DelegationStatus::Failed));
}
#[test]
fn re_recording_a_key_replaces_it() {
let led = PendingStops::default();
let s = sid();
led.record(s, "aid1", DelegationStatus::Completed, Utc::now());
led.record(s, "aid1", DelegationStatus::Failed, Utc::now());
assert_eq!(led.len(), 1, "a redelivered stop must not consume a slot");
assert_eq!(led.peek(s, "aid1"), Some(DelegationStatus::Failed));
}
#[test]
fn clearing_removes_only_the_named_key() {
let led = PendingStops::default();
let s = sid();
led.record(s, "aid1", DelegationStatus::Completed, Utc::now());
led.record(s, "aid2", DelegationStatus::Completed, Utc::now());
led.clear(s, "aid1");
assert_eq!(led.peek(s, "aid1"), None);
assert_eq!(led.peek(s, "aid2"), Some(DelegationStatus::Completed));
}
#[test]
fn the_ledger_evicts_its_oldest_entry_at_capacity() {
let led = PendingStops::default();
let s = sid();
let now = Utc::now();
for i in 0..PENDING_STOP_CAP + 10 {
led.record(s, &format!("aid{i}"), DelegationStatus::Completed, now);
}
assert_eq!(led.len(), PENDING_STOP_CAP, "the buffer must stay bounded");
assert_eq!(led.peek(s, "aid0"), None, "the oldest goes first");
let newest = format!("aid{}", PENDING_STOP_CAP + 9);
assert_eq!(led.peek(s, &newest), Some(DelegationStatus::Completed));
}
#[test]
fn an_expired_entry_is_pruned() {
let led = PendingStops::default();
let s = sid();
let now = Utc::now();
led.record(s, "old", DelegationStatus::Completed, now);
led.record(
s,
"fresh",
DelegationStatus::Completed,
now + chrono::Duration::seconds(PENDING_STOP_TTL_SECS),
);
let dropped = led.prune_at(now + chrono::Duration::seconds(PENDING_STOP_TTL_SECS + 1));
assert_eq!(dropped, 1);
assert_eq!(led.peek(s, "old"), None);
assert_eq!(led.peek(s, "fresh"), Some(DelegationStatus::Completed));
}
}