1use crate::llm::{Message, TokenUsage, ToolDefinition};
33use crate::planning::Task;
34use crate::prompts::PlanningMode;
35use crate::queue::SessionQueueConfig;
36use crate::run::RunRecord;
37use crate::tools::ArtifactStore;
38use crate::trace::TraceEvent;
39use crate::verification::VerificationReport;
40use anyhow::{Context, Result};
41use serde::{Deserialize, Serialize};
42use std::collections::HashMap;
43use std::path::{Path, PathBuf};
44use tokio::fs;
45use tokio::io::AsyncWriteExt;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
53pub enum SessionState {
54 #[default]
55 Unknown = 0,
56 Active = 1,
57 Paused = 2,
58 Completed = 3,
59 Error = 4,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct ContextUsage {
65 pub used_tokens: usize,
66 pub max_tokens: usize,
67 pub percent: f32,
68 pub turns: usize,
69}
70
71impl Default for ContextUsage {
72 fn default() -> Self {
73 Self {
74 used_tokens: 0,
75 max_tokens: 200_000,
76 percent: 0.0,
77 turns: 0,
78 }
79 }
80}
81
82pub const DEFAULT_AUTO_COMPACT_THRESHOLD: f32 = 0.80;
84
85fn default_auto_compact_threshold() -> f32 {
86 DEFAULT_AUTO_COMPACT_THRESHOLD
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct SessionConfig {
92 pub name: String,
93 pub workspace: String,
94 pub system_prompt: Option<String>,
95 pub max_context_length: u32,
96 pub auto_compact: bool,
97 #[serde(default = "default_auto_compact_threshold")]
100 pub auto_compact_threshold: f32,
101 #[serde(default)]
103 pub storage_type: crate::config::StorageBackend,
104 #[serde(skip_serializing_if = "Option::is_none")]
109 pub queue_config: Option<SessionQueueConfig>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub confirmation_policy: Option<crate::hitl::ConfirmationPolicy>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub permission_policy: Option<crate::permissions::PermissionPolicy>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub parent_id: Option<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub security_config: Option<crate::security::SecurityConfig>,
122 #[serde(skip)]
124 pub hook_engine: Option<std::sync::Arc<dyn crate::hooks::HookExecutor>>,
125 #[serde(default)]
127 pub planning_mode: PlanningMode,
128 #[serde(default)]
130 pub goal_tracking: bool,
131}
132
133impl Default for SessionConfig {
134 fn default() -> Self {
135 Self {
136 name: String::new(),
137 workspace: String::new(),
138 system_prompt: None,
139 max_context_length: 0,
140 auto_compact: false,
141 auto_compact_threshold: DEFAULT_AUTO_COMPACT_THRESHOLD,
142 storage_type: crate::config::StorageBackend::default(),
143 queue_config: None,
144 confirmation_policy: None,
145 permission_policy: None,
146 parent_id: None,
147 security_config: None,
148 hook_engine: None,
149 planning_mode: PlanningMode::default(),
150 goal_tracking: false,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct SessionData {
161 pub id: String,
163
164 pub config: SessionConfig,
166
167 pub state: SessionState,
169
170 pub messages: Vec<Message>,
172
173 pub context_usage: ContextUsage,
175
176 pub total_usage: TokenUsage,
178
179 #[serde(default)]
181 pub total_cost: f64,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub model_name: Option<String>,
186
187 #[serde(default)]
189 pub cost_records: Vec<crate::telemetry::LlmCostRecord>,
190
191 pub tool_names: Vec<String>,
193
194 pub thinking_enabled: bool,
196
197 pub thinking_budget: Option<usize>,
199
200 pub created_at: i64,
202
203 pub updated_at: i64,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub llm_config: Option<LlmConfigData>,
209
210 #[serde(default, alias = "todos")]
212 pub tasks: Vec<Task>,
213
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub parent_id: Option<String>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct LlmConfigData {
222 pub provider: String,
223 pub model: String,
224 #[serde(skip_serializing, default)]
226 pub api_key: Option<String>,
227 pub base_url: Option<String>,
228}
229
230impl SessionData {
231 pub fn tool_names_from_definitions(tools: &[ToolDefinition]) -> Vec<String> {
233 tools.iter().map(|t| t.name.clone()).collect()
234 }
235}
236
237#[async_trait::async_trait]
243pub trait SessionStore: Send + Sync {
244 async fn save(&self, session: &SessionData) -> Result<()>;
246
247 async fn load(&self, id: &str) -> Result<Option<SessionData>>;
249
250 async fn delete(&self, id: &str) -> Result<()>;
252
253 async fn list(&self) -> Result<Vec<String>>;
255
256 async fn exists(&self, id: &str) -> Result<bool>;
258
259 async fn save_artifacts(&self, _id: &str, _artifacts: &ArtifactStore) -> Result<()> {
261 Ok(())
262 }
263
264 async fn load_artifacts(&self, _id: &str) -> Result<Option<ArtifactStore>> {
266 Ok(None)
267 }
268
269 async fn save_trace_events(&self, _id: &str, _events: &[TraceEvent]) -> Result<()> {
271 Ok(())
272 }
273
274 async fn load_trace_events(&self, _id: &str) -> Result<Option<Vec<TraceEvent>>> {
276 Ok(None)
277 }
278
279 async fn save_run_records(&self, _id: &str, _records: &[RunRecord]) -> Result<()> {
281 Ok(())
282 }
283
284 async fn load_run_records(&self, _id: &str) -> Result<Option<Vec<RunRecord>>> {
286 Ok(None)
287 }
288
289 async fn save_verification_reports(
291 &self,
292 _id: &str,
293 _reports: &[VerificationReport],
294 ) -> Result<()> {
295 Ok(())
296 }
297
298 async fn load_verification_reports(
300 &self,
301 _id: &str,
302 ) -> Result<Option<Vec<VerificationReport>>> {
303 Ok(None)
304 }
305
306 async fn health_check(&self) -> Result<()> {
308 Ok(())
309 }
310
311 fn backend_name(&self) -> &str {
313 "unknown"
314 }
315}
316
317pub struct FileSessionStore {
330 dir: PathBuf,
332}
333
334impl FileSessionStore {
335 pub async fn new<P: AsRef<Path>>(dir: P) -> Result<Self> {
339 let dir = dir.as_ref().to_path_buf();
340
341 fs::create_dir_all(&dir)
343 .await
344 .with_context(|| format!("Failed to create session directory: {}", dir.display()))?;
345
346 Ok(Self { dir })
347 }
348
349 fn session_path(&self, id: &str) -> PathBuf {
351 self.dir.join(format!("{}.json", safe_session_id(id)))
353 }
354
355 fn artifact_dir(&self, id: &str) -> PathBuf {
356 self.dir.join("artifacts").join(safe_session_id(id))
357 }
358
359 fn trace_path(&self, id: &str) -> PathBuf {
360 self.dir
361 .join("traces")
362 .join(format!("{}.json", safe_session_id(id)))
363 }
364
365 fn verification_path(&self, id: &str) -> PathBuf {
366 self.dir
367 .join("verification")
368 .join(format!("{}.json", safe_session_id(id)))
369 }
370
371 fn runs_path(&self, id: &str) -> PathBuf {
372 self.dir
373 .join("runs")
374 .join(format!("{}.json", safe_session_id(id)))
375 }
376}
377
378fn safe_session_id(id: &str) -> String {
379 id.replace(['/', '\\'], "_").replace("..", "_")
380}
381
382#[async_trait::async_trait]
383impl SessionStore for FileSessionStore {
384 async fn save(&self, session: &SessionData) -> Result<()> {
385 let path = self.session_path(&session.id);
386
387 let json = serde_json::to_string_pretty(session)
389 .with_context(|| format!("Failed to serialize session: {}", session.id))?;
390
391 let unique_suffix = format!(
394 "{}.{}",
395 std::time::SystemTime::now()
396 .duration_since(std::time::UNIX_EPOCH)
397 .unwrap()
398 .as_nanos(),
399 std::process::id()
400 );
401 let temp_path = path.with_extension(format!("json.{}.tmp", unique_suffix));
402
403 let mut file = fs::File::create(&temp_path)
404 .await
405 .with_context(|| format!("Failed to create temp file: {}", temp_path.display()))?;
406
407 file.write_all(json.as_bytes())
408 .await
409 .with_context(|| format!("Failed to write session data: {}", session.id))?;
410
411 file.sync_all()
412 .await
413 .with_context(|| format!("Failed to sync session file: {}", session.id))?;
414
415 fs::rename(&temp_path, &path)
417 .await
418 .with_context(|| format!("Failed to rename session file: {}", session.id))?;
419
420 tracing::debug!("Saved session {} to {}", session.id, path.display());
421 Ok(())
422 }
423
424 async fn load(&self, id: &str) -> Result<Option<SessionData>> {
425 let path = self.session_path(id);
426
427 if !path.exists() {
428 return Ok(None);
429 }
430
431 let json = fs::read_to_string(&path)
432 .await
433 .with_context(|| format!("Failed to read session file: {}", path.display()))?;
434
435 let session: SessionData = serde_json::from_str(&json)
436 .with_context(|| format!("Failed to parse session file: {}", path.display()))?;
437
438 tracing::debug!("Loaded session {} from {}", id, path.display());
439 Ok(Some(session))
440 }
441
442 async fn delete(&self, id: &str) -> Result<()> {
443 let path = self.session_path(id);
444
445 if path.exists() {
446 fs::remove_file(&path)
447 .await
448 .with_context(|| format!("Failed to delete session file: {}", path.display()))?;
449
450 tracing::debug!("Deleted session {} from {}", id, path.display());
451 }
452
453 let artifact_dir = self.artifact_dir(id);
454 if artifact_dir.exists() {
455 fs::remove_dir_all(&artifact_dir).await.with_context(|| {
456 format!(
457 "Failed to delete artifact directory for session {}: {}",
458 id,
459 artifact_dir.display()
460 )
461 })?;
462 }
463
464 let trace_path = self.trace_path(id);
465 if trace_path.exists() {
466 fs::remove_file(&trace_path).await.with_context(|| {
467 format!(
468 "Failed to delete trace file for session {}: {}",
469 id,
470 trace_path.display()
471 )
472 })?;
473 }
474
475 let verification_path = self.verification_path(id);
476 if verification_path.exists() {
477 fs::remove_file(&verification_path).await.with_context(|| {
478 format!(
479 "Failed to delete verification report file for session {}: {}",
480 id,
481 verification_path.display()
482 )
483 })?;
484 }
485
486 let runs_path = self.runs_path(id);
487 if runs_path.exists() {
488 fs::remove_file(&runs_path).await.with_context(|| {
489 format!(
490 "Failed to delete run record file for session {}: {}",
491 id,
492 runs_path.display()
493 )
494 })?;
495 }
496
497 Ok(())
498 }
499
500 async fn list(&self) -> Result<Vec<String>> {
501 let mut session_ids = Vec::new();
502
503 let mut entries = fs::read_dir(&self.dir)
504 .await
505 .with_context(|| format!("Failed to read session directory: {}", self.dir.display()))?;
506
507 while let Some(entry) = entries.next_entry().await? {
508 let path = entry.path();
509
510 if path.extension().is_some_and(|ext| ext == "json") {
511 if let Some(stem) = path.file_stem() {
512 if let Some(id) = stem.to_str() {
513 session_ids.push(id.to_string());
514 }
515 }
516 }
517 }
518
519 Ok(session_ids)
520 }
521
522 async fn exists(&self, id: &str) -> Result<bool> {
523 let path = self.session_path(id);
524 Ok(path.exists())
525 }
526
527 async fn save_artifacts(&self, id: &str, artifacts: &ArtifactStore) -> Result<()> {
528 let artifact_dir = self.artifact_dir(id);
529 artifacts.save_to_dir(&artifact_dir).with_context(|| {
530 format!(
531 "Failed to save artifacts for session {} to {}",
532 id,
533 artifact_dir.display()
534 )
535 })
536 }
537
538 async fn load_artifacts(&self, id: &str) -> Result<Option<ArtifactStore>> {
539 let artifact_dir = self.artifact_dir(id);
540 if !artifact_dir.exists() {
541 return Ok(None);
542 }
543
544 let artifacts = ArtifactStore::load_from_dir(&artifact_dir).with_context(|| {
545 format!(
546 "Failed to load artifacts for session {} from {}",
547 id,
548 artifact_dir.display()
549 )
550 })?;
551 Ok(Some(artifacts))
552 }
553
554 async fn save_trace_events(&self, id: &str, events: &[TraceEvent]) -> Result<()> {
555 let path = self.trace_path(id);
556 if let Some(parent) = path.parent() {
557 fs::create_dir_all(parent).await.with_context(|| {
558 format!("Failed to create trace directory: {}", parent.display())
559 })?;
560 }
561
562 let json = serde_json::to_string_pretty(events)
563 .with_context(|| format!("Failed to serialize trace events for session {id}"))?;
564 fs::write(&path, json)
565 .await
566 .with_context(|| format!("Failed to write trace events to {}", path.display()))?;
567 Ok(())
568 }
569
570 async fn load_trace_events(&self, id: &str) -> Result<Option<Vec<TraceEvent>>> {
571 let path = self.trace_path(id);
572 if !path.exists() {
573 return Ok(None);
574 }
575
576 let json = fs::read_to_string(&path)
577 .await
578 .with_context(|| format!("Failed to read trace events from {}", path.display()))?;
579 let events = serde_json::from_str(&json)
580 .with_context(|| format!("Failed to parse trace events from {}", path.display()))?;
581 Ok(Some(events))
582 }
583
584 async fn save_run_records(&self, id: &str, records: &[RunRecord]) -> Result<()> {
585 let path = self.runs_path(id);
586 if let Some(parent) = path.parent() {
587 fs::create_dir_all(parent)
588 .await
589 .with_context(|| format!("Failed to create run directory: {}", parent.display()))?;
590 }
591
592 let json = serde_json::to_string_pretty(records)
593 .with_context(|| format!("Failed to serialize run records for session {id}"))?;
594 fs::write(&path, json)
595 .await
596 .with_context(|| format!("Failed to write run records to {}", path.display()))?;
597 Ok(())
598 }
599
600 async fn load_run_records(&self, id: &str) -> Result<Option<Vec<RunRecord>>> {
601 let path = self.runs_path(id);
602 if !path.exists() {
603 return Ok(None);
604 }
605
606 let json = fs::read_to_string(&path)
607 .await
608 .with_context(|| format!("Failed to read run records from {}", path.display()))?;
609 let records = serde_json::from_str(&json)
610 .with_context(|| format!("Failed to parse run records from {}", path.display()))?;
611 Ok(Some(records))
612 }
613
614 async fn save_verification_reports(
615 &self,
616 id: &str,
617 reports: &[VerificationReport],
618 ) -> Result<()> {
619 let path = self.verification_path(id);
620 if let Some(parent) = path.parent() {
621 fs::create_dir_all(parent).await.with_context(|| {
622 format!(
623 "Failed to create verification report directory: {}",
624 parent.display()
625 )
626 })?;
627 }
628
629 let json = serde_json::to_string_pretty(reports).with_context(|| {
630 format!("Failed to serialize verification reports for session {id}")
631 })?;
632 fs::write(&path, json).await.with_context(|| {
633 format!("Failed to write verification reports to {}", path.display())
634 })?;
635 Ok(())
636 }
637
638 async fn load_verification_reports(&self, id: &str) -> Result<Option<Vec<VerificationReport>>> {
639 let path = self.verification_path(id);
640 if !path.exists() {
641 return Ok(None);
642 }
643
644 let json = fs::read_to_string(&path).await.with_context(|| {
645 format!(
646 "Failed to read verification reports from {}",
647 path.display()
648 )
649 })?;
650 let reports = serde_json::from_str(&json).with_context(|| {
651 format!(
652 "Failed to parse verification reports from {}",
653 path.display()
654 )
655 })?;
656 Ok(Some(reports))
657 }
658
659 async fn health_check(&self) -> Result<()> {
660 let probe = self.dir.join(".health_check");
662 fs::write(&probe, b"ok")
663 .await
664 .with_context(|| format!("Store directory not writable: {}", self.dir.display()))?;
665 let _ = fs::remove_file(&probe).await;
666 Ok(())
667 }
668
669 fn backend_name(&self) -> &str {
670 "file"
671 }
672}
673
674pub struct MemorySessionStore {
680 sessions: tokio::sync::RwLock<HashMap<String, SessionData>>,
681 artifacts: tokio::sync::RwLock<HashMap<String, ArtifactStore>>,
682 trace_events: tokio::sync::RwLock<HashMap<String, Vec<TraceEvent>>>,
683 run_records: tokio::sync::RwLock<HashMap<String, Vec<RunRecord>>>,
684 verification_reports: tokio::sync::RwLock<HashMap<String, Vec<VerificationReport>>>,
685}
686
687impl MemorySessionStore {
688 pub fn new() -> Self {
689 Self {
690 sessions: tokio::sync::RwLock::new(HashMap::new()),
691 artifacts: tokio::sync::RwLock::new(HashMap::new()),
692 trace_events: tokio::sync::RwLock::new(HashMap::new()),
693 run_records: tokio::sync::RwLock::new(HashMap::new()),
694 verification_reports: tokio::sync::RwLock::new(HashMap::new()),
695 }
696 }
697}
698
699impl Default for MemorySessionStore {
700 fn default() -> Self {
701 Self::new()
702 }
703}
704
705#[async_trait::async_trait]
706impl SessionStore for MemorySessionStore {
707 async fn save(&self, session: &SessionData) -> Result<()> {
708 let mut sessions = self.sessions.write().await;
709 sessions.insert(session.id.clone(), session.clone());
710 Ok(())
711 }
712
713 async fn load(&self, id: &str) -> Result<Option<SessionData>> {
714 let sessions = self.sessions.read().await;
715 Ok(sessions.get(id).cloned())
716 }
717
718 async fn delete(&self, id: &str) -> Result<()> {
719 let mut sessions = self.sessions.write().await;
720 sessions.remove(id);
721 self.artifacts.write().await.remove(id);
722 self.trace_events.write().await.remove(id);
723 self.run_records.write().await.remove(id);
724 self.verification_reports.write().await.remove(id);
725 Ok(())
726 }
727
728 async fn list(&self) -> Result<Vec<String>> {
729 let sessions = self.sessions.read().await;
730 Ok(sessions.keys().cloned().collect())
731 }
732
733 async fn exists(&self, id: &str) -> Result<bool> {
734 let sessions = self.sessions.read().await;
735 Ok(sessions.contains_key(id))
736 }
737
738 async fn save_artifacts(&self, id: &str, artifacts: &ArtifactStore) -> Result<()> {
739 self.artifacts
740 .write()
741 .await
742 .insert(id.to_string(), artifacts.clone());
743 Ok(())
744 }
745
746 async fn load_artifacts(&self, id: &str) -> Result<Option<ArtifactStore>> {
747 Ok(self.artifacts.read().await.get(id).cloned())
748 }
749
750 async fn save_trace_events(&self, id: &str, events: &[TraceEvent]) -> Result<()> {
751 self.trace_events
752 .write()
753 .await
754 .insert(id.to_string(), events.to_vec());
755 Ok(())
756 }
757
758 async fn load_trace_events(&self, id: &str) -> Result<Option<Vec<TraceEvent>>> {
759 Ok(self.trace_events.read().await.get(id).cloned())
760 }
761
762 async fn save_run_records(&self, id: &str, records: &[RunRecord]) -> Result<()> {
763 self.run_records
764 .write()
765 .await
766 .insert(id.to_string(), records.to_vec());
767 Ok(())
768 }
769
770 async fn load_run_records(&self, id: &str) -> Result<Option<Vec<RunRecord>>> {
771 Ok(self.run_records.read().await.get(id).cloned())
772 }
773
774 async fn save_verification_reports(
775 &self,
776 id: &str,
777 reports: &[VerificationReport],
778 ) -> Result<()> {
779 self.verification_reports
780 .write()
781 .await
782 .insert(id.to_string(), reports.to_vec());
783 Ok(())
784 }
785
786 async fn load_verification_reports(&self, id: &str) -> Result<Option<Vec<VerificationReport>>> {
787 Ok(self.verification_reports.read().await.get(id).cloned())
788 }
789
790 fn backend_name(&self) -> &str {
791 "memory"
792 }
793}
794
795#[cfg(test)]
800mod tests {
801 use super::*;
802 use crate::hitl::ConfirmationPolicy;
803 use crate::permissions::PermissionPolicy;
804 use crate::prompts::PlanningMode;
805 use crate::queue::SessionQueueConfig;
806 use tempfile::tempdir;
807
808 fn create_test_session_data() -> SessionData {
809 SessionData {
810 id: "test-session-1".to_string(),
811 config: SessionConfig {
812 name: "Test Session".to_string(),
813 workspace: "/tmp/workspace".to_string(),
814 system_prompt: Some("You are helpful.".to_string()),
815 max_context_length: 200000,
816 auto_compact: false,
817 auto_compact_threshold: DEFAULT_AUTO_COMPACT_THRESHOLD,
818 storage_type: crate::config::StorageBackend::File,
819 queue_config: None,
820 confirmation_policy: None,
821 permission_policy: None,
822 parent_id: None,
823 security_config: None,
824 hook_engine: None,
825 planning_mode: PlanningMode::default(),
826 goal_tracking: false,
827 },
828 state: SessionState::Active,
829 messages: vec![
830 Message::user("Hello"),
831 Message {
832 role: "assistant".to_string(),
833 content: vec![crate::llm::ContentBlock::Text {
834 text: "Hi there!".to_string(),
835 }],
836 reasoning_content: None,
837 },
838 ],
839 context_usage: ContextUsage {
840 used_tokens: 100,
841 max_tokens: 200000,
842 percent: 0.0005,
843 turns: 2,
844 },
845 total_usage: TokenUsage {
846 prompt_tokens: 50,
847 completion_tokens: 50,
848 total_tokens: 100,
849 cache_read_tokens: None,
850 cache_write_tokens: None,
851 },
852 tool_names: vec!["bash".to_string(), "read".to_string()],
853 thinking_enabled: false,
854 thinking_budget: None,
855 created_at: 1700000000,
856 updated_at: 1700000100,
857 llm_config: None,
858 tasks: vec![],
859 parent_id: None,
860 total_cost: 0.0,
861 model_name: None,
862 cost_records: Vec::new(),
863 }
864 }
865
866 fn create_test_verification_report() -> VerificationReport {
867 VerificationReport::new(
868 "program:test",
869 vec![crate::verification::VerificationCheck::required(
870 "check:test",
871 "test",
872 "Run tests",
873 )
874 .with_status(crate::verification::VerificationStatus::Passed)],
875 )
876 }
877
878 async fn create_test_run_records() -> Vec<RunRecord> {
879 let runs = crate::run::InMemoryRunStore::new();
880 let run = runs.create_run("session/a", "persist run").await;
881 runs.record_event(
882 &run.id,
883 crate::agent::AgentEvent::Start {
884 prompt: "persist run".to_string(),
885 },
886 )
887 .await;
888 runs.records().await
889 }
890
891 #[tokio::test]
896 async fn test_file_store_save_and_load() {
897 let dir = tempdir().unwrap();
898 let store = FileSessionStore::new(dir.path()).await.unwrap();
899
900 let session = create_test_session_data();
901
902 store.save(&session).await.unwrap();
904
905 let loaded = store.load(&session.id).await.unwrap();
907 assert!(loaded.is_some());
908
909 let loaded = loaded.unwrap();
910 assert_eq!(loaded.id, session.id);
911 assert_eq!(loaded.config.name, session.config.name);
912 assert_eq!(loaded.messages.len(), 2);
913 assert_eq!(loaded.state, SessionState::Active);
914 }
915
916 #[tokio::test]
917 async fn test_file_store_load_nonexistent() {
918 let dir = tempdir().unwrap();
919 let store = FileSessionStore::new(dir.path()).await.unwrap();
920
921 let loaded = store.load("nonexistent").await.unwrap();
922 assert!(loaded.is_none());
923 }
924
925 #[tokio::test]
926 async fn test_file_store_delete() {
927 let dir = tempdir().unwrap();
928 let store = FileSessionStore::new(dir.path()).await.unwrap();
929
930 let session = create_test_session_data();
931 store.save(&session).await.unwrap();
932
933 assert!(store.exists(&session.id).await.unwrap());
935
936 store.delete(&session.id).await.unwrap();
938
939 assert!(!store.exists(&session.id).await.unwrap());
941 assert!(store.load(&session.id).await.unwrap().is_none());
942 }
943
944 #[tokio::test]
945 async fn test_file_store_save_and_load_artifacts() {
946 let dir = tempdir().unwrap();
947 let store = FileSessionStore::new(dir.path()).await.unwrap();
948 let artifacts = ArtifactStore::new();
949 artifacts.put(crate::tools::ToolArtifact {
950 artifact_id: "tool-output:test:a".to_string(),
951 artifact_uri: "a3s://tool-output/test/a".to_string(),
952 tool_name: "test".to_string(),
953 content: "artifact content".to_string(),
954 original_bytes: 16,
955 shown_bytes: 4,
956 });
957
958 store.save_artifacts("session/a", &artifacts).await.unwrap();
959 let loaded = store
960 .load_artifacts("session/a")
961 .await
962 .unwrap()
963 .expect("artifacts");
964
965 assert_eq!(loaded.len(), 1);
966 assert_eq!(
967 loaded
968 .get("a3s://tool-output/test/a")
969 .expect("artifact")
970 .content,
971 "artifact content"
972 );
973 }
974
975 #[tokio::test]
976 async fn test_file_store_save_and_load_trace_events() {
977 let dir = tempdir().unwrap();
978 let store = FileSessionStore::new(dir.path()).await.unwrap();
979 let event = TraceEvent::tool_execution(
980 "read",
981 true,
982 0,
983 std::time::Duration::from_millis(9),
984 12,
985 Some(&serde_json::json!({
986 "artifact": {
987 "artifact_uri": "a3s://tool-output/read/abc"
988 }
989 })),
990 );
991
992 store
993 .save_trace_events("session/a", std::slice::from_ref(&event))
994 .await
995 .unwrap();
996 let loaded = store
997 .load_trace_events("session/a")
998 .await
999 .unwrap()
1000 .expect("trace events");
1001
1002 assert_eq!(loaded, vec![event]);
1003 }
1004
1005 #[tokio::test]
1006 async fn test_file_store_save_and_load_run_records() {
1007 let dir = tempdir().unwrap();
1008 let store = FileSessionStore::new(dir.path()).await.unwrap();
1009 let records = create_test_run_records().await;
1010
1011 store.save_run_records("session/a", &records).await.unwrap();
1012 let loaded = store
1013 .load_run_records("session/a")
1014 .await
1015 .unwrap()
1016 .expect("run records");
1017
1018 assert_eq!(loaded.len(), 1);
1019 assert_eq!(loaded[0].snapshot.prompt, "persist run");
1020 assert_eq!(loaded[0].events.len(), 1);
1021 }
1022
1023 #[tokio::test]
1024 async fn test_file_store_save_and_load_verification_reports() {
1025 let dir = tempdir().unwrap();
1026 let store = FileSessionStore::new(dir.path()).await.unwrap();
1027 let report = create_test_verification_report();
1028
1029 store
1030 .save_verification_reports("session/a", std::slice::from_ref(&report))
1031 .await
1032 .unwrap();
1033 let loaded = store
1034 .load_verification_reports("session/a")
1035 .await
1036 .unwrap()
1037 .expect("verification reports");
1038
1039 assert_eq!(loaded, vec![report]);
1040 }
1041
1042 #[tokio::test]
1043 async fn test_memory_store_save_load_and_delete_artifacts() {
1044 let store = MemorySessionStore::new();
1045 let session = create_test_session_data();
1046 store.save(&session).await.unwrap();
1047 let artifacts = ArtifactStore::new();
1048 artifacts.put(crate::tools::ToolArtifact {
1049 artifact_id: "tool-output:test:a".to_string(),
1050 artifact_uri: "a3s://tool-output/test/a".to_string(),
1051 tool_name: "test".to_string(),
1052 content: "artifact content".to_string(),
1053 original_bytes: 16,
1054 shown_bytes: 4,
1055 });
1056
1057 store.save_artifacts(&session.id, &artifacts).await.unwrap();
1058 assert!(store
1059 .load_artifacts(&session.id)
1060 .await
1061 .unwrap()
1062 .expect("artifacts")
1063 .get("a3s://tool-output/test/a")
1064 .is_some());
1065
1066 store.delete(&session.id).await.unwrap();
1067 assert!(store.load_artifacts(&session.id).await.unwrap().is_none());
1068 }
1069
1070 #[tokio::test]
1071 async fn test_memory_store_save_load_and_delete_trace_events() {
1072 let store = MemorySessionStore::new();
1073 let session = create_test_session_data();
1074 let event = TraceEvent::tool_execution(
1075 "grep",
1076 false,
1077 1,
1078 std::time::Duration::from_millis(2),
1079 24,
1080 None,
1081 );
1082
1083 store.save(&session).await.unwrap();
1084 store
1085 .save_trace_events(&session.id, std::slice::from_ref(&event))
1086 .await
1087 .unwrap();
1088 let loaded = store
1089 .load_trace_events(&session.id)
1090 .await
1091 .unwrap()
1092 .expect("trace events");
1093 assert_eq!(loaded, vec![event]);
1094
1095 store.delete(&session.id).await.unwrap();
1096 assert!(store
1097 .load_trace_events(&session.id)
1098 .await
1099 .unwrap()
1100 .is_none());
1101 }
1102
1103 #[tokio::test]
1104 async fn test_memory_store_save_load_and_delete_run_records() {
1105 let store = MemorySessionStore::new();
1106 let session = create_test_session_data();
1107 let records = create_test_run_records().await;
1108
1109 store.save(&session).await.unwrap();
1110 store.save_run_records(&session.id, &records).await.unwrap();
1111 let loaded = store
1112 .load_run_records(&session.id)
1113 .await
1114 .unwrap()
1115 .expect("run records");
1116 assert_eq!(loaded.len(), 1);
1117 assert_eq!(loaded[0].events.len(), 1);
1118
1119 store.delete(&session.id).await.unwrap();
1120 assert!(store.load_run_records(&session.id).await.unwrap().is_none());
1121 }
1122
1123 #[tokio::test]
1124 async fn test_memory_store_save_load_and_delete_verification_reports() {
1125 let store = MemorySessionStore::new();
1126 let session = create_test_session_data();
1127 let report = create_test_verification_report();
1128
1129 store.save(&session).await.unwrap();
1130 store
1131 .save_verification_reports(&session.id, std::slice::from_ref(&report))
1132 .await
1133 .unwrap();
1134 let loaded = store
1135 .load_verification_reports(&session.id)
1136 .await
1137 .unwrap()
1138 .expect("verification reports");
1139 assert_eq!(loaded, vec![report]);
1140
1141 store.delete(&session.id).await.unwrap();
1142 assert!(store
1143 .load_verification_reports(&session.id)
1144 .await
1145 .unwrap()
1146 .is_none());
1147 }
1148
1149 #[tokio::test]
1150 async fn test_file_store_list() {
1151 let dir = tempdir().unwrap();
1152 let store = FileSessionStore::new(dir.path()).await.unwrap();
1153
1154 let list = store.list().await.unwrap();
1156 assert!(list.is_empty());
1157
1158 for i in 1..=3 {
1160 let mut session = create_test_session_data();
1161 session.id = format!("session-{}", i);
1162 store.save(&session).await.unwrap();
1163 }
1164
1165 let list = store.list().await.unwrap();
1167 assert_eq!(list.len(), 3);
1168 assert!(list.contains(&"session-1".to_string()));
1169 assert!(list.contains(&"session-2".to_string()));
1170 assert!(list.contains(&"session-3".to_string()));
1171 }
1172
1173 #[tokio::test]
1174 async fn test_file_store_overwrite() {
1175 let dir = tempdir().unwrap();
1176 let store = FileSessionStore::new(dir.path()).await.unwrap();
1177
1178 let mut session = create_test_session_data();
1179 store.save(&session).await.unwrap();
1180
1181 session.messages.push(Message::user("Another message"));
1183 session.updated_at = 1700000200;
1184 store.save(&session).await.unwrap();
1185
1186 let loaded = store.load(&session.id).await.unwrap().unwrap();
1188 assert_eq!(loaded.messages.len(), 3);
1189 assert_eq!(loaded.updated_at, 1700000200);
1190 }
1191
1192 #[tokio::test]
1193 async fn test_file_store_path_traversal_prevention() {
1194 let dir = tempdir().unwrap();
1195 let store = FileSessionStore::new(dir.path()).await.unwrap();
1196
1197 let mut session = create_test_session_data();
1199 session.id = "../../../etc/passwd".to_string();
1200 store.save(&session).await.unwrap();
1201
1202 let files: Vec<_> = std::fs::read_dir(dir.path())
1204 .unwrap()
1205 .filter_map(|e| e.ok())
1206 .collect();
1207 assert_eq!(files.len(), 1);
1208
1209 let loaded = store.load(&session.id).await.unwrap();
1211 assert!(loaded.is_some());
1212 }
1213
1214 #[tokio::test]
1215 async fn test_file_store_with_policies() {
1216 let dir = tempdir().unwrap();
1217 let store = FileSessionStore::new(dir.path()).await.unwrap();
1218
1219 let mut session = create_test_session_data();
1220 session.config.confirmation_policy = Some(ConfirmationPolicy::enabled());
1221 session.config.permission_policy = Some(PermissionPolicy::new().allow("Bash(cargo:*)"));
1222 session.config.queue_config = Some(SessionQueueConfig::default());
1223
1224 store.save(&session).await.unwrap();
1225
1226 let loaded = store.load(&session.id).await.unwrap().unwrap();
1227 assert!(loaded.config.confirmation_policy.is_some());
1228 assert!(loaded.config.permission_policy.is_some());
1229 assert!(loaded.config.queue_config.is_some());
1230 }
1231
1232 #[tokio::test]
1233 async fn test_file_store_with_llm_config() {
1234 let dir = tempdir().unwrap();
1235 let store = FileSessionStore::new(dir.path()).await.unwrap();
1236
1237 let mut session = create_test_session_data();
1238 session.llm_config = Some(LlmConfigData {
1239 provider: "anthropic".to_string(),
1240 model: "claude-3-5-sonnet-20241022".to_string(),
1241 api_key: Some("secret".to_string()), base_url: None,
1243 });
1244
1245 store.save(&session).await.unwrap();
1246
1247 let loaded = store.load(&session.id).await.unwrap().unwrap();
1248 let llm_config = loaded.llm_config.unwrap();
1249 assert_eq!(llm_config.provider, "anthropic");
1250 assert_eq!(llm_config.model, "claude-3-5-sonnet-20241022");
1251 assert!(llm_config.api_key.is_none());
1253 }
1254
1255 #[tokio::test]
1260 async fn test_memory_store_save_and_load() {
1261 let store = MemorySessionStore::new();
1262 let session = create_test_session_data();
1263
1264 store.save(&session).await.unwrap();
1265
1266 let loaded = store.load(&session.id).await.unwrap();
1267 assert!(loaded.is_some());
1268 assert_eq!(loaded.unwrap().id, session.id);
1269 }
1270
1271 #[tokio::test]
1272 async fn test_memory_store_delete() {
1273 let store = MemorySessionStore::new();
1274 let session = create_test_session_data();
1275
1276 store.save(&session).await.unwrap();
1277 assert!(store.exists(&session.id).await.unwrap());
1278
1279 store.delete(&session.id).await.unwrap();
1280 assert!(!store.exists(&session.id).await.unwrap());
1281 }
1282
1283 #[tokio::test]
1284 async fn test_memory_store_list() {
1285 let store = MemorySessionStore::new();
1286
1287 for i in 1..=3 {
1288 let mut session = create_test_session_data();
1289 session.id = format!("session-{}", i);
1290 store.save(&session).await.unwrap();
1291 }
1292
1293 let list = store.list().await.unwrap();
1294 assert_eq!(list.len(), 3);
1295 }
1296
1297 #[test]
1302 fn test_session_data_serialization() {
1303 let session = create_test_session_data();
1304 let json = serde_json::to_string(&session).unwrap();
1305 let parsed: SessionData = serde_json::from_str(&json).unwrap();
1306
1307 assert_eq!(parsed.id, session.id);
1308 assert_eq!(parsed.messages.len(), session.messages.len());
1309 }
1310
1311 #[test]
1312 fn test_tool_names_from_definitions() {
1313 let tools = vec![
1314 crate::llm::ToolDefinition {
1315 name: "bash".to_string(),
1316 description: "Execute bash".to_string(),
1317 parameters: serde_json::json!({}),
1318 },
1319 crate::llm::ToolDefinition {
1320 name: "read".to_string(),
1321 description: "Read file".to_string(),
1322 parameters: serde_json::json!({}),
1323 },
1324 ];
1325
1326 let names = SessionData::tool_names_from_definitions(&tools);
1327 assert_eq!(names, vec!["bash", "read"]);
1328 }
1329
1330 #[tokio::test]
1335 async fn test_file_store_backslash_sanitization() {
1336 let dir = tempdir().unwrap();
1337 let store = FileSessionStore::new(dir.path()).await.unwrap();
1338
1339 let mut session = create_test_session_data();
1340 session.id = r"foo\bar\baz".to_string();
1341 store.save(&session).await.unwrap();
1342
1343 let loaded = store.load(&session.id).await.unwrap();
1344 assert!(loaded.is_some());
1345
1346 let loaded = loaded.unwrap();
1347 assert_eq!(loaded.id, session.id);
1348
1349 let expected_path = dir.path().join("foo_bar_baz.json");
1351 assert!(expected_path.exists());
1352 }
1353
1354 #[tokio::test]
1355 async fn test_file_store_mixed_separator_sanitization() {
1356 let dir = tempdir().unwrap();
1357 let store = FileSessionStore::new(dir.path()).await.unwrap();
1358
1359 let mut session = create_test_session_data();
1360 session.id = r"foo/bar\baz..qux".to_string();
1361 store.save(&session).await.unwrap();
1362
1363 let loaded = store.load(&session.id).await.unwrap();
1364 assert!(loaded.is_some());
1365
1366 let loaded = loaded.unwrap();
1367 assert_eq!(loaded.id, session.id);
1368
1369 let expected_path = dir.path().join("foo_bar_baz_qux.json");
1371 assert!(expected_path.exists());
1372 }
1373
1374 #[tokio::test]
1379 async fn test_file_store_corrupted_json_recovery() {
1380 let dir = tempdir().unwrap();
1381 let store = FileSessionStore::new(dir.path()).await.unwrap();
1382
1383 let corrupted_path = dir.path().join("test-id.json");
1385 tokio::fs::write(&corrupted_path, b"not valid json {{{")
1386 .await
1387 .unwrap();
1388
1389 let result = store.load("test-id").await;
1391 assert!(result.is_err());
1392 }
1393
1394 #[tokio::test]
1399 async fn test_file_store_exists() {
1400 let dir = tempdir().unwrap();
1401 let store = FileSessionStore::new(dir.path()).await.unwrap();
1402
1403 let session = create_test_session_data();
1404
1405 assert!(!store.exists(&session.id).await.unwrap());
1407
1408 store.save(&session).await.unwrap();
1410 assert!(store.exists(&session.id).await.unwrap());
1411
1412 store.delete(&session.id).await.unwrap();
1414 assert!(!store.exists(&session.id).await.unwrap());
1415 }
1416
1417 #[tokio::test]
1418 async fn test_memory_store_exists() {
1419 let store = MemorySessionStore::new();
1420
1421 assert!(!store.exists("unknown-id").await.unwrap());
1423
1424 let session = create_test_session_data();
1426 store.save(&session).await.unwrap();
1427 assert!(store.exists(&session.id).await.unwrap());
1428 }
1429
1430 #[tokio::test]
1431 async fn test_file_store_health_check() {
1432 let dir = tempfile::tempdir().unwrap();
1433 let store = FileSessionStore::new(dir.path()).await.unwrap();
1434 assert!(store.health_check().await.is_ok());
1435 assert_eq!(store.backend_name(), "file");
1436 }
1437
1438 #[tokio::test]
1439 async fn test_file_store_health_check_bad_dir() {
1440 let store = FileSessionStore {
1441 dir: std::path::PathBuf::from("/nonexistent/path/that/does/not/exist"),
1442 };
1443 assert!(store.health_check().await.is_err());
1444 }
1445
1446 #[tokio::test]
1447 async fn test_memory_store_health_check() {
1448 let store = MemorySessionStore::new();
1449 assert!(store.health_check().await.is_ok());
1450 assert_eq!(store.backend_name(), "memory");
1451 }
1452
1453 #[tokio::test]
1458 async fn test_file_store_load_empty_file() {
1459 let dir = tempdir().unwrap();
1460 let store = FileSessionStore::new(dir.path()).await.unwrap();
1461
1462 let empty_path = dir.path().join("empty-session.json");
1464 tokio::fs::write(&empty_path, b"").await.unwrap();
1465
1466 let result = store.load("empty-session").await;
1467 assert!(
1468 result.is_err(),
1469 "Empty file must return error, not Ok(None)"
1470 );
1471 }
1472
1473 #[tokio::test]
1474 async fn test_file_store_load_partial_json() {
1475 let dir = tempdir().unwrap();
1476 let store = FileSessionStore::new(dir.path()).await.unwrap();
1477
1478 let partial_path = dir.path().join("partial-session.json");
1480 tokio::fs::write(&partial_path, b"{\"id\":\"partial-session\",\"message")
1481 .await
1482 .unwrap();
1483
1484 let result = store.load("partial-session").await;
1485 assert!(result.is_err(), "Partial JSON must return error");
1486 }
1487
1488 #[tokio::test]
1489 async fn test_file_store_concurrent_save() {
1490 let dir = tempdir().unwrap();
1491 let store = std::sync::Arc::new(FileSessionStore::new(dir.path()).await.unwrap());
1492
1493 let session = create_test_session_data();
1494 let id = session.id.clone();
1495
1496 store.save(&session).await.unwrap();
1498
1499 let mut handles = Vec::new();
1501 for _ in 0..5 {
1502 let s = store.clone();
1503 let sess = session.clone();
1504 handles.push(tokio::spawn(async move { s.save(&sess).await }));
1505 }
1506 for h in handles {
1507 h.await.unwrap().unwrap();
1508 }
1509
1510 let loaded = store.load(&id).await.unwrap();
1512 assert!(loaded.is_some());
1513 assert_eq!(loaded.unwrap().id, id);
1514 }
1515
1516 #[tokio::test]
1517 async fn test_file_store_load_nonexistent_returns_none() {
1518 let dir = tempdir().unwrap();
1519 let store = FileSessionStore::new(dir.path()).await.unwrap();
1520
1521 let result = store.load("does-not-exist-at-all").await.unwrap();
1522 assert!(result.is_none(), "Missing session must return Ok(None)");
1523 }
1524}