← Projects

Dual-Memory

A two-agent memory system for AI assistants. A fast Writer captures facts into a Neo4j graph, and a slower Editor keeps that graph clean.

Stack
TypeScript, Python, Neo4j, LLMs
Source
github.com/ZhuoqunWei/dual-memory

Most assistant memory either appends text forever or searches a flat vector store. That works at first, then degrades: duplicates pile up, stale facts survive, and relationships are hard to query. Dual-Memory splits the job between two agents with different priorities.

The Writer, a fast TypeScript plugin, extracts facts, embeds them, writes graph nodes, and recalls context. The Editor, a slow Python agent, deduplicates, supersedes stale facts, resolves entities, mines relationships, and scores confidence. Both work on a Neo4j graph. In the example, the memory "lives in Portland" supersedes the older memory "lives in Seattle", which is kept and marked outdated. WRITERfast, runs inlineextract factsembed themwrite graph nodesrecall contextTypeScript pluginNEO4J GRAPHUserkept, marked outdatedlives in Seattlelives in PortlandSUPERSEDESEDITORslow, runs periodicallydeduplicatesupersede stale factsresolve entitiesmine relationshipsscore confidencePython agentevery run can be rolled backwriterecalledit
Two agents, one graph. The Writer captures quickly during conversations; the Editor cleans up later with the whole graph in view. A newer fact supersedes an older one instead of deleting it.

Writer and Editor

Retrieval

Retrieval combines vector similarity with graph expansion, weighted by confidence and salience, with a penalty for superseded facts:

score = cosine
      × (0.6 + 0.4 · confidence)
      × (0.6 + 0.4 · salience)
      × (0.7 if superseded else 1)

Evaluation

A gold-labelled harness pushes 40 synthetic sessions (102 facts) through the real write path, runs the Editor, and asks 50 questions through the real retrieval. It scores duplicates, supersessions, entity merges, and whether the right facts come back. LLM replies and embeddings are recorded, so CI replays the full loop without API keys.

Duplicate precision

0%50%100%12345678Step 1: 55%Step 2: 55%Step 3: 68%Step 4: 81%Step 5: 81%Step 6: 81%Step 7: 89%Step 8: 89%55%89%

Contradiction recall

0%50%100%12345678Step 1: 13%Step 2: 13%Step 3: 7%Step 4: 87%Step 5: 87%Step 6: 87%Step 7: 100%Step 8: 100%13%100%

Retrieval recall@5

0%50%100%12345678Step 1: 82%Step 2: 82%Step 3: 88%Step 4: 81%Step 5: 80%Step 6: 80%Step 7: 86%Step 8: 86%82%86%

LLM calls per run

015030012345678Step 1: 270Step 2: 58Step 3: 57Step 4: 57Step 5: 58Step 6: 58Step 7: 61Step 8: 6127061
  1. Baseline
  2. Relationship pairs from non-hub entities only
  3. True cosine similarity in the duplicate gate
  4. Temporal supersession
  5. Three-tier entity resolution
  6. Idempotent runs and rollback
  7. Editor effort raised to medium
  8. Names in one memory can never be merged
Show the numbers
StepDuplicate precisionContradiction recallRetrieval recall@5LLM calls per run
1. Baseline55%13%82%270
2. Relationship pairs from non-hub entities only55%13%82%58
3. True cosine similarity in the duplicate gate68%7%88%57
4. Temporal supersession81%87%81%57
5. Three-tier entity resolution81%87%80%58
6. Idempotent runs and rollback81%87%80%58
7. Editor effort raised to medium89%100%86%61
8. Names in one memory can never be merged89%100%86%61
One change at a time. Each step adds one change and is measured on the same fixture. Step 2 cut LLM calls by almost 80%; step 4 taught the Editor that a newer fact replaces an older one. The final run reaches 86.5% recall@5 (shown rounded).

Integration tests against a real Neo4j cover idempotency and rollback.

The fixture was also used for tuning, each step is a single LLM sample, and it doesn’t evaluate conversation extraction. Running the Editor trades some recall (91.5% → 86.5%) for better ranking of stale facts.