1use crate::agent::AgentEvent;
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, VecDeque};
9use std::sync::Arc;
10use tokio::sync::{Mutex, RwLock};
11use tokio_util::sync::CancellationToken;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum RunStatus {
16 Created,
17 Planning,
18 Executing,
19 Verifying,
20 Completed,
21 Failed,
22 Cancelled,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct RunEventRecord {
27 pub sequence: usize,
28 pub timestamp_ms: u64,
29 pub event: AgentEvent,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct ActiveToolSnapshot {
34 pub id: String,
35 pub name: String,
36 pub started_at_ms: u64,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RunSnapshot {
41 pub id: String,
42 pub session_id: String,
43 pub status: RunStatus,
44 pub prompt: String,
45 pub created_at_ms: u64,
46 pub updated_at_ms: u64,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub result_text: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub error: Option<String>,
51 pub event_count: usize,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct RunRecord {
56 pub snapshot: RunSnapshot,
57 pub events: Vec<RunEventRecord>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct RunEventPage {
63 pub events: Vec<RunEventRecord>,
64 pub first_available_sequence: Option<usize>,
66 pub latest_sequence_exclusive: usize,
68 pub next_after_sequence: Option<usize>,
70 pub retention_gap: bool,
72 pub has_more: bool,
73}
74
75#[derive(Debug, Default)]
76struct RetainedRunEvents {
77 records: Vec<RunEventRecord>,
78 serialized_bytes: usize,
79}
80
81impl RunSnapshot {
82 fn new(id: String, session_id: String, prompt: String) -> Self {
83 let now = now_ms();
84 Self {
85 id,
86 session_id,
87 status: RunStatus::Created,
88 prompt,
89 created_at_ms: now,
90 updated_at_ms: now,
91 result_text: None,
92 error: None,
93 event_count: 0,
94 }
95 }
96}
97
98#[derive(Debug, Default)]
99pub struct InMemoryRunStore {
100 runs: RwLock<HashMap<String, RunSnapshot>>,
101 events: RwLock<HashMap<String, RetainedRunEvents>>,
102 insertion_order: RwLock<VecDeque<String>>,
105 max_runs: Option<usize>,
108 max_events_per_run: Option<usize>,
113 max_event_bytes_per_run: Option<usize>,
116}
117
118impl InMemoryRunStore {
119 pub fn new() -> Self {
120 Self::default()
121 }
122
123 pub fn with_retention(max_runs: Option<usize>, max_events_per_run: Option<usize>) -> Self {
126 Self::with_retention_limits(max_runs, max_events_per_run, None)
127 }
128
129 pub fn with_retention_limits(
131 max_runs: Option<usize>,
132 max_events_per_run: Option<usize>,
133 max_event_bytes_per_run: Option<usize>,
134 ) -> Self {
135 Self {
136 runs: RwLock::new(HashMap::new()),
137 events: RwLock::new(HashMap::new()),
138 insertion_order: RwLock::new(VecDeque::new()),
139 max_runs,
140 max_events_per_run,
141 max_event_bytes_per_run,
142 }
143 }
144
145 pub async fn create_run(&self, session_id: &str, prompt: &str) -> RunSnapshot {
146 let id = format!("run-{}", uuid::Uuid::new_v4());
150 self.create_run_with_id(id, session_id, prompt).await
151 }
152
153 pub async fn create_run_with_id(
157 &self,
158 id: String,
159 session_id: &str,
160 prompt: &str,
161 ) -> RunSnapshot {
162 let snapshot = RunSnapshot::new(id.clone(), session_id.to_string(), prompt.to_string());
163 {
172 let mut order = self.insertion_order.write().await;
173 let mut events = self.events.write().await;
174 let mut runs = self.runs.write().await;
175 runs.insert(id.clone(), snapshot.clone());
176 events.insert(id.clone(), RetainedRunEvents::default());
177 order.push_back(id);
178 if let Some(cap) = self.max_runs {
179 while order.len() > cap {
180 if let Some(victim) = order.pop_front() {
181 runs.remove(&victim);
182 events.remove(&victim);
183 }
184 }
185 }
186 }
187 snapshot
188 }
189
190 pub async fn record_event(&self, run_id: &str, event: AgentEvent) -> Option<RunSnapshot> {
191 let mut events = self.events.write().await;
192 let mut runs = self.runs.write().await;
193 let run_events = events.get_mut(run_id)?;
194 let run = runs.get_mut(run_id)?;
195
196 let sequence = run.event_count;
202 let record = RunEventRecord {
203 sequence,
204 timestamp_ms: now_ms(),
205 event: event.clone(),
206 };
207 run_events.serialized_bytes = run_events
208 .serialized_bytes
209 .saturating_add(serialized_event_record_len(&record));
210 run_events.records.push(record);
211 trim_retained_events(
212 run_events,
213 self.max_events_per_run,
214 self.max_event_bytes_per_run,
215 );
216 apply_event_to_snapshot(run, &event);
217 run.event_count += 1;
218 run.updated_at_ms = now_ms();
219 Some(run.clone())
220 }
221
222 pub async fn mark_failed(&self, run_id: &str, error: impl Into<String>) -> Option<RunSnapshot> {
223 let mut runs = self.runs.write().await;
224 let run = runs.get_mut(run_id)?;
225 if run.status == RunStatus::Cancelled {
226 return Some(run.clone());
227 }
228 run.status = RunStatus::Failed;
229 run.error = Some(error.into());
230 run.updated_at_ms = now_ms();
231 Some(run.clone())
232 }
233
234 pub async fn mark_cancelled(&self, run_id: &str) -> Option<RunSnapshot> {
235 let mut runs = self.runs.write().await;
236 let run = runs.get_mut(run_id)?;
237 run.status = RunStatus::Cancelled;
238 run.updated_at_ms = now_ms();
239 Some(run.clone())
240 }
241
242 pub async fn snapshot(&self, run_id: &str) -> Option<RunSnapshot> {
243 self.runs.read().await.get(run_id).cloned()
244 }
245
246 pub async fn events(&self, run_id: &str) -> Vec<RunEventRecord> {
247 self.events
248 .read()
249 .await
250 .get(run_id)
251 .map(|events| events.records.clone())
252 .unwrap_or_default()
253 }
254
255 pub async fn event_page(
260 &self,
261 run_id: &str,
262 after_sequence: Option<usize>,
263 limit: usize,
264 ) -> Option<RunEventPage> {
265 let events = self.events.read().await;
267 let runs = self.runs.read().await;
268 let retained = events.get(run_id)?;
269 let run = runs.get(run_id)?;
270 let first_available_sequence = retained.records.first().map(|event| event.sequence);
271 let requested_start = match after_sequence {
272 Some(sequence) => sequence.saturating_add(1),
273 None => 0,
274 };
275 let retention_gap = if requested_start >= run.event_count {
276 false
277 } else {
278 first_available_sequence
279 .map(|first| requested_start < first)
280 .unwrap_or(true)
281 };
282 let mut matching = retained
283 .records
284 .iter()
285 .filter(|event| after_sequence.is_none_or(|cursor| event.sequence > cursor));
286 let page_events = matching.by_ref().take(limit).cloned().collect::<Vec<_>>();
287 let has_more = matching.next().is_some();
288 let next_after_sequence = page_events
289 .last()
290 .map(|event| event.sequence)
291 .or(after_sequence);
292 Some(RunEventPage {
293 events: page_events,
294 first_available_sequence,
295 latest_sequence_exclusive: run.event_count,
296 next_after_sequence,
297 retention_gap,
298 has_more,
299 })
300 }
301
302 pub async fn list(&self) -> Vec<RunSnapshot> {
303 let order = self.insertion_order.read().await;
304 let runs = self.runs.read().await;
305 order
306 .iter()
307 .filter_map(|run_id| runs.get(run_id).cloned())
308 .collect()
309 }
310
311 pub async fn records(&self) -> Vec<RunRecord> {
312 let order = self.insertion_order.read().await;
318 let events = self.events.read().await;
319 let runs = self.runs.read().await;
320 order
321 .iter()
322 .filter_map(|run_id| {
323 let snapshot = runs.get(run_id)?.clone();
324 Some(RunRecord {
325 events: events
326 .get(run_id)
327 .map(|events| events.records.clone())
328 .unwrap_or_default(),
329 snapshot,
330 })
331 })
332 .collect()
333 }
334
335 pub async fn replace_records(&self, records: Vec<RunRecord>) {
336 let mut sorted = records;
340 sorted.sort_by_key(|r| r.snapshot.created_at_ms);
341 if let Some(cap) = self.max_runs {
342 let excess = sorted.len().saturating_sub(cap);
343 if excess > 0 {
344 sorted.drain(..excess);
345 }
346 }
347 let mut run_map = HashMap::new();
348 let mut event_map = HashMap::new();
349 let mut order = VecDeque::with_capacity(sorted.len());
350 for record in sorted {
351 let id = record.snapshot.id.clone();
352 let mut retained = RetainedRunEvents {
360 serialized_bytes: record
361 .events
362 .iter()
363 .map(serialized_event_record_len)
364 .fold(0usize, usize::saturating_add),
365 records: record.events,
366 };
367 trim_retained_events(
368 &mut retained,
369 self.max_events_per_run,
370 self.max_event_bytes_per_run,
371 );
372 event_map.insert(id.clone(), retained);
373 run_map.insert(id.clone(), record.snapshot);
374 order.push_back(id);
375 }
376 let mut stored_order = self.insertion_order.write().await;
380 let mut stored_events = self.events.write().await;
381 let mut stored_runs = self.runs.write().await;
382 *stored_runs = run_map;
383 *stored_events = event_map;
384 *stored_order = order;
385 }
386}
387
388fn serialized_event_record_len(record: &RunEventRecord) -> usize {
389 serde_json::to_vec(record)
390 .map(|encoded| encoded.len())
391 .unwrap_or(usize::MAX)
392}
393
394fn trim_retained_events(
395 events: &mut RetainedRunEvents,
396 max_events: Option<usize>,
397 max_bytes: Option<usize>,
398) {
399 let count_excess = max_events
400 .map(|cap| events.records.len().saturating_sub(cap))
401 .unwrap_or(0);
402 let mut remove_count = count_excess;
403 let mut remaining_bytes = events.serialized_bytes;
404 for record in events.records.iter().take(remove_count) {
405 remaining_bytes = remaining_bytes.saturating_sub(serialized_event_record_len(record));
406 }
407 while max_bytes.is_some_and(|cap| remaining_bytes > cap) && remove_count < events.records.len()
408 {
409 remaining_bytes = remaining_bytes
410 .saturating_sub(serialized_event_record_len(&events.records[remove_count]));
411 remove_count += 1;
412 }
413 for record in events.records.iter().take(remove_count) {
414 events.serialized_bytes = events
415 .serialized_bytes
416 .saturating_sub(serialized_event_record_len(record));
417 }
418 if remove_count > 0 {
419 events.records.drain(..remove_count);
420 }
421}
422
423#[cfg(test)]
424mod retention_tests {
425 use super::*;
426
427 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
428 async fn concurrent_create_and_record_under_cap_does_not_deadlock() {
429 let store = std::sync::Arc::new(InMemoryRunStore::with_retention(Some(10), None));
433 let mut handles = Vec::new();
434 for i in 0..100 {
435 let s = std::sync::Arc::clone(&store);
436 handles.push(tokio::spawn(async move {
437 let r = s.create_run("sess", &format!("p{i}")).await;
438 for _ in 0..5 {
439 s.record_event(
440 &r.id,
441 AgentEvent::TextDelta {
442 text: "x".to_string(),
443 },
444 )
445 .await;
446 }
447 }));
448 }
449 for h in handles {
450 h.await.unwrap();
451 }
452 assert!(store.list().await.len() <= 10);
455 }
456
457 #[tokio::test]
458 async fn replace_records_preserves_cumulative_event_count_after_trim() {
459 let src = InMemoryRunStore::with_retention(None, Some(3));
461 let run = src.create_run("s", "p").await;
462 for _ in 0..10 {
463 src.record_event(
464 &run.id,
465 AgentEvent::TextDelta {
466 text: "x".to_string(),
467 },
468 )
469 .await;
470 }
471 let records = src.records().await;
472 assert_eq!(records.len(), 1);
474 assert_eq!(records[0].events.len(), 3, "buffer trimmed to cap");
475 assert_eq!(records[0].snapshot.event_count, 10, "cumulative preserved");
476
477 let dst = InMemoryRunStore::new();
479 dst.replace_records(records).await;
480 let restored = dst.snapshot(&run.id).await.unwrap();
481 assert_eq!(
482 restored.event_count, 10,
483 "replace_records must NOT reset event_count to the trimmed buffer length"
484 );
485 assert_eq!(dst.events(&run.id).await.len(), 3);
487 }
488
489 #[tokio::test]
490 async fn replace_records_enforces_run_and_event_caps() {
491 let source = InMemoryRunStore::new();
492 for run_index in 0..4 {
493 let run = source
494 .create_run_with_id(
495 format!("run-{run_index}"),
496 "session-1",
497 &format!("prompt-{run_index}"),
498 )
499 .await;
500 for event_index in 0..5 {
501 source
502 .record_event(
503 &run.id,
504 AgentEvent::TextDelta {
505 text: format!("{run_index}:{event_index}"),
506 },
507 )
508 .await;
509 }
510 }
511
512 let restored = InMemoryRunStore::with_retention(Some(2), Some(2));
513 restored.replace_records(source.records().await).await;
514
515 let records = restored.records().await;
516 assert_eq!(
517 records
518 .iter()
519 .map(|record| record.snapshot.id.as_str())
520 .collect::<Vec<_>>(),
521 vec!["run-2", "run-3"],
522 "restore must keep the newest runs under the same FIFO policy as live writes"
523 );
524 for (run_index, record) in records.iter().enumerate() {
525 assert_eq!(record.snapshot.event_count, 5);
526 assert_eq!(record.events.len(), 2);
527 assert_eq!(record.events[0].sequence, 3);
528 assert_eq!(record.events[1].sequence, 4);
529 assert_eq!(record.snapshot.id, format!("run-{}", run_index + 2));
530 }
531 }
532
533 #[tokio::test]
534 async fn replace_records_honors_zero_caps() {
535 let source = InMemoryRunStore::new();
536 let run = source.create_run("session-1", "prompt").await;
537 source
538 .record_event(
539 &run.id,
540 AgentEvent::TextDelta {
541 text: "event".to_string(),
542 },
543 )
544 .await;
545
546 let no_runs = InMemoryRunStore::with_retention(Some(0), Some(0));
547 no_runs.replace_records(source.records().await).await;
548 assert!(no_runs.records().await.is_empty());
549
550 let no_events = InMemoryRunStore::with_retention(None, Some(0));
551 no_events.replace_records(source.records().await).await;
552 let records = no_events.records().await;
553 assert_eq!(records.len(), 1);
554 assert!(records[0].events.is_empty());
555 assert_eq!(records[0].snapshot.event_count, 1);
556 }
557
558 #[tokio::test]
559 async fn max_runs_evicts_oldest() {
560 let store = InMemoryRunStore::with_retention(Some(2), None);
561 let _ = store.create_run("session-1", "prompt-1").await;
562 let r2 = store.create_run("session-1", "prompt-2").await;
563 let r3 = store.create_run("session-1", "prompt-3").await;
564
565 assert_eq!(store.list().await.len(), 2);
567 let ids: Vec<String> = store.list().await.into_iter().map(|r| r.id).collect();
568 assert!(ids.contains(&r2.id));
569 assert!(ids.contains(&r3.id));
570 assert!(store.events(&r2.id).await.is_empty());
571 let surviving_event_count: usize =
573 store.events(&r2.id).await.len() + store.events(&r3.id).await.len();
574 assert_eq!(surviving_event_count, 0);
575 }
576
577 #[tokio::test]
578 async fn max_events_per_run_caps_event_buffer() {
579 let store = InMemoryRunStore::with_retention(None, Some(3));
580 let run = store.create_run("session-1", "prompt").await;
581 for _ in 0..10 {
582 store
583 .record_event(
584 &run.id,
585 AgentEvent::TextDelta {
586 text: "x".to_string(),
587 },
588 )
589 .await;
590 }
591 let events = store.events(&run.id).await;
592 assert_eq!(
593 events.len(),
594 3,
595 "buffer must be capped at max_events_per_run"
596 );
597 let snap = store.snapshot(&run.id).await.unwrap();
600 assert_eq!(snap.event_count, 10);
601 }
602
603 #[tokio::test]
604 async fn max_event_bytes_per_run_drops_oversized_live_event_but_advances_cursor() {
605 let store = InMemoryRunStore::with_retention_limits(None, None, Some(0));
606 let run = store.create_run("session-1", "prompt").await;
607
608 store
609 .record_event(
610 &run.id,
611 AgentEvent::TextDelta {
612 text: "oversized".to_string(),
613 },
614 )
615 .await;
616
617 assert!(store.events(&run.id).await.is_empty());
618 let snapshot = store.snapshot(&run.id).await.unwrap();
619 assert_eq!(snapshot.event_count, 1);
620 }
621
622 #[tokio::test]
623 async fn replace_records_enforces_serialized_event_byte_cap_fifo() {
624 let source = InMemoryRunStore::new();
625 let run = source.create_run("session-1", "prompt").await;
626 for text in ["old", "middle", "new"] {
627 source
628 .record_event(
629 &run.id,
630 AgentEvent::TextDelta {
631 text: text.to_string(),
632 },
633 )
634 .await;
635 }
636 let source_records = source.records().await;
637 let retained_bytes = source_records[0].events[1..]
638 .iter()
639 .map(serialized_event_record_len)
640 .sum();
641
642 let restored = InMemoryRunStore::with_retention_limits(None, None, Some(retained_bytes));
643 restored.replace_records(source_records).await;
644
645 let records = restored.records().await;
646 assert_eq!(records[0].snapshot.event_count, 3);
647 assert_eq!(
648 records[0]
649 .events
650 .iter()
651 .map(|event| event.sequence)
652 .collect::<Vec<_>>(),
653 vec![1, 2]
654 );
655 }
656
657 #[tokio::test]
658 async fn retained_event_sequences_remain_monotonic_after_fifo_trim() {
659 let store = InMemoryRunStore::with_retention(None, Some(3));
660 let run = store.create_run("session-1", "prompt").await;
661
662 for index in 0..10 {
663 store
664 .record_event(
665 &run.id,
666 AgentEvent::TextDelta {
667 text: index.to_string(),
668 },
669 )
670 .await;
671 }
672
673 let sequences = store
674 .events(&run.id)
675 .await
676 .into_iter()
677 .map(|record| record.sequence)
678 .collect::<Vec<_>>();
679 assert_eq!(sequences, vec![7, 8, 9]);
680 assert!(sequences.windows(2).all(|pair| pair[0] < pair[1]));
681 }
682
683 #[tokio::test]
684 async fn restored_run_continues_sequence_from_cumulative_event_count() {
685 let source = InMemoryRunStore::with_retention(None, Some(3));
686 let run = source.create_run("session-1", "prompt").await;
687 for index in 0..10 {
688 source
689 .record_event(
690 &run.id,
691 AgentEvent::TextDelta {
692 text: index.to_string(),
693 },
694 )
695 .await;
696 }
697
698 let restored = InMemoryRunStore::with_retention(None, Some(3));
699 restored.replace_records(source.records().await).await;
700 restored
701 .record_event(
702 &run.id,
703 AgentEvent::TextDelta {
704 text: "after restore".to_string(),
705 },
706 )
707 .await;
708
709 let sequences = restored
710 .events(&run.id)
711 .await
712 .into_iter()
713 .map(|record| record.sequence)
714 .collect::<Vec<_>>();
715 assert_eq!(sequences, vec![8, 9, 10]);
716 assert_eq!(restored.snapshot(&run.id).await.unwrap().event_count, 11);
717 }
718
719 #[tokio::test]
720 async fn event_page_reports_retention_gap_and_paginates_from_cursor() {
721 let store = InMemoryRunStore::with_retention(None, Some(3));
722 let run = store.create_run("session-1", "prompt").await;
723 for index in 0..6 {
724 store
725 .record_event(
726 &run.id,
727 AgentEvent::TextDelta {
728 text: index.to_string(),
729 },
730 )
731 .await;
732 }
733
734 let first = store.event_page(&run.id, None, 2).await.unwrap();
735 assert_eq!(first.first_available_sequence, Some(3));
736 assert_eq!(first.latest_sequence_exclusive, 6);
737 assert!(first.retention_gap);
738 assert!(first.has_more);
739 assert_eq!(first.next_after_sequence, Some(4));
740 assert_eq!(
741 first
742 .events
743 .iter()
744 .map(|event| event.sequence)
745 .collect::<Vec<_>>(),
746 vec![3, 4]
747 );
748
749 let second = store
750 .event_page(&run.id, first.next_after_sequence, 2)
751 .await
752 .unwrap();
753 assert!(!second.retention_gap);
754 assert!(!second.has_more);
755 assert_eq!(second.next_after_sequence, Some(5));
756 assert_eq!(second.events[0].sequence, 5);
757 assert!(store.event_page("missing", None, 10).await.is_none());
758 }
759
760 #[tokio::test]
761 async fn event_page_reports_gap_when_retention_keeps_no_events() {
762 let store = InMemoryRunStore::with_retention(None, Some(0));
763 let run = store.create_run("session-1", "prompt").await;
764 store
765 .record_event(
766 &run.id,
767 AgentEvent::TextDelta {
768 text: "gone".to_string(),
769 },
770 )
771 .await;
772
773 let page = store.event_page(&run.id, None, 10).await.unwrap();
774 assert!(page.events.is_empty());
775 assert_eq!(page.first_available_sequence, None);
776 assert_eq!(page.latest_sequence_exclusive, 1);
777 assert!(page.retention_gap);
778 assert!(!page.has_more);
779 }
780
781 #[tokio::test]
782 async fn unlimited_retention_is_the_default() {
783 let store = InMemoryRunStore::new();
784 for i in 0..50 {
785 let r = store.create_run("s", &format!("p{i}")).await;
786 for _ in 0..20 {
787 store
788 .record_event(
789 &r.id,
790 AgentEvent::TextDelta {
791 text: "y".to_string(),
792 },
793 )
794 .await;
795 }
796 }
797 assert_eq!(store.list().await.len(), 50);
798 }
799}
800
801#[derive(Clone)]
802pub struct RunHandle {
803 id: String,
804 session_id: String,
805 store: Arc<InMemoryRunStore>,
806 cancel_token: Arc<Mutex<Option<CancellationToken>>>,
807 current_run_id: Arc<Mutex<Option<String>>>,
808 hook_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
809}
810
811impl RunHandle {
812 pub(crate) fn new(
813 id: String,
814 session_id: String,
815 store: Arc<InMemoryRunStore>,
816 cancel_token: Arc<Mutex<Option<CancellationToken>>>,
817 current_run_id: Arc<Mutex<Option<String>>>,
818 hook_executor: Option<Arc<dyn crate::hooks::HookExecutor>>,
819 ) -> Self {
820 Self {
821 id,
822 session_id,
823 store,
824 cancel_token,
825 current_run_id,
826 hook_executor,
827 }
828 }
829
830 pub fn id(&self) -> &str {
831 &self.id
832 }
833
834 pub fn session_id(&self) -> &str {
835 &self.session_id
836 }
837
838 pub async fn snapshot(&self) -> Option<RunSnapshot> {
839 self.store.snapshot(&self.id).await
840 }
841
842 pub async fn events(&self) -> Vec<RunEventRecord> {
843 self.store.events(&self.id).await
844 }
845
846 pub async fn status(&self) -> Option<RunStatus> {
847 self.snapshot().await.map(|snapshot| snapshot.status)
848 }
849
850 pub async fn cancel(&self) -> bool {
851 let current_run_id = self.current_run_id.lock().await.clone();
852 if current_run_id.as_deref() != Some(self.id.as_str()) {
853 return false;
854 }
855
856 let token = self.cancel_token.lock().await.clone();
857 if let Some(token) = token {
858 token.cancel();
859 let _ = self.store.mark_cancelled(&self.id).await;
860 if let Some(executor) = &self.hook_executor {
861 executor
862 .record_run_cancelled(&self.id, &self.session_id, Some("cancelled by host"))
863 .await;
864 }
865 true
866 } else {
867 false
868 }
869 }
870}
871
872fn apply_event_to_snapshot(run: &mut RunSnapshot, event: &AgentEvent) {
873 match event {
874 AgentEvent::Start { prompt } => {
875 run.status = RunStatus::Executing;
876 if run.prompt.is_empty() {
877 run.prompt = prompt.clone();
878 }
879 }
880 AgentEvent::PlanningStart { .. } => {
881 run.status = RunStatus::Planning;
882 }
883 AgentEvent::StepStart { .. }
884 | AgentEvent::ToolStart { .. }
885 | AgentEvent::ToolExecutionStart { .. }
886 | AgentEvent::TurnStart { .. }
887 if !matches!(run.status, RunStatus::Planning) =>
888 {
889 run.status = RunStatus::Executing;
890 }
891 AgentEvent::End { text, .. } => {
892 if run.status == RunStatus::Cancelled {
893 return;
894 }
895 run.status = RunStatus::Completed;
896 run.result_text = Some(text.clone());
897 run.error = None;
898 }
899 AgentEvent::Error { message } => {
900 if run.status == RunStatus::Cancelled {
901 return;
902 }
903 run.status = RunStatus::Failed;
904 run.error = Some(message.clone());
905 }
906 _ => {}
907 }
908}
909
910fn now_ms() -> u64 {
911 std::time::SystemTime::now()
912 .duration_since(std::time::UNIX_EPOCH)
913 .map(|duration| duration.as_millis() as u64)
914 .unwrap_or(0)
915}
916
917#[cfg(test)]
918mod tests {
919 use super::*;
920
921 #[tokio::test]
922 async fn run_store_tracks_status_and_events() {
923 let store = InMemoryRunStore::new();
924 let run = store.create_run("session-1", "fix tests").await;
925
926 store
927 .record_event(
928 &run.id,
929 AgentEvent::Start {
930 prompt: "fix tests".to_string(),
931 },
932 )
933 .await;
934 store
935 .record_event(
936 &run.id,
937 AgentEvent::End {
938 text: "done".to_string(),
939 usage: Default::default(),
940 verification_summary: Box::new(
941 crate::verification::VerificationSummary::from_reports(&[]),
942 ),
943 meta: None,
944 },
945 )
946 .await;
947
948 let snapshot = store.snapshot(&run.id).await.unwrap();
949 assert_eq!(snapshot.status, RunStatus::Completed);
950 assert_eq!(snapshot.result_text.as_deref(), Some("done"));
951 assert_eq!(snapshot.event_count, 2);
952 assert_eq!(store.events(&run.id).await.len(), 2);
953 }
954
955 #[tokio::test]
956 async fn run_store_replaces_persisted_records() {
957 let source = InMemoryRunStore::new();
958 let run = source.create_run("session-1", "persist").await;
959 source
960 .record_event(
961 &run.id,
962 AgentEvent::Start {
963 prompt: "persist".to_string(),
964 },
965 )
966 .await;
967
968 let target = InMemoryRunStore::new();
969 target.replace_records(source.records().await).await;
970
971 assert_eq!(target.list().await.len(), 1);
972 assert_eq!(target.events(&run.id).await.len(), 1);
973 assert_eq!(target.snapshot(&run.id).await.unwrap().event_count, 1);
974 }
975
976 #[tokio::test]
977 async fn run_handle_only_cancels_current_run() {
978 let store = Arc::new(InMemoryRunStore::new());
979 let run = store.create_run("session-1", "fix tests").await;
980 let cancel_token = Arc::new(Mutex::new(Some(CancellationToken::new())));
981 let current_run_id = Arc::new(Mutex::new(Some(run.id.clone())));
982 let handle = RunHandle::new(
983 run.id.clone(),
984 run.session_id.clone(),
985 store.clone(),
986 cancel_token,
987 current_run_id.clone(),
988 None,
989 );
990
991 assert!(handle.cancel().await);
992 assert_eq!(handle.status().await, Some(RunStatus::Cancelled));
993
994 *current_run_id.lock().await = Some("other-run".to_string());
995 assert!(!handle.cancel().await);
996 }
997}