use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tracing::error;
use super::autonomous::{AutonomousState, SupervisorVerdict};
#[derive(Debug, Clone)]
pub struct SessionSnapshot {
pub goal_id: String,
pub goal_text_short: String,
pub state: AutonomousState,
pub turns_executed: u32,
pub max_turns: u32,
pub elapsed: std::time::Duration,
pub last_verdict: Option<SupervisorVerdict>,
}
#[derive(Clone, Default)]
pub struct AutonomousRegistry {
inner: Arc<Mutex<HashMap<String, RegistryEntry>>>,
}
struct RegistryEntry {
goal_text: String,
state: AutonomousState,
turns_executed: u32,
max_turns: u32,
started_at: Instant,
last_verdict: Option<SupervisorVerdict>,
}
impl AutonomousRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::too_many_arguments)]
pub fn upsert(
&self,
goal_id: impl Into<String>,
goal_text: impl Into<String>,
state: AutonomousState,
turns_executed: u32,
max_turns: u32,
started_at: Instant,
last_verdict: Option<SupervisorVerdict>,
) {
let goal_id = goal_id.into();
let entry = RegistryEntry {
goal_text: goal_text.into(),
state,
turns_executed,
max_turns,
started_at,
last_verdict,
};
let mut map = self.inner.lock().unwrap_or_else(|e| {
error!("AutonomousRegistry::upsert: mutex poisoned, recovering guard");
e.into_inner()
});
map.insert(goal_id, entry);
}
pub fn remove(&self, goal_id: &str) {
let mut map = self.inner.lock().unwrap_or_else(|e| {
error!("AutonomousRegistry::remove: mutex poisoned, recovering guard");
e.into_inner()
});
map.remove(goal_id);
}
#[must_use]
pub fn list(&self) -> Vec<SessionSnapshot> {
let map = self.inner.lock().unwrap_or_else(|e| {
error!("AutonomousRegistry::list: mutex poisoned, recovering guard");
e.into_inner()
});
map.iter()
.map(|(id, e)| {
let short = if e.goal_text.len() > 80 {
let end = e.goal_text.floor_char_boundary(80);
format!("{}…", &e.goal_text[..end])
} else {
e.goal_text.clone()
};
SessionSnapshot {
goal_id: id.clone(),
goal_text_short: short,
state: e.state,
turns_executed: e.turns_executed,
max_turns: e.max_turns,
elapsed: e.started_at.elapsed(),
last_verdict: e.last_verdict.clone(),
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_by_default() {
let reg = AutonomousRegistry::new();
assert!(reg.list().is_empty());
}
#[test]
fn upsert_and_list() {
let reg = AutonomousRegistry::new();
reg.upsert(
"id1",
"do something useful",
AutonomousState::Running,
3,
20,
Instant::now(),
None,
);
let list = reg.list();
assert_eq!(list.len(), 1);
assert_eq!(list[0].goal_id, "id1");
assert_eq!(list[0].state, AutonomousState::Running);
assert_eq!(list[0].turns_executed, 3);
}
#[test]
fn remove_clears_entry() {
let reg = AutonomousRegistry::new();
reg.upsert(
"id2",
"text",
AutonomousState::Running,
0,
5,
Instant::now(),
None,
);
assert_eq!(reg.list().len(), 1);
reg.remove("id2");
assert!(reg.list().is_empty());
}
#[test]
fn long_goal_text_is_truncated() {
let reg = AutonomousRegistry::new();
let long_text = "a".repeat(200);
reg.upsert(
"id3",
long_text,
AutonomousState::Running,
0,
5,
Instant::now(),
None,
);
let snap = ®.list()[0];
assert!(
snap.goal_text_short.len() <= 84,
"truncation should keep display short"
);
}
#[test]
fn upsert_overwrites_existing_entry() {
let reg = AutonomousRegistry::new();
reg.upsert(
"id5",
"original text",
AutonomousState::Running,
1,
10,
Instant::now(),
None,
);
reg.upsert(
"id5",
"updated text",
AutonomousState::Verifying,
5,
10,
Instant::now(),
None,
);
let list = reg.list();
assert_eq!(list.len(), 1, "upsert should not create duplicates");
assert_eq!(list[0].state, AutonomousState::Verifying);
assert_eq!(list[0].turns_executed, 5);
}
#[test]
fn remove_nonexistent_is_noop() {
let reg = AutonomousRegistry::new();
reg.remove("does-not-exist"); assert!(reg.list().is_empty());
}
#[test]
fn clone_shares_state() {
let reg = AutonomousRegistry::new();
let reg2 = reg.clone();
reg.upsert(
"id4",
"shared",
AutonomousState::Running,
0,
5,
Instant::now(),
None,
);
assert_eq!(reg2.list().len(), 1, "clone should see the same data");
}
}