git diff for what an AI agent knows.
Pick two points in an agent's run and see only what changed in its memory — facts created, reconfirmed, invalidated, superseded — each attributed to the input that caused it.
$ memdiff diff --scope user_42 --from ep2_confirm --to ep3_misread
scope user_42 ep2_confirm → ep3_misread
1 step(s), read at 2026-03-03T12:00:00+00:00
+ CREATED Alice --MANAGES--> Dana
from ep3_misread “Alice manages Dana”
- INVALIDATED Alice --REPORTS_TO--> Dana
from ep3_misread “Alice reports to Dana”
supersedes
~ Alice --REPORTS_TO--> Dana ⇒ Alice --MANAGES--> Dana
inferred, high · timestamp-identity
1 created, 1 invalidatedAn agent misread "I'm covering for Dana while she's on leave" and came away believing it manages its own manager. Two lines, and the turn that caused them.
Execution tracers — LangSmith, Langfuse, Phoenix — show the control-flow graph: which tool ran, which step threw. None of them show the memory-content graph: what the agent believes, how that belief changed, and which input changed it.
memdiff shows the second one.
Requires Python ≥3.10. Docker only if you want the bundled Neo4j.
git clone https://github.com/Aryan-Pillai7/memdiff.git
cd memdiff
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev,api,mem0]"
cp .env.example .env # defaults match the compose file
docker compose up -d neo4j # Neo4j 5.26 · bolt :7687 · browser :7474Load the demo corpus and check the connection:
python scripts/load_fixtures.py
memdiff doctormemdiff scopes # which memory partitions exist
memdiff steps --scope user_42 # points you can diff between
memdiff diff --scope user_42 # the whole run
memdiff diff --scope user_42 --from S1 --to S2 # one window
memdiff diff --scope user_42 --json | jq # for piping
memdiff blame 86b2a038 --scope user_42 # why does it believe this?
memdiff blame "manages Dana" --search # look one up by wordingThe window is (from, to] — what changed after --from, up to and
including --to, the same convention as git diff A B. Both bounds are
optional. --exit-code returns 1 when there are changes.
Usually the question you arrive with is not what changed in this window but why does the agent think this:
$ memdiff blame 438fa47f --scope user_42
Alice --REPORTS_TO--> Dana 438fa47f
“Alice reports to Dana”
retired, asserted 2 times
asserted by ep1_truth 2026-03-01T12:00:00+00:00
“Dana is my manager.”
reconfirmed ep2_confirm 2026-03-02T12:00:00+00:00
“Dana approved my leave request.”
retired by ep3_misread 2026-03-03T12:00:00+00:00
“I'm covering for Dana while she's on leave.”
replaced by 86b2a038 (inferred, high · timestamp-identity)It takes a fact id — copy the short id from diff output. --search looks
one up by wording instead, and an ambiguous search lists candidates rather than
guessing.
memdiff serve # http://127.0.0.1:7777
memdiff serve --target bolt://localhost:7687 --target ~/.mem0 # several at onceA timeline scrubber picks the window, the diff list is the primary view, and
clicking a change expands into its blame chain or a graph neighbourhood. Pass
--target more than once and a store picker appears.
It reads the same library the CLI does — /diff is byte-identical to
memdiff diff --json. Binds to loopback by default: there is no auth and the
graph may contain private conversation history.
The adapter is chosen from the connection target — a bolt:// URI is Graphiti,
a filesystem path is Mem0 — and --adapter overrides.
| Graphiti | Mem0 | |
|---|---|---|
| Status | stable | under development — known limitations |
| Target | bolt:// / neo4j:// URI |
path to the store folder |
| Pinned | graphiti-core==0.29.3 |
mem0ai==2.0.18, Qdrant |
| Shape | typed graph | flat records |
| Diff window | between steps | time range |
| Attribution | provenance-exact | by clock, always marked |
RECONFIRMED / RETROACTIVE |
✅ | declared unsupported |
Every absence is declared rather than rendered blank — see docs/limitations.md for what each store cannot tell you and why.
Mem0 is under development. It lacks durable step boundaries, so attribution is transaction-time only and memdiff cannot localize which conversation turn caused a belief change. The CLI warns and asks to confirm (
--yesskips it); the web view shows a dismissible banner. Nothing is blocked — the limitation is stated so you can weigh it.
| Command | What it runs |
|---|---|
pytest -m "not integration and not seeding" |
Unit tests. No Neo4j, no network. |
pytest -m integration |
Needs Neo4j. Wipes the fixture groups — reload after. |
ruff check . && mypy src |
Lint and types. |
cd web && npm test -- --run |
Frontend tests. |
python scripts/browser_check.py |
Real-browser checks against a running serve. |
src/memdiff/
adapters/graphiti/ Neo4j reader, provenance index, blame
adapters/mem0/ SQLite + Qdrant reader, change-log classifier
core/ store-neutral taxonomy, supersession, blame
cli/ api/ the two surfaces
web/ React + Vite frontend
docs/ contract, schema findings, limitations
Adding a memory system is a new adapter module plus registration — see docs/adapter-contract.md, which also records where that claim held and where it did not.
Early development. Both adapters and both surfaces work end to end; nothing is API-stable yet.
MIT — see LICENSE.