1use anyhow::Result;
7use clap::{Args, Subcommand};
8use colored::Colorize;
9#[cfg(feature = "replay")]
10use memvid_ask_model::run_model_inference;
11#[cfg(feature = "replay")]
12use memvid_core::replay::ActionType;
13use memvid_core::Memvid;
14
15#[derive(Args, Debug)]
17pub struct SessionArgs {
18 #[command(subcommand)]
19 pub command: SessionCommand,
20}
21
22#[derive(Subcommand, Debug)]
23pub enum SessionCommand {
24 Start(SessionStartArgs),
26 End(SessionEndArgs),
28 List(SessionListArgs),
30 View(SessionViewArgs),
32 Checkpoint(SessionCheckpointArgs),
34 Delete(SessionDeleteArgs),
36 Save(SessionSaveArgs),
38 Load(SessionLoadArgs),
40 Replay(SessionReplayArgs),
42 Compare(SessionCompareArgs),
44}
45
46#[derive(Args, Debug)]
47pub struct SessionStartArgs {
48 pub file: String,
50
51 #[arg(short, long)]
53 pub name: Option<String>,
54
55 #[arg(long, default_value = "0")]
57 pub auto_checkpoint: u64,
58}
59
60#[derive(Args, Debug)]
61pub struct SessionEndArgs {
62 pub file: String,
64}
65
66#[derive(Args, Debug)]
67pub struct SessionListArgs {
68 pub file: String,
70
71 #[arg(short, long, default_value = "text")]
73 pub format: String,
74}
75
76#[derive(Args, Debug)]
77pub struct SessionViewArgs {
78 pub file: String,
80
81 #[arg(short, long)]
83 pub session: String,
84
85 #[arg(short, long, default_value = "text")]
87 pub format: String,
88}
89
90#[derive(Args, Debug)]
91pub struct SessionCheckpointArgs {
92 pub file: String,
94}
95
96#[derive(Args, Debug)]
97pub struct SessionDeleteArgs {
98 pub file: String,
100
101 #[arg(short, long)]
103 pub session: String,
104}
105
106#[derive(Args, Debug)]
107pub struct SessionSaveArgs {
108 pub file: String,
110}
111
112#[derive(Args, Debug)]
113pub struct SessionLoadArgs {
114 pub file: String,
116}
117
118#[derive(Args, Debug)]
119pub struct SessionReplayArgs {
120 pub file: String,
122
123 #[arg(short, long)]
125 pub session: String,
126
127 #[arg(long)]
129 pub from_checkpoint: Option<u64>,
130
131 #[arg(short = 'k', long)]
134 pub top_k: Option<usize>,
135
136 #[arg(long)]
138 pub adaptive: bool,
139
140 #[arg(long, default_value = "0.5")]
142 pub min_relevancy: f32,
143
144 #[arg(long)]
146 pub skip_mutations: bool,
147
148 #[arg(long)]
150 pub skip_finds: bool,
151
152 #[arg(long)]
154 pub skip_asks: bool,
155
156 #[arg(long)]
158 pub stop_on_mismatch: bool,
159
160 #[arg(long)]
163 pub audit: bool,
164
165 #[arg(long)]
168 pub use_model: Option<String>,
169
170 #[arg(long)]
172 pub diff: bool,
173
174 #[arg(short, long, default_value = "text")]
176 pub format: String,
177
178 #[cfg(feature = "web")]
180 #[arg(long)]
181 pub web: bool,
182
183 #[cfg(feature = "web")]
185 #[arg(long, default_value = "4242")]
186 pub port: u16,
187
188 #[cfg(feature = "web")]
190 #[arg(long)]
191 pub no_open: bool,
192}
193
194#[derive(Args, Debug)]
195pub struct SessionCompareArgs {
196 pub file: String,
198
199 #[arg(short = 'a', long)]
201 pub session_a: String,
202
203 #[arg(short = 'b', long)]
205 pub session_b: String,
206
207 #[arg(short, long, default_value = "text")]
209 pub format: String,
210}
211
212#[cfg(feature = "replay")]
214pub fn handle_session(args: SessionArgs) -> Result<()> {
215 match args.command {
216 SessionCommand::Start(args) => handle_session_start(args),
217 SessionCommand::End(args) => handle_session_end(args),
218 SessionCommand::List(args) => handle_session_list(args),
219 SessionCommand::View(args) => handle_session_view(args),
220 SessionCommand::Checkpoint(args) => handle_session_checkpoint(args),
221 SessionCommand::Delete(args) => handle_session_delete(args),
222 SessionCommand::Save(args) => handle_session_save(args),
223 SessionCommand::Load(args) => handle_session_load(args),
224 SessionCommand::Replay(args) => handle_session_replay(args),
225 SessionCommand::Compare(args) => handle_session_compare(args),
226 }
227}
228
229#[cfg(not(feature = "replay"))]
230pub fn handle_session(_args: SessionArgs) -> Result<()> {
231 bail!("Session recording requires the 'replay' feature. Rebuild with --features replay")
232}
233
234#[cfg(feature = "replay")]
235fn handle_session_start(args: SessionStartArgs) -> Result<()> {
236 use memvid_core::replay::ReplayConfig;
237
238 let mut mem = Memvid::open(&args.file)?;
239
240 if mem.load_active_session()? {
242 let session_id = mem.active_session_id().unwrap();
243 anyhow::bail!(
244 "Session {} is already active. End it first with `session end`.",
245 session_id
246 );
247 }
248
249 let config = ReplayConfig {
250 auto_checkpoint_interval: args.auto_checkpoint,
251 ..Default::default()
252 };
253
254 let session_id = mem.start_session(args.name, Some(config))?;
255
256 mem.save_active_session()?;
258
259 println!("Started session: {}", session_id);
260 println!("Recording is now active. Run operations like put, find, ask.");
261 println!("End the session with: memvid session end {}", args.file);
262
263 Ok(())
264}
265
266#[cfg(feature = "replay")]
267fn handle_session_end(args: SessionEndArgs) -> Result<()> {
268 let mut mem = Memvid::open(&args.file)?;
269
270 if !mem.load_active_session()? {
272 anyhow::bail!("No active session found. Start one with `session start`.");
273 }
274
275 let session = mem.end_session()?;
276 println!("Ended session: {}", session.session_id);
277 println!(" Actions recorded: {}", session.actions.len());
278 println!(" Checkpoints: {}", session.checkpoints.len());
279 println!(" Duration: {}s", session.duration_secs());
280
281 mem.commit()?;
284
285 mem.save_replay_sessions()?;
287
288 mem.commit()?;
290
291 mem.clear_active_session_file()?;
293
294 println!("Session saved to file.");
295 Ok(())
296}
297
298#[cfg(feature = "replay")]
299fn handle_session_list(args: SessionListArgs) -> Result<()> {
300 let mut mem = Memvid::open(&args.file)?;
301
302 let has_active = mem.load_active_session()?;
304
305 mem.load_replay_sessions()?;
307
308 let sessions = mem.list_sessions();
309
310 if !has_active && sessions.is_empty() {
311 println!("No recorded sessions.");
312 return Ok(());
313 }
314
315 if args.format == "json" {
316 let json = serde_json::to_string_pretty(&sessions)?;
317 println!("{}", json);
318 } else {
319 if has_active {
321 if let Some(session_id) = mem.active_session_id() {
322 println!("Active session (recording):");
323 println!(" {} - currently recording", session_id);
324 println!();
325 }
326 }
327
328 if !sessions.is_empty() {
329 println!("Completed sessions ({}):", sessions.len());
330 for summary in sessions {
331 let name = summary.name.as_deref().unwrap_or("(unnamed)");
332 let status = if summary.ended_secs.is_some() {
333 "completed"
334 } else {
335 "recording"
336 };
337 println!(
338 " {} - {} [{}, {} actions, {} checkpoints]",
339 summary.session_id,
340 name,
341 status,
342 summary.action_count,
343 summary.checkpoint_count
344 );
345 }
346 }
347 }
348
349 Ok(())
350}
351
352#[cfg(feature = "replay")]
353fn handle_session_view(args: SessionViewArgs) -> Result<()> {
354 let mut mem = Memvid::open(&args.file)?;
355
356 mem.load_replay_sessions()?;
358
359 let session_id: uuid::Uuid = args.session.parse()?;
360 let session = mem
361 .get_session(session_id)
362 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?;
363
364 if args.format == "json" {
365 let json = serde_json::to_string_pretty(&session)?;
366 println!("{}", json);
367 } else {
368 let name = session.name.as_deref().unwrap_or("(unnamed)");
369 println!("Session: {} - {}", session.session_id, name);
370 println!(" Created: {}", session.created_secs);
371 if let Some(ended) = session.ended_secs {
372 println!(" Ended: {}", ended);
373 println!(" Duration: {}s", session.duration_secs());
374 } else {
375 println!(" Status: recording");
376 }
377 println!(" Actions: {}", session.actions.len());
378 println!(" Checkpoints: {}", session.checkpoints.len());
379
380 if !session.actions.is_empty() {
381 println!("\nActions:");
382 for (i, action) in session.actions.iter().enumerate().take(20) {
383 println!(
384 " [{}] {} - {:?}",
385 i,
386 action.action_type.name(),
387 action.action_type
388 );
389 }
390 if session.actions.len() > 20 {
391 println!(" ... and {} more", session.actions.len() - 20);
392 }
393 }
394 }
395
396 Ok(())
397}
398
399#[cfg(feature = "replay")]
400fn handle_session_checkpoint(args: SessionCheckpointArgs) -> Result<()> {
401 let mut mem = Memvid::open(&args.file)?;
402
403 let checkpoint_id = mem.create_checkpoint()?;
404 println!("Created checkpoint: {}", checkpoint_id);
405
406 Ok(())
407}
408
409#[cfg(feature = "replay")]
410fn handle_session_delete(args: SessionDeleteArgs) -> Result<()> {
411 let mut mem = Memvid::open(&args.file)?;
412
413 mem.load_replay_sessions()?;
415
416 let session_id: uuid::Uuid = args.session.parse()?;
417 mem.delete_session(session_id)?;
418
419 mem.save_replay_sessions()?;
421 mem.commit()?;
422
423 println!("Deleted session: {}", session_id);
424 Ok(())
425}
426
427#[cfg(feature = "replay")]
428fn handle_session_save(args: SessionSaveArgs) -> Result<()> {
429 let mut mem = Memvid::open(&args.file)?;
430
431 mem.load_replay_sessions()?;
433
434 let session_count = mem.list_sessions().len();
436 if session_count == 0 {
437 println!(
438 "No sessions to save. Sessions are automatically saved when you run `session end`."
439 );
440 return Ok(());
441 }
442
443 mem.save_replay_sessions()?;
445 mem.commit()?;
446
447 println!("Saved {} session(s) to file.", session_count);
448 Ok(())
449}
450
451#[cfg(feature = "replay")]
452fn handle_session_load(args: SessionLoadArgs) -> Result<()> {
453 let mut mem = Memvid::open(&args.file)?;
454
455 mem.load_replay_sessions()?;
456
457 let sessions = mem.list_sessions();
458 println!("Loaded {} sessions from file.", sessions.len());
459
460 Ok(())
461}
462
463#[cfg(feature = "replay")]
464fn handle_session_replay(args: SessionReplayArgs) -> Result<()> {
465 #[cfg(feature = "web")]
467 if args.web {
468 use crate::commands::session_web::start_web_server;
469 use std::path::PathBuf;
470
471 let rt = tokio::runtime::Runtime::new()?;
472 return rt.block_on(start_web_server(
473 args.session.clone(),
474 PathBuf::from(&args.file),
475 args.port,
476 !args.no_open,
477 ));
478 }
479
480 use memvid_core::replay::{ReplayEngine, ReplayExecutionConfig};
481
482 let mut mem = Memvid::open_read_only(&args.file)?;
485
486 mem.load_replay_sessions()?;
488
489 let session_id: uuid::Uuid = args.session.parse()?;
490 let session = mem
491 .get_session(session_id)
492 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?
493 .clone();
494
495 let config = ReplayExecutionConfig {
496 skip_puts: args.skip_mutations,
497 skip_finds: args.skip_finds,
498 skip_asks: args.skip_asks,
499 stop_on_mismatch: args.stop_on_mismatch,
500 verbose: true,
501 top_k: args.top_k,
502 adaptive: args.adaptive,
503 min_relevancy: args.min_relevancy,
504 audit_mode: args.audit,
505 use_model: args.use_model.clone(),
506 generate_diff: args.diff,
507 };
508
509 let mut engine = ReplayEngine::new(&mut mem, config);
510 let result = engine.replay_session_from(&session, args.from_checkpoint)?;
511
512 if args.format == "json" {
513 let json = serde_json::to_string_pretty(&result)?;
514 println!("{}", json);
515 } else {
516 println!();
517 println!("{}", "━".repeat(60).dimmed());
518 println!(
519 "{} {}",
520 "Replaying session:".bold(),
521 session_id.to_string().cyan()
522 );
523 println!("{} {}", "File:".dimmed(), args.file);
524
525 let mode_str = if args.audit {
527 "Audit (frozen retrieval)".green().to_string()
528 } else {
529 "Debug (showing recorded actions)".white().to_string()
530 };
531 println!("{} {}", "Mode:".dimmed(), mode_str);
532
533 if let Some(ref model) = args.use_model {
535 println!("{} {}", "Model Override:".dimmed(), model.yellow());
536 }
537
538 println!("{}", "━".repeat(60).dimmed());
539 println!();
540
541 let total = result.action_results.len();
543 for (action_idx, action_result) in result.action_results.iter().enumerate() {
544 let seq = action_result.sequence;
545 let action_type = &action_result.action_type;
546 let diff = action_result.diff.as_deref().unwrap_or("");
547
548 if diff == "skipped" && action_type != "ASK" {
550 continue;
551 }
552
553 if action_type == "ASK" {
555 let status = if action_result.matched {
557 "✓".green()
558 } else {
559 "✗".red()
560 };
561 println!(
562 "{} Step {}/{} {} {}",
563 status,
564 (seq + 1).to_string().yellow(),
565 total,
566 "ask".cyan().bold(),
567 ""
568 );
569 for line in diff.lines() {
571 println!(" {}", line);
572 }
573
574 if args.audit && args.use_model.is_some() {
576 if let Some(original_action) = session.actions.get(action_idx) {
577 if let ActionType::Ask { query, .. } = &original_action.action_type {
578 let frame_ids = &original_action.affected_frames;
579 if !frame_ids.is_empty() {
580 let mut context_parts = Vec::new();
582 for (idx, frame_id) in frame_ids.iter().enumerate() {
583 if let Ok(content) = mem.frame_text_by_id(*frame_id) {
584 let snippet = if content.len() > 1000 {
586 format!("{}...", &content[..1000])
587 } else {
588 content
589 };
590 context_parts.push(format!("[{}] {}", idx + 1, snippet));
591 }
592 }
593 let frozen_context = context_parts.join("\n\n");
594
595 let model_name = args.use_model.as_ref().unwrap();
597 match run_model_inference(
598 model_name,
599 query,
600 &frozen_context,
601 &[], None,
603 None,
604 None,
605 ) {
606 Ok(inference) => {
607 let new_answer = &inference.answer.answer;
608 let original_answer = &original_action.output_preview;
609
610 println!(
612 " {} \"{}\"",
613 "New Answer:".green().bold(),
614 if new_answer.len() > 150 {
615 format!("{}...", &new_answer[..150])
616 } else {
617 new_answer.clone()
618 }
619 );
620
621 if args.diff {
623 let same = new_answer.trim() == original_answer.trim();
624 if same {
625 println!(
626 " {} {}",
627 "Diff:".dimmed(),
628 "IDENTICAL".green()
629 );
630 } else {
631 println!(
632 " {} {}",
633 "Diff:".dimmed(),
634 "CHANGED".yellow()
635 );
636 }
637 }
638 }
639 Err(e) => {
640 println!(" {} {}", "LLM Error:".red(), e);
641 }
642 }
643 }
644 }
645 }
646 }
647 println!();
648 } else if action_type == "FIND" && diff.contains("DISCOVERY:")
649 || diff.contains("FILTER:")
650 {
651 let colored_diff = if diff.starts_with("FILTER:") {
653 diff.replace("FILTER:", &"FILTER:".red().bold().to_string())
654 .replace("would be MISSED", &"would be MISSED".red().to_string())
655 } else if diff.starts_with("DISCOVERY:") {
656 diff.replace("DISCOVERY:", &"DISCOVERY:".green().bold().to_string())
657 .replace("[NEW]", &"[NEW]".green().to_string())
658 } else {
659 diff.to_string()
660 };
661 println!(
662 " Step {}/{} {} - {}",
663 (seq + 1).to_string().yellow(),
664 total,
665 action_type.cyan(),
666 colored_diff
667 );
668 } else if !diff.is_empty() && diff != "skipped" {
669 let status = if action_result.matched { "✓" } else { "✗" };
671 println!(
672 " {} Step {}/{} {} - {}",
673 if action_result.matched {
674 status.green()
675 } else {
676 status.red()
677 },
678 (seq + 1).to_string().yellow(),
679 total,
680 action_type.cyan(),
681 diff.dimmed()
682 );
683 }
684 }
685
686 println!("{}", "━".repeat(60).dimmed());
687 println!();
688 println!("{}", "Summary:".bold());
689 println!(
690 " Total actions: {}",
691 result.total_actions.to_string().white()
692 );
693 println!(" Matched: {}", result.matched_actions.to_string().green());
694 println!(
695 " Mismatched: {}",
696 result.mismatched_actions.to_string().red()
697 );
698 println!(" Skipped: {}", result.skipped_actions.to_string().yellow());
699 println!(
700 " Match rate: {}",
701 format!("{:.1}%", result.match_rate()).cyan()
702 );
703 println!(" Duration: {}ms", result.total_duration_ms);
704
705 if let Some(cp) = result.from_checkpoint {
706 println!(" Started from checkpoint: {}", cp);
707 }
708
709 println!();
710 if result.is_success() {
711 println!("{}", "✓ Replay completed successfully".green().bold());
712 } else {
713 println!("{}", "✗ Replay found mismatches".red().bold());
714 }
715 }
716
717 Ok(())
718}
719
720#[cfg(feature = "replay")]
721fn handle_session_compare(args: SessionCompareArgs) -> Result<()> {
722 use memvid_core::replay::ReplayEngine;
723
724 let mut mem = Memvid::open(&args.file)?;
725
726 mem.load_replay_sessions()?;
728
729 let session_a_id: uuid::Uuid = args.session_a.parse()?;
730 let session_b_id: uuid::Uuid = args.session_b.parse()?;
731
732 let session_a = mem
733 .get_session(session_a_id)
734 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_a_id))?
735 .clone();
736
737 let session_b = mem
738 .get_session(session_b_id)
739 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_b_id))?
740 .clone();
741
742 let comparison = ReplayEngine::compare_sessions(&session_a, &session_b);
743
744 if args.format == "json" {
745 let json = serde_json::to_string_pretty(&comparison)?;
746 println!("{}", json);
747 } else {
748 println!("Session Comparison:");
749 println!(" Session A: {}", session_a_id);
750 println!(" Session B: {}", session_b_id);
751 println!();
752
753 if comparison.is_identical() {
754 println!("✓ Sessions are identical");
755 println!(" Matching actions: {}", comparison.matching_actions);
756 } else {
757 println!("✗ Sessions differ:");
758 println!(" Matching actions: {}", comparison.matching_actions);
759
760 if !comparison.actions_only_in_a.is_empty() {
761 println!(" Actions only in A: {:?}", comparison.actions_only_in_a);
762 }
763
764 if !comparison.actions_only_in_b.is_empty() {
765 println!(" Actions only in B: {:?}", comparison.actions_only_in_b);
766 }
767
768 if !comparison.differing_actions.is_empty() {
769 println!(" Differing actions:");
770 for diff in &comparison.differing_actions {
771 println!(
772 " [{}] {} vs {} - {}",
773 diff.sequence, diff.action_type_a, diff.action_type_b, diff.description
774 );
775 }
776 }
777 }
778 }
779
780 Ok(())
781}