Skip to content
All posts
Memory model7 min read

Superseded, not overwritten, so March is still there

When a memory changes, the old one gains an end date and a pointer forward. Both survive, which is what lets one store answer both what is true now and what was true then.

Deep shelves of bound archive volumes receding into shadow

In March the team was on Supabase. In July they moved to AWS. A memory store built on UPDATE knows one of those things. Ask it in August what the team uses and it says AWS, correctly. Ask it what was true in March and it has nothing to say, because the sentence that knew is gone.

That second question is most of what a memory is for. So no memory is ever overwritten. When one claim replaces another, both survive: the old one keeps its content and gains an end date and a pointer forward, and the new one points back. That pair is the whole mechanism.

/**
 * One memory replaces another.
 *
 * Both survive. The old one keeps its content and gains an end date and a
 * pointer forward; the new one points back. That pair is what lets a reader
 * ask "what is true now" and "what was true in March" of the same store.
 */
export function supersede(
  older: Memory,
  newer: Memory,
  at: Timestamp
): { older: Memory; newer: Memory } { /* ... */ }

Deletion is a state too, for the same reason. Having used Supabase does not stop having been true because it stopped being current, and the state machine reflects that: a superseded memory can be archived or deleted but never restored to current. Coming back is not a state change, it is a new memory superseding the one that replaced it, which leaves a record of the reversal rather than pretending it never happened.

The windows are half open

Every memory carries a validity window, and superseding writes the old memory's end as the new memory's start. That single shared instant is where the design gets decided. The interval is half open, from inclusive and until exclusive.

/*
 * Validity is HALF-OPEN: [validFrom, validUntil).
 *
 * `supersede` writes the old memory's `validUntil` as the new one's
 * `validFrom` - the "Supabase until July / AWS from July" pattern. Treating
 * both ends as closed made both memories valid at that instant, so "what was
 * true on 1 July" answered with the claim and its replacement, and a clean
 * hand-over registered as a contradiction.
 */

Both consequences of getting that wrong are real bugs we had. A query as of the hand-over instant returned two contradictory memories, and the conflict detector reported a clean replacement as a disagreement, because overlap was computed with closed ends and touching windows overlap. Overlap is now strict, so windows that merely touch do not overlap, and both problems disappear at once because they were the same problem.

One related rule is worth saying out loud: an event's end is not a validity end. A meeting ending does not make the fact of it having happened stop being true, so an event's window runs from when it occurred and never closes. Events are excluded from supersession entirely for the same reason. A meeting held in March is not replaced by a meeting held in July; superseding it would erase a fact rather than update one.

The bar before anything is replaced

Supersession is only ever proposed, and the bar is deliberately high, because getting it wrong buries something that was still true and the loss is invisible. Nobody notices a memory that stopped being returned. There is no error, no gap, just an answer that is quietly missing a piece.

So a proposal requires all of it: the same memory type, a type that expects revision at all, both sides currently live, the same subject, windows that genuinely overlap, and a claim that actually differs. Identical claims are corroboration, not replacement. Subjects are compared on the structured value where both have one, because that is the part with a stable shape, and only fall back to shared entities otherwise. Two sentences that share words might not share a subject.

The confidence attached to a proposal comes from the new memory rather than the old one, which reads backwards until you think about which mistake you are guarding against. Acting on a shaky new claim is what buries a solid old one. Certainty about the replacement is what matters.

Reading the past back

None of this is worth keeping if you cannot query it, so as of is a first class parameter on search rather than a reporting feature bolted on later. It asks what was believed true then, not what is true now.

// What is true now. Historical memories are excluded by default, because a
// list of memories almost always means "what is true now".
const now = await client.search.query({ query: "where does the ledger live" });

// What was true in March, answered by the same store.
const march = await client.search.query({
  query: "where does the ledger live",
  asOf: "2026-03-15T00:00:00Z"
});

// And the chain itself, if you want to see the hand-over.
const memory = await client.memories.get(now.results[0].memory.id);
console.log(memory.supersedes, memory.supersededBy);

The cost of this model is real. Every memory is a chain rather than a row, the conflict detector has to reason about intervals instead of values, and there are invariants to enforce that an UPDATE would not need, such as a memory being unable to be superseded twice or to supersede itself. What you get back is a store where a wrong answer can be traced to the moment it became wrong, and where nothing a user told the system is ever destroyed by the system deciding it knows better now.