# your agent remembers things that stopped being true
Every serious proposal for agent memory has converged on the same container. Markdown pages, optional YAML frontmatter, a vector index in SQLite sitting beside them, the lot zipped so a store moves between vendors as one file.
The obvious dismissal is that this is a lot of ceremony around a folder of Markdown. The boring container is the best decision in the design. A page looks like this.
title: Carbon Fibre Woks created: '2026-03-01T09:00:00Z' updated: '2026-08-22T14:30:00Z' uuid: 6aa615f0-486f-48a7-a210-ba4f5ff18c8b summary: Thermal properties of carbon fibre cookware --- [markdown body]
Every field in that header describes the file. Nothing in it describes the standing of the claim inside the file, which is the gap that decides whether a memory store is useful in month nine.
## Where the shape is right
Pages of prose keep the context a graph of triples strips away, and I am convinced by that. Semantic search reaches every relevant page in one round trip, where walking a knowledge graph costs a tool call per hop. A zipfile travels.
Those are real wins over a pgvector plus Neo4j arrangement nobody wants to operate.
The write path works and the read path works. What has no design at all is the moment a page stops being true.
Every cache I have operated had an eviction story before it had a population story, because population writes itself and eviction is the expensive one. A memory store only ever gets written to. No TTL, no LRU, nothing that marks an entry when the record underneath it changes. Retrieval hits everything ever written.
## Timestamps answer the wrong question
The created and updated fields are facts about bytes. They tell you when a file last moved, which is a different question from whether the sentence in the middle of the page still holds.
Say an agent writes a page in March recording that the billing service reads from a replica. In June somebody moves billing onto the primary. The page is wrong from that afternoon on, its updated stamp still says March, and the migration touched nothing inside a zip archive on a laptop.
The cheap review pass everybody reaches for is sorting by age and dropping whatever nobody has touched in months. It does catch abandoned pages. Stable facts go out with them, because a page correct and untouched for two years looks, by timestamp, exactly like one that rotted eighteen months ago.
-- what "prune by age" actually selects SELECT uuid, title FROM pages WHERE updated < date('now', '-6 months'); -- the retry-policy page nobody edited because it was right -- the auth page nobody edited because nobody reads it -- the replica page that went wrong in June, untouched, still indexed
## An abandoned hypothesis looks exactly like an answer
The comfortable assumption is that irrelevant material simply never gets surfaced by the semantic search. That assumption is where I get off. Similarity measures topic. Truth is invisible to it.
A debugging session that burned four hours in the wrong subsystem produces pages that sit on top of every future query about that subsystem, because they are about it, at length, in the vocabulary of somebody deep in the problem. Nobody searches textbooks and science fiction with the same query. A memory store hands you both out of one index.
The people who have been burned by this turn memory off and leave it off, because one poisoned line contaminates everything downstream of it. In cache terms that is a bad write with unbounded blast radius.
A wrong row in Redis expires. In a warehouse the next rebuild overwrites it. A wrong memory page has no backstop, and it gets read back by a model that treats confident first-person prose from its own past as settled fact. Switching memory off looks like an overcorrection to me, and a rational one while the format gives nobody a way to mark a page as discredited.
## Invalidation is a write path
None of this calls for another database. It calls for fields that let a page be contradicted by something other than a human noticing.
uuid: 6aa615f0-486f-48a7-a210-ba4f5ff18c8b updated: '2026-08-22T14:30:00Z' status: confirmed # confirmed | hypothesis | superseded source: observed # observed | inferred | told evidence: - src/billing/reader.ts@a1b2c3d - PR#4182 supersedes: 1f3c9d20-7b44-4c1e-9a02-8d5f3e6c1b90 ---
Two writers can fill those in. An agent that hits a contradiction while working is one, and it is the unreliable one, since it only writes when it happens to notice.
The writer worth building is a job with no model in it. When a page cites a file and a commit, a scheduled check asks git whether that file moved since that commit, and flags the page when it did. Flagging is the entire job. A flagged page can drop to the bottom of the ranking or come back with a warning stapled to the front.
# CI job: flag memory pages whose cited code has moved for page in pages/*.md; do yq '.evidence[]' "$page" | while read -r ref; do path="${ref%@*}"; sha="${ref#*@}" git diff --quiet "$sha" HEAD -- "$path" \ || yq -i '.status = "suspect"' "$page" done done
That check has a shape I trust from data work, where the useful question about a table is which upstream job last wrote to it and when. A page claiming an activity engine dispatches thirteen handler types out of one config column stays true for exactly as long as one enum has thirteen members, and that is a grep.
Pages that cannot name a file or a table to watch have nothing to check them against, and those are the ones that quietly rot. My instinct is to cap them at a paragraph and let an agent re-derive the rest from the code each time.
## The fields I would add
- ▸status. Retrieval filters on it before it ranks on similarity, so an abandoned hypothesis stops competing for the same slot as a verified answer.
- ▸evidence. Addresses in the world: file paths with commit hashes, table names, ticket ids. An empty list is itself a signal, and pages carrying one should outrank pages that carry none.
- ▸supersedes. A uuid. Writing a corrected page leaves a pointer backwards, so the old page leaves the index while its text stays readable for anyone auditing how the store drifted.
- ▸source. Whether the agent watched the thing happen, worked it out by inference, or was told it by a user who may well have been guessing.
The container survives all of that untouched. Still Markdown in a zip with a vector index beside it, which is why the format is worth building on. The work lands in the index, because the search has to read those columns before it ranks, and a page marked superseded should never come back.
SELECT p.uuid, p.title FROM pages p JOIN vec_index v ON v.uuid = p.uuid WHERE p.status NOT IN ('superseded', 'suspect') ORDER BY v.distance + staleness_penalty(p.updated, p.source) LIMIT 8;
Two of those columns exist in no design I have read, and adding them costs one migration on a SQLite file that already ships in the archive.
staleness_penalty is the piece I have not written. Guessing coefficients without a real corpus to tune against would be theatre, so my first version returns zero and the WHERE clause does all the work.