Skip to main content

ytil_agents/agent/
session_loader.rs

1use std::collections::HashSet;
2use std::path::Path;
3use std::path::PathBuf;
4
5use rootcause::prelude::ResultExt;
6
7use crate::agent::Agent;
8use crate::agent::session::Session;
9use crate::agent::session::SessionKey;
10
11pub mod claude;
12pub mod codex;
13pub mod cursor;
14
15/// Load resumable sessions from every supported local agent store.
16///
17/// # Errors
18/// Returns an error when any supported session store cannot be read or parsed.
19pub fn load_sessions() -> rootcause::Result<Vec<Session>> {
20    let mut sessions = Vec::new();
21    sessions.extend(claude::load_sessions()?);
22    sessions.extend(codex::load_sessions()?);
23    sessions.extend(cursor::load_sessions()?);
24    Ok(sessions)
25}
26
27/// Load only the requested resumable sessions from their owning agent stores.
28///
29/// # Errors
30/// Returns an error when a matching supported session cannot be read or parsed.
31pub fn load_sessions_by_key(keys: &[SessionKey]) -> rootcause::Result<Vec<Session>> {
32    let mut sessions = Vec::new();
33    sessions.extend(claude::load_sessions_by_key(keys)?);
34    sessions.extend(codex::load_sessions_by_key(keys)?);
35    sessions.extend(cursor::load_sessions_by_key(keys)?);
36    Ok(sessions)
37}
38
39fn requested_ids(keys: &[SessionKey], agent: Agent) -> HashSet<&str> {
40    keys.iter()
41        .filter(|key| key.agent() == agent)
42        .map(SessionKey::id)
43        .collect()
44}
45
46fn find_session_paths(
47    root: &Path,
48    matching_file_fn: impl Fn(&std::fs::DirEntry) -> bool,
49    skip_dir_fn: impl Fn(&std::fs::DirEntry) -> bool,
50) -> rootcause::Result<Vec<PathBuf>> {
51    if !root.exists() {
52        return Ok(Vec::new());
53    }
54
55    ytil_sys::file::find_matching_recursively_in_dir(root, matching_file_fn, skip_dir_fn)
56}
57
58fn file_updated_at(path: &Path) -> rootcause::Result<Option<chrono::DateTime<chrono::Utc>>> {
59    let modified = std::fs::metadata(path)
60        .context("failed to read session metadata")
61        .attach_with(|| format!("path={}", path.display()))?
62        .modified()
63        .context("failed to read session modified time")
64        .attach_with(|| format!("path={}", path.display()))?;
65    Ok(Some(chrono::DateTime::<chrono::Utc>::from(modified)))
66}
67
68#[cfg(test)]
69mod tests {
70    #[test]
71    fn test_find_session_paths_missing_root_returns_empty_paths() {
72        let dir = tempfile::tempdir().unwrap();
73        let missing_root = dir.path().join("missing");
74
75        let res = crate::agent::session_loader::find_session_paths(&missing_root, |_| true, |_| false);
76
77        assert2::assert!(let Ok(paths) = res);
78        pretty_assertions::assert_eq!(paths, Vec::<std::path::PathBuf>::new());
79    }
80
81    #[test]
82    fn test_find_session_paths_existing_file_root_returns_error() {
83        let dir = tempfile::tempdir().unwrap();
84        let file_root = dir.path().join("file");
85        std::fs::write(&file_root, b"not a directory").unwrap();
86
87        let res = crate::agent::session_loader::find_session_paths(&file_root, |_| true, |_| false);
88
89        assert2::assert!(let Err(err) = res);
90        assert!(err.to_string().contains("error reading directory"));
91    }
92}