"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. Here is our own proposal: a forget call must take an explicit mode, because forgetting means three different things. ModelBrain's takes revert, object or chain, and a stale write is caught by a revision counter that keeps advancing on a revert, from 1 to 2 to 3, while the version ID goes backward.
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.
"Forget" sounds like one action, but it needs to mean three different things, and collapsing them into a single undifferentiated call is exactly how you get "it un-deleted an old wrong answer." A forget call takes an explicit mode instead. revert, the default, is for a plain "forget this": on a memory that's been corrected before, it tombstones the current version and brings the one before it back as current. object removes just the one version you named, nothing else, even if it's the current one. chain removes a memory's entire correction history at once, every version it ever had. A connector-synced chunk ignores the mode entirely: forgetting it forgets the whole file it came from, and marks that file so it's never automatically re-ingested, even once its content later changes.
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.
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.
This shipped, with a test for each mode at every position in a correction chain, and a wire-level test confirming an unrecognized mode is rejected outright rather than silently falling back to one. 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.