agtrace_runtime/ops/
index.rs1use crate::{Error, Result};
2use agtrace_index::{Database, LogFileRecord, ProjectRecord, SessionRecord};
3use agtrace_providers::ProviderAdapter;
4use std::collections::HashSet;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone)]
8pub enum IndexProgress {
9 IncrementalHint {
10 indexed_files: usize,
11 },
12 LogRootMissing {
13 provider_name: String,
14 log_root: PathBuf,
15 },
16 ProviderScanning {
17 provider_name: String,
18 },
19 ProviderSessionCount {
20 provider_name: String,
21 count: usize,
22 project_hash: String,
23 all_projects: bool,
24 },
25 SessionRegistered {
26 session_id: String,
27 },
28 Completed {
29 total_sessions: usize,
30 scanned_files: usize,
31 skipped_files: usize,
32 },
33}
34
35pub struct IndexService<'a> {
36 db: &'a Database,
37 providers: Vec<(ProviderAdapter, PathBuf)>,
38}
39
40impl<'a> IndexService<'a> {
41 pub fn new(db: &'a Database, providers: Vec<(ProviderAdapter, PathBuf)>) -> Self {
42 Self { db, providers }
43 }
44
45 pub fn run<F>(
46 &self,
47 scope: agtrace_types::ProjectScope,
48 force: bool,
49 mut on_progress: F,
50 ) -> Result<()>
51 where
52 F: FnMut(IndexProgress),
53 {
54 let indexed_files = if force {
55 HashSet::new()
56 } else {
57 self.db
58 .get_all_log_files()?
59 .into_iter()
60 .filter_map(|f| {
61 if should_skip_indexed_file(&f) {
62 Some(f.path)
63 } else {
64 None
65 }
66 })
67 .collect::<HashSet<_>>()
68 };
69
70 if !force {
71 on_progress(IndexProgress::IncrementalHint {
72 indexed_files: indexed_files.len(),
73 });
74 }
75
76 let mut total_sessions = 0;
77 let mut scanned_files = 0;
78 let mut skipped_files = 0;
79
80 for (provider, log_root) in &self.providers {
81 let provider_name = provider.id();
82
83 if !log_root.exists() {
84 on_progress(IndexProgress::LogRootMissing {
85 provider_name: provider_name.to_string(),
86 log_root: log_root.clone(),
87 });
88 continue;
89 }
90
91 on_progress(IndexProgress::ProviderScanning {
92 provider_name: provider_name.to_string(),
93 });
94
95 let sessions = provider
96 .discovery
97 .scan_sessions(log_root)
98 .map_err(Error::Provider)?;
99
100 let filtered_sessions: Vec<_> = sessions
102 .into_iter()
103 .filter(|session| {
104 if let Some(expected_hash) = scope.hash() {
105 if let Some(session_root) = &session.project_root {
106 let session_hash = agtrace_core::project_hash_from_root(&session_root.to_string_lossy());
107 &session_hash == expected_hash
108 } else {
109 if provider_name == "gemini" {
111 use agtrace_providers::gemini::io::extract_project_hash_from_gemini_file;
112 if let Some(session_hash) = extract_project_hash_from_gemini_file(&session.main_file) {
113 &session_hash == expected_hash
114 } else {
115 false
116 }
117 } else {
118 false
119 }
120 }
121 } else {
122 true
123 }
124 })
125 .collect();
126
127 on_progress(IndexProgress::ProviderSessionCount {
128 provider_name: provider_name.to_string(),
129 count: filtered_sessions.len(),
130 project_hash: match &scope {
131 agtrace_types::ProjectScope::All => "<all>".to_string(),
132 agtrace_types::ProjectScope::Specific(hash) => hash.to_string(),
133 },
134 all_projects: matches!(scope, agtrace_types::ProjectScope::All),
135 });
136
137 let mut filtered_sessions = filtered_sessions;
140 filtered_sessions.sort_by_key(|s| s.parent_session_id.is_some());
141
142 for session in filtered_sessions {
143 let mut all_files = vec![session.main_file.display().to_string()];
145 for side_file in &session.sidechain_files {
146 all_files.push(side_file.display().to_string());
147 }
148
149 let all_files_unchanged =
150 !force && all_files.iter().all(|f| indexed_files.contains(f));
151
152 if all_files_unchanged {
153 skipped_files += all_files.len();
154 continue;
155 }
156
157 on_progress(IndexProgress::SessionRegistered {
158 session_id: session.session_id.clone(),
159 });
160
161 let session_project_hash = if let Some(ref root) = session.project_root {
163 agtrace_core::project_hash_from_root(&root.to_string_lossy())
164 } else if provider_name == "gemini" {
165 use agtrace_providers::gemini::io::extract_project_hash_from_gemini_file;
167 extract_project_hash_from_gemini_file(&session.main_file).unwrap_or_else(|| {
168 agtrace_core::project_hash_from_log_path(&session.main_file)
169 })
170 } else {
171 agtrace_core::project_hash_from_log_path(&session.main_file)
173 };
174
175 let project_record = ProjectRecord {
176 hash: session_project_hash.clone(),
177 root_path: session
178 .project_root
179 .as_ref()
180 .map(|p| p.to_string_lossy().to_string()),
181 last_scanned_at: Some(chrono::Utc::now().to_rfc3339()),
182 };
183 self.db.insert_or_update_project(&project_record)?;
184
185 let session_record = SessionRecord {
186 id: session.session_id.clone(),
187 project_hash: session_project_hash,
188 provider: provider_name.to_string(),
189 start_ts: session.timestamp.clone(),
190 end_ts: None,
191 snippet: session.snippet.clone(),
192 is_valid: true,
193 parent_session_id: session.parent_session_id.clone(),
194 spawned_by: session.spawned_by.clone(),
195 };
196 self.db.insert_or_update_session(&session_record)?;
197
198 let to_log_file_record = |path: &PathBuf, role: &str| -> Result<LogFileRecord> {
200 let meta = std::fs::metadata(path).ok();
201 Ok(LogFileRecord {
202 path: path.display().to_string(),
203 session_id: session.session_id.clone(),
204 role: role.to_string(),
205 file_size: meta.as_ref().map(|m| m.len() as i64),
206 mod_time: meta
207 .and_then(|m| m.modified().ok())
208 .map(|t| format!("{:?}", t)),
209 })
210 };
211
212 scanned_files += 1;
213 let main_log_file = to_log_file_record(&session.main_file, "main")?;
214 self.db.insert_or_update_log_file(&main_log_file)?;
215
216 for side_file in &session.sidechain_files {
218 scanned_files += 1;
219 let side_log_file = to_log_file_record(side_file, "sidechain")?;
220 self.db.insert_or_update_log_file(&side_log_file)?;
221 }
222
223 total_sessions += 1;
224 }
225 }
226
227 on_progress(IndexProgress::Completed {
228 total_sessions,
229 scanned_files,
230 skipped_files,
231 });
232
233 Ok(())
234 }
235}
236
237fn should_skip_indexed_file(indexed: &LogFileRecord) -> bool {
238 use std::path::Path;
239
240 let path = Path::new(&indexed.path);
241
242 if !path.exists() {
243 return false;
244 }
245
246 let metadata = match std::fs::metadata(path) {
247 Ok(m) => m,
248 Err(_) => return false,
249 };
250
251 if let Some(db_size) = indexed.file_size {
252 if db_size != metadata.len() as i64 {
253 return false;
254 }
255 } else {
256 return false;
257 }
258
259 if let Some(db_mod_time) = &indexed.mod_time {
260 if let Ok(fs_mod_time) = metadata.modified() {
261 let fs_mod_time_str = format!("{:?}", fs_mod_time);
262 if db_mod_time != &fs_mod_time_str {
263 return false;
264 }
265 } else {
266 return false;
267 }
268 } else {
269 return false;
270 }
271
272 true
273}