agtrace_providers/codex/
discovery.rs1use crate::traits::{LogDiscovery, ProbeResult, SessionIndex};
2use anyhow::Result;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7use super::io::{extract_codex_header, is_empty_codex_session};
8
9pub struct CodexDiscovery;
10
11impl LogDiscovery for CodexDiscovery {
12 fn id(&self) -> &'static str {
13 "codex"
14 }
15
16 fn probe(&self, path: &Path) -> ProbeResult {
17 if !path.is_file() {
18 return ProbeResult::NoMatch;
19 }
20
21 if let Ok(metadata) = std::fs::metadata(path)
22 && metadata.len() == 0
23 {
24 return ProbeResult::NoMatch;
25 }
26
27 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
28 let filename = path.file_name().and_then(|f| f.to_str()).unwrap_or("");
29
30 if is_jsonl && filename.starts_with("rollout-") && !is_empty_codex_session(path) {
31 ProbeResult::match_high()
32 } else {
33 ProbeResult::NoMatch
34 }
35 }
36
37 fn resolve_log_root(&self, _project_root: &Path) -> Option<PathBuf> {
38 None
39 }
40
41 fn scan_sessions(&self, log_root: &Path) -> Result<Vec<SessionIndex>> {
42 let mut sessions: HashMap<String, SessionIndex> = HashMap::new();
43
44 if !log_root.exists() {
45 return Ok(Vec::new());
46 }
47
48 for entry in WalkDir::new(log_root).into_iter().filter_map(|e| e.ok()) {
49 let path = entry.path();
50
51 if self.probe(path) == ProbeResult::NoMatch {
52 continue;
53 }
54
55 let header = match extract_codex_header(path) {
56 Ok(h) => h,
57 Err(_) => continue,
58 };
59
60 let session_id = match header.session_id {
61 Some(id) => id,
62 None => continue,
63 };
64
65 sessions
66 .entry(session_id.clone())
67 .or_insert_with(|| SessionIndex {
68 session_id: session_id.clone(),
69 timestamp: header.timestamp.clone(),
70 latest_mod_time: None, main_file: path.to_path_buf(),
72 sidechain_files: Vec::new(),
73 project_root: header.cwd.clone().map(PathBuf::from),
74 snippet: header.snippet.clone(),
75 });
76 }
77
78 for session in sessions.values_mut() {
82 let all_files = vec![session.main_file.as_path()];
83 session.latest_mod_time = crate::get_latest_mod_time_rfc3339(&all_files);
84 }
85
86 Ok(sessions.into_values().collect())
87 }
88
89 fn extract_session_id(&self, path: &Path) -> Result<String> {
90 let header = extract_codex_header(path)?;
91 header
92 .session_id
93 .ok_or_else(|| anyhow::anyhow!("No session_id in file: {}", path.display()))
94 }
95
96 fn extract_project_hash(&self, path: &Path) -> Result<Option<agtrace_types::ProjectHash>> {
97 let header = extract_codex_header(path)?;
98 Ok(header
99 .cwd
100 .map(|cwd| agtrace_types::project_hash_from_root(&cwd)))
101 }
102
103 fn find_session_files(&self, log_root: &Path, session_id: &str) -> Result<Vec<PathBuf>> {
104 let mut matching_files = Vec::new();
105
106 for entry in WalkDir::new(log_root).into_iter().filter_map(|e| e.ok()) {
107 let path = entry.path();
108
109 if self.probe(path) == ProbeResult::NoMatch {
110 continue;
111 }
112
113 if let Ok(header) = extract_codex_header(path)
114 && header.session_id.as_deref() == Some(session_id)
115 {
116 matching_files.push(path.to_path_buf());
117 }
118 }
119
120 Ok(matching_files)
121 }
122}