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