DEV Community

TECHDEV
TECHDEV

Posted on Edited on Originally published at github.com

My agent's memory contradicted itself and nobody noticed. I built a read-only audit CLI.

My agent's memory contradicted itself and nobody noticed. I built a read-only audit CLI.

Last week I asked a simple question about an agent's memory: at one point in February, what did it believe the database engine was? The answer came back with two values at once. Postgres 15 and Postgres 16. Both marked current. Both believed.

The agent had not reasoned badly. Its memory was rotten, and nothing in the loop ever checked the store itself. A limit changes and both values stay current. An upgrade supersedes a fact that is never marked superseded. A TTL expires and nobody notices. A fact arrives with no record of where it came from. The agent then acts on stale or contradictory memory with full confidence, and the failure looks like a reasoning bug when it is really a data bug.

I built a small tool that audits the memory instead of trusting it. It is called memrot, and it is a read-only CLI over a Relate store (Neo4j's embedded graph database for AI agents). MIT licensed. No server, no LLM, no network, no writes to the store it audits.

memrot demo

The problem: memory layers store, none of them audit

I re-verified the rivals live on 13 November 2026 before writing a word of this:

  • Graphiti (32,787 stars) keeps bi-temporal facts and invalidates them at ingest, but the invalidation is an LLM judgement made silently during ingestion, behind a server. There is no standalone report, no CLI, no CI exit code. You cannot open a store after the fact and ask what is wrong in it.
  • Mem0 (68,903 stars) ingests ADD-only: one LLM pass extracts facts, with no UPDATE or DELETE on write and no second pass to reconcile what is already there. Contradictory and stale memories accumulate in long-running deployments, and the graph features are a paid tier. Nothing audits a Mem0 store from outside.
  • Cognee (33,289 stars) and Letta (25,429 stars) have no first-class contradiction detection and no audit CLI.

Every rival stores memory. None of them lets an operator open a store read-only, ask "what in here is wrong", and get a machine-checkable answer with an exit code. That is the wedge, and memrot is the audit layer they do not have.

What it does

memrot runs seven deterministic checks, each one a Cypher query or a vector search executed inside Relate, not application-side logic:

# Check Rule Severity
1 Contradiction Two current facts, same subject + predicate, different object, overlapping validity windows error
2 Superseded-but-served A fact superseded by another current fact, still marked current error
3 Stale A current fact past its validTo, or older than --stale-days (default 90) with no corroboration and no newer fact on the same subject + predicate warning
4 Orphaned entity An Entity with no relationships at all warning
5 Provenance-less fact A Fact with no FROM_EPISODE edge: no record of where it came from error
6 Validity-order violation validFrom after validTo, or a superseding fact created before the fact it supersedes error
7 Near-duplicate (lexical) Two facts whose trigram embeddings score at or above 0.91: the same claim recorded twice under different wording warning

The answer comes back as an exit code a shell or CI step can use directly: 0 = clean, 1 = error-severity findings, 2 = usage error or store error. Warnings alone do not fail the audit.

memrot seed demo.rl
memrot audit demo.rl
memrot audit demo.rl --format json
memrot audit demo.rl --as-of 2026-03-01 --stale-days 30
memrot believed-at demo.rl --at 2026-02-15
memrot believed-at demo.rl --at 2026-02-15 --subject database
memrot why demo.rl f-db-15
Enter fullscreen mode Exit fullscreen mode

believed-at is time travel: what did the agent believe at this moment. why shows a fact's provenance, its supporters, its contradictions, and the fact that supersedes it.

The demo: a dirty memory, audited for real

The GIF above is a real terminal session, not a mockup. memrot seed writes a small fictional migration memory: 11 facts, 6 entities, 8 episodes, 7 planted faults and 3 clean controls. The audit finds exactly 9 findings, 5 errors and 4 warnings:

$ memrot audit examples/demo.rl --as-of 2026-11-13

memrot audit: examples/demo.rl (as of 2026-11-13)

- Contradictory facts: 2 finding(s) [error]
  ERROR  database.engine has two current values in overlapping windows: "Postgres 15" (f-db-15) and "Postgres 16" (f-db-16).
  ERROR  queue.max_payload has two current values in overlapping windows: "4000 bytes" (f-queue-4000) and "8000 bytes" (f-queue-8000).
- Superseded facts still served: 1 finding(s) [error]
  ERROR  database.engine = "Postgres 15" (f-db-15) was superseded by f-db-16 but is still marked current, so it can still be served.
- Stale facts: 2 finding(s) [warning]
  WARN  cache.ttl = "300s" (f-cache-ttl) expired on 2026-06-01 and is still current. It is also old (recorded 2026-01-05T10:00:00Z) with no corroboration and no newer fact on the same subject and predicate.
  WARN  feature.flag = "on" (f-feature-flag) expired on 2026-04-01 and is still current. It is also old (recorded 2026-05-02T09:30:00Z) with no corroboration and no newer fact on the same subject and predicate.
- Orphaned entities: 1 finding(s) [warning]
  WARN  Entity "LegacyCron" (e-legacycron, kind service) has no relationships: nothing references it and it references nothing.
- Provenance-less facts: 1 finding(s) [error]
  ERROR  deploy.region = "eu-west-1" (f-deploy-region) has no episode: there is no record of where this fact came from.
- Validity-order violations: 1 finding(s) [error]
  ERROR  feature.flag = "on" (f-feature-flag) has validFrom 2026-05-01 after validTo 2026-04-01: the validity window is empty.
- Near-duplicate facts (lexical): 1 finding(s) [warning]
  WARN  database.engine = "Postgres 16" (f-db-16) and database.db_engine = "PostgreSQL 16" (f-db-16b) are near-duplicates (lexical cosine 0.93): the same claim recorded twice under different wording.

9 finding(s): 5 error(s), 4 warning(s)
audit: FAILED (exit 1)
Enter fullscreen mode Exit fullscreen mode

Time travel on the same store shows the contradiction was there from the start:

$ memrot believed-at examples/demo.rl --at 2026-02-15

  cache.ttl = "300s" (f-cache-ttl)
  database.db_engine = "PostgreSQL 16" (f-db-16b)
  database.engine = "Postgres 15" (f-db-15)
  database.engine = "Postgres 16" (f-db-16)
  queue.max_payload = "8000 bytes" (f-queue-8000)

5 fact(s) believed at 2026-02-15.
Enter fullscreen mode Exit fullscreen mode

In February the agent already believed both Postgres 15 and Postgres 16. The March payload change is not believed yet. And memrot why demo.rl f-db-15 traces the bad fact back to its episode ("DB still on Postgres 15", session 2026-02-01), names its superseder (f-db-16) and its contradiction (f-db-16: "Postgres 16").

Every check is asserted by exact fact id in the test suite (16/16 tests pass, against seeded stores with known ground truth), so the audit is proved by CI on every push, not just by the demo.

Install (from source only)

The npm package is not published yet. The name memrot is free on npm, but publishing is blocked on the owner's npm token, so for now install from source:

git clone https://github.com/techdevfun/memrot.git
cd memrot
npm install
npm run build
Enter fullscreen mode Exit fullscreen mode

Requires Node.js 20 or newer. The only runtime dependency is @relate/relate 1.0.1, pinned exact. A clean install from source was measured on 13 November 2026 at about 8 seconds (npm install + npm run build, no cache). Run it from the repo root, or npm link to make memrot available everywhere.

One thing to know before you audit a live agent: memrot opens the store read-only and never writes to it, but Relate has a lock rule I verified live. A .rl file held by a running writer cannot be opened read-only, and relate clone cannot copy it either. Both fail, and memrot prints the remedy instead of the raw error. A directory store can be opened read-only beside a live writer. So: for a .rl file, stop the agent first, then audit; or snapshot it after the writer exits and audit the snapshot.

Also worth knowing: the relate CLI ships with telemetry enabled by default. memrot itself sends nothing (its CI installs only the @relate/relate library, never the CLI), but if you install the relate CLI, run relate telemetry off first.

Where the rivals beat us

Honesty first: memrot is the audit layer, not a replacement memory.

  • Graphiti's per-fact LLM judgement is more accurate per fact. An LLM can tell that two differently-worded statements contradict each other; memrot's deterministic checks cannot. If you need semantic contradiction detection, Graphiti is the better tool for that job, and memrot is the audit you run after it.
  • Mem0 retrieval is more token-efficient. A Mem0 retrieval stays under about 7,000 tokens with no LLM call; memrot is not a retrieval layer at all and does not try to be.

If you need semantic judgement or retrieval, pick a rival. The README says so, and means it. If you need a standalone answer to "what in this store is wrong, and can CI fail on it?", try memrot.

Honest limits

  • No semantic paraphrase detection. Deterministic structural checks catch same-predicate conflicts and lexical near-duplicates. They do not catch a contradiction phrased two different ways ("the queue is Postgres" vs "we moved off Postgres last week"). That is an explicit v1 limit, not a hidden one. Optional embedder-backed vectors for semantic near-duplicates are a v1.1 roadmap item.
  • Check 7 needs embeddings on Fact nodes. Stores not seeded by memrot lack them: the check falls back to a cosine scan when embeddings exist without an index, and reports the check as skipped when no fact carries an embedding. v1.1 will add a memrot index command to build embeddings on a copy of a store.
  • Relate is experimental and moving fast (Neo4j Labs, 1.0.0 on 11/11, 1.0.1 on 12/11, 228 stars at build time). Pin 1.0.1 and re-check before upgrading.
  • memrot reports; it never edits the store. Fixing a finding is a human (or agent) decision with its own audit trail. No writes, no fixes.

Why this shape

A single .rl file is the whole point: a memory an agent carries as one file, auditable anywhere, no server to deploy. Directory mode exists for live auditing beside a writer; :memory: mode exists for tests. The audit runs fully offline: an audit that needs a model call cannot run in CI on every commit.

If your agent has persistent memory, run one audit before the next bad decision:

github.com/techdevfun/memrot

Feedback welcome in the repo issues. The roadmap (embedder-backed semantic near-duplicates, memrot index, --fail-on warning) is in the README.

Top comments (0)