Memory Hygiene: Supersede, Expire, Verify
Memories record what was true when written. The store does not receive world-change events. Verify before you act.
What the Store Does Not Know
A memory store is not a live mirror of the world. It is a collection of snapshots — observations, lessons, and facts recorded at specific moments in time. When the world changes, the store does not receive an event. It does not know that the endpoint moved, the flag was removed, the schema was migrated, or the team adopted a new convention. It only knows what was written into it.
This asymmetry is the root cause of memory rot. A memory can be perfectly accurate when written and confidently wrong a year later. The confidence score does not decay automatically. The heading still reads as authoritative. The source type is still derived or even raw. Nothing in the row signals that the world has moved on.
The stale recall pattern is the canonical example. An agent's memory recommended a flag on a tool that had been removed months earlier. The memory was accurate when written. Nobody told the memory the world changed. The agent recalled it with high confidence, acted on it without checking, and the command failed. The failure was not in the write path — the memory was correct at write time. The failure was in the recall-to-action path: the agent trusted a snapshot as if it were a live fact.
Three Hygiene Mechanisms
Memory hygiene is the discipline of keeping the store clean, current, and trustworthy over time. Three mechanisms work in concert.
1. expires_at: Time-Bounded Working Memory
Working memories are task-scoped scratch facts: the current hypothesis under evaluation, the intermediate state of a multi-step operation, the session-specific preference a user mentioned. They are valuable during a task and worthless — or misleading — afterward.
The expires_at column enforces a TTL. A hygiene job running on a schedule finds working memories past their TTL and soft-deletes them: SET deleted_at = NOW(). The row stays in the table for audit purposes; every live query filters WHERE deleted_at IS NULL.
// Expire working memories past their TTL
async function expireWorkingMemories(): Promise<number> {
const result = await pool.query(
`UPDATE memories
SET deleted_at = NOW()
WHERE memory_type = 'working'
AND expires_at IS NOT NULL
AND expires_at < NOW()
AND deleted_at IS NULL`
)
return result.rowCount ?? 0
}
A working memory that outlives its TTL is a stale memory waiting to contaminate future sessions. If a task-state memory from last week is still live, an agent hydrating today might receive it as context. The expires_at TTL is the simplest and most reliable way to prevent this: set it when you write, and the hygiene job handles the rest.
2. supersedes: The Correction Chain
When a fact changes, the correct action is not to update the memory in place. Updating in place destroys the history of what the agent believed and when. The correct action is to write a new memory with supersedes set to the old memory's ID, then soft-delete the old row.
// Write a correction that supersedes the old memory
async function writeCorrection(
oldMemoryId: string,
correctedFact: Omit<MemoryRecord, "memory_id" | "supersedes" | "deleted_at">
): Promise<string> {
const newId = "mem_" + crypto.randomUUID().slice(0, 8)
await pool.query(
`INSERT INTO memories
(memory_id, namespace, memory_type, heading, content, confidence,
source_type, supersedes, deleted_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NULL, NOW(), NOW())`,
[newId, correctedFact.namespace, correctedFact.memory_type,
correctedFact.heading, correctedFact.content, correctedFact.confidence,
correctedFact.source_type, oldMemoryId]
)
// Soft-delete the superseded row
await pool.query(
"UPDATE memories SET deleted_at = NOW() WHERE memory_id = $1",
[oldMemoryId]
)
return newId
}
The supersede chain gives you full audit history: you can walk the supersedes pointers to reconstruct every version of a belief. The hygiene job's role is to identify chains where the old row was not soft-deleted and clean them up — ensuring only the live head appears in recall results.
3. Verify Before Trust
The most powerful defense against stale recall is a behavioral rule, not a database operation. When an agent recalls a memory that makes an actionable claim — naming a specific file, flag, API endpoint, configuration key, or command argument — it must verify that claim against the live system before acting on it.
The decision point is whether the memory makes a specific claim about a live artifact. Background knowledge — architecture summaries, style notes, conceptual explanations — is low-risk. The agent can use it as context without verification. But "the tool accepts --verbose" or "the endpoint is /api/v2/users" or "the config key is max_retries" are all actionable claims that the world may have invalidated.
The verify step is cheap: check if the file exists, call the endpoint with no side effects, inspect the config schema. The cost of skipping it is a failed operation with a confident error message that is harder to diagnose because it references information the agent believed was correct.
Confidence Decay as Signal
A memory that has not been verified or updated in a long time is not necessarily wrong — but it has not been confirmed recently. Confidence decay is a way to encode this uncertainty without deleting the memory.
The hygiene job can reduce confidence on memories that have not been touched within a staleness horizon — no content update (updated_at) and no previous decay pass (confidence_updated_at, a nullable timestamp column added alongside the canonical schema):
async function flagStaleMemories(horizonMs: number): Promise<number> {
const result = await pool.query(
`UPDATE memories
SET confidence = GREATEST(confidence - 0.1, 0.1),
confidence_updated_at = NOW()
WHERE deleted_at IS NULL
AND GREATEST(updated_at, COALESCE(confidence_updated_at, updated_at))
< NOW() - ($1 * INTERVAL '1 millisecond')`,
[horizonMs]
)
return result.rowCount ?? 0
}
Note what the decay deliberately does not touch: updated_at. That column is reserved for content changes, because the memory-ops lesson's stale-embedding check compares updated_at against the last successful embed job. A decay pass that bumped updated_at would mark every aged row as a stale embedding — triggering pointless re-embeds of content that never changed — and would destroy updated_at as a content-change signal. Recording the pass on the dedicated confidence_updated_at column keeps that contract intact, and also prevents the decay from re-firing every run on its own timestamp bump.
GREATEST(confidence - 0.1, 0.1) ensures confidence never goes to zero — a floor at 0.1 preserves the memory while signaling that it needs verification. When an agent sees a memory with confidence 0.3 that started at 0.9, that is a signal: this was once well-established, but it has not been re-verified in a long time. Check it before acting.
Deleting Is a Feature
There is a temptation to preserve everything in a memory store — the reasoning being that old data is cheap to store and might be useful. This is the wrong frame.
A wrong memory that persists is actively harmful. It competes with correct memories in every semantic search. It can win that competition — high cosine similarity to a query is not a freshness or accuracy signal, it is a relevance signal. A stale memory about a deprecated API endpoint can outscore a correct memory about the new endpoint if the query is more similar to the old wording.
Soft deleting expired and superseded memories is not data loss. The rows stay in the table; deleted_at records when they were retired. Audit history is preserved. What is removed is their participation in live queries. A memory that should not guide an agent's behavior should not appear in recall results.
The hygiene job is the operational expression of the principle: a wrong memory is worse than no memory. Expire what has a TTL. Resolve what has been superseded. Flag what has aged past verification. These are not cleanup operations — they are the maintenance that keeps the store worth trusting.
What's Next
The session-assembly lesson addresses the other side of the lifecycle: what happens at the start of a session when the agent needs to load context from the store. The full store cannot fit in a context window, and dumping it all in is the worst strategy. The session hydration budget — compact index plus ranked full content under a token cap — is how you get the benefit of accumulated memory without paying the cost of loading everything.