Repositories / agent-snapshot.git

agent-snapshot.git

Clone (read-only): git clone http://git.guha-anderson.com/git/agent-snapshot.git

Branch

Print snapshot capture summary

Author
Arjun Guha <a.guha@northeastern.edu>
Date
2026-05-03 17:43:27 -0400
Commit
f3ac0991250d76e869f24550712ca6697bb0f606
src/ocaml/agent_snapshot.ml
index 1f6c70b..57e9cee 100644
--- a/src/ocaml/agent_snapshot.ml
+++ b/src/ocaml/agent_snapshot.ml
@@ -585,6 +585,23 @@ let write_manifest (out : string) (command : string list) (exit_status : int) : 
   in
   Json.to_file ~std:true (concat_path out "manifest.json") (Manifest_json.to_yojson manifest)
 
+let operation_was_recorded (recd : file_record) (operation : string) : bool =
+  Hashtbl.mem recd.operations operation
+
+let print_snapshot_summary () : unit =
+  let updated_files = ref 0 in
+  let uncommitted_read_files = ref 0 in
+  Hashtbl.iter
+    (fun _ recd ->
+      if
+        (operation_was_recorded recd "write" || operation_was_recorded recd "delete")
+        && (Option.is_some recd.after.blob || recd.after.tombstone)
+      then incr updated_files;
+      if operation_was_recorded recd "read" && Option.is_some recd.before.blob then incr uncommitted_read_files)
+    files;
+  Printf.eprintf "Worked in %d repositories. Saved %d updated files. Saved %d read files in the snapshot that were not committed.\n%!"
+    (Hashtbl.length repos) !updated_files !uncommitted_read_files
+
 (** Resolve relative syscall paths against cwd or a directory fd as required by *at syscalls. *)
 let resolve_path (proc : proc_state) (dirfd : int) (path : string) : string =
   if is_absolute path then normalize_path path
@@ -769,6 +786,7 @@ let run_snapshot (output : string option) (command : string list) : int =
     finalize_records ();
     close_blob_writer ());
   write_manifest output command 0;
+  print_snapshot_summary ();
   0
 
 open Cmdliner
tests/test_agent_snapshot.py
index 37becb8..e022d24 100644
--- a/tests/test_agent_snapshot.py
+++ b/tests/test_agent_snapshot.py
@@ -338,6 +338,22 @@ def test_traced_command_options_do_not_need_separator(tmp_path):
     assert Snapshot(out).manifest["command"] == [PYTHON, "-c", "print('direct command')"]
 
 
+def test_run_prints_stderr_summary(tmp_path):
+    out = tmp_path / "snapshot"
+
+    completed = subprocess.run(
+        [str(BIN), "--snapshot-dir", str(out), "/bin/true"],
+        cwd=ROOT,
+        text=True,
+        check=True,
+        capture_output=True,
+    )
+
+    assert completed.stderr.endswith(
+        "Worked in 0 repositories. Saved 0 updated files. Saved 0 read files in the snapshot that were not committed.\n"
+    )
+
+
 def test_rename_records_source_tombstone_and_destination_content(tmp_path):
     # Rename is not just a write: a replay-equivalent snapshot needs to know that
     # the source path stopped existing and that the destination path acquired the