Articles

What a Forget Call Actually Has to Guarantee

“I told it to forget something and it either didn't stick or brought back an even older wrong answer” is one of the most common complaints about AI memory tools. It's a specific, fixable bug, not a vague reliability problem.

One node dissolving into particles while a clean line points to a remaining solid node

The three failure modes, named

An early design modeled every correction as an edge pointing from the old version to the new one: a chain, not a pointer. Chains like that fail in three specific, reproducible ways. A cycle can form in the edge graph, so walking it never reaches a clear answer for what's current. More than one version can stay reachable after a mid-chain delete, so two different "facts" both look live at once. And a revert can leave a dangling edge a later correction never accounts for, so nothing in the graph resolves which one should win. None of these are exotic edge cases; they fall out directly from asking "what's current?" to require walking a chain instead of reading one value.

Delete and exclude are two independent switches, not one word

"Forget" sounds like one action, but a caller can mean four different things by it, and collapsing them into a single call is exactly how you get "it un-deleted an old wrong answer." There are actually two independent settings: whether the existing memory is deleted, and whether its source is excluded from ever being re-ingested. Delete + exclude is the default for a plain "forget this," where the memory is gone and the source won't quietly resurrect it later. Delete only, exclude off is for "remove this copy, it's fine if it comes back naturally," a deliberate, non-default request, since it's the surprising option. Exclude only, delete off leaves existing memory untouched and only stops future ingestion from that source. The one combination that's rejected outright is both switches off, which amounts to asking to forget nothing at all.

A stable ID, a current-version pointer, and a revision counter

Every memory gets one logical ID that never changes, no matter how many times it's corrected. A correction doesn't edit that memory in place; instead, it appends a new version and moves a single current-version pointer to it, incrementing a revision counter by exactly one. Concretely: you tell it "the API runs on .NET 8" (revision 1), and ten days later, "we upgraded to .NET 10." That correction names revision 1 as the version it expects to find current, the daemon checks that it still is, then the pointer moves and the revision becomes 2. recall() only ever returns the version the pointer names, while every prior version stays queryable through expand(), so nothing is erased; it's just no longer what comes back by default.

The bug hiding inside "just check the version ID"

Checking that a correction still targets the version it thinks is current sounds sufficient, until revert enters the picture. Say a session reads "D1," a second session corrects it to "D2," then reverts back to "D1." The pointer is D1 again, the exact version ID the first session already saw. If the check compared version IDs, the first session's now-stale write would sail through, since D1 "looks current" again even though a real update and revert happened in between. This is why the revision counter exists as its own field, separate from the version ID: it still advances on a revert, from 1 to 2 to 3, even though the version ID goes backward from 2 to 1. A stale write checked against the revision, not the ID, gets caught and rejected instead of silently landing.

Where this stands

This is identified and specified, not yet fully implemented. We'd rather say that plainly than let "forget" ship with a guarantee it can't yet keep. The test for whether it's done isn't a demo; it's question three of our own checklist: ask it to forget something, then ask again a different way.