memory-autopsy-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@memory-autopsy-mcplint my stored agent facts for duplicates and contradictions"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
memory-autopsy
ESLint for LLM agent memory — lint stored facts for duplicates, contradictions and staleness before your agent acts on them.
$ memory-autopsy report examples/memory.md examples/facts.json
# memory-autopsy report
Engine: `heuristic` | Facts scanned: 15 | Findings: 7
| Class | Count | Severity |
|---|---|---|
| `contradicted_explicit` | 2 | error |
| `contradicted_implicit` | 3 | error |
| `duplicate` | 1 | info |
| `unsourced` | 1 | info |
## Findings
- **contradicted_implicit** — entity `billing-store` `examples/memory.md`:5: slot 'anchor:uses' value conflict ('postgresql' -> 'mysql')
- flagged: "billing-store: uses PostgreSQL 14 for the billing store" (observed 2026-01-05)
- survivor: "billing-store: uses MySQL 8 for the billing store" (observed 2026-06-02)
- **contradicted_explicit** — entity `Globex` `examples/memory.md`:15: newer fact has transition trigger 'left' on shared anchor 'northwind'
...
## Fix plan (dry-run — pass --apply --yes-i-know to execute)
- supersede examples/memory.md:15 — "works at Northwind" (valid_to 2026-05-01)
new: Globex: employer = "Northwind" ; valid_to=2026-05-01 -- 2026-01-01 (source: profile)
- delete examples/memory.md:11 — "cache-tier: uses redis 7" ([duplicate] identical to 'cache-tier: uses redis 7')The problem it was built for
A production agent kept acting on stale provider/cron facts its own memory contradicted — an old "uses provider X" claim sat next to a newer "migrated off provider X" note, and the agent kept recommending provider X with full confidence. Nothing in the toolchain noticed: version control tracks the file, not the claims inside it; RAG retrieves both facts and lets the model arbitrate; and paraphrased contradictions share no keywords, so grep cannot see them either.
Agent memory is source code for beliefs. It deserves a linter.
Related MCP server: viberevert
What v0.4.0 adds
Alias-aware entity resolution. Entity identity is no longer exact-match: declared
(aka …)aliases and conservative normalized forms (case, punctuation,Ltd/Inc/Corp/Companysuffixes) fold into one entity at ingest, soacmeandAcme Corpare linted — and time-travelled — as one. Two names that merely look alike are never silently merged: they come back as anunresolved_aliasfinding for a human to settle. See Alias resolution below.Verify-and-confirm contradiction layer. A candidate pair that the dated slot rule cannot sequence (equal dates, one side undated) is now re-checked from structure alone: when both sides are canonical
slot = "value"records, slot-equality decides and the conflict is confirmed; when only prose evidence exists, the pair is hedged aslikely_conflictwith its reason string and a composite confidence (cosine + structured agreement) — never asserted, never auto-fixed. See The grey zone below.undo—--applyis reversible. Every rewrite snapshots the file to<name>.bak-<ts>first and prints the path;memory-autopsy undo <file>restores the newest snapshot byte-for-byte. See Undo below.10 grey-zone eval scenarios (58 total) written and labelled before the engines ran, including the two v0.1.0 paraphrase false positives. All declared targets pass under
--strict.
What v0.3.0 adds
Time travel (
as-of) —memory-autopsy as-of 2026-03-01 store.jsonlanswers what was considered true on that date:valid_from/valid_towindows are honoured,observed_atis the fallback, records with no temporal anchor are reported asunanchoredinstead of guessed. See Time travel below.In-loop write gate (
admit) — the SDK call an agent makes before persisting a fact. Returnsaccepted/requires_review/rejectedwith the stable ids it disagrees with and the supersede edit that would resolve the conflict. See Write gate below.MCP server —
memory-autopsy-mcpspeaks JSON-RPC over stdio, exposingmemory_lintandmemory_admitto any MCP client (Claude Desktop, Cursor, Hermes …). Read-only: it never writes a store, never calls the network. See Agent self-audit below.Canonical timeline example — examples/timeline.jsonl shows the supersession chain: the same billing store is PostgreSQL 14 until 2026-04-01, MySQL 8 after.
What v0.2.0 adds
Canonical fact model — a JSONL store, one fact per line, with a stable content-hash
id,slot/value,valid_from/valid_to,source_class,fidelityandstatus_verb.Fidelity bands — every record is tagged
FULL(an explicitslot = "value"parse),PARTIAL(entity written, claim read as prose) orRESIDUAL(entity inferred). The tag travels into every finding, so a fuzzy parse never presents itself as a proven one.Slot-conflict detection (layer 2b) — claims are decomposed into the slots they can prove, and two claims in one slot with different values conflict. Equal heads are refinement, not contradiction.
Provenance-capped verdicts — reliability is wording weight bounded by source class and fidelity; an unclassified claim can never outrank a primary-source fact, whatever it says and whenever it claims to be from. See docs/threat-model.md.
Supersede, not delete — a contradicted claim is stamped
valid_toand kept as history. Only redundant duplicates are removed.
Findings classes
Class | Layer | Severity | Meaning |
| 1 | info | Same claim stored twice (normalized-text hash match) |
| 2 | error | Newer fact carries a transition trigger on a shared anchor ("works at X" → "left X") |
| 2b/2c/3 | error | Same slot, different value, no retraction — the class grep cannot see |
| 2c/3 | review | Prose evidence only, no observation order: the pair probably disagrees, but nothing proves it. Reason string included, never auto-fixed |
| 2b/2c/3 | warn | Versioned slot or bare version bump ("redis 6" → "redis 7") superseded by a later observation |
| 0 | info | Claim has neither an |
| 0 | info | Claim already carries |
| 0 | info | Two store lines claim the same stable content id |
| 0 | review | Two entity names look alike and were not merged — confirm whether they are one entity |
| 0 | info | Empty, unreadable or malformed input (never a crash) |
| 3 | warn | Ollama unreachable — embedding layer skipped, heuristics still ran |
How a scan works
flowchart TD
A[Load memory files: markdown, JSON or JSONL] --> B[Parse: explicit slot=value, section blocks, heading blocks, bullets, JSON keys]
B --> C[Fidelity band + alias index + stable ids]
C --> C2{look-alike names?}
C2 -->|yes| C3[unresolved_alias finding — never merged]
C2 -->|no| D[Group claims by canonical entity, sort by observed date]
C3 --> D
D --> E[Layer 1: normalized-text hash duplicates]
E --> F[Layer 2: transition trigger on a shared anchor]
F --> G[Layer 2b: concrete slot conflict, needs an observation order]
G --> G2{both sides slot=value records?}
G2 -->|yes, no order| G3[Layer 2c recheck: slot-equality decides]
G2 -->|prose, no order| G4[Layer 2c hedge: likely_conflict, reason + confidence]
G3 --> H{engine is embed?}
G4 --> H
G -->|ordered| H
H -->|no| K[Report findings, each carrying its fidelity band]
H -->|yes| I{Ollama reachable?}
I -->|no| J[DEGRADED finding, fall back to heuristics]
I -->|yes| L[Layer 3: bge-m3 similarity proposes, the recheck decides, composite confidence reported]
L --> K
J --> K
K --> M[Exit code: 0 clean, 1 findings, 2 usage]Eval
eval/scenarios.json holds 58 hand-written synthetic scenarios (no real
or personal data): the 32 from v0.1.0, frozen with their original labels, the
16 written for v0.2.0, and the 10-scenario grey-zone batch written for
v0.4.0 — labels first, engines second. These are the real measured numbers
from this repo (Ollama bge-m3, 1024d):
class | heuristic P | heuristic R | embed P | embed R |
contradicted_explicit | 1.000 | 1.000 | 1.000 | 1.000 |
contradicted_implicit | 0.950 | 1.000 | 0.950 | 1.000 |
likely_conflict | 1.000 | 1.000 | 1.000 | 1.000 |
duplicate | 1.000 | 1.000 | 1.000 | 1.000 |
stale | 1.000 | 0.857 | 1.000 | 0.857 |
unsourced | 1.000 | 1.000 | 1.000 | 1.000 |
ok (no false findings) | 1.000 | 1.000 | 1.000 | 1.000 |
What the grey-zone batch changed
Same 58 scenarios, same gold labels, engine before vs after the v0.4.0 recheck (the labels were committed before the engine change — the delta is not a relabelling):
class | heuristic before | heuristic after | embed before | embed after |
contradicted_implicit R | 0.737 | 1.000 | 1.000 | 1.000 |
contradicted_implicit P | 0.933 | 0.950 | 0.864 | 0.950 |
likely_conflict R | 0.000 | 1.000 | 0.000 | 1.000 |
ok P | 0.762 | 1.000 | 1.000 | 1.000 |
stale R | 0.714 | 0.857 | 0.857 | 0.857 |
unsourced P | 0.571 | 1.000 | 1.000 | 1.000 |
What moved, and why:
Grey-zone false negatives are gone. Four scenarios that both engines missed — canonical records disagreeing with no observation order (equal dates, one side undated, neither dated, a version bump with equal dates) — are now confirmed from
slot = "value"equality alone. Heuristiccontradicted_implicitrecall 0.737 → 1.000,stalerecall 0.714 → 0.857.Over-claiming is gone. The two prose-only candidates the embedding engine used to assert as
contradicted_implicitare now reported aslikely_conflict;okprecision is 1.000 on both engines and the hedged class scores 1.000/1.000.Paraphrase false positives stayed at 0 / 0 (5 tagged paraphrase scenarios, including the two v0.1.0 false positives re-created as
gz-para-01/gz-para-02).The one remaining miss is the documented, frozen
stale-05label boundary — unchanged from v0.2.0 and deliberately not tuned away.
Against the v0.1.0 baseline, on the same 32 scenarios with the same gold labels:
class | v0.1.0 P/R | current P/R | delta |
contradicted_implicit | 0.000 / 0.000 | 0.889 / 1.000 | +0.889 / +1.000 |
ok | 0.438 / 0.875 | 1.000 / 1.000 | +0.562 / +0.125 |
stale | 0.800 / 0.800 | 1.000 / 0.800 | +0.200 / +0.000 |
duplicate, unsourced, contradicted_explicit | 1.000 / 1.000 | 1.000 / 1.000 | unchanged |
Reproduce everything in one command (--strict exits 1 if a declared target
is missed):
python eval/run_eval.py --strictThe protocol — gold labels written first, one run, no per-scenario tuning, and the one deliberate label disagreement kept visible — is documented in eval/README.md.
Ingest formats
Shape | Example | Fidelity | Notes |
explicit syntax |
|
| proves its own slot and value |
explicit, full |
|
| alias, observation date and source in one line |
explicit, validity range |
|
|
|
|
|
| entity, claim, optional date |
dated heading block |
|
| bullets inherit the heading date |
plain bullet |
|
|
|
inline source |
| — | per-record provenance and source class |
JSON, flexible keys |
| as parsed | bare lines and wrapped lists accepted |
canonical JSONL | as parsed | one fact per line, stable ids, stably ordered |
Dates parse leniently on every supported interpreter (the tool ships its own
fromisoformat polyfill; nothing relies on the 3.11 parser). Undated claims
are still linted; undated and unsourced claims are flagged. Hostile input
— empty files, malformed JSON, a file that is not UTF-8 — comes back as a
bad_input finding instead of a traceback.
Alias resolution
Entity identity is built once at ingest (and reused by scan, reconcile,
as-of and the admit write gate) from three sources, in decreasing order of
how much they prove:
a declaration —
(aka "Acme Corp")on an explicit line, or analiaseslist on a JSON/JSONL record. An author said so;the conservative normalized form — case, whitespace and punctuation collapse, then trailing legal-entity suffixes (
Ltd,Inc,Corp,Company,LLC,GmbH, …) are dropped.acmeandAcme Corpare one entity without anyone annotating it;nothing else. Two names that merely look alike are never folded: a silent merge would fabricate a contradiction between two real entities (or erase a real one). Instead the pair comes back as
unresolved_aliaswith its edit distance and similarity, for a human to settle.
$ memory-autopsy scan examples/aliases.md
unresolved_alias L0 Acme Corp :: 'Acme Corp' (form 'acme') and 'Acma' (form 'acma') differ by 1 edit(s), similarity 0.75 — NOT merged; confirm whether they are one entity
contradicted_explicit L2 Acme Corp examples/aliases.md:1 :: newer fact has transition trigger 'left' on shared anchor 'globex'
contradicted_implicit L2 conf=1.0 billing examples/aliases.md:4 :: slot-equality: datastore 'self-hosted postgres 15 cluster' vs 'aurora mysql compatible cluster' (no observation order: undated and undated — the two canonical records disagree, neither is newer)
== 3 findings (contradicted_explicit=1, contradicted_implicit=1, unresolved_alias=1) | 5 facts | engine=heuristicThat one run shows all three rules at once: Acme Corp and acme were folded
(first line and second line are one entity, so the transition fires at all),
Acma was not folded despite being one edit away, and the two
datastore records on lines 4–5 — which have no dates and therefore no
observation order — are confirmed as a contradiction from slot = "value"
equality alone. as-of reports the same ambiguity under
unresolved_aliases, so a temporal read can never silently answer for two
entities it could not tell apart.
The grey zone
Similarity proposes; structure disposes. Layer 3 (Ollama bge-m3) only
proposes candidate pairs from BAND_GREY (0.45) upward — the class always
comes from the deterministic recheck:
Both sides are … | Decision | Reported as |
canonical records, same slot, different value | slot-equality decides (a proof — no order needed) |
|
canonical records, same slot, same head | a refinement, not a conflict | nothing |
canonical records, different slots | independent facts | nothing |
prose, with an observation order | layer 2b's dated slot analysis |
|
prose, no order, similarity ≥ 0.60 | hedged — cosine can suspect, not prove |
|
prose, similarity < 0.60 | too weak to suspect | nothing |
Every layer-3 finding reports a composite confidence — the cosine blended with how much the structure proved (canonical record 1.0, anchor/time cue 0.6, copula/prepositional frame 0.35). The pair below is the whole point of the layer: two dated prose claims whose frames barely overlap, so the heuristic engine refuses to pair them on a guess —
$ memory-autopsy scan examples/greyzone.md
clean: 2 facts, 0 findings (engine=heuristic)
$ memory-autopsy scan examples/greyzone.md --engine embed
likely_conflict L3 score=0.661 conf=0.505 billing-store examples/greyzone.md:1 :: moderate claim similarity (0.66) with prose-only: slot 'f:billing' value conflict ('postgres' -> 'mysql'); composite confidence 0.51 (cosine 0.66 + structured 0.35)
== 1 findings (likely_conflict=1) | 2 facts | engine=embed— and the embedding engine puts it in front of a human with both signals on the record, instead of either dropping it or declaring it a contradiction.
A hedge is a review item, not a verdict: likely_conflict is never planned as
a fix, never rewritten by --apply, and never blocks a write on its own — the
admit gate surfaces it as a warning instead.
Verdicts: reliability, capped by provenance
reconcile builds a per-entity verdict matrix. Conflicts rank by later
observed_at, then higher source class, then capped reliability, then
an explicit status verb, then source order.
Reliability is min(wording weight, source-class ceiling, fidelity ceiling):
we verified the billing store moved -> 1.00 (primary) -> 1.00
we verified the billing store moved -> 1.00 (unclassified) -> 0.20
rumor has it the billing store moved -> 0.20 (primary) -> 0.20The source-class ceiling is the provenance cap: when a primary-source
claim exists for an entity, unclassified claims are removed from the ranking
pool before anything is compared, so 40 padded junk sources cannot crowd out
one documented fact. Capped claims are reported as CAPPED, not hidden.
$ memory-autopsy reconcile examples/memory.md examples/facts.json --registry examples/registry.yamlThe weight table lives in memory_autopsy/verdict.py and is overridable per
project with --registry (a YAML subset — cues.<word>: <weight>,
class.<class>: <weight>, fidelity.<band>: <weight>).
Reconcile output
# memory-autopsy reconcile
Sources: 2 | Entities: 9 | Claims considered: 15
## Globex
| Source | Claim | Observed | Status | Verdict |
|---|---|---|---|---|
| examples/memory.md | left Northwind | 2026-05-01 | — | WINNER |
**Canonical**: left Northwind *(observed 2026-05-01 | class unclassified | reliability 0.20)*Time travel (as-of)
"What was considered true on 2026-03-01?" — the payoff of keeping
valid_from/valid_to around. A record with an explicit window is in
effect at D when valid_from <= D < valid_to (the window is an assertion
about the world, so it is trusted even when the record was observed later);
a record with no window was believed from the moment it was observed
(observed_at <= D); a record with neither is reported as unanchored
instead of being silently folded into the answer. Conflicts among
in-effect records resolve with the same provenance-cap rules the linter
uses, so the two views of one store can never disagree.
The same store answers differently per date (real output, both commands run against examples/timeline.jsonl):
$ memory-autopsy as-of 2026-03-01 examples/timeline.jsonl
# memory-autopsy as-of 2026-03-01
Store: examples/timeline.jsonl | facts read: 7 | in effect: 4 | entities: 3
Conflicts resolved at this date: 1
## billing-store
| Slot | Value | Observed | Class | Reliability | Verdict |
|---|---|---|---|---|---|
| tool | PostgreSQL 14 | 2026-01-05 | primary | 0.70 | CURRENT |
## edge-proxy
| Slot | Value | Observed | Class | Reliability | Verdict |
|---|---|---|---|---|---|
| gateway | haproxy | 2026-02-01 | unclassified | 0.20 | CAPPED |
| gateway | kong | 2026-03-01 | primary | 0.70 | CURRENT |
- **CAPPED** gateway = 'haproxy' (id `c1ab7a22a4dcb381`) — unclassified claim, removed from the ranking by the primary-source fact `a7cbc633c4d1bdf6`
## nightly-sync
| Slot | Value | Observed | Class | Reliability | Verdict |
|---|---|---|---|---|---|
| schedule | 02:00 UTC | 2026-01-01 | primary | 0.70 | CURRENT |
## Not in effect on 2026-03-01
| Entity | Value | Reason |
|---|---|---|
| nightly-sync | 23:30 UTC | not_yet_valid |
| billing-store | MySQL 8 | not_yet_valid |
## Unanchored (no date, no validity window)
- Initech: 'grafana' (id `145a8166667a9e33`)Four months later the same store flips: billing-store is now MySQL 8
(CURRENT) with PostgreSQL 14 listed under Not in effect with reason
expired, and nightly-sync moved to 23:30 UTC.
$ memory-autopsy as-of 2026-07-01 examples/timeline.jsonl
# memory-autopsy as-of 2026-07-01
Store: examples/timeline.jsonl | facts read: 7 | in effect: 4 | entities: 3
Conflicts resolved at this date: 1
## billing-store
| Slot | Value | Observed | Class | Reliability | Verdict |
|---|---|---|---|---|---|
| tool | MySQL 8 | 2026-06-02 | primary | 0.70 | CURRENT |
...
## Not in effect on 2026-07-01
| Entity | Value | Reason |
|---|---|---|
| nightly-sync | 02:00 UTC | expired |
| billing-store | PostgreSQL 14 | expired |--json returns the same payload machine-readably (beliefs,
out_of_window with the reason per record, unanchored, facts_read,
conflicts_resolved); --entity NAME narrows to one entity.
Undo
--apply rewrites files in place, so it snapshots them first — every
mutated file gets a timestamped <name>.bak-<ts> copy next to it (the plain
<name>.bak from earlier versions is still written too), the path is printed,
and memory-autopsy undo <file> restores the newest snapshot. The round trip
is byte-exact; a test asserts it (tests/test_undo.py).
$ memory-autopsy report memory.md --apply --yes-i-know
applied 4 fix(es) across 1 file(s):
supersede memory.md:5 — "billing-store: uses PostgreSQL 14 for the billing store" (valid_to 2026-06-02)
new: billing-store: tool = "PostgreSQL 14 for the billing store" ; valid_to=2026-06-02 -- 2026-01-05 (source: infra-doc)
delete memory.md:11 — "cache-tier: uses redis 7" ([duplicate] identical to 'cache-tier: uses redis 7' (normalized hash match))
supersede memory.md:15 — "works at Northwind" (valid_to 2026-05-01)
new: Globex: employer = "Northwind" ; valid_to=2026-05-01 -- 2026-01-01 (source: profile)
supersede memory.md:17 — "project Helios is active" (valid_to 2026-07-15)
new: Umbra: project = "Helios is active" ; valid_to=2026-07-15 -- 2026-01-15 (source: roadmap)
rewrote memory.md (backup: memory.md.bak, snapshot: memory.md.bak-20261017-090055-968964)
undo with: memory-autopsy undo <file> (restores the newest snapshot)
$ memory-autopsy undo memory.md
restored memory.md from memory.md.bak-20261017-090055-968964Snapshots accumulate (memory.md.bak-<first timestamp>,
memory.md.bak-<second timestamp>, …) and sort chronologically, so undo
always restores the newest one; the snapshots themselves are never deleted by
the tool, and undo is idempotent rather than destructive. They are
git-ignored (*.bak, *.bak-*).
Write gate (admit)
The SDK call for the write path: check one incoming fact before your
agent persists it. admit is pure — no network, no writes — and returns a
typed verdict with evidence, not just a boolean:
accepted— safe to persist (the canonical 12-field record is included);requires_review— the fact disagrees with something already stored: every conflict carries the stable ids of the store facts involved, plus the supersede edit (stampvalid_toon the stored claim, never delete it) that would resolve the conflict;rejected— the fact cannot be normalized at all, or the store is unreadable (the gate never runs blind).
from memory_autopsy.api import admit
verdict = admit({"entity": "billing-store", "slot": "tool",
"value": "MariaDB", "date": "2026-07-01",
"source": "ops"}, "examples/store.jsonl")
print(verdict.status) # requires_review
print(verdict.conflict_ids) # ['8a0ccbab525e5078']
print(verdict.supersede) # the exact edit that would resolve itReal output of that call:
status: requires_review | reason: conflicts with 1 stored fact(s)
conflict_ids: ['8a0ccbab525e5078']
supersede: {
"action": "supersede",
"target_id": "8a0ccbab525e5078",
"target_is_candidate": false,
"target_shape": "jsonl",
"valid_to": "2026-07-01",
"new_line": "billing-store: tool = \"MySQL 8\" ; valid_to=2026-07-01 -- 2026-06-02 (source: infra-doc)",
"reason": "[contradicted_implicit] slot 'anchor:uses' value conflict ('mysql' -> 'mariadb')"
}Detection is the very same engine scan runs, so what the gate blocks and
what the linter reports cannot drift apart. An idempotent re-admit of the
identical fact is accepted (with already_present=True), and only
active store facts can conflict — a claim already stamped valid_to is
history and cannot block a new write.
Agent self-audit (MCP)
memory-autopsy-mcp (or python -m memory_autopsy.mcp_tool) speaks
newline-delimited JSON-RPC over stdio — the dialect MCP clients use for
local servers. Two tools, both read-only: the server never writes a store
and never makes a network call; stdout carries only protocol responses.
Register it with any MCP client, for example Claude Desktop:
{
"mcpServers": {
"memory-autopsy": {
"command": "memory-autopsy-mcp"
}
}
}Your agent then calls memory_lint(store) before trusting a memory file,
and memory_admit(store, fact) before writing one — the same gate and the
same engine as the CLI, so an in-loop check and an offline audit can never
disagree. Real round-trip (spawn, initialize, tools/list, tools/call):
$ initialize -> {"name": "memory-autopsy", "version": "0.4.0"}
$ tools/list -> ['memory_lint', 'memory_admit']
$ tools/call memory_lint(examples/store.jsonl) -> finding_count: 2 | counts: {"contradicted_explicit": 1, "stale": 1}Threat model
Short version: the provenance cap defends against padded volumetric poisoning in a test harness — it is a linter, not a security control, and it does not authenticate sources. A forged primary-source claim wins outright. docs/threat-model.md states what is defended, what is not, and how each claim is tested.
Install
pip install . # or: pipx install .
# run from source without installing:
PYTHONPATH=src python -m memory_autopsy.cli scan examples/memory.mdOn Arch/CachyOS-style systems with an external-managed python (PEP 668), pip refuses to install into the system interpreter — use pipx or a venv:
pipx install .
# or
python -m venv .venv && .venv/bin/pip install -e .The embedding layer needs a local Ollama with bge-m3
(ollama pull bge-m3). Without it, use --engine heuristic (the default) —
fully offline.
Usage
# lint memory files (heuristic engine, offline)
memory-autopsy scan MEMORY.md FACTS.json STORE.jsonl
# add the embedding layer for implicit contradictions
memory-autopsy scan MEMORY.md --engine embed
# machine-readable, for cron/CI (every finding carries its reliability)
memory-autopsy scan MEMORY.md --json | jq '.counts'
# per-entity verdict matrix across sources, with the provenance cap applied
memory-autopsy reconcile snapshot-a.md snapshot-b.md --registry registry.yaml
# report + fix plan (dry-run by default)
memory-autopsy report MEMORY.md
memory-autopsy report MEMORY.md --apply --yes-i-know # snapshots, then rewrites
memory-autopsy undo MEMORY.md # restore the newest snapshot
# time travel: what was considered true on a given date
memory-autopsy as-of 2026-03-01 STORE.jsonl
memory-autopsy as-of 2026-07-01 STORE.jsonl --entity billing-store --json
# MCP server for agent self-audit (stdio JSON-RPC)
memory-autopsy-mcpEnv: OLLAMA_BASE_URL (default http://localhost:11434), EMBED_MODEL
(default bge-m3).
Troubleshooting cookbook
Finding | Root cause | Fix |
| Same fact pasted into memory twice (often by two agents/sessions) |
|
| A transition happened ("left", "sunset", "switched") and the old fact was never retracted | Apply the fix plan: the old claim is stamped |
| Value changed without any shared keyword ("02:00" → "23:30") — invisible to grep | Confirm the survivor by |
| Two prose claims that probably disagree, but nothing proves it (no | Read both claims and settle them by hand; pin the survivor as an explicit |
| Versioned record (version, deadline, role) updated by a later observation | Apply, or re-date the old claim if it is still true in a narrower context |
| Claim written from memory/paraphrase with no date or provenance | Re-observe the fact and add |
| Two entity names that differ by an edit or two — the tool refuses to guess whether they are one entity | If they are one: add |
| Expected: a claim already carrying | None — that is the record of a resolved conflict |
| Empty file, malformed JSON/JSONL, or a file that is not UTF-8 | Fix the input; the scan never crashes on it |
| Ollama not running / wrong |
|
Exit code 2 | No readable input files / | Pass existing files; add |
Roadmap
Baseline mode:
--baselineto record current findings and fail only on new ones (like ESLint's baseline workflow).Direct adapters for common agent memory stores (SQLite memory backends).
Cross-format supersede: stamp
valid_toback into a JSONL store line instead of only rewriting markdown.Project rule file (
memory-autopsy.toml) for site-specific anchors and transitions, alongside the existing--registryreliability overrides.
Tests
python -m unittest discover -s tests -t .206 stdlib-only unit tests, including the Ollama-down fallback (wrong port),
the 40-source poisoning case, alias and fidelity matrices, degraded-input
handling, supersede rewriting, the temporal-read (as-of) matrix, the
admit write-gate matrix, a live MCP stdio round-trip (spawn, initialize,
tools/list, tools/call, terminate), and a Python 3.9 syntax/API guard that
runs on every source file. v0.4.0 adds three matrices:
tests/test_alias.py— the alias matrix (declared aliases, conservative normalized forms,Ltd/Inc/Corp/Companysuffixes) and the ambiguity guard: look-alike names are reported, never merged, by bothscanandas-of;tests/test_greyzone.py— the verify-and-confirm contract: canonical records decide without an observation order, prose is hedged, a hedge is never auto-fixed and never blocks a write, and the composite confidence is the documented blend of both signals;tests/test_undo.py— apply → undo round trip is byte-exact for markdown and JSON stores, the newest snapshot wins, andundoon a file that was never applied exits 2.
License
MIT — see LICENSE. Copyright (c) 2026 Brodewa.
Available Tools
2 toolsmemory_admitA
Write-path gate: check one incoming fact against a memory store before persisting it. Returns accepted / requires_review / rejected, the stable ids of the store facts it disagrees with, and the supersede edit that would resolve the conflict.
| Name | Required | Description | Default |
|---|---|---|---|
| fact | Yes | the fact to admit; flexible keys (fact/text/claim, date/observed_at, entity/subject, source, slot/value) | |
| store | Yes | path to the memory store file |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden and does much of it well: it discloses the verdict vocabulary (accepted/requires_review/rejected) and the payload (conflicting stable ids, a supersede edit). It does not explicitly state whether 'accepted' means the tool itself persists the fact or the caller does, which is the one behavioral ambiguity left.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, zero filler. The role ('Write-path gate') is front-loaded, followed by the operation and then the return contract, which is exactly the order an agent needs.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With no output schema, the description compensates by enumerating the three verdicts and the conflict/supersede payload, which is enough to act on the result. The only gap is the post-'accepted' workflow — whether it writes or hands off — which an agent calling this in a write path would want to know.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and both parameters (fact, store) are documented in the schema, including the flexible key set, so the description need not re-explain them. The description adds only that exactly one fact is checked per call, which is marginal beyond the schema baseline of 3.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description gives a precise verb+resource ('check one incoming fact against a memory store') and frames it as a 'write-path gate', which tells the agent the exact role this plays in a write flow. It does not, however, distinguish itself from the sibling memory_lint, so the agent must infer the boundary between the two.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
'check one incoming fact ... before persisting it' gives a clear condition of use: this is the pre-write validation step. There are no explicit exclusions or alternatives named (e.g. when to prefer memory_lint), so it stops short of full routing guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
memory_lintA
Lint a memory store (markdown / JSON / JSONL) for exact duplicates, contradictions and staleness. Returns the scanner's findings; each one carries the fidelity band of the claim it came from.
| Name | Required | Description | Default |
|---|---|---|---|
| store | Yes | path to the memory store file | |
| engine | No | detection engine; 'embed' needs a local Ollama (default: heuristic) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. 'Lint' conventionally implies a non-mutating scan, but the description never explicitly confirms the store is not modified, nor does it mention permissions, cost, or that the embed engine requires a local Ollama (that detail lives only in the schema). It does add useful disclosure about the shape of findings via the fidelity band.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tightly written sentences with zero filler: the operation and its formats lead, the checks follow, and the return characterization closes. Every clause earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a two-parameter read-only scanner with no output schema, the description covers the resource, formats, checks, and the structure of returned findings. The main omissions — explicit confirmation of non-mutation and engine prerequisites — are minor.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, and both parameters (store path, engine enum with its Ollama caveat) are documented there, so the baseline is 3. The description's mention of accepted store formats adds a small amount of context for the 'store' path but no syntax or format details beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (lint) and resource (memory store), plus the accepted formats (markdown/JSON/JSONL) and the three checks performed (duplicates, contradictions, staleness). It does not explicitly differentiate itself from the sibling memory_admit, but the verb makes the distinction inferable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied — run it to audit a store for duplicates, contradictions and staleness — but there is no explicit when/when-not guidance and the sibling memory_admit is never referenced. Nothing tells the agent whether linting should precede or follow admission.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
2 tool updates
v0.4.0- First observed
memory_admit - First observed
memory_lint
TDQS
Scored across 2 tools
memory_lint audits an existing store for duplicates, contradictions and staleness, while memory_admit gates a single incoming fact before it is written. These are clearly different phases (bulk audit vs. write-path check), so misselection is unlikely, though both are 'inspect memory' operations and could blur for an unfamiliar agent.
Both tools use the same predictable memory_<verb> snake_case pattern (memory_lint, memory_admit) with no style mixing. Naming is fully consistent and readable.
Two tools is on the thin side even for a narrow domain; lint and admit cover the two documented entry points but leave little room for related operations the domain implies (review queue, applying edits). Borderline but not egregiously mismatched.
The pair covers an audit pass and a write gate, but there are dead ends: memory_admit returns a supersede edit and a requires_review state with no tool to apply the edit or resolve queued review items, and no retrieval/update surface for stored memories. Notable missing operations, though the core two stages are present.
Maintenance
Related MCP Connectors
Check claims against a fact-store: consistent, contradicts, or unverifiable — with a receipt.
MCP server (stdio): validate JSON against JSON Schema (draft-07 / 2020-12) via the AgentForge API
Statically audits MCP tool surfaces for token cost, schema quality, and design issues.
MCP server (stdio): lint OpenAPI specs with Spectral via the AgentForge API
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceServes offline document validation and evaluation operations of the Judgment Pack Specification to MCP clients over stdio, enabling agents to validate and evaluate JPS documents as tool calls. Supports conformance validation, experimental evaluation with disposition and error classes, and corpus testing.2Apache 2.0
- AlicenseAqualityBmaintenanceLocal MCP server for AI agents and vibe coding safety: deterministic risk checks (payments, auth, database, secrets, infrastructure), sessions, checkpoints, policy inspection, and fix prompts. Runs over stdio against a local git repository; no language model judges risk.8Apache 2.0
- AlicenseNot gradedqualityBmaintenanceExposes an offline data-contract runtime to MCP-capable agents, enabling them to lint ODCS contracts and validate CSV datasets against them via stdio.MIT
- FlicenseNot gradedqualityBmaintenanceEnables AI agents and MCP clients to decompose text into atomic claims and cross-verify them against reference knowledge to flag ungrounded claims, numerical contradictions, and hallucinated entities. Runs deterministically with zero dependencies over JSON-RPC stdio, avoiding LLM round-trips for routine verification tasks.7-