Skip to main content

Memory Graph

The memory graph is DriftGuard's core data structure. It is a directed graph where nodes represent normalized text concepts and edges represent causal relationships.

Two independent graphs​

DriftGuard actually maintains two memory graphs side by side:

  • the mistake graph — built from record() calls
  • the success graph — built from record_success() calls

They share the same structure, merge engine, and prune engine, but are completely separate stores with their own persistence files/tables. Recording a mistake never affects the success graph and vice versa. See Success Memory for details on the success graph and how it surfaces reinforcements.

The rest of this page describes the structure that both graphs share.

Structure​

Every record() call adds a three-node chain:

action node → feedback node → outcome node

Example:

"increase salt" → "too salty" → "dish ruined"

Semantic merging​

When a new event arrives, the merge engine checks for similar existing nodes before creating new ones.

guard.record("increase salt", "too salty", "dish ruined")
guard.record("add more salt", "over-seasoned", "dish ruined")

The second call merges "add more salt" into the existing "increase salt" node because their cosine similarity exceeds the action threshold. The frequency counter increments. No duplicate node is created.

This means two paraphrased mistakes reinforce one memory rather than fragmenting the graph.

Node attributes​

AttributeDescription
typeaction, feedback, or outcome
embeddingNormalized float32 vector
frequencyTimes merged or seen
first_seenUTC datetime of first record
last_seenUTC datetime of most recent reinforcement

Edge attributes​

AttributeDescription
frequencyTimes this causal link was observed
weightEdge weight (default 1.0)
created_atUTC datetime of first creation

Graph stats​

print(guard.stats())
# → {'mistakes': {'nodes': 9, 'edges': 8}, 'successes': {'nodes': 4, 'edges': 3}}

guard.stats() always reports both graphs — mistakes and successes — each with their own nodes/edges counts.

Pruning​

Call guard.prune() on a schedule to keep both graphs healthy:

result = guard.prune()
print(result)
# → {
# 'status': 'pruned',
# 'mistakes': {
# 'before': {'nodes': 12, 'edges': 10},
# 'after': {'nodes': 9, 'edges': 8},
# 'details': {...}
# },
# 'successes': {
# 'before': {'nodes': 5, 'edges': 4},
# 'after': {'nodes': 4, 'edges': 3},
# 'details': {...}
# }
# }

prune() runs a full deep-prune pass over the mistake graph and the success graph in a single call. Pruning removes:

  1. Weak edges — seen fewer times than prune_edge_min_frequency (default 2)
  2. Stale nodes — not updated within prune_node_stale_days (default 60)
  3. Isolated nodes — no edges remaining after step 1 and 2

Persistence​

Both graphs save automatically after every record() / record_success() call, and load automatically when DriftGuard is instantiated.

See Storage Backends for JSON, SQLite, and Postgres options.