1use anyhow::Result;
7use clap::{Args, Subcommand};
8use colored::Colorize;
9use memvid_core::Memvid;
10#[cfg(feature = "replay")]
11use memvid_core::replay::ActionType;
12#[cfg(feature = "replay")]
13use memvid_ask_model::run_model_inference;
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, name, status, summary.action_count, summary.checkpoint_count
340 );
341 }
342 }
343 }
344
345 Ok(())
346}
347
348#[cfg(feature = "replay")]
349fn handle_session_view(args: SessionViewArgs) -> Result<()> {
350 let mut mem = Memvid::open(&args.file)?;
351
352 mem.load_replay_sessions()?;
354
355 let session_id: uuid::Uuid = args.session.parse()?;
356 let session = mem
357 .get_session(session_id)
358 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?;
359
360 if args.format == "json" {
361 let json = serde_json::to_string_pretty(&session)?;
362 println!("{}", json);
363 } else {
364 let name = session.name.as_deref().unwrap_or("(unnamed)");
365 println!("Session: {} - {}", session.session_id, name);
366 println!(" Created: {}", session.created_secs);
367 if let Some(ended) = session.ended_secs {
368 println!(" Ended: {}", ended);
369 println!(" Duration: {}s", session.duration_secs());
370 } else {
371 println!(" Status: recording");
372 }
373 println!(" Actions: {}", session.actions.len());
374 println!(" Checkpoints: {}", session.checkpoints.len());
375
376 if !session.actions.is_empty() {
377 println!("\nActions:");
378 for (i, action) in session.actions.iter().enumerate().take(20) {
379 println!(
380 " [{}] {} - {:?}",
381 i, action.action_type.name(), action.action_type
382 );
383 }
384 if session.actions.len() > 20 {
385 println!(" ... and {} more", session.actions.len() - 20);
386 }
387 }
388 }
389
390 Ok(())
391}
392
393#[cfg(feature = "replay")]
394fn handle_session_checkpoint(args: SessionCheckpointArgs) -> Result<()> {
395 let mut mem = Memvid::open(&args.file)?;
396
397 let checkpoint_id = mem.create_checkpoint()?;
398 println!("Created checkpoint: {}", checkpoint_id);
399
400 Ok(())
401}
402
403#[cfg(feature = "replay")]
404fn handle_session_delete(args: SessionDeleteArgs) -> Result<()> {
405 let mut mem = Memvid::open(&args.file)?;
406
407 mem.load_replay_sessions()?;
409
410 let session_id: uuid::Uuid = args.session.parse()?;
411 mem.delete_session(session_id)?;
412
413 mem.save_replay_sessions()?;
415 mem.commit()?;
416
417 println!("Deleted session: {}", session_id);
418 Ok(())
419}
420
421#[cfg(feature = "replay")]
422fn handle_session_save(args: SessionSaveArgs) -> Result<()> {
423 let mut mem = Memvid::open(&args.file)?;
424
425 mem.load_replay_sessions()?;
427
428 let session_count = mem.list_sessions().len();
430 if session_count == 0 {
431 println!("No sessions to save. Sessions are automatically saved when you run `session end`.");
432 return Ok(());
433 }
434
435 mem.save_replay_sessions()?;
437 mem.commit()?;
438
439 println!("Saved {} session(s) to file.", session_count);
440 Ok(())
441}
442
443#[cfg(feature = "replay")]
444fn handle_session_load(args: SessionLoadArgs) -> Result<()> {
445 let mut mem = Memvid::open(&args.file)?;
446
447 mem.load_replay_sessions()?;
448
449 let sessions = mem.list_sessions();
450 println!("Loaded {} sessions from file.", sessions.len());
451
452 Ok(())
453}
454
455#[cfg(feature = "replay")]
456fn handle_session_replay(args: SessionReplayArgs) -> Result<()> {
457 #[cfg(feature = "web")]
459 if args.web {
460 use crate::commands::session_web::start_web_server;
461 use std::path::PathBuf;
462
463 let rt = tokio::runtime::Runtime::new()?;
464 return rt.block_on(start_web_server(
465 args.session.clone(),
466 PathBuf::from(&args.file),
467 args.port,
468 !args.no_open,
469 ));
470 }
471
472 use memvid_core::replay::{ReplayEngine, ReplayExecutionConfig};
473
474 let mut mem = Memvid::open_read_only(&args.file)?;
477
478 mem.load_replay_sessions()?;
480
481 let session_id: uuid::Uuid = args.session.parse()?;
482 let session = mem
483 .get_session(session_id)
484 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?
485 .clone();
486
487 let config = ReplayExecutionConfig {
488 skip_puts: args.skip_mutations,
489 skip_finds: args.skip_finds,
490 skip_asks: args.skip_asks,
491 stop_on_mismatch: args.stop_on_mismatch,
492 verbose: true,
493 top_k: args.top_k,
494 adaptive: args.adaptive,
495 min_relevancy: args.min_relevancy,
496 audit_mode: args.audit,
497 use_model: args.use_model.clone(),
498 generate_diff: args.diff,
499 };
500
501 let mut engine = ReplayEngine::new(&mut mem, config);
502 let result = engine.replay_session_from(&session, args.from_checkpoint)?;
503
504 if args.format == "json" {
505 let json = serde_json::to_string_pretty(&result)?;
506 println!("{}", json);
507 } else {
508 println!();
509 println!("{}", "━".repeat(60).dimmed());
510 println!("{} {}", "Replaying session:".bold(), session_id.to_string().cyan());
511 println!("{} {}", "File:".dimmed(), args.file);
512
513 let mode_str = if args.audit {
515 "Audit (frozen retrieval)".green().to_string()
516 } else {
517 "Debug (showing recorded actions)".white().to_string()
518 };
519 println!("{} {}", "Mode:".dimmed(), mode_str);
520
521 if let Some(ref model) = args.use_model {
523 println!("{} {}", "Model Override:".dimmed(), model.yellow());
524 }
525
526 println!("{}", "━".repeat(60).dimmed());
527 println!();
528
529 let total = result.action_results.len();
531 for (action_idx, action_result) in result.action_results.iter().enumerate() {
532 let seq = action_result.sequence;
533 let action_type = &action_result.action_type;
534 let diff = action_result.diff.as_deref().unwrap_or("");
535
536 if diff == "skipped" && action_type != "ASK" {
538 continue;
539 }
540
541 if action_type == "ASK" {
543 let status = if action_result.matched {
545 "✓".green()
546 } else {
547 "✗".red()
548 };
549 println!(
550 "{} Step {}/{} {} {}",
551 status,
552 (seq + 1).to_string().yellow(),
553 total,
554 "ask".cyan().bold(),
555 ""
556 );
557 for line in diff.lines() {
559 println!(" {}", line);
560 }
561
562 if args.audit && args.use_model.is_some() {
564 if let Some(original_action) = session.actions.get(action_idx) {
565 if let ActionType::Ask { query, .. } = &original_action.action_type {
566 let frame_ids = &original_action.affected_frames;
567 if !frame_ids.is_empty() {
568 let mut context_parts = Vec::new();
570 for (idx, frame_id) in frame_ids.iter().enumerate() {
571 if let Ok(content) = mem.frame_text_by_id(*frame_id) {
572 let snippet = if content.len() > 1000 {
574 format!("{}...", &content[..1000])
575 } else {
576 content
577 };
578 context_parts.push(format!("[{}] {}", idx + 1, snippet));
579 }
580 }
581 let frozen_context = context_parts.join("\n\n");
582
583 let model_name = args.use_model.as_ref().unwrap();
585 match run_model_inference(
586 model_name,
587 query,
588 &frozen_context,
589 &[], None,
591 None,
592 None,
593 ) {
594 Ok(inference) => {
595 let new_answer = &inference.answer.answer;
596 let original_answer = &original_action.output_preview;
597
598 println!(" {} \"{}\"", "New Answer:".green().bold(),
600 if new_answer.len() > 150 {
601 format!("{}...", &new_answer[..150])
602 } else {
603 new_answer.clone()
604 });
605
606 if args.diff {
608 let same = new_answer.trim() == original_answer.trim();
609 if same {
610 println!(" {} {}", "Diff:".dimmed(), "IDENTICAL".green());
611 } else {
612 println!(" {} {}", "Diff:".dimmed(), "CHANGED".yellow());
613 }
614 }
615 }
616 Err(e) => {
617 println!(" {} {}", "LLM Error:".red(), e);
618 }
619 }
620 }
621 }
622 }
623 }
624 println!();
625 } else if action_type == "FIND" && diff.contains("DISCOVERY:") || diff.contains("FILTER:") {
626 let colored_diff = if diff.starts_with("FILTER:") {
628 diff.replace("FILTER:", &"FILTER:".red().bold().to_string())
629 .replace("would be MISSED", &"would be MISSED".red().to_string())
630 } else if diff.starts_with("DISCOVERY:") {
631 diff.replace("DISCOVERY:", &"DISCOVERY:".green().bold().to_string())
632 .replace("[NEW]", &"[NEW]".green().to_string())
633 } else {
634 diff.to_string()
635 };
636 println!(
637 " Step {}/{} {} - {}",
638 (seq + 1).to_string().yellow(),
639 total,
640 action_type.cyan(),
641 colored_diff
642 );
643 } else if !diff.is_empty() && diff != "skipped" {
644 let status = if action_result.matched { "✓" } else { "✗" };
646 println!(
647 " {} Step {}/{} {} - {}",
648 if action_result.matched { status.green() } else { status.red() },
649 (seq + 1).to_string().yellow(),
650 total,
651 action_type.cyan(),
652 diff.dimmed()
653 );
654 }
655 }
656
657 println!("{}", "━".repeat(60).dimmed());
658 println!();
659 println!("{}", "Summary:".bold());
660 println!(" Total actions: {}", result.total_actions.to_string().white());
661 println!(" Matched: {}", result.matched_actions.to_string().green());
662 println!(" Mismatched: {}", result.mismatched_actions.to_string().red());
663 println!(" Skipped: {}", result.skipped_actions.to_string().yellow());
664 println!(" Match rate: {}", format!("{:.1}%", result.match_rate()).cyan());
665 println!(" Duration: {}ms", result.total_duration_ms);
666
667 if let Some(cp) = result.from_checkpoint {
668 println!(" Started from checkpoint: {}", cp);
669 }
670
671 println!();
672 if result.is_success() {
673 println!("{}", "✓ Replay completed successfully".green().bold());
674 } else {
675 println!("{}", "✗ Replay found mismatches".red().bold());
676 }
677 }
678
679 Ok(())
680}
681
682#[cfg(feature = "replay")]
683fn handle_session_compare(args: SessionCompareArgs) -> Result<()> {
684 use memvid_core::replay::ReplayEngine;
685
686 let mut mem = Memvid::open(&args.file)?;
687
688 mem.load_replay_sessions()?;
690
691 let session_a_id: uuid::Uuid = args.session_a.parse()?;
692 let session_b_id: uuid::Uuid = args.session_b.parse()?;
693
694 let session_a = mem
695 .get_session(session_a_id)
696 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_a_id))?
697 .clone();
698
699 let session_b = mem
700 .get_session(session_b_id)
701 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_b_id))?
702 .clone();
703
704 let comparison = ReplayEngine::compare_sessions(&session_a, &session_b);
705
706 if args.format == "json" {
707 let json = serde_json::to_string_pretty(&comparison)?;
708 println!("{}", json);
709 } else {
710 println!("Session Comparison:");
711 println!(" Session A: {}", session_a_id);
712 println!(" Session B: {}", session_b_id);
713 println!();
714
715 if comparison.is_identical() {
716 println!("✓ Sessions are identical");
717 println!(" Matching actions: {}", comparison.matching_actions);
718 } else {
719 println!("✗ Sessions differ:");
720 println!(" Matching actions: {}", comparison.matching_actions);
721
722 if !comparison.actions_only_in_a.is_empty() {
723 println!(
724 " Actions only in A: {:?}",
725 comparison.actions_only_in_a
726 );
727 }
728
729 if !comparison.actions_only_in_b.is_empty() {
730 println!(
731 " Actions only in B: {:?}",
732 comparison.actions_only_in_b
733 );
734 }
735
736 if !comparison.differing_actions.is_empty() {
737 println!(" Differing actions:");
738 for diff in &comparison.differing_actions {
739 println!(
740 " [{}] {} vs {} - {}",
741 diff.sequence, diff.action_type_a, diff.action_type_b, diff.description
742 );
743 }
744 }
745 }
746 }
747
748 Ok(())
749}