Skip to main content

ytil_agents/agent/session_loader/
codex.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
11/// Load Codex sessions from the local Codex session store.
12///
13/// # Errors
14/// Returns an error when the Codex sessions directory cannot be read or a
15/// session file cannot be parsed.
16pub fn load_sessions() -> rootcause::Result<Vec<Session>> {
17    let root = ytil_sys::dir::build_home_path(Agent::Codex.sessions_root_path())?;
18    let session_paths = crate::agent::session_loader::find_session_paths(
19        &root,
20        |entry| entry.path().extension().is_some_and(|ext| ext == "jsonl"),
21        |_| false,
22    )?;
23
24    load_sessions_from_paths(session_paths, |_| true)
25}
26
27/// Load only requested Codex sessions from the local Codex session store.
28///
29/// # Errors
30/// Returns an error when a matching Codex session file cannot be read or parsed.
31pub fn load_sessions_by_key(keys: &[SessionKey]) -> rootcause::Result<Vec<Session>> {
32    let root = ytil_sys::dir::build_home_path(Agent::Codex.sessions_root_path())?;
33    load_sessions_from_root_by_key(&root, keys)
34}
35
36fn load_sessions_from_root_by_key(root: &Path, keys: &[SessionKey]) -> rootcause::Result<Vec<Session>> {
37    let requested_ids = crate::agent::session_loader::requested_ids(keys, Agent::Codex);
38    if requested_ids.is_empty() {
39        return Ok(Vec::new());
40    }
41    let session_paths = crate::agent::session_loader::find_session_paths(
42        root,
43        |entry| codex_session_path_matches_requested_id(&entry.path(), &requested_ids),
44        |_| false,
45    )?;
46
47    load_sessions_from_paths(session_paths, |session| requested_ids.contains(session.id.as_str()))
48}
49
50fn load_sessions_from_paths(
51    session_paths: Vec<PathBuf>,
52    keep_session: impl Fn(&Session) -> bool,
53) -> rootcause::Result<Vec<Session>> {
54    let mut sessions = Vec::new();
55    for session_path in session_paths {
56        let content = std::fs::read_to_string(&session_path)
57            .context("failed to read Codex session file")
58            .attach_with(|| format!("path={}", session_path.display()))?;
59        let session_name = session_path
60            .file_stem()
61            .and_then(|name| name.to_str())
62            .unwrap_or_default();
63        let codex_session = crate::agent::session_parser::codex::parse(&content, session_name)
64            .attach_with(|| format!("path={}", session_path.display()))?;
65        if codex_session.is_subagent {
66            continue;
67        }
68        let session = codex_session.into_session(session_path);
69        if session.workspace.is_dir() && keep_session(&session) {
70            sessions.push(session);
71        }
72    }
73
74    Ok(sessions)
75}
76
77fn codex_session_path_matches_requested_id(path: &Path, requested_ids: &HashSet<&str>) -> bool {
78    path.extension().is_some_and(|ext| ext == "jsonl")
79        && path.file_stem().and_then(|name| name.to_str()).is_some_and(|stem| {
80            requested_ids
81                .iter()
82                .any(|id| stem == *id || stem.strip_suffix(id).is_some_and(|prefix| prefix.ends_with('-')))
83        })
84}
85
86#[cfg(test)]
87mod tests {
88    use tempfile::tempdir;
89
90    use super::*;
91
92    #[test]
93    fn test_load_sessions_from_root_by_key_only_parses_matching_codex_files() {
94        let dir = tempdir().expect("tempdir should be created");
95        let root = dir.path().join("sessions");
96        let workspace = dir.path().join("workspace");
97        std::fs::create_dir_all(&root).expect("session root should be created");
98        std::fs::create_dir_all(&workspace).expect("workspace should be created");
99        std::fs::write(
100            root.join("rollout-2026-01-01-target.jsonl"),
101            codex_content("target", &workspace),
102        )
103        .expect("target session should be written");
104        std::fs::write(root.join("rollout-2026-01-01-other.jsonl"), "not json\n")
105            .expect("nonmatching session should be written");
106
107        assert2::assert!(
108            let Ok(sessions) = load_sessions_from_root_by_key(&root, &[SessionKey::new(Agent::Codex, "target")])
109        );
110
111        pretty_assertions::assert_eq!(sessions.len(), 1);
112        pretty_assertions::assert_eq!(sessions[0].id, "target");
113    }
114
115    fn codex_content(id: &str, workspace: &Path) -> String {
116        format!(
117            "{{\"timestamp\":\"2026-03-20T06:30:20.312Z\",\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"timestamp\":\"2026-03-20T06:30:20.312Z\",\"cwd\":\"{}\"}}}}\n",
118            workspace.display()
119        )
120    }
121}