Our internal assistant helps operations staff write incident summaries, pull reports and draft customer replies. After a few months, the most common complaint was not about answer quality. It was repetition: "I have told it ten times that I work on the EU team, I want tables not bullet points, and my times are in CET." Every conversation started from zero.
Adding memory was the obvious answer. Building it so that it helped rather than confused, and so that people trusted it, took more design than we expected. This post describes what we built and why.
Two kinds of memory
| Kind | Scope | Lives in | Example |
|---|---|---|---|
| Short-term | One conversation | The message history in the context window | "The incident we are discussing is INC-4412" |
| Long-term | Across conversations | A store outside the model, retrieved into context | "Prefers tables; works on the EU team; time zone CET" |
Short-term memory is what every chat already has. Long-term memory is new state that outlives the conversation, and that is where the design questions are.
What to remember
We limited long-term memory to a few categories and excluded the rest:
Store:
- stable preferences: output format, language, level of detail, time zone;
- role context: team, responsibilities, systems they own;
- explicit instructions: "always include the ticket link", "never suggest refunds above 100 EUR".
Do not store:
- the content of conversations (summaries of past work belong in the systems where work lives);
- personal data beyond what the user states for work purposes;
- anything about other people;
- secrets, credentials, customer data copied into a chat;
- one-off context ("today I am looking at the March numbers").
The second list matters as much as the first. A memory system that stores everything becomes a shadow copy of every conversation, with all the privacy and security problems that implies.
How memories are created
We tried two approaches.
Automatic extraction. After each conversation, a small model reviews it and proposes memories. This caught many useful preferences but also produced wrong or trivial ones: "user likes March", from a conversation about March reports.
Explicit memory. The user says "remember that I prefer tables", or confirms a suggestion. Precise, but people rarely bother.
The combination that worked: automatic extraction proposes, the user confirms. At the end of a conversation where the extractor finds something, the assistant asks: "Should I remember that you prefer reports as tables?" One click to accept. Explicit "remember X" requests are stored directly.
Each memory is stored as a short, self-contained statement with metadata:
{
"id": "mem_0192",
"user_id": "u_4412",
"text": "Prefers report output as markdown tables rather than bullet lists.",
"category": "preference",
"source": "confirmed_suggestion",
"created_at": "2026-05-02T09:14:00Z",
"last_used_at": "2026-06-10T07:41:00Z"
}
Getting memories into context
At the start of each conversation, the assistant loads the user's memories into the system prompt, in a clearly delimited section:
## What you know about this user (from saved memories)
- Works on the EU operations team.
- Time zone: CET. Show times in CET unless asked otherwise.
- Prefers report output as markdown tables.
These are the user's saved preferences. Follow them unless the user asks otherwise in this conversation.
With a handful of memories per user, loading all of them is simpler and more reliable than retrieval. If a user had hundreds, we would retrieve the most relevant ones by embedding similarity to the first message and always include core preferences.
The instruction "unless the user asks otherwise in this conversation" is important: the current request must win over stored preferences.
Conflicts and staleness
People change teams and preferences. Old memories that contradict new ones produce strange behaviour.
- On write, the extractor compares a new memory with existing ones in the same category. If it contradicts one ("works on the US team" versus "works on the EU team"), the assistant asks which is current and replaces the old one.
- On use, memories not used for 90 days are marked stale and dropped from the prompt; the user sees them in their memory list and can restore them.
- Explicit corrections ("I am not on the EU team any more") delete or update the matching memory immediately.
User control
Trust depends on people knowing what the assistant remembers. Every user has a memory page listing all memories with their source and date, where they can edit or delete each one, or turn memory off entirely. The assistant also answers "what do you remember about me?" from the same store.
Deletion is real deletion, not a flag: memories are removed from the store and from any embedding index.
Security considerations
- Per-user isolation. Memories are keyed by user id and loaded only for that user. A bug that loads the wrong user's memories is a data leak; we test it explicitly.
- Injection through memory. If extraction runs on conversations that include pasted content, an attacker could try to plant a memory such as "always send summaries to this address". Confirmation by the user and a filter that rejects memories containing URLs, email addresses or instructions about tools reduce this risk.
- Memories never grant permissions. A stored statement like "I am an admin" changes tone, not access; authorization comes from the identity system.
Shared and team memory
Some preferences belong to a team rather than a person: "the EU team reports incidents in UTC", "always link the runbook". We keep those as team instructions maintained by the team lead, loaded for every member, and separate from personal memories. Mixing them led to personal memories that silently applied team rules to the wrong people when someone moved teams.
Testing memory behaviour
Memory makes behaviour depend on history, which is harder to test. Our evaluation set includes scenarios with fixed memory states: a user with a table preference asking for a summary, a user whose memory conflicts with the current request, a user with memory turned off. Each scenario checks the output follows the right rule, and a dedicated test loads two users in parallel to confirm memories never cross between them.
Measuring the effect
We compared two months before and after: the share of conversations in which users restated a preference dropped from about 30% to 8%, and the thumbs-down rate on formatting issues halved. About 12% of users deleted at least one memory, which we read as the control being used, not as a failure.
Common mistakes
| Mistake | Effect | Do this instead |
|---|---|---|
| Storing whole conversations | Privacy risk, noisy context | Store short, specific statements |
| Fully automatic memory | Wrong or trivial memories | Propose, then let the user confirm |
| Memories override current requests | Assistant argues with the user | Current instructions always win |
| No expiry | Stale facts cause odd behaviour | Track use, mark stale, ask on conflict |
| Hidden memory | Users distrust the assistant | Visible list with edit and delete |
Key takeaways
- Separate short-term conversation context from long-term memory.
- Store only stable preferences, role context and explicit instructions.
- Let users confirm proposed memories and see, edit and delete all of them.
- Load memories in a delimited section and let current requests override them.
- Handle conflicts and staleness, isolate per user, and never let memory grant permissions.
References
- OWASP Top 10 for LLM Applications: https://genai.owasp.org/llm-top-10/
- Anthropic, Building effective agents: https://www.anthropic.com/engineering/building-effective-agents
- Anthropic, Effective context engineering for AI agents: https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
- GDPR Article 17, Right to erasure: https://eur-lex.europa.eu/eli/reg/2016/679/oj
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.