1use anyhow::Result;
10use colored::*;
11use std::collections::HashSet;
12use std::io::Write;
13use std::path::{Path, PathBuf};
14
15use crate::error::CsmError;
16use crate::models::ChatSession;
17use crate::storage::{
18 add_session_to_index, cleanup_state_cache, close_vscode_and_wait, diagnose_workspace_sessions,
19 fix_session_memento, get_workspace_storage_db, is_session_file_extension, is_vscode_running,
20 parse_session_file, parse_session_json, read_chat_session_index, rebuild_model_cache,
21 recover_from_json_bak, register_all_sessions_from_directory, reopen_vscode,
22 repair_workspace_sessions, trim_session_jsonl,
23};
24use crate::workspace::{
25 discover_workspaces, find_workspace_by_path, normalize_path,
26 recover_orphaned_sessions_from_old_hashes,
27};
28
29fn confirm_close_vscode(force: bool) -> bool {
32 if force {
33 return true;
34 }
35 print!(
36 "{} VS Code will be closed. Continue? [y/N] ",
37 "[?]".yellow()
38 );
39 std::io::stdout().flush().ok();
40 let mut input = String::new();
41 if std::io::stdin().read_line(&mut input).is_err() {
42 return false;
43 }
44 matches!(input.trim().to_lowercase().as_str(), "y" | "yes")
45}
46
47pub fn resolve_path(path: Option<&str>) -> PathBuf {
49 match path {
50 Some(p) => {
51 let path = PathBuf::from(p);
52 path.canonicalize().unwrap_or(path)
53 }
54 None => std::env::current_dir().unwrap_or_default(),
55 }
56}
57
58pub fn register_all(
60 project_path: Option<&str>,
61 merge: bool,
62 force: bool,
63 close_vscode: bool,
64 reopen: bool,
65) -> Result<()> {
66 let path = resolve_path(project_path);
67 let should_close = close_vscode || reopen;
69
70 if merge {
71 println!(
72 "{} Merging and registering all sessions for: {}",
73 "[CSM]".cyan().bold(),
74 path.display()
75 );
76
77 let path_str = path.to_string_lossy().to_string();
79 return crate::commands::history_merge(
80 Some(&path_str),
81 None, force, false, );
85 }
86
87 println!(
88 "{} Registering all sessions for: {}",
89 "[CSM]".cyan().bold(),
90 path.display()
91 );
92
93 let path_str = path.to_string_lossy().to_string();
95 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
96 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
97
98 let chat_sessions_dir = ws_path.join("chatSessions");
99
100 if !chat_sessions_dir.exists() {
101 println!(
102 "{} No chatSessions directory found at: {}",
103 "[!]".yellow(),
104 chat_sessions_dir.display()
105 );
106 return Ok(());
107 }
108
109 let vscode_was_running = is_vscode_running();
111 if vscode_was_running {
112 if should_close {
113 if !confirm_close_vscode(force) {
114 println!("{} Aborted.", "[!]".yellow());
115 return Ok(());
116 }
117 println!(" {} Closing VS Code (saving state)...", "[*]".yellow());
118 close_vscode_and_wait(30)?;
119 println!(" {} VS Code closed.", "[OK]".green());
120 } else if !force {
121 println!(
122 "{} VS Code is running. Its in-memory cache will overwrite index changes.",
123 "[!]".yellow()
124 );
125 println!(
126 " Use {} to close VS Code first, register, and reopen.",
127 "--reopen".cyan()
128 );
129 println!(
130 " Use {} to just close VS Code first.",
131 "--close-vscode".cyan()
132 );
133 println!(
134 " Use {} to write anyway (works after restarting VS Code).",
135 "--force".cyan()
136 );
137 return Err(CsmError::VSCodeRunning.into());
138 }
139 }
140
141 let sessions_on_disk = count_sessions_in_directory(&chat_sessions_dir)?;
143 println!(
144 " Found {} session files on disk",
145 sessions_on_disk.to_string().green()
146 );
147
148 let registered = register_all_sessions_from_directory(&ws_id, &chat_sessions_dir, true)?;
150
151 println!(
152 "\n{} Registered {} sessions in VS Code's index",
153 "[OK]".green().bold(),
154 registered.to_string().cyan()
155 );
156
157 if reopen && vscode_was_running {
159 println!(" {} Reopening VS Code...", "[*]".yellow());
160 reopen_vscode(Some(&path_str))?;
161 println!(
162 " {} VS Code launched. Sessions should appear in Copilot Chat history.",
163 "[OK]".green()
164 );
165 } else if should_close && vscode_was_running {
166 println!(
167 "\n{} VS Code was closed. Reopen it to see the recovered sessions.",
168 "[!]".yellow()
169 );
170 println!(" Run: {}", format!("code {}", path.display()).cyan());
171 } else if force && vscode_was_running {
172 println!(
174 "\n{} VS Code caches the session index in memory.",
175 "[!]".yellow()
176 );
177 println!(" To see the new sessions, do one of the following:");
178 println!(
179 " * Press {} and run {}",
180 "Ctrl+Shift+P".cyan(),
181 "Developer: Reload Window".cyan()
182 );
183 println!(" * Or restart VS Code");
184 }
185
186 Ok(())
187}
188
189pub fn register_sessions(
191 ids: &[String],
192 titles: Option<&[String]>,
193 project_path: Option<&str>,
194 force: bool,
195) -> Result<()> {
196 let path = resolve_path(project_path);
197
198 let path_str = path.to_string_lossy().to_string();
200 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
201 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
202
203 let chat_sessions_dir = ws_path.join("chatSessions");
204
205 if !force && is_vscode_running() {
207 println!(
208 "{} VS Code is running. Use {} to register anyway.",
209 "[!]".yellow(),
210 "--force".cyan()
211 );
212 return Err(CsmError::VSCodeRunning.into());
213 }
214
215 let db_path = get_workspace_storage_db(&ws_id)?;
217
218 let mut registered_count = 0;
219
220 if let Some(titles) = titles {
221 println!(
223 "{} Registering {} sessions by title:",
224 "[CSM]".cyan().bold(),
225 titles.len()
226 );
227
228 let sessions = find_sessions_by_titles(&chat_sessions_dir, titles)?;
229
230 for (session, session_path) in sessions {
231 let session_id = session.session_id.clone().unwrap_or_else(|| {
232 session_path
233 .file_stem()
234 .map(|s| s.to_string_lossy().to_string())
235 .unwrap_or_default()
236 });
237 let title = session.title();
238
239 add_session_to_index(
240 &db_path,
241 &session_id,
242 &title,
243 session.last_message_date,
244 session.is_imported,
245 &session.initial_location,
246 session.is_empty(),
247 )?;
248
249 let id_display = if session_id.len() > 12 {
250 &session_id[..12]
251 } else {
252 &session_id
253 };
254 println!(
255 " {} {} (\"{}\")",
256 "[OK]".green(),
257 id_display.cyan(),
258 title.yellow()
259 );
260 registered_count += 1;
261 }
262 } else {
263 println!(
265 "{} Registering {} sessions by ID:",
266 "[CSM]".cyan().bold(),
267 ids.len()
268 );
269
270 for session_id in ids {
271 match find_session_file(&chat_sessions_dir, session_id) {
272 Ok(session_file) => {
273 let session = parse_session_file(&session_file)?;
274
275 let title = session.title();
276 let actual_session_id = session
277 .session_id
278 .clone()
279 .unwrap_or_else(|| session_id.to_string());
280
281 add_session_to_index(
282 &db_path,
283 &actual_session_id,
284 &title,
285 session.last_message_date,
286 session.is_imported,
287 &session.initial_location,
288 session.is_empty(),
289 )?;
290
291 let id_display = if actual_session_id.len() > 12 {
292 &actual_session_id[..12]
293 } else {
294 &actual_session_id
295 };
296 println!(
297 " {} {} (\"{}\")",
298 "[OK]".green(),
299 id_display.cyan(),
300 title.yellow()
301 );
302 registered_count += 1;
303 }
304 Err(e) => {
305 println!(
306 " {} {} - {}",
307 "[ERR]".red(),
308 session_id.cyan(),
309 e.to_string().red()
310 );
311 }
312 }
313 }
314 }
315
316 println!(
317 "\n{} Registered {} sessions in VS Code's index",
318 "[OK]".green().bold(),
319 registered_count.to_string().cyan()
320 );
321
322 if force && is_vscode_running() {
323 println!(
324 " {} Sessions should appear in VS Code immediately",
325 "->".cyan()
326 );
327 }
328
329 Ok(())
330}
331
332pub fn list_orphaned(project_path: Option<&str>) -> Result<()> {
334 let path = resolve_path(project_path);
335
336 println!(
337 "{} Finding orphaned sessions for: {}",
338 "[CSM]".cyan().bold(),
339 path.display()
340 );
341
342 let path_str = path.to_string_lossy().to_string();
344 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
345 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
346
347 let chat_sessions_dir = ws_path.join("chatSessions");
348
349 if !chat_sessions_dir.exists() {
350 println!("{} No chatSessions directory found", "[!]".yellow());
351 return Ok(());
352 }
353
354 let db_path = get_workspace_storage_db(&ws_id)?;
356 let index = read_chat_session_index(&db_path)?;
357 let indexed_ids: HashSet<String> = index.entries.keys().cloned().collect();
358
359 println!(
360 " {} sessions currently in VS Code's index",
361 indexed_ids.len().to_string().cyan()
362 );
363
364 let mut orphaned_sessions = Vec::new();
366
367 let mut session_files: std::collections::HashMap<String, PathBuf> =
369 std::collections::HashMap::new();
370 for entry in std::fs::read_dir(&chat_sessions_dir)? {
371 let entry = entry?;
372 let path = entry.path();
373 if path
374 .extension()
375 .map(is_session_file_extension)
376 .unwrap_or(false)
377 {
378 if let Some(stem) = path.file_stem() {
379 let stem_str = stem.to_string_lossy().to_string();
380 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
381 if !session_files.contains_key(&stem_str) || is_jsonl {
382 session_files.insert(stem_str, path);
383 }
384 }
385 }
386 }
387
388 for (_, path) in &session_files {
389 if let Ok(session) = parse_session_file(path) {
390 let session_id = session.session_id.clone().unwrap_or_else(|| {
391 path.file_stem()
392 .map(|s| s.to_string_lossy().to_string())
393 .unwrap_or_default()
394 });
395
396 if !indexed_ids.contains(&session_id) {
397 let title = session.title();
398 let msg_count = session.requests.len();
399 orphaned_sessions.push((session_id, title, msg_count, path.clone()));
400 }
401 }
402 }
403
404 if orphaned_sessions.is_empty() {
405 println!(
406 "\n{} No orphaned sessions found - all sessions are registered!",
407 "[OK]".green().bold()
408 );
409 return Ok(());
410 }
411
412 println!(
413 "\n{} Found {} orphaned sessions (on disk but not in index):\n",
414 "[!]".yellow().bold(),
415 orphaned_sessions.len().to_string().red()
416 );
417
418 for (session_id, title, msg_count, _path) in &orphaned_sessions {
419 let id_display = if session_id.len() > 12 {
420 &session_id[..12]
421 } else {
422 session_id
423 };
424 println!(
425 " {} {} ({} messages)",
426 id_display.cyan(),
427 format!("\"{}\"", title).yellow(),
428 msg_count
429 );
430 }
431
432 println!("\n{} To register all orphaned sessions:", "->".cyan());
433 println!(" csm register all --force");
434 println!("\n{} To register specific sessions:", "->".cyan());
435 println!(" csm register session <ID1> <ID2> ... --force");
436
437 Ok(())
438}
439
440fn count_sessions_in_directory(dir: &PathBuf) -> Result<usize> {
442 let mut session_ids: HashSet<String> = HashSet::new();
443 for entry in std::fs::read_dir(dir)? {
444 let entry = entry?;
445 let path = entry.path();
446 if path
447 .extension()
448 .map(is_session_file_extension)
449 .unwrap_or(false)
450 {
451 if let Some(stem) = path.file_stem() {
452 session_ids.insert(stem.to_string_lossy().to_string());
453 }
454 }
455 }
456 Ok(session_ids.len())
457}
458
459fn find_session_file(chat_sessions_dir: &PathBuf, session_id: &str) -> Result<PathBuf> {
461 let exact_jsonl = chat_sessions_dir.join(format!("{}.jsonl", session_id));
463 if exact_jsonl.exists() {
464 return Ok(exact_jsonl);
465 }
466 let exact_json = chat_sessions_dir.join(format!("{}.json", session_id));
467 if exact_json.exists() {
468 return Ok(exact_json);
469 }
470
471 let mut best_match: Option<PathBuf> = None;
473 for entry in std::fs::read_dir(chat_sessions_dir)? {
474 let entry = entry?;
475 let path = entry.path();
476
477 if path
478 .extension()
479 .map(is_session_file_extension)
480 .unwrap_or(false)
481 {
482 let filename = path
483 .file_stem()
484 .map(|s| s.to_string_lossy().to_string())
485 .unwrap_or_default();
486
487 if filename.starts_with(session_id) {
488 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
489 if best_match.is_none() || is_jsonl {
490 best_match = Some(path.clone());
491 if is_jsonl {
492 return Ok(path);
493 }
494 }
495 continue;
496 }
497
498 if let Ok(session) = parse_session_file(&path) {
500 if let Some(ref sid) = session.session_id {
501 if sid.starts_with(session_id) || sid == session_id {
502 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
503 if best_match.is_none() || is_jsonl {
504 best_match = Some(path.clone());
505 }
506 }
507 }
508 }
509 }
510 }
511
512 best_match.ok_or_else(|| CsmError::SessionNotFound(session_id.to_string()).into())
513}
514
515fn find_sessions_by_titles(
517 chat_sessions_dir: &PathBuf,
518 titles: &[String],
519) -> Result<Vec<(ChatSession, PathBuf)>> {
520 let mut matches = Vec::new();
521 let title_patterns: Vec<String> = titles.iter().map(|t| t.to_lowercase()).collect();
522
523 let mut session_files: std::collections::HashMap<String, PathBuf> =
525 std::collections::HashMap::new();
526 for entry in std::fs::read_dir(chat_sessions_dir)? {
527 let entry = entry?;
528 let path = entry.path();
529 if path
530 .extension()
531 .map(is_session_file_extension)
532 .unwrap_or(false)
533 {
534 if let Some(stem) = path.file_stem() {
535 let stem_str = stem.to_string_lossy().to_string();
536 let is_jsonl = path.extension().is_some_and(|e| e == "jsonl");
537 if !session_files.contains_key(&stem_str) || is_jsonl {
538 session_files.insert(stem_str, path);
539 }
540 }
541 }
542 }
543
544 for (_, path) in &session_files {
545 if let Ok(session) = parse_session_file(path) {
546 let session_title = session.title().to_lowercase();
547
548 for pattern in &title_patterns {
549 if session_title.contains(pattern) {
550 matches.push((session, path.clone()));
551 break;
552 }
553 }
554 }
555 }
556
557 if matches.is_empty() {
558 println!(
559 "{} No sessions found matching the specified titles",
560 "[!]".yellow()
561 );
562 }
563
564 Ok(matches)
565}
566
567pub fn register_recursive(
569 root_path: Option<&str>,
570 max_depth: Option<usize>,
571 force: bool,
572 dry_run: bool,
573 exclude_patterns: &[String],
574) -> Result<()> {
575 let root = resolve_path(root_path);
576
577 println!(
578 "{} Scanning for workspaces recursively from: {}",
579 "[CSM]".cyan().bold(),
580 root.display()
581 );
582
583 if dry_run {
584 println!("{} Dry run mode - no changes will be made", "[!]".yellow());
585 }
586
587 if !force && !dry_run && is_vscode_running() {
589 println!(
590 "{} VS Code is running. Use {} to register anyway.",
591 "[!]".yellow(),
592 "--force".cyan()
593 );
594 println!(" Note: VS Code uses WAL mode so this is generally safe.");
595 return Err(CsmError::VSCodeRunning.into());
596 }
597
598 let workspaces = discover_workspaces()?;
600 println!(
601 " Found {} VS Code workspaces to check",
602 workspaces.len().to_string().cyan()
603 );
604
605 let mut workspace_map: std::collections::HashMap<String, Vec<&crate::models::Workspace>> =
607 std::collections::HashMap::new();
608 for ws in &workspaces {
609 if let Some(ref project_path) = ws.project_path {
610 let normalized = normalize_path(project_path);
611 workspace_map.entry(normalized).or_default().push(ws);
612 }
613 }
614
615 let exclude_matchers: Vec<glob::Pattern> = exclude_patterns
617 .iter()
618 .filter_map(|p| glob::Pattern::new(p).ok())
619 .collect();
620
621 let default_excludes = [
623 "node_modules",
624 ".git",
625 "target",
626 "build",
627 "dist",
628 ".venv",
629 "venv",
630 "__pycache__",
631 ".cache",
632 "vendor",
633 ".cargo",
634 ];
635
636 let mut total_dirs_scanned = 0;
637 let mut workspaces_found = 0;
638 let mut total_sessions_registered = 0;
639 let mut workspaces_with_orphans: Vec<(String, usize, usize)> = Vec::new();
640
641 walk_directory(
643 &root,
644 &root,
645 0,
646 max_depth,
647 &workspace_map,
648 &exclude_matchers,
649 &default_excludes,
650 force,
651 dry_run,
652 &mut total_dirs_scanned,
653 &mut workspaces_found,
654 &mut total_sessions_registered,
655 &mut workspaces_with_orphans,
656 )?;
657
658 println!("\n{}", "═".repeat(60).cyan());
660 println!("{} Recursive scan complete", "[OK]".green().bold());
661 println!("{}", "═".repeat(60).cyan());
662 println!(
663 " Directories scanned: {}",
664 total_dirs_scanned.to_string().cyan()
665 );
666 println!(
667 " Workspaces found: {}",
668 workspaces_found.to_string().cyan()
669 );
670 println!(
671 " Sessions registered: {}",
672 total_sessions_registered.to_string().green()
673 );
674
675 if !workspaces_with_orphans.is_empty() {
676 println!("\n {} Workspaces with orphaned sessions:", "[+]".green());
677 for (path, orphaned, registered) in &workspaces_with_orphans {
678 let reg_str = if dry_run {
679 format!("would register {}", registered)
680 } else {
681 format!("registered {}", registered)
682 };
683 println!(
684 " {} ({} orphaned, {})",
685 path.cyan(),
686 orphaned.to_string().yellow(),
687 reg_str.green()
688 );
689 }
690 }
691
692 if total_sessions_registered > 0 && !dry_run {
693 println!(
694 "\n{} VS Code caches the session index in memory.",
695 "[!]".yellow()
696 );
697 println!(" To see the new sessions, do one of the following:");
698 println!(
699 " * Run: {} (if CSM extension is installed)",
700 "code --command csm.reloadAndShowChats".cyan()
701 );
702 println!(
703 " * Or press {} in VS Code and run {}",
704 "Ctrl+Shift+P".cyan(),
705 "Developer: Reload Window".cyan()
706 );
707 println!(" * Or restart VS Code");
708 }
709
710 Ok(())
711}
712
713#[allow(clippy::too_many_arguments)]
715fn walk_directory(
716 current_dir: &Path,
717 root: &Path,
718 current_depth: usize,
719 max_depth: Option<usize>,
720 workspace_map: &std::collections::HashMap<String, Vec<&crate::models::Workspace>>,
721 exclude_matchers: &[glob::Pattern],
722 default_excludes: &[&str],
723 force: bool,
724 dry_run: bool,
725 total_dirs_scanned: &mut usize,
726 workspaces_found: &mut usize,
727 total_sessions_registered: &mut usize,
728 workspaces_with_orphans: &mut Vec<(String, usize, usize)>,
729) -> Result<()> {
730 if let Some(max) = max_depth {
732 if current_depth > max {
733 return Ok(());
734 }
735 }
736
737 *total_dirs_scanned += 1;
738
739 let dir_name = current_dir
741 .file_name()
742 .map(|n| n.to_string_lossy().to_string())
743 .unwrap_or_default();
744
745 if default_excludes.contains(&dir_name.as_str()) {
747 return Ok(());
748 }
749
750 let relative_path = current_dir
752 .strip_prefix(root)
753 .unwrap_or(current_dir)
754 .to_string_lossy();
755 for pattern in exclude_matchers {
756 if pattern.matches(&relative_path) || pattern.matches(&dir_name) {
757 return Ok(());
758 }
759 }
760
761 let normalized_path = normalize_path(¤t_dir.to_string_lossy());
763 if let Some(workspace_entries) = workspace_map.get(&normalized_path) {
764 *workspaces_found += 1;
765
766 for ws in workspace_entries {
767 if ws.has_chat_sessions {
769 let chat_sessions_dir = &ws.chat_sessions_path;
770
771 match count_orphaned_sessions(&ws.hash, chat_sessions_dir) {
773 Ok((on_disk, in_index, orphaned_count)) => {
774 if orphaned_count > 0 {
775 let display_path = ws.project_path.as_deref().unwrap_or(&ws.hash);
776
777 if dry_run {
778 println!(
779 " {} {} - {} sessions on disk, {} in index, {} orphaned",
780 "[DRY]".yellow(),
781 display_path.cyan(),
782 on_disk.to_string().white(),
783 in_index.to_string().white(),
784 orphaned_count.to_string().yellow()
785 );
786 workspaces_with_orphans.push((
787 display_path.to_string(),
788 orphaned_count,
789 orphaned_count,
790 ));
791 } else {
792 match register_all_sessions_from_directory(
794 &ws.hash,
795 chat_sessions_dir,
796 force,
797 ) {
798 Ok(registered) => {
799 *total_sessions_registered += registered;
800 println!(
801 " {} {} - registered {} sessions",
802 "[+]".green(),
803 display_path.cyan(),
804 registered.to_string().green()
805 );
806 workspaces_with_orphans.push((
807 display_path.to_string(),
808 orphaned_count,
809 registered,
810 ));
811 }
812 Err(e) => {
813 println!(
814 " {} {} - error: {}",
815 "[!]".red(),
816 display_path.cyan(),
817 e
818 );
819 }
820 }
821 }
822 }
823 }
824 Err(e) => {
825 let display_path = ws.project_path.as_deref().unwrap_or(&ws.hash);
826 println!(
827 " {} {} - error checking: {}",
828 "[!]".yellow(),
829 display_path,
830 e
831 );
832 }
833 }
834 }
835 }
836 }
837
838 match std::fs::read_dir(current_dir) {
840 Ok(entries) => {
841 for entry in entries.flatten() {
842 let path = entry.path();
843 if path.is_dir() {
844 let name = path
846 .file_name()
847 .map(|n| n.to_string_lossy().to_string())
848 .unwrap_or_default();
849 if name.starts_with('.') {
850 continue;
851 }
852
853 walk_directory(
854 &path,
855 root,
856 current_depth + 1,
857 max_depth,
858 workspace_map,
859 exclude_matchers,
860 default_excludes,
861 force,
862 dry_run,
863 total_dirs_scanned,
864 workspaces_found,
865 total_sessions_registered,
866 workspaces_with_orphans,
867 )?;
868 }
869 }
870 }
871 Err(e) => {
872 if e.kind() != std::io::ErrorKind::PermissionDenied {
874 eprintln!(
875 " {} Could not read {}: {}",
876 "[!]".yellow(),
877 current_dir.display(),
878 e
879 );
880 }
881 }
882 }
883
884 Ok(())
885}
886
887fn count_orphaned_sessions(
889 workspace_id: &str,
890 chat_sessions_dir: &Path,
891) -> Result<(usize, usize, usize)> {
892 let db_path = get_workspace_storage_db(workspace_id)?;
894 let indexed_sessions = read_chat_session_index(&db_path)?;
895 let indexed_ids: HashSet<String> = indexed_sessions.entries.keys().cloned().collect();
896
897 let mut disk_sessions: HashSet<String> = HashSet::new();
899
900 for entry in std::fs::read_dir(chat_sessions_dir)? {
901 let entry = entry?;
902 let path = entry.path();
903
904 if path
905 .extension()
906 .map(is_session_file_extension)
907 .unwrap_or(false)
908 {
909 if let Some(stem) = path.file_stem() {
910 disk_sessions.insert(stem.to_string_lossy().to_string());
911 }
912 }
913 }
914
915 let on_disk = disk_sessions.len();
916 let orphaned = disk_sessions
917 .iter()
918 .filter(|id| !indexed_ids.contains(*id))
919 .count();
920
921 Ok((on_disk, indexed_ids.len(), orphaned))
922}
923
924pub fn register_repair(
926 project_path: Option<&str>,
927 all: bool,
928 recursive: bool,
929 max_depth: Option<usize>,
930 exclude_patterns: &[String],
931 dry_run: bool,
932 force: bool,
933 close_vscode: bool,
934 reopen: bool,
935) -> Result<()> {
936 if all {
937 return register_repair_all(force, close_vscode, reopen);
938 }
939
940 if recursive {
941 return register_repair_recursive(
942 project_path,
943 max_depth,
944 exclude_patterns,
945 dry_run,
946 force,
947 close_vscode,
948 reopen,
949 );
950 }
951
952 let path = resolve_path(project_path);
953 let should_close = close_vscode || reopen;
954
955 println!(
956 "{} Repairing sessions for: {}",
957 "[CSM]".cyan().bold(),
958 path.display()
959 );
960
961 let path_str = path.to_string_lossy().to_string();
963 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
964 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
965
966 let chat_sessions_dir = ws_path.join("chatSessions");
967
968 if !chat_sessions_dir.exists() {
969 println!(
970 "{} No chatSessions directory found at: {}",
971 "[!]".yellow(),
972 chat_sessions_dir.display()
973 );
974 return Ok(());
975 }
976
977 let vscode_was_running = is_vscode_running();
979 if vscode_was_running {
980 if should_close {
981 if !confirm_close_vscode(force) {
982 println!("{} Aborted.", "[!]".yellow());
983 return Ok(());
984 }
985 println!(" {} Closing VS Code (saving state)...", "[*]".yellow());
986 close_vscode_and_wait(30)?;
987 println!(" {} VS Code closed.", "[OK]".green());
988 } else if !force {
989 println!(
990 "{} VS Code is running. Its in-memory cache will overwrite index changes.",
991 "[!]".yellow()
992 );
993 println!(
994 " Use {} to close VS Code first, or {} to force.",
995 "--reopen".cyan(),
996 "--force".cyan()
997 );
998 return Err(CsmError::VSCodeRunning.into());
999 }
1000 }
1001
1002 println!(
1004 " {} Pass 0: Recovering orphaned sessions from old workspace hashes...",
1005 "[*]".cyan()
1006 );
1007 match recover_orphaned_sessions_from_old_hashes(&path_str) {
1008 Ok(0) => {}
1009 Ok(n) => {
1010 println!(
1011 " {} Recovered {} orphaned session(s) from old workspace hashes",
1012 "[OK]".green(),
1013 n.to_string().cyan()
1014 );
1015 }
1016 Err(e) => {
1017 println!(
1018 " {} Failed to recover orphaned sessions: {}",
1019 "[WARN]".yellow(),
1020 e
1021 );
1022 }
1023 }
1024
1025 println!(
1028 " {} Pass 0.5: Recovering sessions from .json.bak files...",
1029 "[*]".cyan()
1030 );
1031 match recover_from_json_bak(&chat_sessions_dir) {
1032 Ok(0) => {}
1033 Ok(n) => {
1034 println!(
1035 " {} Recovered {} session(s) from .json.bak backups",
1036 "[OK]".green(),
1037 n.to_string().cyan()
1038 );
1039 }
1040 Err(e) => {
1041 println!(
1042 " {} Failed to recover from .json.bak: {}",
1043 "[WARN]".yellow(),
1044 e
1045 );
1046 }
1047 }
1048
1049 println!(
1050 " {} Pass 1: Compacting JSONL files & fixing compat fields...",
1051 "[*]".cyan()
1052 );
1053 println!(
1054 " {} Pass 1.5: Converting skeleton .json files...",
1055 "[*]".cyan()
1056 );
1057 println!(" {} Pass 2: Fixing cancelled modelState...", "[*]".cyan());
1058 let (compacted, index_fixed) = repair_workspace_sessions(&ws_id, &chat_sessions_dir, true)?;
1059
1060 println!(" {} Pass 3: Index rebuilt.", "[*]".cyan());
1061 println!(
1062 "\n{} Repair complete: {} files compacted, {} index entries synced",
1063 "[OK]".green().bold(),
1064 compacted.to_string().cyan(),
1065 index_fixed.to_string().cyan()
1066 );
1067
1068 let mut deleted_json = 0;
1070 if chat_sessions_dir.exists() {
1071 let mut jsonl_sessions: HashSet<String> = HashSet::new();
1072 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1073 let entry = entry?;
1074 let p = entry.path();
1075 if p.extension().is_some_and(|e| e == "jsonl") {
1076 if let Some(stem) = p.file_stem() {
1077 jsonl_sessions.insert(stem.to_string_lossy().to_string());
1078 }
1079 }
1080 }
1081 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1082 let entry = entry?;
1083 let p = entry.path();
1084 if p.extension().is_some_and(|e| e == "json") {
1085 if let Some(stem) = p.file_stem() {
1086 if jsonl_sessions.contains(&stem.to_string_lossy().to_string()) {
1087 let bak = p.with_extension("json.bak");
1089 std::fs::rename(&p, &bak)?;
1090 println!(
1091 " {} Backed up stale .json: {} → {}",
1092 "[*]".yellow(),
1093 p.file_name().unwrap_or_default().to_string_lossy(),
1094 bak.file_name().unwrap_or_default().to_string_lossy()
1095 );
1096 deleted_json += 1;
1097 }
1098 }
1099 }
1100 }
1101 if deleted_json > 0 {
1102 repair_workspace_sessions(&ws_id, &chat_sessions_dir, true)?;
1104 println!(
1105 " {} Removed {} stale .json duplicates (backed up as .json.bak)",
1106 "[OK]".green(),
1107 deleted_json
1108 );
1109 }
1110 }
1111
1112 let db_path = get_workspace_storage_db(&ws_id)?;
1114 println!(
1115 " {} Pass 4: Rebuilding model cache (agentSessions.model.cache)...",
1116 "[*]".cyan()
1117 );
1118 match read_chat_session_index(&db_path) {
1119 Ok(index) => match rebuild_model_cache(&db_path, &index) {
1120 Ok(n) => {
1121 println!(
1122 " {} Model cache rebuilt with {} entries",
1123 "[OK]".green(),
1124 n.to_string().cyan()
1125 );
1126 }
1127 Err(e) => {
1128 println!(
1129 " {} Failed to rebuild model cache: {}",
1130 "[WARN]".yellow(),
1131 e
1132 );
1133 }
1134 },
1135 Err(e) => {
1136 println!(
1137 " {} Failed to read index for model cache rebuild: {}",
1138 "[WARN]".yellow(),
1139 e
1140 );
1141 }
1142 }
1143
1144 println!(
1146 " {} Pass 5: Cleaning up state cache (agentSessions.state.cache)...",
1147 "[*]".cyan()
1148 );
1149 {
1150 let mut valid_ids: HashSet<String> = HashSet::new();
1152 if chat_sessions_dir.exists() {
1153 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1154 let entry = entry?;
1155 let p = entry.path();
1156 if p.extension().is_some_and(|e| e == "jsonl") {
1157 if let Some(stem) = p.file_stem() {
1158 valid_ids.insert(stem.to_string_lossy().to_string());
1159 }
1160 }
1161 }
1162 }
1163 match cleanup_state_cache(&db_path, &valid_ids) {
1164 Ok(0) => {
1165 println!(" {} State cache: all entries valid", "[OK]".green());
1166 }
1167 Ok(n) => {
1168 println!(
1169 " {} State cache: removed {} stale entries",
1170 "[OK]".green(),
1171 n.to_string().cyan()
1172 );
1173 }
1174 Err(e) => {
1175 println!(
1176 " {} Failed to cleanup state cache: {}",
1177 "[WARN]".yellow(),
1178 e
1179 );
1180 }
1181 }
1182
1183 println!(
1185 " {} Pass 6: Fixing session memento (last active session)...",
1186 "[*]".cyan()
1187 );
1188 let preferred_id = read_chat_session_index(&db_path).ok().and_then(|idx| {
1190 idx.entries
1191 .iter()
1192 .filter(|(_, e)| !e.is_empty)
1193 .max_by_key(|(_, e)| e.last_message_date)
1194 .map(|(id, _)| id.clone())
1195 });
1196 match fix_session_memento(&db_path, &valid_ids, preferred_id.as_deref()) {
1197 Ok(true) => {
1198 println!(
1199 " {} Memento updated to point to: {}",
1200 "[OK]".green(),
1201 preferred_id
1202 .as_deref()
1203 .unwrap_or("(first valid session)")
1204 .cyan()
1205 );
1206 }
1207 Ok(false) => {
1208 println!(
1209 " {} Memento already points to a valid session",
1210 "[OK]".green()
1211 );
1212 }
1213 Err(e) => {
1214 println!(" {} Failed to fix memento: {}", "[WARN]".yellow(), e);
1215 }
1216 }
1217 }
1218
1219 if reopen && vscode_was_running {
1221 println!(" {} Reopening VS Code...", "[*]".yellow());
1222 reopen_vscode(Some(&path_str))?;
1223 println!(
1224 " {} VS Code launched. Sessions should now load correctly.",
1225 "[OK]".green()
1226 );
1227 } else if should_close && vscode_was_running {
1228 println!(
1229 "\n{} VS Code was closed. Reopen it to see the repaired sessions.",
1230 "[!]".yellow()
1231 );
1232 println!(" Run: {}", format!("code {}", path.display()).cyan());
1233 }
1234
1235 Ok(())
1236}
1237
1238fn register_repair_recursive(
1240 root_path: Option<&str>,
1241 max_depth: Option<usize>,
1242 exclude_patterns: &[String],
1243 dry_run: bool,
1244 force: bool,
1245 close_vscode: bool,
1246 reopen: bool,
1247) -> Result<()> {
1248 let root = resolve_path(root_path);
1249 let should_close = close_vscode || reopen;
1250
1251 println!(
1252 "{} Recursively scanning for workspaces to repair from: {}",
1253 "[CSM]".cyan().bold(),
1254 root.display()
1255 );
1256
1257 if dry_run {
1258 println!("{} Dry run mode — no changes will be made", "[!]".yellow());
1259 }
1260
1261 let vscode_was_running = is_vscode_running();
1263 if vscode_was_running && !dry_run {
1264 if should_close {
1265 if !confirm_close_vscode(force) {
1266 println!("{} Aborted.", "[!]".yellow());
1267 return Ok(());
1268 }
1269 println!(" {} Closing VS Code (saving state)...", "[*]".yellow());
1270 close_vscode_and_wait(30)?;
1271 println!(" {} VS Code closed.\n", "[OK]".green());
1272 } else if !force {
1273 println!(
1274 "{} VS Code is running. Its in-memory cache will overwrite index changes.",
1275 "[!]".yellow()
1276 );
1277 println!(
1278 " Use {} to close VS Code first, or {} to force.",
1279 "--reopen".cyan(),
1280 "--force".cyan()
1281 );
1282 return Err(CsmError::VSCodeRunning.into());
1283 }
1284 }
1285
1286 let workspaces = discover_workspaces()?;
1288 println!(
1289 " Found {} VS Code workspaces to check",
1290 workspaces.len().to_string().cyan()
1291 );
1292
1293 let mut workspace_map: std::collections::HashMap<String, Vec<&crate::models::Workspace>> =
1295 std::collections::HashMap::new();
1296 for ws in &workspaces {
1297 if let Some(ref project_path) = ws.project_path {
1298 let normalized = normalize_path(project_path);
1299 workspace_map.entry(normalized).or_default().push(ws);
1300 }
1301 }
1302
1303 let exclude_matchers: Vec<glob::Pattern> = exclude_patterns
1305 .iter()
1306 .filter_map(|p| glob::Pattern::new(p).ok())
1307 .collect();
1308
1309 let default_excludes = [
1310 "node_modules",
1311 ".git",
1312 "target",
1313 "build",
1314 "dist",
1315 ".venv",
1316 "venv",
1317 "__pycache__",
1318 ".cache",
1319 "vendor",
1320 ".cargo",
1321 ];
1322
1323 let mut total_dirs_scanned = 0usize;
1324 let mut workspaces_found = 0usize;
1325 let mut total_compacted = 0usize;
1326 let mut total_synced = 0usize;
1327 let mut total_issues_found = 0usize;
1328 let mut total_issues_fixed = 0usize;
1329 let mut repair_results: Vec<(String, usize, bool, String)> = Vec::new(); fn walk_for_repair(
1333 dir: &Path,
1334 root: &Path,
1335 current_depth: usize,
1336 max_depth: Option<usize>,
1337 workspace_map: &std::collections::HashMap<String, Vec<&crate::models::Workspace>>,
1338 exclude_matchers: &[glob::Pattern],
1339 default_excludes: &[&str],
1340 dry_run: bool,
1341 force: bool,
1342 total_dirs_scanned: &mut usize,
1343 workspaces_found: &mut usize,
1344 total_compacted: &mut usize,
1345 total_synced: &mut usize,
1346 total_issues_found: &mut usize,
1347 total_issues_fixed: &mut usize,
1348 repair_results: &mut Vec<(String, usize, bool, String)>,
1349 ) -> Result<()> {
1350 if let Some(max) = max_depth {
1351 if current_depth > max {
1352 return Ok(());
1353 }
1354 }
1355
1356 *total_dirs_scanned += 1;
1357
1358 let normalized = normalize_path(&dir.to_string_lossy());
1360 if let Some(ws_list) = workspace_map.get(&normalized) {
1361 for ws in ws_list {
1362 if ws.has_chat_sessions && ws.chat_session_count > 0 {
1363 *workspaces_found += 1;
1364
1365 let display_name = ws.project_path.as_deref().unwrap_or(&ws.hash);
1366
1367 let chat_dir = ws.workspace_path.join("chatSessions");
1369 match crate::storage::diagnose_workspace_sessions(&ws.hash, &chat_dir) {
1370 Ok(diag) => {
1371 let issue_count = diag.issues.len();
1372 *total_issues_found += issue_count;
1373
1374 if issue_count == 0 {
1375 println!(
1376 " {} {} — {} sessions, healthy",
1377 "[OK]".green(),
1378 display_name.cyan(),
1379 ws.chat_session_count
1380 );
1381 repair_results.push((
1382 display_name.to_string(),
1383 0,
1384 true,
1385 "healthy".to_string(),
1386 ));
1387 } else {
1388 let issue_kinds: Vec<String> = {
1389 let mut kinds: Vec<String> = Vec::new();
1390 for issue in &diag.issues {
1391 let s = format!("{}", issue.kind);
1392 if !kinds.contains(&s) {
1393 kinds.push(s);
1394 }
1395 }
1396 kinds
1397 };
1398
1399 println!(
1400 " {} {} — {} sessions, {} issue(s): {}",
1401 "[!]".yellow(),
1402 display_name.cyan(),
1403 ws.chat_session_count,
1404 issue_count,
1405 issue_kinds.join(", ")
1406 );
1407
1408 if !dry_run {
1409 if let Some(ref project_path) = ws.project_path {
1411 match crate::workspace::recover_orphaned_sessions_from_old_hashes(project_path) {
1412 Ok(0) => {}
1413 Ok(n) => {
1414 println!(
1415 " {} Recovered {} orphaned session(s) from old hashes",
1416 "[OK]".green(),
1417 n
1418 );
1419 }
1420 Err(_) => {} }
1422 }
1423
1424 match repair_workspace_sessions(
1425 &ws.hash,
1426 &chat_dir,
1427 force || true,
1428 ) {
1429 Ok((compacted, synced)) => {
1430 *total_compacted += compacted;
1431 *total_synced += synced;
1432 *total_issues_fixed += issue_count;
1433
1434 let mut deleted_json = 0;
1436 let mut jsonl_sessions: HashSet<String> =
1437 HashSet::new();
1438 if let Ok(entries) = std::fs::read_dir(&chat_dir) {
1439 for entry in entries.flatten() {
1440 let p = entry.path();
1441 if p.extension().is_some_and(|e| e == "jsonl") {
1442 if let Some(stem) = p.file_stem() {
1443 jsonl_sessions.insert(
1444 stem.to_string_lossy().to_string(),
1445 );
1446 }
1447 }
1448 }
1449 }
1450 if let Ok(entries) = std::fs::read_dir(&chat_dir) {
1451 for entry in entries.flatten() {
1452 let p = entry.path();
1453 if p.extension().is_some_and(|e| e == "json") {
1454 if let Some(stem) = p.file_stem() {
1455 if jsonl_sessions.contains(
1456 &stem.to_string_lossy().to_string(),
1457 ) {
1458 let bak =
1459 p.with_extension("json.bak");
1460 let _ = std::fs::rename(&p, &bak);
1461 deleted_json += 1;
1462 }
1463 }
1464 }
1465 }
1466 }
1467 if deleted_json > 0 {
1468 let _ = repair_workspace_sessions(
1469 &ws.hash, &chat_dir, true,
1470 );
1471 }
1472
1473 let _ = repair_workspace_db_caches(
1475 &ws.hash, &chat_dir, false,
1476 );
1477
1478 let detail = format!(
1479 "{} compacted, {} synced{}",
1480 compacted,
1481 synced,
1482 if deleted_json > 0 {
1483 format!(
1484 ", {} stale .json backed up",
1485 deleted_json
1486 )
1487 } else {
1488 String::new()
1489 }
1490 );
1491 println!(" {} Fixed: {}", "[OK]".green(), detail);
1492 repair_results.push((
1493 display_name.to_string(),
1494 issue_count,
1495 true,
1496 detail,
1497 ));
1498 }
1499 Err(e) => {
1500 println!(" {} Failed: {}", "[ERR]".red(), e);
1501 repair_results.push((
1502 display_name.to_string(),
1503 issue_count,
1504 false,
1505 e.to_string(),
1506 ));
1507 }
1508 }
1509 } else {
1510 for issue in &diag.issues {
1512 println!(
1513 " {} {} — {}",
1514 "→".bright_black(),
1515 issue.session_id[..8.min(issue.session_id.len())]
1516 .to_string(),
1517 issue.kind
1518 );
1519 }
1520 repair_results.push((
1521 display_name.to_string(),
1522 issue_count,
1523 true,
1524 "dry run".to_string(),
1525 ));
1526 }
1527 }
1528 }
1529 Err(e) => {
1530 println!(" {} {} — scan failed: {}", "[ERR]".red(), display_name, e);
1531 }
1532 }
1533 }
1534 }
1535 }
1536
1537 let entries = match std::fs::read_dir(dir) {
1539 Ok(e) => e,
1540 Err(_) => return Ok(()),
1541 };
1542
1543 for entry in entries {
1544 let entry = match entry {
1545 Ok(e) => e,
1546 Err(_) => continue,
1547 };
1548 let path = entry.path();
1549 if !path.is_dir() {
1550 continue;
1551 }
1552
1553 let dir_name = entry.file_name().to_string_lossy().to_string();
1554
1555 if dir_name.starts_with('.') {
1557 continue;
1558 }
1559
1560 if default_excludes.iter().any(|e| dir_name == *e) {
1562 continue;
1563 }
1564
1565 if exclude_matchers.iter().any(|p| p.matches(&dir_name)) {
1567 continue;
1568 }
1569
1570 walk_for_repair(
1571 &path,
1572 root,
1573 current_depth + 1,
1574 max_depth,
1575 workspace_map,
1576 exclude_matchers,
1577 default_excludes,
1578 dry_run,
1579 force,
1580 total_dirs_scanned,
1581 workspaces_found,
1582 total_compacted,
1583 total_synced,
1584 total_issues_found,
1585 total_issues_fixed,
1586 repair_results,
1587 )?;
1588 }
1589
1590 Ok(())
1591 }
1592
1593 walk_for_repair(
1594 &root,
1595 &root,
1596 0,
1597 max_depth,
1598 &workspace_map,
1599 &exclude_matchers,
1600 &default_excludes,
1601 dry_run,
1602 force,
1603 &mut total_dirs_scanned,
1604 &mut workspaces_found,
1605 &mut total_compacted,
1606 &mut total_synced,
1607 &mut total_issues_found,
1608 &mut total_issues_fixed,
1609 &mut repair_results,
1610 )?;
1611
1612 println!("\n{}", "═".repeat(60).cyan());
1614 println!("{} Recursive repair scan complete", "[OK]".green().bold());
1615 println!("{}", "═".repeat(60).cyan());
1616 println!(
1617 " Directories scanned: {}",
1618 total_dirs_scanned.to_string().cyan()
1619 );
1620 println!(
1621 " Workspaces found: {}",
1622 workspaces_found.to_string().cyan()
1623 );
1624 println!(
1625 " Issues detected: {}",
1626 if total_issues_found > 0 {
1627 total_issues_found.to_string().yellow()
1628 } else {
1629 total_issues_found.to_string().green()
1630 }
1631 );
1632 if !dry_run {
1633 println!(
1634 " Issues fixed: {}",
1635 total_issues_fixed.to_string().green()
1636 );
1637 println!(
1638 " Files compacted: {}",
1639 total_compacted.to_string().cyan()
1640 );
1641 println!(
1642 " Index entries synced: {}",
1643 total_synced.to_string().cyan()
1644 );
1645 }
1646
1647 let failed_count = repair_results.iter().filter(|(_, _, ok, _)| !ok).count();
1648 if failed_count > 0 {
1649 println!(
1650 "\n {} {} workspace(s) had repair errors",
1651 "[!]".yellow(),
1652 failed_count.to_string().red()
1653 );
1654 }
1655
1656 if reopen && vscode_was_running {
1658 println!(" {} Reopening VS Code...", "[*]".yellow());
1659 reopen_vscode(None)?;
1660 println!(
1661 " {} VS Code launched. Sessions should now load correctly.",
1662 "[OK]".green()
1663 );
1664 } else if should_close && vscode_was_running {
1665 println!(
1666 "\n{} VS Code was closed. Reopen it to see the repaired sessions.",
1667 "[!]".yellow()
1668 );
1669 }
1670
1671 Ok(())
1672}
1673
1674fn repair_workspace_db_caches(
1682 workspace_id: &str,
1683 chat_sessions_dir: &Path,
1684 verbose: bool,
1685) -> Result<()> {
1686 let db_path = get_workspace_storage_db(workspace_id)?;
1687 if !db_path.exists() {
1688 return Ok(());
1689 }
1690
1691 match recover_from_json_bak(chat_sessions_dir) {
1693 Ok(0) => {}
1694 Ok(n) => {
1695 if verbose {
1696 println!(
1697 " {} Recovered {} session(s) from .json.bak",
1698 "[OK]".green(),
1699 n
1700 );
1701 }
1702 }
1703 Err(e) => {
1704 if verbose {
1705 println!(
1706 " {} .json.bak recovery failed: {}",
1707 "[WARN]".yellow(),
1708 e
1709 );
1710 }
1711 }
1712 }
1713
1714 match read_chat_session_index(&db_path) {
1716 Ok(index) => {
1717 if let Ok(n) = rebuild_model_cache(&db_path, &index) {
1718 if verbose && n > 0 {
1719 println!(
1720 " {} Model cache rebuilt ({} entries)",
1721 "[OK]".green(),
1722 n
1723 );
1724 }
1725 }
1726 }
1727 Err(_) => {}
1728 }
1729
1730 let mut valid_ids: HashSet<String> = HashSet::new();
1732 if chat_sessions_dir.exists() {
1733 if let Ok(entries) = std::fs::read_dir(chat_sessions_dir) {
1734 for entry in entries.flatten() {
1735 let p = entry.path();
1736 if p.extension().is_some_and(|e| e == "jsonl") {
1737 if let Some(stem) = p.file_stem() {
1738 valid_ids.insert(stem.to_string_lossy().to_string());
1739 }
1740 }
1741 }
1742 }
1743 }
1744
1745 match cleanup_state_cache(&db_path, &valid_ids) {
1747 Ok(n) if n > 0 && verbose => {
1748 println!(
1749 " {} State cache: removed {} stale entries",
1750 "[OK]".green(),
1751 n
1752 );
1753 }
1754 _ => {}
1755 }
1756
1757 let preferred_id = read_chat_session_index(&db_path).ok().and_then(|idx| {
1759 idx.entries
1760 .iter()
1761 .filter(|(_, e)| !e.is_empty)
1762 .max_by_key(|(_, e)| e.last_message_date)
1763 .map(|(id, _)| id.clone())
1764 });
1765 match fix_session_memento(&db_path, &valid_ids, preferred_id.as_deref()) {
1766 Ok(true) if verbose => {
1767 println!(
1768 " {} Memento updated to: {}",
1769 "[OK]".green(),
1770 preferred_id.as_deref().unwrap_or("(first valid)"),
1771 );
1772 }
1773 _ => {}
1774 }
1775
1776 Ok(())
1777}
1778
1779fn register_repair_all(force: bool, close_vscode: bool, reopen: bool) -> Result<()> {
1781 let should_close = close_vscode || reopen;
1782
1783 println!(
1784 "{} Repairing all workspaces with chat sessions...\n",
1785 "[CSM]".cyan().bold(),
1786 );
1787
1788 let vscode_was_running = is_vscode_running();
1790 if vscode_was_running {
1791 if should_close {
1792 if !confirm_close_vscode(force) {
1793 println!("{} Aborted.", "[!]".yellow());
1794 return Ok(());
1795 }
1796 println!(" {} Closing VS Code (saving state)...", "[*]".yellow());
1797 close_vscode_and_wait(30)?;
1798 println!(" {} VS Code closed.\n", "[OK]".green());
1799 } else if !force {
1800 println!(
1801 "{} VS Code is running. Its in-memory cache will overwrite index changes.",
1802 "[!]".yellow()
1803 );
1804 println!(
1805 " Use {} to close VS Code first, or {} to force.",
1806 "--reopen".cyan(),
1807 "--force".cyan()
1808 );
1809 return Err(CsmError::VSCodeRunning.into());
1810 }
1811 }
1812
1813 let workspaces = discover_workspaces()?;
1814 let ws_with_sessions: Vec<_> = workspaces
1815 .iter()
1816 .filter(|w| w.has_chat_sessions && w.chat_session_count > 0)
1817 .collect();
1818
1819 if ws_with_sessions.is_empty() {
1820 println!("{} No workspaces with chat sessions found.", "[!]".yellow());
1821 return Ok(());
1822 }
1823
1824 println!(
1825 " Found {} workspaces with chat sessions\n",
1826 ws_with_sessions.len().to_string().cyan()
1827 );
1828
1829 let mut total_compacted = 0usize;
1830 let mut total_synced = 0usize;
1831 let mut succeeded = 0usize;
1832 let mut failed = 0usize;
1833
1834 for (i, ws) in ws_with_sessions.iter().enumerate() {
1835 let display_name = ws.project_path.as_deref().unwrap_or(&ws.hash);
1836 println!(
1837 "[{}/{}] {} {}",
1838 i + 1,
1839 ws_with_sessions.len(),
1840 "===".dimmed(),
1841 display_name.cyan()
1842 );
1843
1844 let chat_sessions_dir = ws.workspace_path.join("chatSessions");
1845 if !chat_sessions_dir.exists() {
1846 println!(
1847 " {} No chatSessions directory, skipping.\n",
1848 "[!]".yellow()
1849 );
1850 continue;
1851 }
1852
1853 match repair_workspace_sessions(&ws.hash, &chat_sessions_dir, true) {
1854 Ok((compacted, index_fixed)) => {
1855 let mut deleted_json = 0;
1857 let mut jsonl_sessions: HashSet<String> = HashSet::new();
1858 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1859 let entry = entry?;
1860 let p = entry.path();
1861 if p.extension().is_some_and(|e| e == "jsonl") {
1862 if let Some(stem) = p.file_stem() {
1863 jsonl_sessions.insert(stem.to_string_lossy().to_string());
1864 }
1865 }
1866 }
1867 for entry in std::fs::read_dir(&chat_sessions_dir)? {
1868 let entry = entry?;
1869 let p = entry.path();
1870 if p.extension().is_some_and(|e| e == "json") {
1871 if let Some(stem) = p.file_stem() {
1872 if jsonl_sessions.contains(&stem.to_string_lossy().to_string()) {
1873 let bak = p.with_extension("json.bak");
1874 std::fs::rename(&p, &bak)?;
1875 deleted_json += 1;
1876 }
1877 }
1878 }
1879 }
1880 if deleted_json > 0 {
1881 repair_workspace_sessions(&ws.hash, &chat_sessions_dir, true)?;
1882 }
1883
1884 let _ = repair_workspace_db_caches(&ws.hash, &chat_sessions_dir, false);
1886
1887 total_compacted += compacted;
1888 total_synced += index_fixed;
1889 succeeded += 1;
1890 println!(
1891 " {} {} compacted, {} synced{}\n",
1892 "[OK]".green(),
1893 compacted,
1894 index_fixed,
1895 if deleted_json > 0 {
1896 format!(", {} stale .json backed up", deleted_json)
1897 } else {
1898 String::new()
1899 }
1900 );
1901 }
1902 Err(e) => {
1903 failed += 1;
1904 println!(" {} {}\n", "[ERR]".red(), e);
1905 }
1906 }
1907 }
1908
1909 println!(
1910 "{} Repair complete: {}/{} workspaces, {} compacted, {} index entries synced",
1911 "[OK]".green().bold(),
1912 succeeded.to_string().green(),
1913 ws_with_sessions.len(),
1914 total_compacted.to_string().cyan(),
1915 total_synced.to_string().cyan()
1916 );
1917 if failed > 0 {
1918 println!(
1919 " {} {} workspace(s) had errors",
1920 "[!]".yellow(),
1921 failed.to_string().red()
1922 );
1923 }
1924
1925 if reopen && vscode_was_running {
1927 println!(" {} Reopening VS Code...", "[*]".yellow());
1928 reopen_vscode(None)?;
1929 println!(
1930 " {} VS Code launched. Sessions should now load correctly.",
1931 "[OK]".green()
1932 );
1933 } else if should_close && vscode_was_running {
1934 println!(
1935 "\n{} VS Code was closed. Reopen it to see the repaired sessions.",
1936 "[!]".yellow()
1937 );
1938 }
1939
1940 Ok(())
1941}
1942
1943pub fn register_trim(
1950 project_path: Option<&str>,
1951 keep: usize,
1952 session_id: Option<&str>,
1953 all: bool,
1954 threshold_mb: u64,
1955 force: bool,
1956) -> Result<()> {
1957 let path = resolve_path(project_path);
1958
1959 println!(
1960 "{} Trimming oversized sessions for: {}",
1961 "[CSM]".cyan().bold(),
1962 path.display()
1963 );
1964
1965 let path_str = path.to_string_lossy().to_string();
1967 let (ws_id, ws_path, _folder) = find_workspace_by_path(&path_str)?
1968 .ok_or_else(|| CsmError::WorkspaceNotFound(path.display().to_string()))?;
1969
1970 let chat_sessions_dir = ws_path.join("chatSessions");
1971
1972 if !chat_sessions_dir.exists() {
1973 println!(
1974 "{} No chatSessions directory found at: {}",
1975 "[!]".yellow(),
1976 chat_sessions_dir.display()
1977 );
1978 return Ok(());
1979 }
1980
1981 if !force && is_vscode_running() {
1983 println!(
1984 "{} VS Code is running. Use {} to force.",
1985 "[!]".yellow(),
1986 "--force".cyan()
1987 );
1988 return Err(CsmError::VSCodeRunning.into());
1989 }
1990
1991 let mut trimmed_count = 0;
1992
1993 if let Some(sid) = session_id {
1994 let jsonl_path = chat_sessions_dir.join(format!("{}.jsonl", sid));
1996 if !jsonl_path.exists() {
1997 return Err(
1998 CsmError::InvalidSessionFormat(format!("Session not found: {}", sid)).into(),
1999 );
2000 }
2001
2002 let size_mb = std::fs::metadata(&jsonl_path)?.len() / (1024 * 1024);
2003 println!(
2004 " {} Trimming {} ({}MB, keeping last {} requests)...",
2005 "[*]".cyan(),
2006 sid,
2007 size_mb,
2008 keep
2009 );
2010
2011 match trim_session_jsonl(&jsonl_path, keep) {
2012 Ok((orig, kept, orig_mb, new_mb)) => {
2013 println!(
2014 " {} Trimmed: {} → {} requests, {:.1}MB → {:.1}MB",
2015 "[OK]".green(),
2016 orig,
2017 kept,
2018 orig_mb,
2019 new_mb
2020 );
2021 trimmed_count += 1;
2022 }
2023 Err(e) => {
2024 println!(" {} Failed to trim {}: {}", "[ERR]".red(), sid, e);
2025 }
2026 }
2027 } else if all {
2028 for entry in std::fs::read_dir(&chat_sessions_dir)? {
2030 let entry = entry?;
2031 let p = entry.path();
2032 if p.extension().is_some_and(|e| e == "jsonl") {
2033 let size = std::fs::metadata(&p)?.len();
2034 let size_mb_val = size / (1024 * 1024);
2035
2036 if size_mb_val >= threshold_mb {
2037 let stem = p
2038 .file_stem()
2039 .map(|s| s.to_string_lossy().to_string())
2040 .unwrap_or_default();
2041 println!(
2042 " {} Trimming {} ({}MB, keeping last {} requests)...",
2043 "[*]".cyan(),
2044 stem,
2045 size_mb_val,
2046 keep
2047 );
2048
2049 match trim_session_jsonl(&p, keep) {
2050 Ok((orig, kept, orig_mb, new_mb)) => {
2051 println!(
2052 " {} Trimmed: {} → {} requests, {:.1}MB → {:.1}MB",
2053 "[OK]".green(),
2054 orig,
2055 kept,
2056 orig_mb,
2057 new_mb
2058 );
2059 trimmed_count += 1;
2060 }
2061 Err(e) => {
2062 println!(" {} Failed to trim {}: {}", "[WARN]".yellow(), stem, e);
2063 }
2064 }
2065 }
2066 }
2067 }
2068 } else {
2069 let mut largest: Option<(PathBuf, u64)> = None;
2071
2072 for entry in std::fs::read_dir(&chat_sessions_dir)? {
2073 let entry = entry?;
2074 let p = entry.path();
2075 if p.extension().is_some_and(|e| e == "jsonl") {
2076 let size = std::fs::metadata(&p)?.len();
2077 let size_mb_val = size / (1024 * 1024);
2078
2079 if size_mb_val >= threshold_mb {
2080 if largest.as_ref().map_or(true, |(_, s)| size > *s) {
2081 largest = Some((p, size));
2082 }
2083 }
2084 }
2085 }
2086
2087 match largest {
2088 Some((p, size)) => {
2089 let stem = p
2090 .file_stem()
2091 .map(|s| s.to_string_lossy().to_string())
2092 .unwrap_or_default();
2093 let size_mb_val = size / (1024 * 1024);
2094 println!(
2095 " {} Trimming largest session: {} ({}MB, keeping last {} requests)...",
2096 "[*]".cyan(),
2097 stem,
2098 size_mb_val,
2099 keep
2100 );
2101
2102 match trim_session_jsonl(&p, keep) {
2103 Ok((orig, kept, orig_mb, new_mb)) => {
2104 println!(
2105 " {} Trimmed: {} → {} requests, {:.1}MB → {:.1}MB",
2106 "[OK]".green(),
2107 orig,
2108 kept,
2109 orig_mb,
2110 new_mb
2111 );
2112 trimmed_count += 1;
2113 }
2114 Err(e) => {
2115 println!(" {} Failed to trim: {}", "[ERR]".red(), e);
2116 }
2117 }
2118 }
2119 None => {
2120 println!(
2121 " {} No sessions found over {}MB threshold. Use {} to lower the threshold.",
2122 "[*]".cyan(),
2123 threshold_mb,
2124 "--threshold-mb".cyan()
2125 );
2126 }
2127 }
2128 }
2129
2130 if trimmed_count > 0 {
2131 let _ = repair_workspace_sessions(&ws_id, &chat_sessions_dir, true);
2133 println!(
2134 "\n{} Trim complete: {} session(s) trimmed. Full history backed up as .jsonl.bak",
2135 "[OK]".green().bold(),
2136 trimmed_count.to_string().cyan()
2137 );
2138 }
2139
2140 Ok(())
2141}