Chapter 5 of 7 · 15 min read
A single TTL is too short for a name and too long for a trip
On this page
A retention job is the simplest thing in a memory system to write and the easiest to get catastrophically wrong. It is a time to live, or TTL: one duration, applied to every row in the store, after which the row is deleted whatever it happens to say. It takes an afternoon, it runs at night, and it never fails.
What it cannot do is be right, and the reason is worth stating before any of the machinery. The number in that job is not a property of the store; it is a claim about how long a fact stays true. A child's name has an expected lifetime measured in decades. A hotel booking has an expected lifetime printed inside the fact itself. Averaging those into one number produces a policy that is simultaneously too aggressive and too lax, and adjusting the number moves the failure from one side to the other without reducing it. That is the shape to hold on to: the two failures are not a trade-off along one dial, they are the same dial being asked to answer two unrelated questions.
Retention belongs to the fact class
The unit of policy is the class of fact, which the schema from chapter 2 already gives you: attributes are named, so attributes can be grouped, and each group gets a lifetime, a decay curve and an action to take when the clock runs out.
| Class | Examples | Time behaviour | On expiry |
|---|---|---|---|
| Identity | name, date of birth, children, native language | no decay | never expires |
| Slow attributes | employer, job title, home city, plan tier | half-life 24 months | demote, flag for verification |
| Stated preferences | diet, seat, tone, notification settings | half-life 12 months, reset on confirmation | demote |
| Relationships | manager, teammates, household members | half-life 18 months | demote |
| Working context | current sprint, active ticket, file under discussion | hard expiry 60 days idle | delete |
| Commitments | trips, bookings, deadlines, leave | absolute expiry from stated end date + 7 days | archive as past event |
| Episodes | "sister visited in November" | hard expiry 90 days | archive |
| Session state | "the file I mentioned", "that one" | end of session | delete |
| Regulated | health conditions, legal matters, financial detail | policy-driven, usually shortest | hard delete |
Three different mechanisms appear in that table and they are worth separating, because they answer different questions and are not interchangeable settings of one thing. Hard expiry is a deadline measured from the last time anything happened: at day 60 the working-context row stops being a memory, because sixty days of silence about a ticket is good evidence the ticket is closed. Decay is continuous: a slow attribute's confidence falls smoothly, so it keeps being used while it is probably still true and quietly stops being asserted as it ages, following the read-time confidence bands from chapter 4. Absolute expiry uses a date carried in the fact rather than any duration at all, which is the mechanism a hotel booking needs and the one most stores lack.
from datetime import datetime, timedelta
from math import exp, log
class Policy:
def __init__(self, half_life_days=None, hard_ttl_days=None,
use_stated_end=False, on_expiry="delete"):
self.half_life_days = half_life_days
self.hard_ttl_days = hard_ttl_days
self.use_stated_end = use_stated_end
self.on_expiry = on_expiry
RETENTION = {
"identity": Policy(),
"slow_attribute": Policy(half_life_days=730, on_expiry="verify"),
"preference": Policy(half_life_days=365, on_expiry="demote"),
"relationship": Policy(half_life_days=545, on_expiry="demote"),
"working_context": Policy(hard_ttl_days=60, on_expiry="delete"),
"commitment": Policy(use_stated_end=True, on_expiry="archive"),
"episode": Policy(hard_ttl_days=90, on_expiry="archive"),
"session": Policy(hard_ttl_days=0, on_expiry="delete"),
}
def effective_confidence(fact, now: datetime) -> float:
p = RETENTION[fact.klass]
if p.use_stated_end and fact.valid_to:
return fact.confidence if now <= fact.valid_to + timedelta(days=7) else 0.0
if p.hard_ttl_days is not None:
age = (now - fact.last_confirmed_at).days
return fact.confidence if age <= p.hard_ttl_days else 0.0
if p.half_life_days is None:
return fact.confidence
age = (now - fact.observed_at).total_seconds() / 86_400
return fact.confidence * exp(-log(2) * age / p.half_life_days)The order of the branches is the policy, and it is worth reading as a priority rather than as a sequence of unrelated cases. A stated end date wins over everything, because a date the user gave is better information than any duration you assigned to a class. A hard TTL comes next, since it is a deadline and deadlines are not negotiable by degrees. Only if neither applies does the smooth decay run, and a class with no half-life at all falls through to the stored confidence unchanged, which is how identity facts never expire.
Two entries in that dictionary are worth reading closely because they are the extremes. Session state carries a hard TTL of zero days, which is not a mistake: measured from the last confirmation, a TTL of zero means the row stops counting the moment the conversation stops touching it, which is the correct lifetime for a phrase like "that one" whose referent exists only inside the exchange that produced it. And commitments add seven days to the stated end date rather than expiring on it. That grace period exists because the end of an arrangement is not the end of its relevance — somebody who checked out on the twelfth may still be asking about the invoice on the fourteenth — and because a date extracted from a sentence is more likely to be slightly early than badly wrong.
The regulated row is the one that does not follow from any of this reasoning, and it is worth marking as different. Its lifetime is not chosen from how fast the facts change; it is imposed, usually shorter than usefulness would suggest, and its expiry action is hard deletion because retaining the row is itself the exposure. Everything else in the table is an engineering judgement that a measurement could revise. That row is a constraint the measurements are not allowed to argue with.
Note that decay uses observed_at while hard TTLs use last_confirmed_at, and the two timestamps are doing genuinely different jobs. A fact's confidence should fall from when it was true, so that the November backfill of a 2023 CRM value is read at 0.6 × 0.44 rather than at full strength — and that factor is computed at the moment of reading rather than stamped on the row, so it keeps falling as long as the row sits there. Working context, by contrast, is about whether the thread is still live, and a reference in conversation genuinely does keep it alive, so touching it should reset the clock. Applying one timestamp to both mechanisms is how a hotel booking survives past its checkout date: it gets mentioned, the mention looks like activity, and activity is being read as continued truth.
Running the decay for a home_city fact written at confidence 0.91 with a 24-month half-life gives the trajectory below, and the row marked in the last column is where the read-time bands from chapter 4 change the agent's behaviour.
| Age | Effective confidence | How it is used |
|---|---|---|
| 0 | 0.91 | asserted plainly |
| 6 months | 0.77 | asserted plainly |
| 12 months | 0.64 | injected with provenance; agent may check |
| 18 months | 0.54 | injected with provenance |
| 24 months | 0.46 | still injected, hedged |
| 36 months | 0.32 | dropped from the prompt, retained in store |
The middle column is the half-life doing its one arithmetical job, and checking a row is how the parameter stops being a magic number. At twenty-four months — one half-life — the value is half of what it started at, which is what 0.46 next to 0.91 says. At twelve months it is halfway there in the exponent rather than in the value, so the factor is the square root of one half, which is a bit above seven tenths, giving 0.64. At thirty-six months it has been one and a half half-lives, so the factor is one half times that same square root, and the value lands at 0.32. Nothing in the table has to be looked up; it is one curve read at six points.
What the third column adds is that the curve is not what the agent experiences. The agent experiences three steps, at the band boundaries from chapter 4, and the whole of the first year happens inside one band. So the practical effect of a half-life is not "confidence declines"; it is "the fact stops being asserted plainly at about a year and stops being used at about three", and those two dates are what a half-life should be chosen against.
Absolute expiry is the cheapest large win
Commitments are the class where the fact states its own end, and extracting that end date turns retention from a guess into a comparison. "I'm at the Hilton Lisbon until 12 April" should produce valid_to = 2025-04-12, after which the fact is not merely less confident, it is false. No half-life expresses that, because the confidence curve is flat until the date and then drops to zero — a shape no exponential can take, whatever its parameter.
{
"attribute": "temporary_location",
"klass": "commitment",
"value": {"place": "Hilton Lisbon", "city": "Lisbon"},
"observed_at": "2025-04-02T11:20:00Z",
"valid_from": "2025-04-02",
"valid_to": "2025-04-12",
"confidence": 0.93
}Whether that field gets populated is decided by the extraction step from 10.11, and this is the dependency that makes absolute expiry cheap in principle and absent in practice. If the prompt does not ask for valid_from and valid_to, and does not resolve relative expressions like "next Tuesday" or "for the next two weeks" against the message timestamp, then every commitment lands as an undated assertion and inherits whatever generic TTL its class carries. The retention job downstream cannot recover the date, because the sentence it came from is no longer in front of it.
In the store this chapter opens with, 61% of memories that contained an explicit temporal bound in their text had no end date recorded. Those were also the rows most likely to produce a confidently wrong answer, and the reason is not coincidence: a fact with a date in it tends to be a fact about something the agent will act on, since people state end dates for arrangements and not for opinions. So the class with the highest extraction failure rate and the class with the highest cost per failure are the same class.
Testing the policy in both directions
A retention policy has two failure modes and they need separate tests, because a policy that looks good on one is usually being scored on the failure it does not commit. A store that deletes nothing is perfect on one test and a store that deletes everything is perfect on the other.
For deletions that mattered, replay. Take a month of real conversations, run each one twice — once against the full store, once against the store as the policy would have left it — and diff the answers. In the deployment above, 1,240 conversations produced 31 answers that differed, of which a reviewer judged 9 materially worse. That is a 0.7% regression rate.
The denominator there is the part to get right, and there are two tempting wrong ones. Dividing the 31 differing answers by the 1,240 conversations measures how often the policy changed anything, which is not a defect rate — an answer can differ and be equally good, or better. Dividing the 9 by the 31 measures how bad the changes were when they happened, which is a property of the changes rather than of the store. The rate you want is the 9 over the 1,240: the chance that running this policy makes a randomly chosen conversation worse. That is a number you can argue about rather than a feeling.
The nine cases clustered almost entirely in one class: relationships mentioned once and never repeated, which is exactly the shape of a child's name written during one holiday conversation. The fix was to move relationship from a hard TTL to a long half-life with demotion, so the fact stops being asserted without being destroyed.
For retention that outlived the truth, sample and check. Draw 200 facts per class older than that class's half-life and verify each against whatever ground truth exists — a system of record where there is one, an in-conversation confirmation question where there is not.
| Class | Sample age | Wrong when checked | Implication |
|---|---|---|---|
active_project | 90 days | 61% | 60-day hard TTL is generous |
employer | 18 months | 14% | 24-month half-life is roughly right |
home_city | 18 months | 6% | could decay slower |
dietary_preference | 18 months | 3% | 12-month half-life is too fast |
child_name | 24 months | 0% | correctly exempt |
Read the last column as a set of directions and the table is a controller rather than a report. Each implication compares a measured wrong rate against the lifetime currently assigned, and moves the lifetime the way the gap points: active_project is wrong more often than not at ninety days while its TTL is sixty, so sixty is if anything too long; dietary_preference is wrong three times in a hundred at eighteen months while its half-life retires it at twelve, so twelve is too fast. Note that home_city and dietary_preference are sampled at the same age and differ by only a few points, and their implications point the same way for that reason — the table is consistent, and a row whose implication contradicted its neighbours at the same age would be the thing to investigate.
Those two tables are the defence of the policy, and they are what a single TTL cannot produce, because a single TTL has one knob and five classes pulling it in different directions. The staleness numbers set the lifetimes and the replay numbers stop you from setting them too short; both come from data you can collect in an afternoon on an existing store.
The four expiry actions in the policy table are worth distinguishing precisely, because "expired" is usually implemented as DELETE and that is the strongest of the four and rarely the one you want. Demotion drops the fact below the injection threshold while leaving it in the store, so a later confirmation restores it and a reviewer investigating a complaint can still see it. Verification marks the fact as needing a check before it is used for an action, which is a different thing from not being used at all — the agent may still say "I have you in Berlin, is that current?". Archiving moves a commitment out of the live set and into the record of things that happened, so a trip that ended in April stops being a location and becomes an episode the agent can reason about in the past tense. Hard deletion is reserved for the regulated class and for session state, where retaining the row is itself the problem.
Choosing among them per class costs one column and does something more useful than it looks, which is to change how much the durations have to be right. A fact that is demoted three months too early is recoverable, because the row is still there and a confirming observation restores it. A fact that is deleted three months too early is not. So the expiry action sets the cost of a duration error, and picking the weakest action that meets the requirement removes most of the pressure on getting the durations exactly right.
Storage, eviction and the boundary with supersession
Retention here is about read-time correctness rather than storage. Chapter 3 put a year of appends at roughly 63 GB of rows and a terabyte of vectors, which is a real bill and a poor reason to delete anything, since the money saved by dropping a fact is a fraction of a cent and the cost of dropping the wrong one is a user who has to repeat themselves. Reason about expiry as a question of what should be allowed to reach the prompt, and treat storage as a separate concern with its own answer, usually tiering old rows to cheaper storage rather than deleting them.
That distinction matters when reading the literature, because the two goals produce different policies from the same vocabulary. The engineering write-ups on eviction and forgetting from the memory-store vendors, including mem0's material on interference and consolidation, are worth reading here with the distinction in mind, since several of their eviction strategies are optimising a capacity budget rather than answer quality. A strategy that evicts the least-recently-used row is right for a cache with a fixed size and wrong for a store whose problem is a fact that stopped being true, and the two look identical in a configuration file.
Expiry is also not supersession, and the two cover disjoint failure modes rather than overlapping ones. Chapter 3's mechanism needs a contradicting fact to arrive; it handles the user who says they moved. Decay handles the silence — the user who moved and never mentioned it, whose home_city will never be contradicted by anything because nobody is going to tell the assistant. A system needs both, which is why a store with excellent conflict resolution can still be full of confidently wrong facts, and why a store with excellent retention still needs the machinery of the previous four chapters for everything a user does say out loud.
Neither mechanism helps if a fact that survived retention, correct in the store and correctly confident, simply loses the ranking at read time to something newer and less relevant.
Memory — conflict and decay — read the current version at https://daviddominguez.dev/learn/en/memory-conflict-and-decay/a-single-ttl-is-too-short-for-a-name-and-too-long-for-a-trip