Long-term memory makes an agent smarter and quietly makes it a liability. Stale facts turn true information false. Poisoned facts, whether from a noisy conversation or an attacker, survive across sessions. The under-engineered fix is the write path. Here is a memory-hygiene layer, with code.
An agent we run had been quietly getting one thing wrong for weeks. A user had told it, months earlier, where they worked. The agent did what a good memory system is supposed to do: it extracted the fact, wrote it to long-term memory, and recalled it in later sessions to sound helpful and personal. Then the user changed jobs. Nothing in the system knew that. The old fact was still the highest-relevance memory for that user, so the agent kept surfacing it, confidently, in a context where being wrong actually mattered.
There was no error. No exception, no failed eval, no alert. The retrieval worked perfectly. It retrieved a fact that used to be true. That is the uncomfortable thing about agent memory: the failure mode is not a crash, it is confident continuity of something that stopped being true.
We added long-term memory to make the agent smarter. What we had actually added was a place where wrong information could live indefinitely and be trusted forever. And once I started pulling on that thread, it became clear the problem has two doors, not one.
The industry talks about agent memory the way it talks about any capability: a thing you add to make the product better. Persist context, personalize responses, stop asking the user the same question twice. All real benefits. But memory is the one component that turns a momentary mistake into a permanent one.
Everything else an agent does is scoped to a turn. A bad model output affects one response. A prompt injection, in a stateless agent, ends when the session ends. Memory breaks that containment. A fact written to memory is retrieved and acted on across every future session, which means the blast radius of a single bad write is unbounded in time. The OWASP Agentic Top 10 now lists Memory and Context Poisoning as its own category, ASI06, precisely because the effect and the cause are temporally decoupled. Something gets written today, and the agent behaves wrongly weeks later, long after anyone would think to connect the two.
That temporal decoupling is what makes memory failures so hard to debug and so easy to ignore. You do not see the write. You see the wrong behavior, much later, with no obvious trigger. So the first mental shift is this: memory is not a feature you bolt on. It is a store of durable, high-trust state that your agent reads back as truth, and it deserves the same suspicion you would give any other untrusted input.
The first door is staleness, and it is the one almost nobody engineers for.
A fact in memory is a snapshot of a world that keeps moving. A user’s employer, their address, a product’s price, an API’s method name, the status of a project. Every one of these is true when written and becomes false the moment the world changes, with nothing in the memory store aware that anything happened.
The reason this is hard is that retrieval is similarity-based, and similarity cannot tell you what is current. When a fact changes, the vector store often holds both the old value and the new one, and they sit at nearly identical embedding similarity to the query, because they are the same kind of fact about the same entity. Recent work on temporal validity in retrieval memory shows the agent then either abstains or, worse, serves the superseded fact with full confidence. The STALE benchmark exists this year specifically to ask whether agents can even tell when their own memories are no longer valid, and the honest answer is that most cannot.
Staleness is not an attack. No adversary is involved. It is the default entropy of storing facts about a changing world, and it is the most common way agent memory goes wrong in production.
The second door is poisoning: a fact that was never true getting into memory in the first place. This happens two ways.
By accident, the agent extracts a wrong fact from a noisy conversation. A user says something sarcastically, or hypothetically, or corrects themselves a sentence later, and the extraction step captures the wrong version and commits it. Memory systems that let the model decide what to remember inherit every extraction error the model makes, and then make those errors durable.
By intent, an attacker writes to memory through ordinary use. This is the part the security research has moved fast on. The MINJA memory-injection work showed that an attacker can poison an agent’s long-term memory using nothing but normal queries, no special access, with injection success rates above ninety percent in the systems tested. Because the poisoned fact persists, the attack and its payoff are separated in time, which is exactly what makes it dangerous and hard to catch.
Accidental or adversarial, the shape is the same: a false fact enters the store, gains the same trust as every legitimate fact, and gets retrieved as truth. Which points at the thing both failure modes have in common.
Look at how much effort goes into the output side of an LLM system. Structured-output libraries to force valid JSON. Guardrails on responses. Validators, retries, verifier steps. We treat everything coming out of the model as suspect, and we are right to.
Now look at the memory write path in most agents. The model extracts a fact, and it goes straight into the store. No validation, no provenance, no expiry, no confidence. We spend enormous energy distrusting what the model says to the user, and then we take what the model says to remember and write it down as gospel.
That asymmetry is the bug. Memory is write-once, trust-forever, and it should not be. The fix is not a better model or a bigger context window. It is a hygiene layer on the write path and the retrieval path, built out of controls that are individually simple and collectively the difference between memory as an asset and memory as a slow leak. Here is what that layer looks like.
The unit of the layer is the memory record. Do not store a bare string. Store a fact with the metadata you need to distrust it later.
from datetime import datetime, timedeltafrom typing import Optionalfrom pydantic import BaseModel, Fieldclass MemoryRecord(BaseModel): subject: str # the entity this fact is about predicate: str # what kind of fact (employer, address, price) value: str # the fact itself confidence: float = Field(ge=0.0, le=1.0) source_session: str # provenance: where this came from extracted_by: str # which model wrote it written_at: datetime valid_until: Optional[datetime] = None # temporal validity, not forever superseded_by: Optional[str] = None # id of the record that replaced it
Every field past value exists so a future retrieval can decide how much to trust this fact. Now the controls.
Validate on write. Before a fact is persisted, it passes a gate. The gate does two jobs: it rejects anything that looks like an instruction rather than a fact (the accidental and adversarial injection guard), and it applies the same business-rule validation you would apply to any input.
import reINJECTION_PATTERNS = re.compile( r"(ignore (previous|above)|system prompt|you are now|http[s]?://|This one gate closes most accidental extraction errors and a large share of the injection surface, because both tend to look nothing like a short, declarative fact.
**Attach provenance to every fact.** The record above already carries `source_session` and `extracted_by`. This is not bookkeeping. When memory does go wrong, provenance is the only thing that lets you find every fact that came from a poisoned session and evict them together, instead of hunting one bad memory at a time.
**Give facts a temporal validity, not just a value.** This is the direct fix for staleness. High-churn predicates get a short expiry. A user’s employer or a live price should not be trusted for a year the way a birthday can be.
TTL_BY_PREDICATE = { "employer": timedelta(days=90), "address": timedelta(days=180), "price": timedelta(hours=24), "birthday": None, # does not go stale} php def with_validity(record: MemoryRecord) -> MemoryRecord: ttl = TTL_BY_PREDICATE.get(record.predicate) if ttl is not None: record.valid_until = record.written_at + ttl return record
At retrieval, an expired fact is not deleted, it is demoted: still visible for audit, no longer served as current truth. That single rule bounds both the staleness window and, usefully, the window any poisoned entry can influence behavior.
**Score confidence and let unused memory decay.** A fact retrieved and confirmed useful should get stronger. A fact that never gets touched should fade. This is what Mem0 implements as memory decay, where an untouched memory can survive eviction and still be down-ranked to a fraction of its retrieval score. You can apply the same idea at rank time.
``` php
def retrieval_score(record: MemoryRecord, similarity: float, now: datetime) -> float: score = similarity * record.confidence if record.valid_until and now > record.valid_until: score *= 0.3 # expired: keep but heavily demote age_days = (now - record.written_at).days score *= max(0.5, 1.0 - age_days / 365) # gentle recency decay return score
Now similarity is one input among several, not the only one. A perfectly similar but expired or low-confidence fact loses to a slightly less similar but current and trusted one, which is exactly the ranking you want.
Resolve contradictions instead of accumulating them. When a new fact contradicts an existing one, the wrong move is to store both and let retrieval flip a coin. The right move is to supersede. This is the ADD, UPDATE, DELETE, NOOP decision that Mem0 makes on every write, and it is the mechanism that keeps a job change from leaving two live employers in the store.
def reconcile(new: MemoryRecord, existing: list[MemoryRecord]) -> str: for old in existing: if old.subject == new.subject and old.predicate == new.predicate: if old.value != new.value and new.confidence >= old.confidence: old.superseded_by = new.subject # mark old as replaced return "UPDATE" return "ADD"
Isolate memory per user. This is an OWASP control and a one-line design decision that prevents an entire class of cross-tenant leakage and poisoning: never let one user’s writes land in a memory partition another user can read. Partition on write, filter on read, no exceptions.
If you have been running an agent with long-term memory and none of the above, assume some of your memory is already wrong, and go look. Three cheap passes find most of it.
Sample and verify. Pull a random set of high-retrieval memories and check them against ground truth. The staleness rate you find is your baseline, and it is usually higher than anyone expects. Scan for injections. Run the write-path validator retroactively over the existing store; anything it now rejects is a fact that should never have been written. And review by provenance. Group memories by source session and look for sessions that produced an outsized number of facts or facts that all point the same suspicious direction, which is the signature of both a noisy extraction and a deliberate injection.
None of this replaces the rest of your reliability work. It slots next to it. The write-path validator is the same instinct as hardening the human checkpoint: put a review step where an unverified thing becomes a trusted thing. The provenance and decay signals are memory-layer telemetry, and they belong in the same instrumentation you already run for the rest of the system, alongside your observability stack. Memory is just one more place where an AI agent in production reads something back as truth, and every place like that needs a gate.
The broader point is that memory hygiene is not a library you install. It is a design stance: treat the write path as untrusted input, and give every stored fact enough metadata to be doubted later. That is engineering work, the same custom build discipline you would apply to any part of the system that carries durable state, and it is the difference between memory that compounds intelligence and memory that compounds errors.
We reach for memory because we want agents that learn and remember. The catch is that an agent remembers wrong facts exactly as faithfully as right ones, and it holds onto them long after the moment that would let you catch the mistake.
Staleness and poisoning look like different problems, one an accident of time and one an act of intent, but they fail through the same unguarded door, and they are fixed by the same layer. The read path already gets all our suspicion. The write path deserves the same.
So the question worth taking back to your own agent is simple: right now, what stops a wrong fact from being written to your memory store, and what would ever tell you it was there? If you do not have an answer, that gate is the highest-leverage thing you can build this quarter.
What is AI agent memory poisoning?
It is when a false fact gets written into an agent’s long-term memory and is then retrieved and acted on as truth in later sessions. It can happen by accident, when the agent extracts a wrong fact from a noisy conversation, or by attack, when an adversary writes malicious content through ordinary use. OWASP classifies it as ASI06 in its Agentic Top 10.
How is memory poisoning different from prompt injection?
Prompt injection is usually session-scoped, so its effect ends when the session ends. Memory poisoning persists: the poisoned fact survives across sessions, so the agent can behave wrongly weeks after the write. The cause and the visible effect are separated in time, which makes it harder to detect and debug.
What are stale facts in agent memory?
A stale fact is information that was true when stored and became false when the world changed, such as a user’s employer after a job change or a price after an update. Because retrieval is similarity-based, the store often holds both the old and new values at nearly identical similarity, and the agent can serve the outdated one with full confidence.
How do you prevent bad data from entering agent memory?
Treat the write path as untrusted input. Validate every fact before persisting it, reject entries that look like instructions rather than facts, attach provenance and a confidence score, and give high-churn facts a short temporal validity so they expire instead of lingering. Isolate memory per user, and resolve contradictions by superseding rather than accumulating.
Which tools help manage agent memory in production?
Frameworks such as Mem0 and Zep provide memory management with mechanisms like fact extraction, ADD/UPDATE/DELETE reconciliation, eviction, decay, and temporal knowledge graphs. They give you the plumbing, but the hygiene policy- what to validate, what to expire, and what to isolate- is still a design decision you own.
Pratik K Rupareliya is Co-Founder and Head of Strategy at Intuz, where the team builds and runs production AI agents for companies in regulated and high-stakes industries. Across 16-plus years and 700-plus products, the recurring lesson is that the durable-state parts of a system, memory included, need the same discipline as any untrusted input. He writes about production AI. Based in California, USA, and Ahmedabad. More at intuz.com*.*
AI Agent Memory Fails Two Ways, and Both Persist. Here Is the Hygiene Layer. was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.