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}
14
15#[derive(Debug)]
16struct CachedNativeEntry {
17 modified_at: SystemTime,
18 size_bytes: u64,
19 metadata: CachedNativeMetadata,
20}
21
22#[derive(Debug, Default)]
23struct NativeScanCacheInner {
24 entries: HashMap<PathBuf, CachedNativeEntry>,
25 parsed_files: u64,
26}
27
28#[derive(Debug, Clone, Default)]
33pub struct NativeScanCache {
34 inner: Arc<Mutex<NativeScanCacheInner>>,
35}
36
37impl NativeScanCache {
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn parsed_files(&self) -> u64 {
44 self.lock().parsed_files
45 }
46
47 fn lock(&self) -> std::sync::MutexGuard<'_, NativeScanCacheInner> {
48 self.inner
49 .lock()
50 .unwrap_or_else(|poisoned| poisoned.into_inner())
51 }
52
53 fn cached(
54 &self,
55 path: &Path,
56 modified_at: SystemTime,
57 size_bytes: u64,
58 ) -> Option<CachedNativeMetadata> {
59 let inner = self.lock();
60 let entry = inner.entries.get(path)?;
61 (entry.modified_at == modified_at && entry.size_bytes == size_bytes)
62 .then(|| entry.metadata.clone())
63 }
64
65 fn store(
66 &self,
67 path: &Path,
68 modified_at: SystemTime,
69 size_bytes: u64,
70 metadata: CachedNativeMetadata,
71 ) {
72 let mut inner = self.lock();
73 inner.parsed_files = inner.parsed_files.saturating_add(1);
74 inner.entries.insert(
75 path.to_owned(),
76 CachedNativeEntry {
77 modified_at,
78 size_bytes,
79 metadata,
80 },
81 );
82 }
83
84 pub(super) fn claude_metadata(
87 &self,
88 path: &Path,
89 modified_at: SystemTime,
90 size_bytes: u64,
91 parse: impl FnOnce() -> Result<Option<(String, PathBuf, String)>>,
92 ) -> Result<Option<(String, PathBuf, String)>> {
93 if let Some(CachedNativeMetadata::Claude(metadata)) =
94 self.cached(path, modified_at, size_bytes)
95 {
96 return Ok(metadata);
97 }
98 let metadata = parse()?;
99 self.store(
100 path,
101 modified_at,
102 size_bytes,
103 CachedNativeMetadata::Claude(metadata.clone()),
104 );
105 Ok(metadata)
106 }
107
108 pub(super) fn codex_metadata(
110 &self,
111 path: &Path,
112 modified_at: SystemTime,
113 size_bytes: u64,
114 parse: impl FnOnce() -> Result<Option<CodexSessionMetadata>>,
115 ) -> Result<Option<CodexSessionMetadata>> {
116 if let Some(CachedNativeMetadata::Codex(metadata)) =
117 self.cached(path, modified_at, size_bytes)
118 {
119 return Ok(metadata);
120 }
121 let metadata = parse()?;
122 self.store(
123 path,
124 modified_at,
125 size_bytes,
126 CachedNativeMetadata::Codex(metadata.clone()),
127 );
128 Ok(metadata)
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct LocatedNativeSession {
136 pub native_session_id: String,
137 pub source_path: PathBuf,
138}
139
140#[derive(Debug, Clone)]
142pub struct NativeSessionListing {
143 pub native_session_id: String,
144 pub title: String,
145 pub modified_at: SystemTime,
146 pub git_branch: String,
147 pub size_bytes: u64,
148 pub cwd: PathBuf,
149 pub unavailable_reason: Option<&'static str>,
151 pub natively_archived: bool,
153}
154
155pub fn locate_native_session(
157 harness: HarnessKind,
158 home: &Path,
159 selection: &ClaudeSessionSelection,
160) -> Result<LocatedNativeSession> {
161 let (native_session_id, source_path) = match harness {
162 HarnessKind::Muse => {
163 return muse::locate(&mj_checkpoint::native::muse_sessions_root(home)?, selection);
164 }
165 HarnessKind::Codex => {
166 let located = locate_codex_session(home, selection)?;
167 (located.native_session_id, located.jsonl_path)
168 }
169 HarnessKind::Claude => {
170 let located = locate_claude_session(home, selection)?;
171 (located.native_session_id, located.jsonl_path)
172 }
173 HarnessKind::Kimi => {
174 let located = locate_kimi_session(home, selection)?;
175 (located.native_session_id, located.session_path)
176 }
177 HarnessKind::Grok => {
178 let located = locate_grok_session(home, selection)?;
179 (located.native_session_id, located.session_path)
180 }
181 };
182 Ok(LocatedNativeSession {
183 native_session_id,
184 source_path,
185 })
186}
187
188pub fn native_session_id_from_path(harness: HarnessKind, path: &Path) -> Option<String> {
199 match harness {
200 HarnessKind::Claude => path.file_stem()?.to_str().map(str::to_owned),
201 HarnessKind::Codex => codex_rollout_id_from_path(path).map(str::to_owned),
202 HarnessKind::Kimi | HarnessKind::Grok => path.file_name()?.to_str().map(str::to_owned),
203 HarnessKind::Muse => path.parent()?.file_name()?.to_str().map(str::to_owned),
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct NativeSessionSource {
213 pub native_session_id: String,
214 pub source_path: PathBuf,
215 pub modified_at: SystemTime,
218}
219
220pub fn list_native_session_sources(
226 harness: HarnessKind,
227 home: &Path,
228) -> Result<Vec<NativeSessionSource>> {
229 let sources = match harness {
230 HarnessKind::Kimi => kimi_indexed_candidates(home, &home.join("sessions"))?
231 .into_iter()
232 .map(|candidate| NativeSessionSource {
233 native_session_id: candidate.native_session_id,
234 source_path: candidate.session_path,
235 modified_at: candidate.modified_at,
236 })
237 .collect(),
238 HarnessKind::Grok => grok::grok_candidates(&home.join("sessions"))?
239 .into_iter()
240 .map(|candidate| NativeSessionSource {
241 native_session_id: candidate.native_session_id,
242 source_path: candidate.session_path,
243 modified_at: candidate.modified_at,
244 })
245 .collect(),
246 HarnessKind::Muse => muse::list_sources(&mj_checkpoint::native::muse_sessions_root(home)?)?,
247 other => bail!("{other:?} keeps one session per file; there is nothing to enumerate"),
248 };
249 Ok(sources)
250}
251
252pub fn native_session_title(harness: HarnessKind, source_path: &Path) -> Option<String> {
256 match harness {
257 HarnessKind::Kimi => {
258 kimi_state_listing_metadata(source_path, Path::new("")).map(|(title, _, _)| title)
259 }
260 HarnessKind::Grok => grok::grok_listing_metadata(source_path).0,
261 _ => None,
262 }
263}
264
265pub fn read_native_transcript(
267 harness: HarnessKind,
268 source_path: &Path,
269) -> Result<ClaudeTranscript> {
270 match harness {
271 HarnessKind::Muse => muse::read_transcript(source_path),
272 HarnessKind::Codex => read_codex_transcript(source_path),
273 HarnessKind::Claude => read_claude_transcript(source_path),
274 HarnessKind::Kimi => read_kimi_transcript(source_path),
275 HarnessKind::Grok => read_grok_transcript(source_path),
276 }
277}
278
279pub fn scan_native_sessions(
281 harness: HarnessKind,
282 home: &Path,
283 cache: &NativeScanCache,
284 mut report: impl FnMut(SessionScanProgress<NativeSessionListing>),
285) -> Result<()> {
286 let mut forward = |scanned, total, session| {
287 report(SessionScanProgress {
288 scanned,
289 total,
290 session,
291 });
292 };
293 match harness {
294 HarnessKind::Muse => muse::scan(
295 &mj_checkpoint::native::muse_sessions_root(home)?,
296 |progress| {
297 forward(progress.scanned, progress.total, progress.session);
298 },
299 ),
300 HarnessKind::Codex => scan_codex_sessions(home, cache, |progress| {
301 let session = progress.session.map(|session| NativeSessionListing {
302 unavailable_reason: session.history_mode.import_issue(),
303 native_session_id: session.native_session_id,
304 title: session.title,
305 modified_at: session.modified_at,
306 git_branch: session.git_branch,
307 size_bytes: session.size_bytes,
308 cwd: session.cwd,
309 natively_archived: session.natively_archived,
310 });
311 forward(progress.scanned, progress.total, session);
312 }),
313 HarnessKind::Claude => scan_claude_sessions(home, cache, |progress| {
314 let session = progress.session.map(|session| NativeSessionListing {
315 native_session_id: session.native_session_id,
316 title: session.title,
317 modified_at: session.modified_at,
318 git_branch: session.git_branch,
319 size_bytes: session.size_bytes,
320 cwd: session.cwd,
321 unavailable_reason: None,
322 natively_archived: false,
323 });
324 forward(progress.scanned, progress.total, session);
325 }),
326 HarnessKind::Kimi => scan_kimi_sessions(home, |progress| {
327 let session = progress.session.map(|session| NativeSessionListing {
328 native_session_id: session.native_session_id,
329 title: session.title,
330 modified_at: session.modified_at,
331 git_branch: session.git_branch,
332 size_bytes: session.size_bytes,
333 cwd: session.cwd,
334 unavailable_reason: None,
335 natively_archived: false,
336 });
337 forward(progress.scanned, progress.total, session);
338 }),
339 HarnessKind::Grok => scan_grok_sessions(home, |progress| {
340 let session = progress.session.map(|session| NativeSessionListing {
341 native_session_id: session.native_session_id,
342 title: session.title,
343 modified_at: session.modified_at,
344 git_branch: session.git_branch,
345 size_bytes: session.size_bytes,
346 cwd: session.cwd,
347 unavailable_reason: None,
348 natively_archived: false,
349 });
350 forward(progress.scanned, progress.total, session);
351 }),
352 }
353}