Working vs. Durable Memory
Also known as: scratchpad and store, session vs. long-term memory, externalized state
Separate the scratch state a task needs right now from the facts that should outlive the session, and store each differently.
The problem
"Memory" gets treated as one undifferentiated thing, usually a vector store everything is dumped into. The result fails in both directions. Transient scratch — intermediate results, a step counter, a half-built list — gets persisted and pollutes future sessions with stale detail. Durable facts — the user's timezone, their stated preferences, their role — live only in a conversation and vanish the moment it ends, so the user is asked the same question every week.
The pattern
Model two stores with different lifecycles and different write policies. Working memory is scoped to a run, holds arbitrary scratch state, is written freely and discarded at the end. Durable memory is scoped to a user or organization, holds a small curated set of stable facts, is written deliberately through an extraction step, and is loaded into context at the start of every session.
Use when
- The product spans multiple sessions and users notice being asked the same things repeatedly
- Agents perform long tasks needing scratch state larger than the context window
- You are already dumping everything into a vector store and retrieval quality is degrading
- Preferences, constraints, or profile facts should persist but conversation detail should not
Avoid when
- Single-session, stateless products where nothing should persist at all
- Privacy or regulatory constraints make persisting user facts undesirable
- The task fits comfortably in context and finishes in one session
Tradeoffs
GainDurable memory stays small and high-signal because scratch never enters it
CostTwo stores with different semantics, and a rule for what qualifies as durable
GainWorking memory can be arbitrarily large — it lives outside the context window
CostThe agent must learn to read and write it deliberately rather than just remembering
GainDurable facts are inspectable and correctable by the user
CostAn extraction step per session, plus conflict resolution when facts change
The distinction is lifecycle, not storage technology. Working memory is deleted when the run ends; durable memory is meant to be true next month. Anything you cannot confidently place in one of those two buckets probably does not belong in memory at all.
Working memory: a scratchpad with tools
Working memory is best exposed as plain read/write/list tools over a run-scoped namespace. Files or key-value entries both work. Its value is that it removes the ceiling on task state: an agent processing four hundred records keeps its progress and partial results outside the window, and its context stays small no matter how long the run goes.
class WorkingMemory:
"""Run-scoped. Freely written, never promoted, deleted when the run ends."""
def __init__(self, run_id: str):
self.ns = f"run:{run_id}"
def write(self, key: str, value: str) -> str:
kv.set(f"{self.ns}:{key}", value, ttl=timedelta(hours=24))
return f"wrote {key} ({len(value)} chars)" # a receipt, not an echo
def read(self, key: str) -> str:
return kv.get(f"{self.ns}:{key}") or f"{key} is not set"
def list(self, prefix: str = "") -> list[str]:
return kv.keys(f"{self.ns}:{prefix}*")
def discard(self) -> None:
kv.delete_prefix(self.ns)Have the write tool return a receipt rather than echoing the content back. Echoing puts everything you just externalized straight back into the context, which defeats the purpose entirely — a surprisingly common bug.
Durable memory: extract, do not accumulate
The mistake with long-term memory is writing continuously — every turn embedded and stored — which produces a store full of transient noise that drowns the few facts worth keeping. Extract instead: at the end of a session, run one pass that proposes durable facts against explicit criteria, and write only those.
class DurableFact(BaseModel):
kind: Literal["preference", "profile", "constraint", "relationship"]
fact: str
confidence: float
source_turn: int
EXTRACT = """Identify facts from this session that will still be true and useful in
a month.
Qualifies: stated preferences, role and team, recurring constraints, systems they
work with, how they like output formatted.
Does not qualify: anything about this specific task, transient states, one-off
requests, anything the user is likely to change tomorrow.
If nothing qualifies, return an empty list. Most sessions produce zero or one fact.""""Most sessions produce zero or one fact" is doing real work in that prompt. Without it, extractors return eight facts per session and durable memory becomes the noisy dump you were trying to avoid.
Loading and reconciling
Durable memory is small enough to load wholesale into the system prompt — a few dozen lines, not a retrieval call. Because it is small, contradictions are detectable: when a new fact conflicts with a stored one, supersede rather than append, keeping the old value in history. A store holding both "prefers Python" and "prefers Go" is worse than a store holding neither.
def commit(user_id: str, facts: list[DurableFact]) -> None:
existing = memory.for_user(user_id)
for f in facts:
if f.confidence < 0.7:
continue
if (conflict := find_conflict(existing, f)):
memory.supersede(conflict.id, f, reason="contradicted in later session")
else:
memory.add(user_id, f)
def system_prompt_for(user_id: str) -> str:
facts = memory.for_user(user_id, limit=40)
if not facts:
return BASE_SYSTEM
return BASE_SYSTEM + "\n\nWhat you know about this user:\n" + bullets(f.fact for f in facts)- Working memory: run-scoped, freely written, TTL'd, never promoted automatically
- Durable memory: extracted deliberately at session end against explicit criteria
- Write receipts, not echoes, or externalized state comes straight back into context
- Supersede contradictory facts instead of accumulating both
- Keep durable memory small enough to load whole — if it needs retrieval, it has become a corpus
Let users see and edit their durable memory. It is the cheapest trust feature in a memory-enabled product, it turns a creepy surprise into a controllable one, and user corrections are excellent training signal for the extractor.