Memory Write Policies: What's Worth Remembering
Agent-memory admission control with extraction, distillation, deduplication, and linking.
A coding agent is six months into a long-running engagement with one user. The long-term episodic store holds 47,000 entries; every user turn, every tool result, every assistant reply, faithfully appended on every call. The agent feels worse than it did at month two. Retrieval pulls textually similar episodes that are no longer relevant; the recency × importance × similarity rerank is sorting through a haystack that is now 95% noise; the hierarchical memory hot tier gets pre-paged with stale facts because the warm tier’s top-K is dominated by old write-spam. None of the storage layers are broken. The substrate works. The substrate is too good at storing everything, and the team never built a defensible answer to the upstream question: which writes should ever have entered the store in the first place? That question is what memory write policies answer.
Write pipeline
The pattern that has converged across Mem0, LangMem, Letta, and A-MEM is a four-stage pipeline. Knowing the stages by name is what makes the design decisions in each framework legible:
Stage 1; Triage. Given a candidate turn or exchange, decide whether it’s worth processing at all. The cheapest possible filter: skip system messages, skip empty assistant turns, skip pure conversational fillers (“Got it”, “Thanks”, “Can you repeat that?”). No model call; a regex or token-count heuristic does the work. The triage stage is what keeps the write pipeline from running expensive downstream stages over the high-volume floor of pure noise. Skipping triage and going straight to LLM extraction is the most common production cost bug; you’ll spend $5 a day per active user on extraction calls for “ok” turns.
Stage 2; Extract / distill. Given a turn that cleared triage, transform it into the shape you actually want to store. Three common shapes: keep the raw episode (journal-only), extract one or more structured facts ({type, subject, value, confidence}, checkpoint-only), or both (hybrid). The extract stage is typically a small-model call (Mem0’s default is GPT-5-mini, Letta’s classifier path uses a smaller model, most hand-rolled systems use Haiku or Llama-3-8B-Instruct), and the prompt anchors the schema and the confidence scale. The fact-shaped output is itself the unit of admission downstream; a single turn might extract zero facts (if it was pure conversation) or three (a preference, an event, a corrective). The distill step is what separates “memory” from “transcript.”
Stage 3; Dedupe / resolve. Given an extracted fact, check whether it duplicates or contradicts something already in the store. Dedup against existing facts is typically a hybrid lookup: exact-match on a stable identifier (e.g., a normalized entity name) plus embedding similarity above a threshold. Conflict detection is harder; the new fact might update an old one (“user moved from SF to NYC”), supersede it (“user no longer works at Acme”), or contradict it (likely a hallucination, should be flagged for review). The 2026 production answer has converged: don’t try to resolve conflicts at write time in the synchronous path; write the new fact, mark the old one with a back-reference, and let the read-time rerank or a background reconciliation pass surface the conflict. The full ADD/UPDATE/DELETE/NOOP resolver, the user-vs-system arbitration policy, and the contradiction-density curve all get the dedicated treatment in the memory conflict and forgetting article. A-MEM’s Zettelkasten-inspired linking extends this idea further; every new memory dynamically updates the contextual attributes of related existing memories at write time, building an explicit knowledge graph as a side effect of the write path.
Stage 4; Persist / index. Given an admitted fact, embed it, write it to the storage tier, and update any auxiliary indices (entity graph, by-time index, by-user index). This is the cheapest physical operation in the pipeline; it dominates only if stages 1-3 have already filtered hard. Persist is also where the tier decision lands: a high-confidence durable preference goes to the hot/core tier, a moderate-confidence fact goes to the warm/recall tier, a low-confidence raw episode goes to the cold/archival tier.
The shape of a defensible memory subsystem is all four stages run on every write, with the cost of each stage bounded and the failure of each stage handled explicitly. Skipping stage 1 floods stages 2-4 with garbage; skipping stage 2 turns the store into a transcript dump; skipping stage 3 lets contradictions accumulate; skipping stage 4 means the fact never makes it into a queryable tier. Production failures usually trace to a missing stage rather than to a bad stage.
When write policy runs
The single biggest cost lever in a memory write policy is when it runs relative to the user-facing turn.
Hot-path admission (synchronous). Every turn ends with the full write pipeline running synchronously before the agent returns control to the user. Pros: the next turn (in this session or any other) sees the new memory; no risk of dropping memories if the process crashes between the user turn and a deferred pass. Cons: the user-facing latency includes the full extract-dedupe-persist cost; on a turn that didn’t need a memory, you’ve paid for stages 1-2 anyway; the per-turn LLM call for extraction roughly doubles the agent’s per-turn cost. Best fit: short interactive conversations where same-session continuity matters and the user tolerates a 200-500ms latency tax.
Deferred at session-end (batched). The agent buffers candidate writes in working memory through the session; when the session closes (explicit logout, idle timeout, or natural turn-of-topic) a single batched write pass processes the entire session. Pros: amortizes the extraction cost across many turns (one call instead of N); produces higher-quality extracted facts because the model sees full context; no per-turn latency tax. Cons: in-session continuity has to come from the working-memory scratchpad or the conversation buffer, not from long-term memory; if the session ends abruptly (process crash, network drop) all of it is lost; the user can’t reference a fact from earlier in the session via memory retrieval; only via the in-context conversation log. Best fit: customer-support sessions, bounded interactions with a defined end, anywhere the value of cross-session continuity dominates the value of within-session continuity.
Background / sleep-time (asynchronous). A separate process consumes a raw journal (every turn, written unfiltered) and runs the extract-dedupe-persist pipeline in the background, on a queue, often using a different model than the foreground agent. LangMem’s background memory manager is the productized version of this; Letta’s sleep-time agents are a richer variant that also consolidates the existing store. Pros: zero impact on user-facing latency; full session context available to the extractor; can use larger, slower, more accurate models than the foreground agent can afford; the journal serves as a durable audit log even if the background pipeline fails. Cons: there is a window where new memories aren’t yet queryable (the consistency lag from journal to indexed tier); operationally more complex; you now have two pipelines, a queue, and a failure mode where the journal grows faster than the extractor drains it; and the journal itself is unbounded by default, which puts the write-amplification problem back at the storage layer if you don’t add log compaction. Best fit: high-throughput multi-tenant systems where per-turn latency is critical and the operator has the engineering budget to run and monitor a background pipeline.
The defensible production answer is a mix: a fast triage on the hot path (sub-millisecond, no model call), a deferred extract pass at session-end for the common case, and a background reconciliation pass that runs nightly to dedupe, consolidate, and clean up. The shape mirrors what databases do: the WAL writes synchronously for durability, the checkpoint flushes asynchronously for read performance, and the autovacuum runs in the background to clean up bloat. The same three-tier pattern, applied one layer up.
Distilling turns into facts
The extract stage is where most of the policy’s intelligence lives, and it has converged on a remarkably consistent prompt shape across frameworks. The pattern, distilled from the Mem0 fact-extraction prompt and LangMem’s memory manager:
- Anchor the schema. Tell the model exactly the structured shape you want:
{type: "preference"|"fact"|"event"|"correction", subject: string, predicate: string, object: string, confidence: 0-1, valid_from: timestamp?}. The schema is what makes the extracted facts dedupable downstream; without consistent shape, the dedup stage can’t compare apples to apples. Thevalid_fromfield is the upstream half of the bi-temporal model that the temporal-reasoning article works on the read side; writing it consistently here is what lets the read path answer “as of when?” queries later. - Anchor the confidence scale. “0.2 = a guess from one ambiguous turn, 0.5 = an inference from a single clear statement, 0.9 = the user explicitly stated it, 1.0 = a system-of-record import.” Without anchoring, confidence scores cluster at 0.7 ± 0.1 and become useless for downstream filtering.
- Anchor what not to extract. “Do not extract: small talk, the assistant’s own opinions, content that depends on session-specific context, hypothetical or counterfactual statements.” This is the negative-example list that prevents the extract stage from over-producing; without it, the model will extract a “preference” from “I think pizza could be good for dinner tonight” and clutter the store.
- Return JSON, return an array. Always return a list, even if zero or one. The extractor returns zero facts as often as it returns one; zero-fact turns are the common case for any conversational system, and the policy has to handle them as a normal output, not an error.
The single-pass ADD-only redesign Mem0 shipped in 2026 is worth knowing as a specific instance of this pattern. The old pipeline ran two LLM passes: the first extracted candidate facts, the second compared each candidate against the existing store and emitted an ADD / UPDATE / DELETE / NOOP decision. The new pipeline runs one LLM pass that emits ADD-only writes and defers all UPDATE/DELETE reconciliation to either retrieval-time scoring or an asynchronous consolidation job. Reported result: 60-70% fewer write-time LLM calls, with the read path absorbing the conflict-resolution work via the recency-weighted rerank from the Generative Agents paper; newer ADDs naturally outscore older ones for the same fact. The trade-off is that the store now contains both versions of any fact-with-history, and the dedup work happens at read time rather than write time. The redesign is a classic case of move the cost off the hot path; the read path’s bounded top-K naturally drops the older versions, and the cheaper writes pay for themselves immediately.
Admission-controlled write gate in Python
The smallest interesting build: a four-stage write pipeline with triage, distill, dedupe, and persist, against Chroma for the checkpoint tier and a sqlite-backed journal. Uses the Anthropic SDK for the extract stage. Install: pip install anthropic chromadb.
| |
the journal write is unconditional; even turns that fail triage land in the journal; the journal is the audit trail, and we want a future run of the extractor (possibly with a better prompt, possibly with a larger model) to be able to revisit them. The four stages are explicit and individually skippable: production tuning happens at the stage boundary, not inside the stages. Want cheaper writes? Tighten triage. Want higher precision? Raise the confidence threshold in stage 3. Want background-only? Set hot_path=False everywhere and run process_pending_journal from cron. The dedup pass uses both exact-match and similarity; fact_key is the stable identifier for known-duplicate detection, the similarity check catches semantic duplicates. A real production system would also pass the new fact through an LLM “is this an update to an existing fact?” check when similarity is in the 0.6-0.9 ambiguous range; for clarity that stage is omitted here.