1use anyhow::Result;
7use clap::{Args, Subcommand};
8use colored::Colorize;
9use memvid_core::Memvid;
10
11#[derive(Args, Debug)]
13pub struct SessionArgs {
14 #[command(subcommand)]
15 pub command: SessionCommand,
16}
17
18#[derive(Subcommand, Debug)]
19pub enum SessionCommand {
20 Start(SessionStartArgs),
22 End(SessionEndArgs),
24 List(SessionListArgs),
26 View(SessionViewArgs),
28 Checkpoint(SessionCheckpointArgs),
30 Delete(SessionDeleteArgs),
32 Save(SessionSaveArgs),
34 Load(SessionLoadArgs),
36 Replay(SessionReplayArgs),
38 Compare(SessionCompareArgs),
40}
41
42#[derive(Args, Debug)]
43pub struct SessionStartArgs {
44 pub file: String,
46
47 #[arg(short, long)]
49 pub name: Option<String>,
50
51 #[arg(long, default_value = "0")]
53 pub auto_checkpoint: u64,
54}
55
56#[derive(Args, Debug)]
57pub struct SessionEndArgs {
58 pub file: String,
60}
61
62#[derive(Args, Debug)]
63pub struct SessionListArgs {
64 pub file: String,
66
67 #[arg(short, long, default_value = "text")]
69 pub format: String,
70}
71
72#[derive(Args, Debug)]
73pub struct SessionViewArgs {
74 pub file: String,
76
77 #[arg(short, long)]
79 pub session: String,
80
81 #[arg(short, long, default_value = "text")]
83 pub format: String,
84}
85
86#[derive(Args, Debug)]
87pub struct SessionCheckpointArgs {
88 pub file: String,
90}
91
92#[derive(Args, Debug)]
93pub struct SessionDeleteArgs {
94 pub file: String,
96
97 #[arg(short, long)]
99 pub session: String,
100}
101
102#[derive(Args, Debug)]
103pub struct SessionSaveArgs {
104 pub file: String,
106}
107
108#[derive(Args, Debug)]
109pub struct SessionLoadArgs {
110 pub file: String,
112}
113
114#[derive(Args, Debug)]
115pub struct SessionReplayArgs {
116 pub file: String,
118
119 #[arg(short, long)]
121 pub session: String,
122
123 #[arg(long)]
125 pub from_checkpoint: Option<u64>,
126
127 #[arg(short = 'k', long)]
130 pub top_k: Option<usize>,
131
132 #[arg(long)]
134 pub adaptive: bool,
135
136 #[arg(long, default_value = "0.5")]
138 pub min_relevancy: f32,
139
140 #[arg(long)]
142 pub skip_mutations: bool,
143
144 #[arg(long)]
146 pub skip_finds: bool,
147
148 #[arg(long)]
150 pub skip_asks: bool,
151
152 #[arg(long)]
154 pub stop_on_mismatch: bool,
155
156 #[arg(short, long, default_value = "text")]
158 pub format: String,
159
160 #[cfg(feature = "web")]
162 #[arg(long)]
163 pub web: bool,
164
165 #[cfg(feature = "web")]
167 #[arg(long, default_value = "4242")]
168 pub port: u16,
169
170 #[cfg(feature = "web")]
172 #[arg(long)]
173 pub no_open: bool,
174}
175
176#[derive(Args, Debug)]
177pub struct SessionCompareArgs {
178 pub file: String,
180
181 #[arg(short = 'a', long)]
183 pub session_a: String,
184
185 #[arg(short = 'b', long)]
187 pub session_b: String,
188
189 #[arg(short, long, default_value = "text")]
191 pub format: String,
192}
193
194#[cfg(feature = "replay")]
196pub fn handle_session(args: SessionArgs) -> Result<()> {
197 match args.command {
198 SessionCommand::Start(args) => handle_session_start(args),
199 SessionCommand::End(args) => handle_session_end(args),
200 SessionCommand::List(args) => handle_session_list(args),
201 SessionCommand::View(args) => handle_session_view(args),
202 SessionCommand::Checkpoint(args) => handle_session_checkpoint(args),
203 SessionCommand::Delete(args) => handle_session_delete(args),
204 SessionCommand::Save(args) => handle_session_save(args),
205 SessionCommand::Load(args) => handle_session_load(args),
206 SessionCommand::Replay(args) => handle_session_replay(args),
207 SessionCommand::Compare(args) => handle_session_compare(args),
208 }
209}
210
211#[cfg(not(feature = "replay"))]
212pub fn handle_session(_args: SessionArgs) -> Result<()> {
213 bail!("Session recording requires the 'replay' feature. Rebuild with --features replay")
214}
215
216#[cfg(feature = "replay")]
217fn handle_session_start(args: SessionStartArgs) -> Result<()> {
218 use memvid_core::replay::ReplayConfig;
219
220 let mut mem = Memvid::open(&args.file)?;
221
222 if mem.load_active_session()? {
224 let session_id = mem.active_session_id().unwrap();
225 anyhow::bail!(
226 "Session {} is already active. End it first with `session end`.",
227 session_id
228 );
229 }
230
231 let config = ReplayConfig {
232 auto_checkpoint_interval: args.auto_checkpoint,
233 ..Default::default()
234 };
235
236 let session_id = mem.start_session(args.name, Some(config))?;
237
238 mem.save_active_session()?;
240
241 println!("Started session: {}", session_id);
242 println!("Recording is now active. Run operations like put, find, ask.");
243 println!("End the session with: memvid session end {}", args.file);
244
245 Ok(())
246}
247
248#[cfg(feature = "replay")]
249fn handle_session_end(args: SessionEndArgs) -> Result<()> {
250 let mut mem = Memvid::open(&args.file)?;
251
252 if !mem.load_active_session()? {
254 anyhow::bail!("No active session found. Start one with `session start`.");
255 }
256
257 let session = mem.end_session()?;
258 println!("Ended session: {}", session.session_id);
259 println!(" Actions recorded: {}", session.actions.len());
260 println!(" Checkpoints: {}", session.checkpoints.len());
261 println!(" Duration: {}s", session.duration_secs());
262
263 mem.commit()?;
266
267 mem.save_replay_sessions()?;
269
270 mem.commit()?;
272
273 mem.clear_active_session_file()?;
275
276 println!("Session saved to file.");
277 Ok(())
278}
279
280#[cfg(feature = "replay")]
281fn handle_session_list(args: SessionListArgs) -> Result<()> {
282 let mut mem = Memvid::open(&args.file)?;
283
284 let has_active = mem.load_active_session()?;
286
287 mem.load_replay_sessions()?;
289
290 let sessions = mem.list_sessions();
291
292 if !has_active && sessions.is_empty() {
293 println!("No recorded sessions.");
294 return Ok(());
295 }
296
297 if args.format == "json" {
298 let json = serde_json::to_string_pretty(&sessions)?;
299 println!("{}", json);
300 } else {
301 if has_active {
303 if let Some(session_id) = mem.active_session_id() {
304 println!("Active session (recording):");
305 println!(" {} - currently recording", session_id);
306 println!();
307 }
308 }
309
310 if !sessions.is_empty() {
311 println!("Completed sessions ({}):", sessions.len());
312 for summary in sessions {
313 let name = summary.name.as_deref().unwrap_or("(unnamed)");
314 let status = if summary.ended_secs.is_some() {
315 "completed"
316 } else {
317 "recording"
318 };
319 println!(
320 " {} - {} [{}, {} actions, {} checkpoints]",
321 summary.session_id, name, status, summary.action_count, summary.checkpoint_count
322 );
323 }
324 }
325 }
326
327 Ok(())
328}
329
330#[cfg(feature = "replay")]
331fn handle_session_view(args: SessionViewArgs) -> Result<()> {
332 let mut mem = Memvid::open(&args.file)?;
333
334 mem.load_replay_sessions()?;
336
337 let session_id: uuid::Uuid = args.session.parse()?;
338 let session = mem
339 .get_session(session_id)
340 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?;
341
342 if args.format == "json" {
343 let json = serde_json::to_string_pretty(&session)?;
344 println!("{}", json);
345 } else {
346 let name = session.name.as_deref().unwrap_or("(unnamed)");
347 println!("Session: {} - {}", session.session_id, name);
348 println!(" Created: {}", session.created_secs);
349 if let Some(ended) = session.ended_secs {
350 println!(" Ended: {}", ended);
351 println!(" Duration: {}s", session.duration_secs());
352 } else {
353 println!(" Status: recording");
354 }
355 println!(" Actions: {}", session.actions.len());
356 println!(" Checkpoints: {}", session.checkpoints.len());
357
358 if !session.actions.is_empty() {
359 println!("\nActions:");
360 for (i, action) in session.actions.iter().enumerate().take(20) {
361 println!(
362 " [{}] {} - {:?}",
363 i, action.action_type.name(), action.action_type
364 );
365 }
366 if session.actions.len() > 20 {
367 println!(" ... and {} more", session.actions.len() - 20);
368 }
369 }
370 }
371
372 Ok(())
373}
374
375#[cfg(feature = "replay")]
376fn handle_session_checkpoint(args: SessionCheckpointArgs) -> Result<()> {
377 let mut mem = Memvid::open(&args.file)?;
378
379 let checkpoint_id = mem.create_checkpoint()?;
380 println!("Created checkpoint: {}", checkpoint_id);
381
382 Ok(())
383}
384
385#[cfg(feature = "replay")]
386fn handle_session_delete(args: SessionDeleteArgs) -> Result<()> {
387 let mut mem = Memvid::open(&args.file)?;
388
389 mem.load_replay_sessions()?;
391
392 let session_id: uuid::Uuid = args.session.parse()?;
393 mem.delete_session(session_id)?;
394
395 mem.save_replay_sessions()?;
397 mem.commit()?;
398
399 println!("Deleted session: {}", session_id);
400 Ok(())
401}
402
403#[cfg(feature = "replay")]
404fn handle_session_save(args: SessionSaveArgs) -> Result<()> {
405 let mut mem = Memvid::open(&args.file)?;
406
407 mem.load_replay_sessions()?;
409
410 let session_count = mem.list_sessions().len();
412 if session_count == 0 {
413 println!("No sessions to save. Sessions are automatically saved when you run `session end`.");
414 return Ok(());
415 }
416
417 mem.save_replay_sessions()?;
419 mem.commit()?;
420
421 println!("Saved {} session(s) to file.", session_count);
422 Ok(())
423}
424
425#[cfg(feature = "replay")]
426fn handle_session_load(args: SessionLoadArgs) -> Result<()> {
427 let mut mem = Memvid::open(&args.file)?;
428
429 mem.load_replay_sessions()?;
430
431 let sessions = mem.list_sessions();
432 println!("Loaded {} sessions from file.", sessions.len());
433
434 Ok(())
435}
436
437#[cfg(feature = "replay")]
438fn handle_session_replay(args: SessionReplayArgs) -> Result<()> {
439 #[cfg(feature = "web")]
441 if args.web {
442 use crate::commands::session_web::start_web_server;
443 use std::path::PathBuf;
444
445 let rt = tokio::runtime::Runtime::new()?;
446 return rt.block_on(start_web_server(
447 args.session.clone(),
448 PathBuf::from(&args.file),
449 args.port,
450 !args.no_open,
451 ));
452 }
453
454 use memvid_core::replay::{ReplayEngine, ReplayExecutionConfig};
455
456 let mut mem = Memvid::open_read_only(&args.file)?;
459
460 mem.load_replay_sessions()?;
462
463 let session_id: uuid::Uuid = args.session.parse()?;
464 let session = mem
465 .get_session(session_id)
466 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?
467 .clone();
468
469 let config = ReplayExecutionConfig {
470 skip_puts: args.skip_mutations,
471 skip_finds: args.skip_finds,
472 skip_asks: args.skip_asks,
473 stop_on_mismatch: args.stop_on_mismatch,
474 verbose: true,
475 top_k: args.top_k,
476 adaptive: args.adaptive,
477 min_relevancy: args.min_relevancy,
478 };
479
480 let mut engine = ReplayEngine::new(&mut mem, config);
481 let result = engine.replay_session_from(&session, args.from_checkpoint)?;
482
483 if args.format == "json" {
484 let json = serde_json::to_string_pretty(&result)?;
485 println!("{}", json);
486 } else {
487 println!("{} {}", "Replay Results for Session:".bold(), session_id.to_string().cyan());
488 println!(" Total actions: {}", result.total_actions.to_string().white());
489 println!(" Matched: {}", result.matched_actions.to_string().green());
490 println!(" Mismatched: {}", result.mismatched_actions.to_string().red());
491 println!(" Skipped: {}", result.skipped_actions.to_string().yellow());
492 println!(" Match rate: {}", format!("{:.1}%", result.match_rate()).cyan());
493 println!(" Duration: {}ms", result.total_duration_ms);
494
495 if let Some(cp) = result.from_checkpoint {
496 println!(" Started from checkpoint: {}", cp);
497 }
498
499 if result.is_success() {
500 println!("\n{}", "✓ Replay successful - all actions matched".green().bold());
501 } else {
502 println!("\n{}", "✗ Replay found mismatches:".red().bold());
503 for action_result in result
504 .action_results
505 .iter()
506 .filter(|r| !r.matched && r.diff.as_ref().map_or(true, |d| d != "skipped"))
507 {
508 let diff = action_result.diff.as_deref().unwrap_or("unknown");
509 let colored_diff = if diff.starts_with("FILTER:") {
511 diff.replace("FILTER:", &"FILTER:".red().bold().to_string())
512 .replace("would be MISSED", &"would be MISSED".red().to_string())
513 } else if diff.starts_with("DISCOVERY:") {
514 diff.replace("DISCOVERY:", &"DISCOVERY:".green().bold().to_string())
515 .replace("[NEW]", &"[NEW]".green().to_string())
516 } else {
517 diff.to_string()
518 };
519 println!(
520 " [{}] {} - {}",
521 action_result.sequence.to_string().yellow(),
522 action_result.action_type.cyan(),
523 colored_diff
524 );
525 }
526 }
527 }
528
529 Ok(())
530}
531
532#[cfg(feature = "replay")]
533fn handle_session_compare(args: SessionCompareArgs) -> Result<()> {
534 use memvid_core::replay::ReplayEngine;
535
536 let mut mem = Memvid::open(&args.file)?;
537
538 mem.load_replay_sessions()?;
540
541 let session_a_id: uuid::Uuid = args.session_a.parse()?;
542 let session_b_id: uuid::Uuid = args.session_b.parse()?;
543
544 let session_a = mem
545 .get_session(session_a_id)
546 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_a_id))?
547 .clone();
548
549 let session_b = mem
550 .get_session(session_b_id)
551 .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_b_id))?
552 .clone();
553
554 let comparison = ReplayEngine::compare_sessions(&session_a, &session_b);
555
556 if args.format == "json" {
557 let json = serde_json::to_string_pretty(&comparison)?;
558 println!("{}", json);
559 } else {
560 println!("Session Comparison:");
561 println!(" Session A: {}", session_a_id);
562 println!(" Session B: {}", session_b_id);
563 println!();
564
565 if comparison.is_identical() {
566 println!("✓ Sessions are identical");
567 println!(" Matching actions: {}", comparison.matching_actions);
568 } else {
569 println!("✗ Sessions differ:");
570 println!(" Matching actions: {}", comparison.matching_actions);
571
572 if !comparison.actions_only_in_a.is_empty() {
573 println!(
574 " Actions only in A: {:?}",
575 comparison.actions_only_in_a
576 );
577 }
578
579 if !comparison.actions_only_in_b.is_empty() {
580 println!(
581 " Actions only in B: {:?}",
582 comparison.actions_only_in_b
583 );
584 }
585
586 if !comparison.differing_actions.is_empty() {
587 println!(" Differing actions:");
588 for diff in &comparison.differing_actions {
589 println!(
590 " [{}] {} vs {} - {}",
591 diff.sequence, diff.action_type_a, diff.action_type_b, diff.description
592 );
593 }
594 }
595 }
596 }
597
598 Ok(())
599}