Skip to content

18 min read

Durable preferences and transient states need different write rules

On this page

A language model has no recollection between calls, so each turn is answered from whatever text the system puts in front of it, and when a session ends its transcript stops being an input to anything. Long-term memory is the store a team builds to survive that boundary: a small set of statements about a user, held outside any single conversation, fetched back and placed into the prompt when that user returns days or months later. It exists because the alternative is re-elicitation, which means that everything a customer established on Tuesday has to be established again on Friday, at a cost users notice for anything that took more than a sentence to pin down. Most readers have met the result from the outside: the "Memory updated" notice in a consumer chat assistant, a coding assistant that keeps reaching for pnpm because you corrected it once in March, the settings page listing what a product believes about you.

Everything in such a store had to be put there, and the step that puts it there is the subject of this topic. An assistant runs a write gate after each turn, a step that reads what the user just said, proposes candidate facts, and applies a policy that admits or rejects each one before it becomes a stored record. The write decision is that policy: which sentences from a conversation are worth keeping, who decides — a model call, a set of rules, or the user saying so explicitly — and what each admitted record is allowed to claim. In an engineer's terms the whole apparatus is a table of rows keyed by user, an extraction prompt and a few lines of policy code, which is the shape it takes in LangGraph's long-term memory store and in the equivalents shipped by other agent frameworks. The read side of the system, which picks records for a given question and assembles them into a prompt, is a separate problem with its own machinery, and it can only ever surface what the gate let through.

This chapter is about where the admission line goes. The two sentences that make the boundary visible sit inside a single utterance a customer of an analytics product called Arbor typed into its support assistant: "I'm in Berlin this week for the conference — keep it short, I'm on my phone." Read as claims about the user, one said she was located in Berlin and one said she preferred concise answers. Both were accurate descriptions of something she had just said about herself, a gate that admits candidates on accuracy admits both, and six weeks later the few lines of policy that admitted them were what produced a 02:00 CET export window for a customer sitting in Vancouver.

Three properties decide admission, and only one of them is about truth

Start with what an accuracy check can and cannot separate. Ask "is this true?" of the location claim and of the preference claim on the day they were written and the answer is yes to both. Ask it again several weeks later and the answer is still yes to one of them and no to the other, and nothing about the check itself changed in between. The quantity the gate measured — correctness at the moment of writing — was identical across the pair, while the property that decides whether the record does damage was not. That is the whole shape of the failure, and it is why an admission policy built around truth cannot catch it: the policy is holding a number constant across two records that differ on the axis it never looked at.

The axis it never looked at is durability, meaning how long the claim goes on being true after it is written. Location during a conference has a half-life measured in days, in the sense that a week later you would be roughly even money on the claim still holding. A preference for metric units has a half-life measured in years, and quite possibly for the life of the account. So the write decision is a judgement about the future of a claim rather than about its present correctness, and that is the part most write gates never ask about.

Durability is the first of three properties worth checking before anything is admitted. The second is evidence strength: how much of a general claim the utterance actually supports. "Keep it short, I'm on my phone" carries its own scope limiter inside it — the user explained the reason, and the reason was situational. Compare "I always want units in metric, I've asked twice before", which asserts generality and cites its own history. The third property is scope: what the fact is supposed to govern. Metric units govern every answer the user ever receives. A choice of Python over Ruby in code examples governs one project. A request to skip the preamble governs one conversation, and often only one turn of it.

Running those three properties over a handful of candidate utterances gives a boundary you can hand to another engineer.

UtteranceDurabilityEvidence strengthScopeDecision
"Always give me units in metric."YearsExplicit, asserted as generalGlobalWrite, no expiry
"I'm in Berlin this week for a conference."~7 daysExplicit but self-limitingGlobal while trueSkip, or write with expires_at
"Keep it short, I'm on my phone."HoursSituational, cause statedThis sessionSkip
"Our warehouse is BigQuery, not Snowflake."YearsExplicit correction of a factGlobalWrite, no expiry
"For the billing-service repo, use pnpm."Months–yearsExplicit, scoped by the userOne projectWrite, scoped to that project
"Ugh, that's wrong again."MinutesNone — an affect signalThis turnSkip

The table is more useful read as a set of gaps than as a set of verdicts. Between the first row and the second, only durability moves — both utterances are explicit and both are stated globally — and the decision flips, which is the isolated demonstration that durability is doing real work. Between the second row and the third, durability moves further and scope narrows, and the disposition goes from conditional to a flat skip. Between the first row and the fifth, durability and evidence are much the same and only scope changes, and what changes in the decision is not whether to write but what the record is allowed to cover. Each pair holds two properties still and varies the third, which is the only way to see what each column is contributing.

The last row is worth dwelling on because gates that reason freely about "what did we learn about the user" produce it constantly. An expression of frustration is information about a moment, and a record like "user is easily frustrated by long answers" is a psychological inference dressed as a fact, with no utterance behind it that the user would recognise as their own.

The three properties interact rather than voting independently, and the interaction has a shape worth working out rather than memorising. Start from what each property controls. Durability controls how many future turns a wrong record can reach, because a record with no end date is eligible for every retrieval until somebody removes it. Scope controls what fraction of those turns it can reach, because a record about one repository only surfaces on turns about that repository. Evidence controls the probability that the record is wrong in the first place. The damage a record can do is therefore roughly the third of those multiplied by the first two, and that product is what tells you how the properties combine.

Run the combinations through it. High durability with weak evidence is the dangerous quadrant, because a high error probability is being multiplied by a large number of reachable turns — the claim will sit in the store for years on the strength of one ambiguous sentence, and this is where "user is a beginner" records come from, written after someone asked what a percentile is. Low durability with strong evidence is comparatively harmless if you honour the expiry, since the record removes itself before it can do much and the first term in the product is small. Narrow scope forgives weak evidence, because it shrinks the second term: a wrong claim about how one repository is built surfaces only on turns about that repository, whereas a wrong global claim surfaces everywhere. So when the three disagree, durability and scope decide the shape of the record — its expiry and what it is allowed to cover — and evidence decides whether it gets written at all.

A transient fact is skipped or it carries an expiry

Once durability is a property you check, the Berlin record has exactly two acceptable dispositions, and the argument that there are only two is worth walking rather than asserting. A stored record can be retired by exactly one of two things: a clock the record carries, or a later utterance that contradicts it. A record with no end date has no clock, so the only remaining retirement path is the utterance. Now ask what a user does when a conference ends. They fly home and say nothing about it, because the end of a trip is not news anybody volunteers to a support assistant. So for a claim of this kind the utterance path never fires, and a record with no clock is a record with no retirement path at all.

That leaves the two dispositions. The record can be skipped, on the grounds that the assistant already has the current session's transcript and does not need a persistent copy of something the conversation is carrying anyway. Or it can be written with an explicit end date, so the record removes itself when the claim stops being true. What it cannot be is written the way a preference is written, with no expiry, because the store has no other mechanism that will ever notice the conference ended.

That means the record schema needs somewhere to put the answer. A minimal shape looks like this:

json
{
  "id": "mem_7c1e04",
  "user_id": "usr_4821",
  "kind": "preference",
  "statement": "Prefers measurements in metric units.",
  "scope": "global",
  "confidence": 0.91,
  "expires_at": null,
  "source": {
    "session_id": "ses_9d2a",
    "turn": 12,
    "quote": "always give me units in metric, please",
    "observed_at": "2026-02-04T09:41:07Z"
  },
  "gate": "model_v3",
  "created_at": "2026-02-04T09:41:09Z"
}

The Berlin fact, if written at all, differs in four fields:

json
{
  "id": "mem_7c1e05",
  "user_id": "usr_4821",
  "kind": "situation",
  "statement": "Travelling in Berlin for a conference.",
  "scope": "global",
  "confidence": 0.88,
  "expires_at": "2026-02-11T00:00:00Z",
  "source": {
    "session_id": "ses_9d2a",
    "turn": 12,
    "quote": "I'm in Berlin this week for the conference",
    "observed_at": "2026-02-04T09:41:07Z"
  },
  "gate": "model_v3",
  "created_at": "2026-02-04T09:41:09Z"
}

It is worth naming those four rather than trusting the eye, because two of them carry the argument and two are bookkeeping that the reader will otherwise conflate with them. kind moves from preference to situation, which is the classification the durability judgement produced. statement differs, because it is a different claim. confidence differs, because the gate was slightly less sure. And expires_at moves from null to a date, which is the only field that changes what the store will do later. The id and the quote inside source also differ between the two objects, and they are not part of the four, because they differ between any two records ever written — an identifier and the sentence a record came from are unique by construction, so a difference in them tells you nothing about the policy. Everything else, including scope, is identical: both claims are global, because the Berlin claim really does govern every answer while it holds. Durability and scope are separate axes, and this pair is the case that shows them coming apart.

expires_at has to be enforced at read time as well as by whatever sweeper deletes expired rows, and the reason is a gap rather than a bug. A sweeper runs on a schedule. Between two runs of a nightly job the store contains rows whose end date has passed and which have not yet been deleted, so a nightly job leaves up to twenty-four hours during which an expired record is still returned by a similarity search, the read-side lookup that scores stored records for their fit to the current question. This is the second place in the chapter where a constant is doing duty for something that varies: whether the column exists is a fixed property of the schema and reads as the defence, while the thing that actually decides whether an expired row reaches a prompt is how long ago the sweeper ran, which varies from zero to a full sweep interval. The read filter is one clause — expires_at IS NULL OR expires_at > now() — and it is the clause people forget, having congratulated themselves on adding the column.

Setting the value is the harder part, since the utterance rarely gives you a date. "This week" is tractable if you know the observation timestamp, because the utterance names a duration and the timestamp supplies the origin it is measured from, and a duration plus an origin is a date. "I'm on parental leave for a bit" is not, because "a bit" names no duration at all and no timestamp will supply one. The honest response there is a default: a situation record with no stated end gets thirty days, which is short enough to bound the damage and long enough to be useful. Guessing conservatively here is cheap in a way that guessing conservatively about durable preferences is not, because a situation that outlives its expiry will be restated by the user, whereas a preference the user has to restate every month reads as an assistant that does not listen.

An expiry is not a cache TTL — a time to live — even though it looks like one, and the comparison is worth drawing precisely because it is the first thing an engineer reaches for. A cache entry and a memory record both carry a clock, both stop being usable when the clock runs out, and both are cheap to write. Where the comparison breaks is on what happens next. When a cache entry expires, the next read recomputes it from the source, so the cost of a short TTL is latency and nothing else. When a memory record expires there is nothing to recompute from, because the source was a sentence a human said once in a conversation that may be beyond your retention window. Expiry here means the fact is gone unless the user says it again, which is why the setting is a policy decision about how much re-asking you are willing to do rather than a performance knob.

What you keep, you owe the user a view of

The same boundary that decides admission decides what you are on the hook for afterwards. If a record survives a session and shapes later answers, the user is entitled to see it and to remove it, which is both a product expectation and, for anything covered by GDPR or CCPA — the European and Californian data protection regimes, which give a person the right to see and erase data held about them — a legal one with response deadlines attached. That obligation reaches backwards into the write decision in a way that is easy to miss: a record you cannot render as a sentence the user would recognise is a record you should not have written, because your only options when they ask are to show them something that reads as surveillance or to hide it.

It is worth separating what the law requires from what the design decides, because they are not the same thing and conflating them makes the second look non-negotiable when it is a choice somebody made. The requirement is that a person can see and erase what is held about them. The design decision that follows is stronger: that a record must be renderable as a sentence the user would recognise as their own, tested at write time rather than at disclosure time. Nothing external compels that. It is chosen because a disclosure obligation you can only satisfy at the moment of disclosure is one you discover you cannot satisfy at exactly the wrong moment.

There is a blunt test for this that costs nothing to apply at write time. Take the statement field and imagine it printed in a list titled "What Arbor remembers about you", with the date beside it. If the sentence would read to the user as something they told you, the record is admissible on this criterion. If it would read as something you concluded about them — their skill level, their mood, their seniority, how much they trust the product — it fails, regardless of how well-founded the inference is. Inferences are not forbidden because they are always wrong; they are forbidden as memory records because they cannot survive being shown, and anything you would hide from the user is something you have no clean way to correct when they dispute it.

This is the practical reason the source block in the schema is not optional. When a user opens a memory settings page and sees "Prefers measurements in metric units", the useful version of that page shows the sentence they typed and the date, so the record is arguable. They can look at the quote and say the assistant misread it. Without provenance — the trail showing where a claim came from, in this case the quote, the session and the date — a disputed record turns into an argument between a user and an opaque assertion, and the only resolution available is deletion.

Deletion has an admission-side consequence too. Every record is something you will eventually need to find and remove on request, including copies of it that have leaked into derived artefacts — session summaries, a profile blob assembled nightly, an embedding index holding the numeric vectors that similarity search actually compares, a warehouse table someone built for analytics. The cost of a record is not its bytes; it is the number of places its content can end up multiplied by the difficulty of enumerating them later. That product is what makes the difference between a store that admits four facts per user, which is a deletion request you can serve in one query, and a store that admits four hundred, including inferences with no quotable source, which is a deletion request that turns into a project. The residual on that reckoning is that it counts places and not the effort of each: one copy in a system whose owner has left the company can cost more than a dozen copies inside your own schema.

Where durability stops helping

Drawing the line on durability, evidence and scope decides whether a candidate is admitted. It says nothing about how records are organised once admitted, about the difference between remembering a fact and remembering an episode, or about how competing records are reconciled at read time — those are separate problems with their own machinery, covered elsewhere.

It also does not solve staleness for the records it does admit, and the reason is that durability and correctness are different quantities that a single write-time judgement cannot both settle. A durable preference can go stale without ever expiring: a user who wanted metric units in 2024 may have moved to a team that reports in imperial, and no timestamp predicts that. Durability licenses one conclusion only, which is that the record is worth the cost of writing, because the expected number of turns it will usefully serve is large. The residual it leaves untouched is whether the claim is still true on any particular one of those turns, and that residual grows with exactly the property that made the record worth writing.

Nor is this boundary a substitute for asking. The most reliable way to establish that a preference is durable is that the user said so, or confirmed it when offered, and a write policy that never asks is choosing to guess where it could have known.

The rules so far are stated as if the two kinds of mistake — admitting the Berlin record, and failing to admit the metric-units record — were roughly equivalent errors to be balanced against each other. They are not close to equivalent, and the size of the gap is the reason a serious write policy leans as conservative as this one does. A missed write is discovered by the person standing in front of you and corrected in a sentence; a bad write is discovered by nobody, and goes on being applied. That asymmetry, and the evidence rule it forces, is the subject of the next chapter.

Memory — the write decision — read the current version at https://daviddominguez.dev/learn/en/memory-the-write-decision/durable-preferences-and-transient-states-need-different-write-rules

Back to top