All articles
January 24, 2026 6 min read

Long-term memory across sessions: persistent user profiles

Ask an agent your favourite colour today and it knows; ask it next week and it's forgotten — because chat history isn't memory. Here's the pattern that makes an agent remember: extract facts, store a profile, inject it next session.

Written forEngineeringProduct
AgentsMemoryPersonalization

There are two very different things people call 'memory', and conflating them is why agents feel forgetful. Ephemeral state is what an agent knows within a run — the conversation so far, held in its state and context. Persistent memory is what it knows about you across runs, weeks apart. Chat history gives you the first; it can't give you the second, because a new session starts with an empty message list. If the user told you their favourite colour is red three months ago, no amount of scrolling this session's history recalls it.

Recent turns · windowRolling summaryLong-term memoryContextLLM
Persistent facts live outside any single conversation: extracted from what the user says, stored in a profile, and injected back into the context at the start of the next session.

The pattern: extract, store, inject

Cross-session memory is a loop that runs around the conversation, not inside it:

  • Extract — when the user states a durable fact ('my favourite colour is red', 'I always want SI units'), an extraction step pulls it out as a structured, validated value.
  • Store — that fact is written to a persistent user profile in a database, keyed by user, as an upsert so it updates rather than duplicates.
  • Inject — at the start of the next session, you load the profile and put the relevant facts into the system prompt, so the model begins already knowing them.
Extract via a validated tool, store as an upsert
class Preference(BaseModel):
    key: str
    value: str

@tool
def remember(pref: Preference) -> str:
    """Save a durable user preference the user has stated."""
    db.users.update_one(
        {'_id': user_id},
        {'$set': {f'preferences.{pref.key}': pref.value}},  # upsert into a sub-document
        upsert=True,
    )
    return 'saved'

# next session: load the profile and inject it into the system prompt
profile = db.users.find_one({'_id': user_id}).get('preferences', {})
system_prompt += f"\nKnown user facts: {profile}"   # e.g. favourite_colour: red

Why extraction beats storing raw history

You might think 'just save the whole transcript and retrieve from it', and that's a valid tool (RAG over history — see the next post). But for stable facts about a user, extraction into a structured profile is far better: it's compact (a few key-values, not megabytes of chat), it's precise (the favourite colour is a field, not a sentence buried in a log), and it's cheap to inject (a small block in the system prompt, not a retrieval round-trip). Validate the extracted fact with a schema so you store 'red', not a hallucinated paragraph.

Ephemeral vs persistent, kept separate

Keep the two memories distinct. Ephemeral conversation state is managed within the run — a sliding window, a rolling summary (the memory-and-state post). Persistent facts live in the profile store and are injected at session start. Muddling them — trying to keep three months of history 'in context', or writing every passing remark to the permanent profile — is how you get both a bloated context and a profile full of noise. Extract what's durable; let the rest stay ephemeral.

Chat history is what the agent is saying; a user profile is what it knows about you. Remembering your favourite colour next month isn't a longer context window — it's a fact you extracted, stored, and handed back.
Building something with LLMs?
I help teams ship GenAI that’s reliable and cost-efficient.
Let’s talk