Skip to main content
Glama

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/Company suffixes) fold into one entity at ingest, so acme and Acme Corp are linted — and time-travelled — as one. Two names that merely look alike are never silently merged: they come back as an unresolved_alias finding 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 as likely_conflict with its reason string and a composite confidence (cosine + structured agreement) — never asserted, never auto-fixed. See The grey zone below.

  • undo — --apply is 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.jsonl answers what was considered true on that date: valid_from/valid_to windows are honoured, observed_at is the fallback, records with no temporal anchor are reported as unanchored instead of guessed. See Time travel below.

  • In-loop write gate (admit) — the SDK call an agent makes before persisting a fact. Returns accepted / requires_review / rejected with the stable ids it disagrees with and the supersede edit that would resolve the conflict. See Write gate below.

  • MCP server — memory-autopsy-mcp speaks JSON-RPC over stdio, exposing memory_lint and memory_admit to 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, fidelity and status_verb.

  • Fidelity bands — every record is tagged FULL (an explicit slot = "value" parse), PARTIAL (entity written, claim read as prose) or RESIDUAL (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_to and kept as history. Only redundant duplicates are removed.

Findings classes

Class

Layer

Severity

Meaning

duplicate

1

info

Same claim stored twice (normalized-text hash match)

contradicted_explicit

2

error

Newer fact carries a transition trigger on a shared anchor ("works at X" → "left X")

contradicted_implicit

2b/2c/3

error

Same slot, different value, no retraction — the class grep cannot see

likely_conflict

2c/3

review

Prose evidence only, no observation order: the pair probably disagrees, but nothing proves it. Reason string included, never auto-fixed

stale

2b/2c/3

warn

Versioned slot or bare version bump ("redis 6" → "redis 7") superseded by a later observation

unsourced

0

info

Claim has neither an observed_at date nor a source reference

superseded

0

info

Claim already carries valid_to — history, not a live conflict

duplicate_id

0

info

Two store lines claim the same stable content id

unresolved_alias

0

review

Two entity names look alike and were not merged — confirm whether they are one entity

bad_input

0

info

Empty, unreadable or malformed input (never a crash)

engine_degraded

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. Heuristic contradicted_implicit recall 0.737 → 1.000, stale recall 0.714 → 0.857.

  • Over-claiming is gone. The two prose-only candidates the embedding engine used to assert as contradicted_implicit are now reported as likely_conflict; ok precision 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-05 label 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 --strict

The 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

acme: employer = "Acme Corp"

FULL

proves its own slot and value

explicit, full

acme (aka "Acme Corp"): employer = "Acme Corp" -- 2026-09-01 chat

FULL

alias, observation date and source in one line

explicit, validity range

svc: version = "1.4" ; valid_to=2026-06-01 -- 2026-01-01 changelog

FULL

valid_from/valid_to honoured

§ claim block

Acme § works at Globex § 2026-01-02

PARTIAL

entity, claim, optional date

dated heading block

### 2026-09-01 | Cron fix + - payments: deadline 2026-10-05

PARTIAL

bullets inherit the heading date

plain bullet

- Acme works at Globex (2026-03-04)

PARTIAL/RESIDUAL

RESIDUAL when the entity is inferred

inline source

- Acme role engineer (source: ops-review | primary)

—

per-record provenance and source class

JSON, flexible keys

fact/text/claim, date/observed_at, entity/subject, source/provenance, plus slot/value/aliases/source_class

as parsed

bare lines and wrapped lists accepted

canonical JSONL

see examples/store.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:

  1. a declaration — (aka "Acme Corp") on an explicit line, or an aliases list on a JSON/JSONL record. An author said so;

  2. the conservative normalized form — case, whitespace and punctuation collapse, then trailing legal-entity suffixes (Ltd, Inc, Corp, Company, LLC, GmbH, …) are dropped. acme and Acme Corp are one entity without anyone annotating it;

  3. 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_alias with 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=heuristic

That 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)

contradicted_implicit / stale, conf=1.0

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

contradicted_implicit / stale

prose, no order, similarity ≥ 0.60

hedged — cosine can suspect, not prove

likely_conflict + reason string

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.20

The 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.yaml

The 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-968964

Snapshots 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 (stamp valid_to on 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 it

Real 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.md

On 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-mcp

Env: OLLAMA_BASE_URL (default http://localhost:11434), EMBED_MODEL (default bge-m3).

Troubleshooting cookbook

Finding

Root cause

Fix

duplicate

Same fact pasted into memory twice (often by two agents/sessions)

report --apply --yes-i-know removes the later copy, keeps the dated one

contradicted_explicit

A transition happened ("left", "sunset", "switched") and the old fact was never retracted

Apply the fix plan: the old claim is stamped valid_to, the transition becomes the live fact

contradicted_implicit

Value changed without any shared keyword ("02:00" → "23:30") — invisible to grep

Confirm the survivor by observed_at and source class, then apply

likely_conflict

Two prose claims that probably disagree, but nothing proves it (no slot = "value" records, no observation order, similarity in the grey band)

Read both claims and settle them by hand; pin the survivor as an explicit slot = "value" record so the tool can prove it next time

stale

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

unsourced

Claim written from memory/paraphrase with no date or provenance

Re-observe the fact and add (source: ...) or a JSON source field

unresolved_alias

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 (aka "...") to the explicit line. If not: nothing — the finding is the record of the question

superseded

Expected: a claim already carrying valid_to

None — that is the record of a resolved conflict

bad_input

Empty file, malformed JSON/JSONL, or a file that is not UTF-8

Fix the input; the scan never crashes on it

engine_degraded

Ollama not running / wrong OLLAMA_BASE_URL / bge-m3 not pulled

ollama serve && ollama pull bge-m3, or stay on --engine heuristic

Exit code 2

No readable input files / --apply without --yes-i-know / undo on a file with no snapshot

Pass existing files; add --yes-i-know when you really mean it; undo only undoes something that was applied

Roadmap

  • Baseline mode: --baseline to 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_to back 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 --registry reliability 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/Company suffixes) and the ambiguity guard: look-alike names are reported, never merged, by both scan and as-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, and undo on a file that was never applied exits 2.

License

MIT — see LICENSE. Copyright (c) 2026 Brodewa.

Available Tools

2 tools
memory_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.

NameRequiredDescriptionDefault
factYesthe fact to admit; flexible keys (fact/text/claim, date/observed_at, entity/subject, source, slot/value)
storeYespath to the memory store file

TDQS

A4/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines4/5

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.

NameRequiredDescriptionDefault
storeYespath to the memory store file
engineNodetection engine; 'embed' needs a local Ollama (default: heuristic)

TDQS

A3.6/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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.

  1. 2 tool updatesv0.4.0
    • First observedmemory_admit
    • First observedmemory_lint

TDQS

A3.7/5.0

Scored across 2 tools

Disambiguation4/5

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.

Naming Consistency5/5

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.

Tool Count3/5

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.

Completeness3/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Serves 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.
    2
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    Local 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.
    8
    Apache 2.0
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables 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
    -