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
188#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct NativeSessionSource {
194 pub native_session_id: String,
195 pub source_path: PathBuf,
196 pub modified_at: SystemTime,
199}
200
201pub fn list_native_session_sources(
207 harness: HarnessKind,
208 home: &Path,
209) -> Result<Vec<NativeSessionSource>> {
210 let sources = match harness {
211 HarnessKind::Kimi => kimi_indexed_candidates(home, &home.join("sessions"))?
212 .into_iter()
213 .map(|candidate| NativeSessionSource {
214 native_session_id: candidate.native_session_id,
215 source_path: candidate.session_path,
216 modified_at: candidate.modified_at,
217 })
218 .collect(),
219 HarnessKind::Grok => grok::grok_candidates(&home.join("sessions"))?
220 .into_iter()
221 .map(|candidate| NativeSessionSource {
222 native_session_id: candidate.native_session_id,
223 source_path: candidate.session_path,
224 modified_at: candidate.modified_at,
225 })
226 .collect(),
227 HarnessKind::Muse => muse::list_sources(&mj_checkpoint::native::muse_sessions_root(home)?)?,
228 other => bail!("{other:?} keeps one session per file; there is nothing to enumerate"),
229 };
230 Ok(sources)
231}
232
233pub fn native_session_title(harness: HarnessKind, source_path: &Path) -> Option<String> {
237 match harness {
238 HarnessKind::Kimi => {
239 kimi_state_listing_metadata(source_path, Path::new("")).map(|(title, _, _)| title)
240 }
241 HarnessKind::Grok => grok::grok_listing_metadata(source_path).0,
242 _ => None,
243 }
244}
245
246pub fn read_native_transcript(
248 harness: HarnessKind,
249 source_path: &Path,
250) -> Result<ClaudeTranscript> {
251 match harness {
252 HarnessKind::Muse => muse::read_transcript(source_path),
253 HarnessKind::Codex => read_codex_transcript(source_path),
254 HarnessKind::Claude => read_claude_transcript(source_path),
255 HarnessKind::Kimi => read_kimi_transcript(source_path),
256 HarnessKind::Grok => read_grok_transcript(source_path),
257 }
258}
259
260pub fn scan_native_sessions(
262 harness: HarnessKind,
263 home: &Path,
264 cache: &NativeScanCache,
265 mut report: impl FnMut(SessionScanProgress<NativeSessionListing>),
266) -> Result<()> {
267 let mut forward = |scanned, total, session| {
268 report(SessionScanProgress {
269 scanned,
270 total,
271 session,
272 });
273 };
274 match harness {
275 HarnessKind::Muse => muse::scan(
276 &mj_checkpoint::native::muse_sessions_root(home)?,
277 |progress| {
278 forward(progress.scanned, progress.total, progress.session);
279 },
280 ),
281 HarnessKind::Codex => scan_codex_sessions(home, cache, |progress| {
282 let session = progress.session.map(|session| NativeSessionListing {
283 unavailable_reason: session.history_mode.import_issue(),
284 native_session_id: session.native_session_id,
285 title: session.title,
286 modified_at: session.modified_at,
287 git_branch: session.git_branch,
288 size_bytes: session.size_bytes,
289 cwd: session.cwd,
290 natively_archived: session.natively_archived,
291 });
292 forward(progress.scanned, progress.total, session);
293 }),
294 HarnessKind::Claude => scan_claude_sessions(home, cache, |progress| {
295 let session = progress.session.map(|session| NativeSessionListing {
296 native_session_id: session.native_session_id,
297 title: session.title,
298 modified_at: session.modified_at,
299 git_branch: session.git_branch,
300 size_bytes: session.size_bytes,
301 cwd: session.cwd,
302 unavailable_reason: None,
303 natively_archived: false,
304 });
305 forward(progress.scanned, progress.total, session);
306 }),
307 HarnessKind::Kimi => scan_kimi_sessions(home, |progress| {
308 let session = progress.session.map(|session| NativeSessionListing {
309 native_session_id: session.native_session_id,
310 title: session.title,
311 modified_at: session.modified_at,
312 git_branch: session.git_branch,
313 size_bytes: session.size_bytes,
314 cwd: session.cwd,
315 unavailable_reason: None,
316 natively_archived: false,
317 });
318 forward(progress.scanned, progress.total, session);
319 }),
320 HarnessKind::Grok => scan_grok_sessions(home, |progress| {
321 let session = progress.session.map(|session| NativeSessionListing {
322 native_session_id: session.native_session_id,
323 title: session.title,
324 modified_at: session.modified_at,
325 git_branch: session.git_branch,
326 size_bytes: session.size_bytes,
327 cwd: session.cwd,
328 unavailable_reason: None,
329 natively_archived: false,
330 });
331 forward(progress.scanned, progress.total, session);
332 }),
333 }
334}