1use std::collections::{HashMap, VecDeque};
55use std::sync::{Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
56use std::time::Duration;
57
58use harn_clock::{Clock, RealClock};
59use serde::{Deserialize, Serialize};
60use tokio::sync::Notify;
61
62#[derive(Clone, Debug, Serialize, Deserialize)]
65pub struct InboxEntry {
66 pub sequence: u64,
69 pub session_id: String,
70 pub kind: String,
71 pub content: String,
72 pub source: String,
73 pub ts_ms: i64,
74}
75
76#[derive(Default)]
77struct InboxState {
78 entries: VecDeque<InboxEntry>,
79 seq: u64,
80 notify: std::sync::Arc<Notify>,
81}
82
83struct InboxRegistry {
84 inboxes: Mutex<HashMap<String, InboxState>>,
85 sync_cv: Condvar,
86}
87
88impl InboxRegistry {
89 fn new() -> Self {
90 Self {
91 inboxes: Mutex::new(HashMap::new()),
92 sync_cv: Condvar::new(),
93 }
94 }
95}
96
97fn registry() -> &'static InboxRegistry {
98 static REGISTRY: OnceLock<InboxRegistry> = OnceLock::new();
99 REGISTRY.get_or_init(InboxRegistry::new)
100}
101
102#[cfg(test)]
103fn reset_gate() -> &'static Mutex<()> {
104 static RESET_GATE: OnceLock<Mutex<()>> = OnceLock::new();
105 RESET_GATE.get_or_init(|| Mutex::new(()))
106}
107
108#[cfg(test)]
109pub(crate) fn lock_reset_for_test() -> MutexGuard<'static, ()> {
110 reset_gate().lock().unwrap_or_else(PoisonError::into_inner)
111}
112
113fn lock_map(reg: &InboxRegistry) -> MutexGuard<'_, HashMap<String, InboxState>> {
114 reg.inboxes.lock().unwrap_or_else(PoisonError::into_inner)
115}
116
117fn clock_arc() -> std::sync::Arc<dyn Clock> {
120 static CLOCK: OnceLock<std::sync::Arc<dyn Clock>> = OnceLock::new();
121 CLOCK
122 .get_or_init(|| std::sync::Arc::new(RealClock::new()) as std::sync::Arc<dyn Clock>)
123 .clone()
124}
125
126pub fn install_clock(clock: std::sync::Arc<dyn Clock>) {
130 static SLOT: OnceLock<std::sync::Arc<dyn Clock>> = OnceLock::new();
131 let _ = SLOT.set(clock);
132}
133
134pub fn push(session_id: &str, kind: &str, content: &str, source: &str) {
141 let reg = registry();
142 let notify = {
143 let mut map = lock_map(reg);
144 let state = map.entry(session_id.to_string()).or_default();
145 state.seq = state.seq.wrapping_add(1).max(1);
146 let entry = InboxEntry {
147 sequence: state.seq,
148 session_id: session_id.to_string(),
149 kind: kind.to_string(),
150 content: content.to_string(),
151 source: source.to_string(),
152 ts_ms: harn_clock::now_wall_ms(&*clock_arc()),
153 };
154 state.entries.push_back(entry);
155 state.notify.clone()
156 };
157 reg.sync_cv.notify_all();
158 notify.notify_waiters();
159}
160
161pub fn drain(session_id: &str) -> Vec<InboxEntry> {
163 let reg = registry();
164 let mut map = lock_map(reg);
165 map.get_mut(session_id)
166 .map(|state| state.entries.drain(..).collect())
167 .unwrap_or_default()
168}
169
170pub fn drain_where<F>(session_id: &str, mut predicate: F) -> Vec<InboxEntry>
173where
174 F: FnMut(&InboxEntry) -> bool,
175{
176 let reg = registry();
177 let mut map = lock_map(reg);
178 let Some(state) = map.get_mut(session_id) else {
179 return Vec::new();
180 };
181 let mut taken = Vec::new();
182 let mut kept = VecDeque::with_capacity(state.entries.len());
183 for entry in state.entries.drain(..) {
184 if predicate(&entry) {
185 taken.push(entry);
186 } else {
187 kept.push_back(entry);
188 }
189 }
190 state.entries = kept;
191 taken
192}
193
194pub fn requeue_front(entry: InboxEntry) {
199 let reg = registry();
200 let mut map = lock_map(reg);
201 let state = map.entry(entry.session_id.clone()).or_default();
202 state.entries.push_front(entry);
203}
204
205pub fn pending_count(session_id: &str) -> usize {
207 let reg = registry();
208 let map = lock_map(reg);
209 map.get(session_id)
210 .map(|state| state.entries.len())
211 .unwrap_or(0)
212}
213
214pub fn clear_session(session_id: &str) {
216 let reg = registry();
217 let mut map = lock_map(reg);
218 map.remove(session_id);
219}
220
221pub fn reset() {
227 #[cfg(test)]
228 let _reset_guard = lock_reset_for_test();
229 let reg = registry();
230 let mut map = lock_map(reg);
231 map.clear();
232}
233
234#[cfg(test)]
236pub fn session_count() -> usize {
237 let reg = registry();
238 let map = lock_map(reg);
239 map.len()
240}
241
242pub fn wait_sync(session_id: &str, timeout: Duration) -> bool {
250 let reg = registry();
251 let mut map = match reg.inboxes.lock() {
252 Ok(g) => g,
253 Err(p) => p.into_inner(),
254 };
255 if has_pending(&map, session_id) {
256 return true;
257 }
258 let start = std::time::Instant::now();
259 loop {
260 let remaining = match timeout.checked_sub(start.elapsed()) {
261 Some(remaining) if !remaining.is_zero() => remaining,
262 _ => return has_pending(&map, session_id),
263 };
264 let (next_guard, wait_result) = match reg.sync_cv.wait_timeout(map, remaining) {
265 Ok(pair) => pair,
266 Err(poison) => {
267 let pair = poison.into_inner();
268 (pair.0, pair.1)
269 }
270 };
271 map = next_guard;
272 if has_pending(&map, session_id) {
273 return true;
274 }
275 if wait_result.timed_out() {
276 return false;
277 }
278 }
279}
280
281fn has_pending(map: &HashMap<String, InboxState>, session_id: &str) -> bool {
282 map.get(session_id)
283 .map(|s| !s.entries.is_empty())
284 .unwrap_or(false)
285}
286
287pub async fn wait_async(session_id: &str, timeout: Duration, clock: &dyn Clock) -> bool {
300 if pending_count(session_id) > 0 {
301 return true;
302 }
303 let notify = {
304 let reg = registry();
305 let mut map = lock_map(reg);
306 map.entry(session_id.to_string())
307 .or_default()
308 .notify
309 .clone()
310 };
311 let sleep = clock.sleep(timeout);
312 tokio::pin!(sleep);
313 loop {
314 let notified = notify.notified();
317 tokio::pin!(notified);
318 if pending_count(session_id) > 0 {
319 return true;
320 }
321 tokio::select! {
322 biased;
323 _ = &mut notified => {
324 if pending_count(session_id) > 0 {
325 return true;
326 }
327 }
328 () = &mut sleep => {
329 return pending_count(session_id) > 0;
330 }
331 }
332 }
333}
334
335#[cfg(any(test, feature = "vm-bench-internals"))]
338pub fn snapshot(session_id: &str) -> Vec<InboxEntry> {
339 let reg = registry();
340 let map = lock_map(reg);
341 map.get(session_id)
342 .map(|state| state.entries.iter().cloned().collect())
343 .unwrap_or_default()
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349 use harn_clock::PausedClock;
350 use time::OffsetDateTime;
351
352 fn fresh_session_id() -> String {
353 format!("test-{}", uuid::Uuid::now_v7())
357 }
358
359 #[test]
360 fn push_then_drain_preserves_fifo_order() {
361 let sid = fresh_session_id();
362 push(&sid, "tool_result", "first", "test");
363 push(&sid, "tool_result", "second", "test");
364 push(&sid, "file_edited", "third", "test");
365 let entries = drain(&sid);
366 assert_eq!(entries.len(), 3);
367 assert_eq!(entries[0].content, "first");
368 assert_eq!(entries[1].content, "second");
369 assert_eq!(entries[2].content, "third");
370 assert!(entries[0].sequence < entries[1].sequence);
371 assert!(entries[1].sequence < entries[2].sequence);
372 }
373
374 #[test]
375 fn drain_where_partitions_by_kind() {
376 let sid = fresh_session_id();
377 push(&sid, "tool_result", "a", "test");
378 push(&sid, "file_edited", "b", "test");
379 push(&sid, "tool_result", "c", "test");
380 let taken = drain_where(&sid, |e| e.kind == "file_edited");
381 assert_eq!(taken.len(), 1);
382 assert_eq!(taken[0].content, "b");
383 let remaining = drain(&sid);
384 assert_eq!(remaining.len(), 2);
385 assert_eq!(remaining[0].content, "a");
386 assert_eq!(remaining[1].content, "c");
387 }
388
389 #[test]
390 fn requeue_front_keeps_unwanted_entry_at_head() {
391 let sid = fresh_session_id();
392 push(&sid, "tool_result", "first", "test");
393 let mut entries = drain(&sid);
394 assert_eq!(entries.len(), 1);
395 let entry = entries.remove(0);
396 requeue_front(entry);
397 let again = drain(&sid);
398 assert_eq!(again[0].content, "first");
399 }
400
401 #[tokio::test]
409 async fn wait_async_returns_when_push_happens() {
410 let sid = fresh_session_id();
411 let clock = PausedClock::new(OffsetDateTime::UNIX_EPOCH);
412 let waiter_sid = sid.clone();
413 let waiter_clock = clock.clone();
414 let waiter = tokio::spawn(async move {
415 wait_async(&waiter_sid, Duration::from_mins(1), &*waiter_clock).await
416 });
417 tokio::task::yield_now().await;
419 push(&sid, "tool_result", "hello", "test");
420 assert!(waiter.await.expect("join"));
421 let entries = drain(&sid);
423 assert_eq!(entries.len(), 1);
424 }
425
426 #[tokio::test]
427 async fn wait_async_times_out_when_silent() {
428 let sid = fresh_session_id();
429 let clock = PausedClock::new(OffsetDateTime::UNIX_EPOCH);
430 let clock_advance = clock.clone();
436 let advancer = tokio::spawn(async move {
437 tokio::task::yield_now().await;
438 clock_advance.advance(Duration::from_millis(50));
439 });
440 let result = wait_async(&sid, Duration::from_millis(50), &*clock).await;
441 advancer.await.ok();
442 assert!(!result);
443 }
444
445 #[test]
446 fn pending_count_tracks_pushes_and_drains() {
447 let sid = fresh_session_id();
448 assert_eq!(pending_count(&sid), 0);
449 push(&sid, "tool_result", "x", "test");
450 push(&sid, "tool_result", "y", "test");
451 assert_eq!(pending_count(&sid), 2);
452 let _ = drain(&sid);
453 assert_eq!(pending_count(&sid), 0);
454 }
455
456 #[test]
457 fn clear_session_drops_pending_entries() {
458 let sid = fresh_session_id();
459 push(&sid, "tool_result", "x", "test");
460 clear_session(&sid);
461 assert_eq!(pending_count(&sid), 0);
462 }
463}