1use super::*;
2
3use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6#[derive(Debug, Clone)]
8enum CachedNativeMetadata {
9 Claude(Option<(String, PathBuf, String)>),
11 Codex(Option<CodexSessionMetadata>),
13 IndexEligible(bool),
14}
15
16#[derive(Debug)]
17struct CachedNativeEntry {
18 modified_at: SystemTime,
19 size_bytes: u64,
20 metadata: CachedNativeMetadata,
21}
22
23#[derive(Debug, Default)]
24struct NativeScanCacheInner {
25 entries: HashMap<PathBuf, CachedNativeEntry>,
26 parsed_files: u64,
27}
28
29#[derive(Debug, Clone, Default)]
34pub struct NativeScanCache {
35 inner: Arc<Mutex<NativeScanCacheInner>>,
36}
37
38impl NativeScanCache {
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn parsed_files(&self) -> u64 {
45 self.lock().parsed_files
46 }
47
48 pub(crate) fn index_eligible(&self, kind: HarnessKind, path: &Path) -> Result<bool> {
51 if kind == HarnessKind::Claude
52 && path
53 .components()
54 .any(|component| component.as_os_str() == "subagents")
55 {
56 return Ok(false);
57 }
58 let metadata = fs::metadata(path)?;
59 let modified_at = metadata.modified()?;
60 let size_bytes = metadata.len();
61 if let Some(CachedNativeMetadata::IndexEligible(eligible)) =
62 self.cached(path, modified_at, size_bytes)
63 {
64 return Ok(eligible);
65 }
66 let eligible = match kind {
67 HarnessKind::Codex => !codex::codex_session_summary(path)?.subagent,
68 HarnessKind::Claude => !claude::claude_native_summary(path)?.subagent,
69 other => bail!("{other:?} has its own top-level source enumeration"),
70 };
71 self.store(
72 path,
73 modified_at,
74 size_bytes,
75 CachedNativeMetadata::IndexEligible(eligible),
76 );
77 Ok(eligible)
78 }
79
80 fn lock(&self) -> std::sync::MutexGuard<'_, NativeScanCacheInner> {
81 self.inner
82 .lock()
83 .unwrap_or_else(|poisoned| poisoned.into_inner())
84 }
85
86 fn cached(
87 &self,
88 path: &Path,
89 modified_at: SystemTime,
90 size_bytes: u64,
91 ) -> Option<CachedNativeMetadata> {
92 let inner = self.lock();
93 let entry = inner.entries.get(path)?;
94 (entry.modified_at == modified_at && entry.size_bytes == size_bytes)
95 .then(|| entry.metadata.clone())
96 }
97
98 fn store(
99 &self,
100 path: &Path,
101 modified_at: SystemTime,
102 size_bytes: u64,
103 metadata: CachedNativeMetadata,
104 ) {
105 let mut inner = self.lock();
106 inner.parsed_files = inner.parsed_files.saturating_add(1);
107 inner.entries.insert(
108 path.to_owned(),
109 CachedNativeEntry {
110 modified_at,
111 size_bytes,
112 metadata,
113 },
114 );
115 }
116
117 pub(super) fn claude_metadata(
120 &self,
121 path: &Path,
122 modified_at: SystemTime,
123 size_bytes: u64,
124 parse: impl FnOnce() -> Result<Option<(String, PathBuf, String)>>,
125 ) -> Result<Option<(String, PathBuf, String)>> {
126 if let Some(CachedNativeMetadata::Claude(metadata)) =
127 self.cached(path, modified_at, size_bytes)
128 {
129 return Ok(metadata);
130 }
131 let metadata = parse()?;
132 self.store(
133 path,
134 modified_at,
135 size_bytes,
136 CachedNativeMetadata::Claude(metadata.clone()),
137 );
138 Ok(metadata)
139 }
140
141 pub(super) fn codex_metadata(
143 &self,
144 path: &Path,
145 modified_at: SystemTime,
146 size_bytes: u64,
147 parse: impl FnOnce() -> Result<Option<CodexSessionMetadata>>,
148 ) -> Result<Option<CodexSessionMetadata>> {
149 if let Some(CachedNativeMetadata::Codex(metadata)) =
150 self.cached(path, modified_at, size_bytes)
151 {
152 return Ok(metadata);
153 }
154 let metadata = parse()?;
155 self.store(
156 path,
157 modified_at,
158 size_bytes,
159 CachedNativeMetadata::Codex(metadata.clone()),
160 );
161 Ok(metadata)
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct LocatedNativeSession {
169 pub native_session_id: String,
170 pub source_path: PathBuf,
171}
172
173#[derive(Debug, Clone)]
175pub struct NativeSessionListing {
176 pub native_session_id: String,
177 pub title: String,
178 pub modified_at: SystemTime,
179 pub git_branch: String,
180 pub size_bytes: u64,
181 pub cwd: PathBuf,
182 pub unavailable_reason: Option<&'static str>,
184 pub natively_archived: bool,
186}
187
188pub fn locate_native_session(
190 harness: HarnessKind,
191 home: &Path,
192 selection: &ClaudeSessionSelection,
193) -> Result<LocatedNativeSession> {
194 let (native_session_id, source_path) = match harness {
195 HarnessKind::Muse => {
196 return muse::locate(&mj_checkpoint::native::muse_sessions_root(home)?, selection);
197 }
198 HarnessKind::Codex => {
199 let located = locate_codex_session(home, selection)?;
200 (located.native_session_id, located.jsonl_path)
201 }
202 HarnessKind::Claude => {
203 let located = locate_claude_session(home, selection)?;
204 (located.native_session_id, located.jsonl_path)
205 }
206 HarnessKind::Kimi => {
207 let located = locate_kimi_session(home, selection)?;
208 (located.native_session_id, located.session_path)
209 }
210 HarnessKind::Grok => {
211 let located = locate_grok_session(home, selection)?;
212 (located.native_session_id, located.session_path)
213 }
214 };
215 Ok(LocatedNativeSession {
216 native_session_id,
217 source_path,
218 })
219}
220
221pub fn native_session_id_from_path(harness: HarnessKind, path: &Path) -> Option<String> {
232 match harness {
233 HarnessKind::Claude => path.file_stem()?.to_str().map(str::to_owned),
234 HarnessKind::Codex => codex_rollout_id_from_path(path).map(str::to_owned),
235 HarnessKind::Kimi | HarnessKind::Grok => path.file_name()?.to_str().map(str::to_owned),
236 HarnessKind::Muse => path.parent()?.file_name()?.to_str().map(str::to_owned),
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct NativeSessionSource {
246 pub native_session_id: String,
247 pub source_path: PathBuf,
248 pub modified_at: SystemTime,
251}
252
253pub fn list_native_session_sources(
259 harness: HarnessKind,
260 home: &Path,
261) -> Result<Vec<NativeSessionSource>> {
262 let sources = match harness {
263 HarnessKind::Kimi => kimi_indexed_candidates(home, &home.join("sessions"))?
264 .into_iter()
265 .map(|candidate| NativeSessionSource {
266 native_session_id: candidate.native_session_id,
267 source_path: candidate.session_path,
268 modified_at: candidate.modified_at,
269 })
270 .collect(),
271 HarnessKind::Grok => grok::grok_candidates(&home.join("sessions"))?
272 .into_iter()
273 .map(|candidate| NativeSessionSource {
274 native_session_id: candidate.native_session_id,
275 source_path: candidate.session_path,
276 modified_at: candidate.modified_at,
277 })
278 .collect(),
279 HarnessKind::Muse => muse::list_sources(&mj_checkpoint::native::muse_sessions_root(home)?)?,
280 other => bail!("{other:?} keeps one session per file; there is nothing to enumerate"),
281 };
282 Ok(sources)
283}
284
285pub fn native_session_title(harness: HarnessKind, source_path: &Path) -> Option<String> {
289 match harness {
290 HarnessKind::Kimi => {
291 kimi_state_listing_metadata(source_path, Path::new("")).map(|(title, _, _)| title)
292 }
293 HarnessKind::Grok => grok::grok_listing_metadata(source_path).0,
294 _ => None,
295 }
296}
297
298pub fn read_native_transcript(
300 harness: HarnessKind,
301 source_path: &Path,
302) -> Result<ClaudeTranscript> {
303 match harness {
304 HarnessKind::Muse => muse::read_transcript(source_path),
305 HarnessKind::Codex => read_codex_transcript(source_path),
306 HarnessKind::Claude => read_claude_transcript(source_path),
307 HarnessKind::Kimi => read_kimi_transcript(source_path),
308 HarnessKind::Grok => read_grok_transcript(source_path),
309 }
310}
311
312pub fn scan_native_sessions(
314 harness: HarnessKind,
315 home: &Path,
316 cache: &NativeScanCache,
317 mut report: impl FnMut(SessionScanProgress<NativeSessionListing>),
318) -> Result<()> {
319 let mut forward = |scanned, total, session| {
320 report(SessionScanProgress {
321 scanned,
322 total,
323 session,
324 });
325 };
326 match harness {
327 HarnessKind::Muse => muse::scan(
328 &mj_checkpoint::native::muse_sessions_root(home)?,
329 |progress| {
330 forward(progress.scanned, progress.total, progress.session);
331 },
332 ),
333 HarnessKind::Codex => scan_codex_sessions(home, cache, |progress| {
334 let session = progress.session.map(|session| NativeSessionListing {
335 unavailable_reason: session.history_mode.import_issue(),
336 native_session_id: session.native_session_id,
337 title: session.title,
338 modified_at: session.modified_at,
339 git_branch: session.git_branch,
340 size_bytes: session.size_bytes,
341 cwd: session.cwd,
342 natively_archived: session.natively_archived,
343 });
344 forward(progress.scanned, progress.total, session);
345 }),
346 HarnessKind::Claude => scan_claude_sessions(home, cache, |progress| {
347 let session = progress.session.map(|session| NativeSessionListing {
348 native_session_id: session.native_session_id,
349 title: session.title,
350 modified_at: session.modified_at,
351 git_branch: session.git_branch,
352 size_bytes: session.size_bytes,
353 cwd: session.cwd,
354 unavailable_reason: None,
355 natively_archived: false,
356 });
357 forward(progress.scanned, progress.total, session);
358 }),
359 HarnessKind::Kimi => scan_kimi_sessions(home, |progress| {
360 let session = progress.session.map(|session| NativeSessionListing {
361 native_session_id: session.native_session_id,
362 title: session.title,
363 modified_at: session.modified_at,
364 git_branch: session.git_branch,
365 size_bytes: session.size_bytes,
366 cwd: session.cwd,
367 unavailable_reason: None,
368 natively_archived: false,
369 });
370 forward(progress.scanned, progress.total, session);
371 }),
372 HarnessKind::Grok => scan_grok_sessions(home, |progress| {
373 let session = progress.session.map(|session| NativeSessionListing {
374 native_session_id: session.native_session_id,
375 title: session.title,
376 modified_at: session.modified_at,
377 git_branch: session.git_branch,
378 size_bytes: session.size_bytes,
379 cwd: session.cwd,
380 unavailable_reason: None,
381 natively_archived: false,
382 });
383 forward(progress.scanned, progress.total, session);
384 }),
385 }
386}