# Stale Memories in AI Agents: Invalidate, Supersede or Decay?

> Long-term memory fails quietly: the agent recalls a fact that stopped being true. Three ways to handle it, and how Zekra, Mem0, Graphiti and Engram do.

*2026-10-15T14:50:45Z*

# Stale Memories in AI Agents: Invalidate, Supersede or Decay?

Give an agent long-term memory and the first failure you'll see isn't forgetting. It's remembering the wrong thing with confidence.

In March the agent stores "the API runs on Laravel." In June you migrate to Go and tell it so. In July it recalls both, ranks the older one higher because it has been retrieved more often, and writes a Laravel answer.

That's a stale memory. Storage and retrieval work fine. What's missing is a decision about what happens to a memory once something newer contradicts it.

**The short answer:** there are three tools. **Invalidate** marks a fact as no longer true without deleting it. **Supersede** links the old fact to the new one that replaces it. **Decay** lowers the weight of facts nobody has used in a while. You usually need the first two at write time and the third at ranking time. Deleting rows is almost never the right tool.

## Why stale memories are hard

Vector search doesn't know about time. "We deploy with Docker Compose" and "We no longer deploy with Docker Compose, we use Kubernetes" sit very close in embedding space. Ask "how do we deploy?" and both come back. The model then has to guess which one is current, and it often guesses wrong.

The fix has to happen at one of two points:

1. **Write time:** when a new memory arrives, decide whether it adds, repeats, updates or contradicts what's already stored.
2. **Read time:** when recalling, filter out what's no longer valid and down-rank what's old or unused.

## Strategy 1: Invalidate (soft delete with a timestamp)

Invalidation sets an `invalid_at` timestamp on the old row instead of removing it. Normal recall skips invalid rows, but they're still there for audits and for "what did we believe on date X?" questions.

```sql
-- the current view
SELECT * FROM memories
WHERE invalid_at IS NULL;

-- the view as of a past instant
SELECT * FROM memories
WHERE created_at <= $1
  AND (invalid_at IS NULL OR invalid_at > $1);
```

This is the core idea in [Graphiti](https://github.com/getzep/graphiti), the temporal knowledge-graph engine behind Zep. Its README says it plainly: "Facts have validity windows. When information changes, old facts are invalidated — not deleted." It tracks time bi-temporally (when a fact was true vs when the system learned it), which is the most complete version of this approach.

The [Mem0 paper](https://arxiv.org/abs/2504.19413) (Chhikara et al., April 2025) takes the same line for its graph variant: conflicting relationships are marked invalid "rather than physically removing them to enable temporal reasoning."

**Good for:** facts that flip ("we use X" → "we don't use X"), compliance, debugging the agent.
**Watch out for:** you need a reliable trigger. Something has to decide that the new fact contradicts the old one.

## Strategy 2: Supersede (replace and keep the chain)

Superseding is invalidation plus a pointer: the old row gets `invalid_at = now()` and `superseded_by = <new id>`. You can walk the chain from any version to the current one.

It fits facts that evolve rather than flip: a price, a team size, a version number, a decision that got refined.

There are two common ways to decide that a new memory supersedes an old one:

- **By identity.** The caller supplies a stable key. [Engram](https://github.com/Gentleman-Programming/engram), a Go memory server for coding agents, uses a `topic_key` such as `architecture/auth-model`. Saving with the same key updates that topic instead of adding a near-duplicate. This is deterministic and cheap, but only works when the writer knows the key.
- **By similarity.** Embed the new text, find the nearest existing memory and update it if similarity passes a threshold. It needs no cooperation from the writer, but the threshold has to be tuned and it can merge things that only look alike.

Mem0's paper describes an LLM-driven version: for each candidate fact, a tool call picks **ADD, UPDATE, DELETE or NOOP** against the most similar stored memories. It's flexible, at the cost of a model call per write. Mem0's current docs describe the hosted and open-source `add` as accumulating memories, with explicit update and delete calls when you need them, so check which behaviour your version has before relying on it.

## Strategy 3: Decay (rank old, unused memories lower)

Decay doesn't make anything invalid. It just changes the ranking score so that memories nobody has touched in a long time lose to fresh or frequently used ones.

The best-known reference is the [Generative Agents paper](https://arxiv.org/abs/2304.03442) (Park et al., 2023). Its retrieval score combines **recency**, **importance** and **relevance**, and recency is an exponential decay over time since the memory was last retrieved, with a decay factor of 0.995 per game hour.

A minimal version of that score:

```python
import math

def score(mem, relevance, now_hours, decay=0.995):
    recency = decay ** (now_hours - mem.last_accessed_hours)
    return relevance + mem.importance + recency  # normalise each to 0..1 first
```

**Good for:** large memory stores where old trivia crowds out useful facts.
**Watch out for:** decay alone never fixes a contradiction. A wrong fact used every day will never decay. That's why decay belongs next to invalidation, not in place of it.

## How Zekra handles it

[Zekra](https://github.com/fadymondy/zekra) is the open-source (AGPL-3.0) memory server I build in Go. It exposes memory to agents over MCP. Here's what the code does today, and what it doesn't.

Every write goes through a write decision in `writedecision.go` with two cosine thresholds:

```go
const (
    simDuplicate = 0.97 // near-identical: don't store again
    simSupersede = 0.88 // same subject: replace the old row
)
```

The decision runs in this order:

| Condition | Decision |
|---|---|
| Normalised text identical | **NOOP** |
| Similarity ≥ 0.88 and the new text negates ("no longer", "is wrong", "correction:", "retract"…) | **INVALIDATE** |
| Similarity ≥ 0.97 | **NOOP** |
| Similarity ≥ 0.88 | **UPDATE** (supersede) |
| Otherwise | **ADD** |

A few details matter in practice:

- **Superseding is soft.** The old row gets `invalid_at = now()` and `superseded_by = <new id>`. Nothing is hard-deleted, and the `memory_forget` tool also only invalidates.
- **A NOOP still counts.** It bumps `access_count` and `last_accessed_at`, so repeating a fact reinforces it instead of duplicating it.
- **Identity beats similarity for imports.** If a write carries a `source_ref` (an issue id, a file path), that ref is treated as identity: an existing ref updates exactly that row. This was added after similarity-only imports collapsed 108 separate release notes into 2.
- **Time travel is built in.** Recall accepts `since`, `until` and `asOf`. With `asOf`, Zekra returns the rows whose validity window contains that instant, including ones that were superseded later.

And the honest limits:

- **The negation check is a keyword list, on purpose.** The code comment says it's "deliberately conservative": a missed negation falls back to UPDATE, never to a wrong deletion. A contradiction that doesn't use those words ("we moved to Go") is treated as an update, not an invalidation. A real contradiction detector is planned as a later LLM-based step.
- **Decay isn't implemented yet.** The spec describes it ("lower importance over time, weighted so salient memories decay slowest"), but it's on the roadmap, not in the code. Ranking today is hybrid vector + BM25 search with reranking, plus importance computed at write time.

## Comparing the approaches fairly

| | Trigger for staleness | Keeps history | Cost per write | Main risk |
|---|---|---|---|---|
| **Mem0 (paper)** | LLM picks ADD/UPDATE/DELETE/NOOP | Graph variant: yes | One LLM call | Model judgement errors |
| **Graphiti** | Fact extraction + temporal edges | Yes, bi-temporal | LLM extraction | Pipeline complexity |
| **Engram** | Caller reuses a `topic_key`; review tools for conflicts | Topic updated in place | None (deterministic) | Writer must pick keys |
| **Zekra** | Cosine thresholds + negation cues; `source_ref` identity | Yes (`superseded_by`, `asOf`) | One embedding | Misses implicit contradictions |

None of these is "the right one." LLM-judged writes catch more contradictions but cost more and can be wrong. Deterministic rules are cheap and predictable but miss subtle cases. Keys are perfect when the writer knows them and useless when it doesn't.

## A practical recipe

If you're adding long-term memory to an agent this quarter:

1. **Never hard-delete by default.** Add `invalid_at` and `superseded_by` columns from day one.
2. **Use identity when you have it.** Ticket ids, file paths and URLs make far better supersede keys than similarity.
3. **Use similarity bands for everything else**, with a conservative contradiction rule. Tune the thresholds on your own data.
4. **Filter invalid rows at recall**, and support `asOf` so you can debug what the agent believed last week.
5. **Add decay last**, as a ranking term, once the store is big enough that old trivia actually crowds results.
6. **Let the agent say "that's wrong."** Expose a forget/invalidate tool so a user correction becomes data, not just a chat message.

## FAQ

**Should I just delete outdated memories?** Only for privacy or legal erasure. Otherwise you lose the audit trail and the ability to explain past answers.

**Is a bigger context window a fix?** No. More context means more stale facts competing in the prompt. Validity has to be decided before the prompt is built.

**Does decay replace invalidation?** No. Decay handles unused facts. Invalidation handles wrong ones.

## Next steps

- Read how this design came out of real use in [Agent memory in practice: lessons from building CaBrain](https://fadymondy.com/en/blog/agent-memory-in-practice-cabrain).
- Browse the Zekra source on [GitHub](https://github.com/fadymondy/zekra).
- Need memory or an MCP server inside your own product? See [MCP server development](https://fadymondy.com/en/services/mcp-server-development).

---

Source: https://zekra.dev/en/blog/stale-memories-ai-agents-invalidate-supersede-decay
