1use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13use std::time::Instant;
14
15use tracing::error;
16
17use super::autonomous::{AutonomousState, SupervisorVerdict};
18
19#[derive(Debug, Clone)]
24pub struct SessionSnapshot {
25 pub goal_id: String,
27 pub goal_text_short: String,
29 pub state: AutonomousState,
31 pub turns_executed: u32,
33 pub max_turns: u32,
35 pub elapsed: std::time::Duration,
37 pub last_verdict: Option<SupervisorVerdict>,
39}
40
41#[derive(Clone, Default)]
56pub struct AutonomousRegistry {
57 inner: Arc<Mutex<HashMap<String, RegistryEntry>>>,
58}
59
60struct RegistryEntry {
61 goal_text: String,
62 state: AutonomousState,
63 turns_executed: u32,
64 max_turns: u32,
65 started_at: Instant,
66 last_verdict: Option<SupervisorVerdict>,
67}
68
69impl AutonomousRegistry {
70 #[must_use]
72 pub fn new() -> Self {
73 Self::default()
74 }
75
76 #[allow(clippy::too_many_arguments)]
81 pub fn upsert(
82 &self,
83 goal_id: impl Into<String>,
84 goal_text: impl Into<String>,
85 state: AutonomousState,
86 turns_executed: u32,
87 max_turns: u32,
88 started_at: Instant,
89 last_verdict: Option<SupervisorVerdict>,
90 ) {
91 let goal_id = goal_id.into();
92 let entry = RegistryEntry {
93 goal_text: goal_text.into(),
94 state,
95 turns_executed,
96 max_turns,
97 started_at,
98 last_verdict,
99 };
100 let mut map = self.inner.lock().unwrap_or_else(|e| {
101 error!("AutonomousRegistry::upsert: mutex poisoned, recovering guard");
102 e.into_inner()
103 });
104 map.insert(goal_id, entry);
105 }
106
107 pub fn remove(&self, goal_id: &str) {
109 let mut map = self.inner.lock().unwrap_or_else(|e| {
110 error!("AutonomousRegistry::remove: mutex poisoned, recovering guard");
111 e.into_inner()
112 });
113 map.remove(goal_id);
114 }
115
116 #[must_use]
120 pub fn list(&self) -> Vec<SessionSnapshot> {
121 let map = self.inner.lock().unwrap_or_else(|e| {
122 error!("AutonomousRegistry::list: mutex poisoned, recovering guard");
123 e.into_inner()
124 });
125 map.iter()
126 .map(|(id, e)| {
127 let short = if e.goal_text.len() > 80 {
128 let end = e.goal_text.floor_char_boundary(80);
129 format!("{}…", &e.goal_text[..end])
130 } else {
131 e.goal_text.clone()
132 };
133 SessionSnapshot {
134 goal_id: id.clone(),
135 goal_text_short: short,
136 state: e.state,
137 turns_executed: e.turns_executed,
138 max_turns: e.max_turns,
139 elapsed: e.started_at.elapsed(),
140 last_verdict: e.last_verdict.clone(),
141 }
142 })
143 .collect()
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn empty_by_default() {
153 let reg = AutonomousRegistry::new();
154 assert!(reg.list().is_empty());
155 }
156
157 #[test]
158 fn upsert_and_list() {
159 let reg = AutonomousRegistry::new();
160 reg.upsert(
161 "id1",
162 "do something useful",
163 AutonomousState::Running,
164 3,
165 20,
166 Instant::now(),
167 None,
168 );
169 let list = reg.list();
170 assert_eq!(list.len(), 1);
171 assert_eq!(list[0].goal_id, "id1");
172 assert_eq!(list[0].state, AutonomousState::Running);
173 assert_eq!(list[0].turns_executed, 3);
174 }
175
176 #[test]
177 fn remove_clears_entry() {
178 let reg = AutonomousRegistry::new();
179 reg.upsert(
180 "id2",
181 "text",
182 AutonomousState::Running,
183 0,
184 5,
185 Instant::now(),
186 None,
187 );
188 assert_eq!(reg.list().len(), 1);
189 reg.remove("id2");
190 assert!(reg.list().is_empty());
191 }
192
193 #[test]
194 fn long_goal_text_is_truncated() {
195 let reg = AutonomousRegistry::new();
196 let long_text = "a".repeat(200);
197 reg.upsert(
198 "id3",
199 long_text,
200 AutonomousState::Running,
201 0,
202 5,
203 Instant::now(),
204 None,
205 );
206 let snap = ®.list()[0];
207 assert!(
208 snap.goal_text_short.len() <= 84,
209 "truncation should keep display short"
210 );
211 }
212
213 #[test]
214 fn upsert_overwrites_existing_entry() {
215 let reg = AutonomousRegistry::new();
216 reg.upsert(
217 "id5",
218 "original text",
219 AutonomousState::Running,
220 1,
221 10,
222 Instant::now(),
223 None,
224 );
225 reg.upsert(
226 "id5",
227 "updated text",
228 AutonomousState::Verifying,
229 5,
230 10,
231 Instant::now(),
232 None,
233 );
234 let list = reg.list();
235 assert_eq!(list.len(), 1, "upsert should not create duplicates");
236 assert_eq!(list[0].state, AutonomousState::Verifying);
237 assert_eq!(list[0].turns_executed, 5);
238 }
239
240 #[test]
241 fn remove_nonexistent_is_noop() {
242 let reg = AutonomousRegistry::new();
243 reg.remove("does-not-exist"); assert!(reg.list().is_empty());
245 }
246
247 #[test]
248 fn clone_shares_state() {
249 let reg = AutonomousRegistry::new();
250 let reg2 = reg.clone();
251 reg.upsert(
252 "id4",
253 "shared",
254 AutonomousState::Running,
255 0,
256 5,
257 Instant::now(),
258 None,
259 );
260 assert_eq!(reg2.list().len(), 1, "clone should see the same data");
261 }
262}