Skip to content

feat: graph snapshot + meta sidecar for post-session audit traceability#12

Open
ramaseshanms wants to merge 2 commits intokunal12203:mainfrom
ramaseshanms:feature/graph-snapshot-audit-trail
Open

feat: graph snapshot + meta sidecar for post-session audit traceability#12
ramaseshanms wants to merge 2 commits intokunal12203:mainfrom
ramaseshanms:feature/graph-snapshot-audit-trail

Conversation

@ramaseshanms
Copy link
Copy Markdown

Hey — following up on issue #10.

Thanks for the detailed feedback, built it exactly to your spec.

What's in this PR

  • Snapshots info_graph.json to .dual-graph/graph_snapshots/ before every rescan — crash-safe, previous state is always preserved
  • Writes a .meta.json sidecar per snapshot containing scan trigger, file count, and mcp_tool_calls.jsonl line offset at that moment
  • Rotation keeps last 5 snapshot pairs — no disk bloat
  • .gitignore updated automatically
  • Snapshot logic is fully isolated — any write error fails silently, scans are never blocked

How to replay a hallucinated action

  1. Find the timestamp from mcp_tool_calls.jsonl when things went wrong
  2. Open the matching info_graph_<ts>.json — that's the exact graph state that drove the bad recommendation
  3. Check info_graph_<ts>.meta.jsonaction_log_offset to find the exact line in the action log where that session began

Tested on

  • macOS
  • Linux
  • Windows

Happy to adjust anything before merge.

@ramaseshanms
Copy link
Copy Markdown
Author

Demo: snapshot audit trail in action

Ran a simulation of 3 successive rescans to show the full lifecycle — snapshot creation, meta sidecar linking, and post-incident debugging.

Setup: 3 files in the graph, Claude uses graph_read/graph_retrieve between rescans, and Scan 2 introduces a stale import edge that causes a bad recommendation.

Full demo output (click to expand)
=================================================================
  DEMO: Graph Snapshot Audit Trail (PR #12)
=================================================================

--- Scan 1 - Initial project scan (clean graph) ---

  [graph_builder] Wrote initial info_graph.json
  Graph nodes: 3
  Graph edges: 2
  Tool calls this scan: 2

--- Scan 2 - Rescan picks up STALE import edge ---

  [snapshot] Saving current info_graph.json before overwrite...
  [snapshot] Done.

  [graph_builder] Overwrote info_graph.json with new scan
  Graph nodes: 4
  Graph edges: 4
  Tool calls this scan: 3

--- Scan 3 - Manual rescan after Claude edited wrong file ---

  [snapshot] Saving current info_graph.json before overwrite...
  [snapshot] Done.

  [graph_builder] Overwrote info_graph.json with new scan
  Graph nodes: 4
  Graph edges: 3
  Tool calls this scan: 1


=================================================================
  RESULT: Contents of graph_snapshots/
=================================================================

  info_graph_2026-03-21T21-26-06.json                   426 bytes
  info_graph_2026-03-21T21-26-06.meta.json              116 bytes
  info_graph_2026-03-21T21-26-08.json                   699 bytes
  info_graph_2026-03-21T21-26-08.meta.json              118 bytes

=================================================================
  META SIDECAR CONTENTS
=================================================================

  info_graph_2026-03-21T21-26-06.meta.json
    timestamp:         2026-03-21T21-26-06
    scan_trigger:      auto
    file_count:        3
    action_log_offset: 2

  info_graph_2026-03-21T21-26-08.meta.json
    timestamp:         2026-03-21T21-26-08
    scan_trigger:      manual
    file_count:        4
    action_log_offset: 5

=================================================================
  mcp_tool_calls.jsonl (full action log)
=================================================================

  Line 1: graph_continue       {}
  Line 2: graph_read           {"file": "src/main.py"}
  Line 3: graph_retrieve       {"query": "payment flow"}
  Line 4: graph_read           {"file": "src/payments.py"}
  Line 5: graph_read           {"file": "src/db.py"}
  Line 6: graph_scan           {"directory": "."}

=================================================================
  DEBUGGING: "Why did Claude edit src/db.py?"
=================================================================

  1. Claude edited src/db.py based on a bad graph recommendation.
     User ran a manual rescan. info_graph.json is now overwritten.
     The stale edge that caused the bad routing is GONE from the
     current graph.

  2. But we have the snapshot: info_graph_2026-03-21T21-26-08.json
     This captured the graph state BEFORE the manual rescan.

     FOUND THE SMOKING GUN:
       Edge type:  STALE_EDGE
       src/payments.py --> src/db.py
       This stale edge routed Claude to src/db.py incorrectly.

  3. Meta sidecar: info_graph_2026-03-21T21-26-08.meta.json
     action_log_offset = 5
     --> Lines 6+ in mcp_tool_calls.jsonl
         show exactly what Claude did after this graph was active.

  4. Action log entries after this snapshot:
       Line 6: graph_scan           {"directory": "."}

=================================================================
  WITHOUT THIS PR: stale edge gone, no way to trace the cause
  WITH THIS PR:    full audit trail preserved across rescans
=================================================================

Key takeaway: After a rescan, info_graph.json is overwritten and the stale edge that caused the bad routing is gone. But the snapshot preserved it — and the meta sidecar's action_log_offset points directly to the action log entries that followed, making replay unambiguous.

@kunal12203
Copy link
Copy Markdown
Owner

Hey, really well done, the fail-safe design is exactly right and the test coverage is thorough. We want to get this in.

A few things to fix before merge:

  1. graph_snapshot.py never reaches users The script is called from ~/.dual-graph/graph_snapshot.py but the install script doesn't download it. Every user would silently skip snapshots forever without knowing. You'll need to add graph_snapshot.py to the install script downloads alongside dual_graph_launch.sh and the other files.

  2. Dead code in _rotate_snapshots meta = oldest.with_suffix("").with_suffix(".meta.json") # computed but never used meta_path = oldest.parent / oldest.name.replace(".json", ".meta.json") # this is the one actually used The meta variable is assigned and immediately shadowed by meta_path. Safe to remove the first line.

  3. .gitignore change is redundant .dual-graph/graph_snapshots/ is already covered by the .dual-graph/ rule above it. Not a blocker but worth cleaning up.

  4. Needs a rebase We landed some changes to dg.ps1, dgc.ps1, and dual_graph_launch.sh today (telemetry removal + Apache 2.0). Should be a minor rebase but needed before merge.

Fix those and this is ready to go, really useful feature. Thanks!

@ramaseshanms ramaseshanms force-pushed the feature/graph-snapshot-audit-trail branch from 8a50db4 to 6146d90 Compare March 31, 2026 07:57
@ramaseshanms
Copy link
Copy Markdown
Author

All four items addressed — rebased and force-pushed.

1. graph_snapshot.py now downloaded by install script
Added the curl line alongside dgc, dg, and graperoot. Users on a fresh install will get the file automatically; no more silent skip.

2. Dead code in _rotate_snapshots removed
The shadowed meta variable is gone. meta_path is the only path now — no ambiguity.

3. .gitignore cleaned up
Dropped the redundant .dual-graph/graph_snapshots/ entry and the duplicate .dual-graph/ line. The top-level rule covers everything.

4. Rebased onto current main
Picked up the graperoot download + bash_profile / bash_profile PATH fix from the telemetry/Apache 2.0 commits. Conflict in install.sh was straightforward — kept both the graperoot line and the new graph_snapshot.py line.

Ready for another look whenever you get a chance.

…raceability

Saves timestamped copy of info_graph.json to graph_snapshots/
before each rescan so bad graph recommendations can be traced
post-session. Meta sidecar links each snapshot to its
mcp_tool_calls.jsonl offset for unambiguous replay.
Rotates to last 5 pairs. Fails silently to never block scans.
- Remove unused `meta` variable in _rotate_snapshots (shadowed by meta_path)
- Remove redundant .dual-graph/graph_snapshots/ gitignore entry (covered by .dual-graph/)
- Remove duplicate .dual-graph/ gitignore entry
- Add graph_snapshot.py download to install.sh so users actually get the file
@ramaseshanms ramaseshanms force-pushed the feature/graph-snapshot-audit-trail branch from 6146d90 to 23a232b Compare March 31, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants