Memory is not context: a safe storage boundary for agents
Separate live state from durable user-stated facts, then add provenance, expiry, deletion, and intent-based retrieval.
Context is the information assembled for the current inference. Memory is information persisted from earlier conversations and retrieved later. Treating them as the same store lets an old observation override a current source of truth.
The boundary is straightforward: if an authoritative system can answer the question now, query that system. Memory is reserved for durable information the user supplied and that no authoritative service owns.
Classify before storing
This section defines a memory record that carries provenance and lifecycle information. A text fragment and embedding are not enough for safe recall.
A durable-memory record needs an explicit lifecycle contract:
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
class MemoryKind(StrEnum):
PREFERENCE = "preference"
USER_FACT = "user_fact"
GOAL = "goal"
@dataclass(frozen=True)
class Memory:
id: str
user_id: str
kind: MemoryKind
text: str
source_message_id: str
created_at: datetime
expires_at: datetime | None
superseded_at: datetime | None = None
Do not create memory records for account status, permissions, balances, inventory, device state, or any value already maintained by another service. Those values can change without a conversation, which makes a recalled copy stale by construction.
The source_message_id supports audit and deletion. expires_at handles facts whose durability is uncertain. superseded_at prevents an older preference from being treated as current after a correction.
Extract candidates at a session boundary
This section keeps extraction away from the response path and gives it a coherent unit of conversation. A session boundary can be explicit or based on inactivity, but it should be deterministic.
The extractor should return bounded candidates rather than writing directly. Each candidate needs a known kind, normalized text, source message, and confidence so a deterministic storage policy can accept, reject, or request confirmation.
The numerical threshold is an example, not a universal default. Calibrate it with labeled conversations and review false positives, because storing a wrong memory is more persistent than generating a wrong sentence once.
Run a deterministic policy after extraction. Reject secrets, credentials, highly sensitive categories outside the product’s consent model, and facts with an authoritative owner. Consider asking for confirmation before storing sensitive or consequential memories.
Store semantic and lexical representations
This section creates a retrieval store without coupling the article to a specific database vendor. Semantic similarity is useful for paraphrases, while lexical matching preserves names and exact terms.
A retrieval store needs the memory text, semantic and lexical representations, user scope, kind, provenance, creation and expiry times, supersession state, and deletion state. The exact vector type and index syntax depend on the database.
The vector type and index syntax vary by database. The important constraints are tenant scoping, soft or hard deletion, expiry, provenance, and the ability to exclude superseded records before ranking.
Encrypt storage according to the sensitivity of the content. Never place raw memory text in analytics events or model traces by default.
Retrieve on intent and verify freshness
This section prevents every prompt from carrying a bundle of loosely related memories. Retrieval should run when the current message calls for prior personal information or when a product flow explicitly permits personalization.
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass(frozen=True)
class RecallRequest:
user_id: str
query: str
allowed_kinds: frozenset[MemoryKind]
limit: int = 5
def eligible(memory: Memory, request: RecallRequest) -> bool:
now = datetime.now(timezone.utc)
return (
memory.user_id == request.user_id
and memory.kind in request.allowed_kinds
and (memory.expires_at is None or memory.expires_at > now)
and memory.superseded_at is None
)
Tenant filtering must occur in the database query, not only after retrieval. The in-memory function illustrates the policy but is not an authorization boundary.
Treat retrieved memory as a user-stated claim, not current platform truth. Prompt labeling can say “The user previously said…” and include the observation date. If the statement conflicts with an authoritative source, the source of truth wins and the memory should be corrected or retired.
Support correction and deletion
This section covers the user controls that make persistent memory operable. A memory system needs read, update, and delete paths before it needs sophisticated ranking.
When a user corrects a fact, store the new record and mark the old one as superseded in one transaction. When a user requests deletion, remove the text, embedding, derived summaries, and any queued copies. Maintain a deletion audit containing identifiers and timestamps rather than deleted content.
Evaluation should test cross-user isolation, expired-memory exclusion, correction precedence, deletion propagation, prompt-injection content inside a stored memory, and conflict with live state. Retrieval quality alone does not cover these failure modes.
Memory is useful when it adds durable, consented information to fresh context. Provenance, expiry, correction, deletion, and intent-based retrieval keep it from becoming an ungoverned cache of yesterday’s truth.