Memory and privacy
Be able to design memories that respect deletion, consent and data minimisation.
Prerequisites
- EPersonal data and anonymisationrequired
- FEpisodic memory for agentsrequired
Intuition
An agent memory is a collection of personal data that grows by itself. That makes it one of the most sensitive parts of an AI system.
Four questions that have to be answered before the memory is built:
| The question | Why |
|---|---|
| What is saved — and what is not? | data minimisation |
| For how long? | storage minimisation |
| Can the user see and change it? | rectification and openness |
| What happens on deletion? | the right to be forgotten |
The hardest is the last one. Deleting a row in the database is simple. But the memory may have spread: into summaries, into embeddings, into logs, into a model that has been fine-tuned on it.
The basic principle is therefore to design for deletion from the start — not to solve it when the request arrives.
Formal
Where a piece of personal data can end up, and how hard it is to remove:
| The place | Deletion |
|---|---|
| The memory entry | easy |
| Summaries | requires regeneration without that entry |
| Embeddings in the index | easy if the id is traceable, otherwise hard |
| Conversation logs | easy if they have a retention time |
| Audit logs | should not be deleted — but must not contain more than necessary |
| Backups | technically hard; handled with a retention routine |
| Fine-tuned model weights | in practice impossible |
The last row is the reason not to fine-tune on the users' memories. If you do, deletion becomes a promise you cannot keep.
Design principles that work:
- Save references, not copies. Let summaries point at the source memories so that they can be regenerated when something is deleted.
- Categorise on writing. Every memory gets a type (preference, goal, fact, sensitive) and a retention time that follows from the type.
- Filter on writing, not on reading. What is never saved cannot leak.
- A traceable id through the whole chain — from the memory entry to the embedding to the summary.
- Deletion is an operation, not a manual process. It should be possible to run and verify.
What should never be saved in an agent memory:
| The category | Why |
|---|---|
| Personal identity numbers, addresses, contact details of third parties | not necessary for the function |
| Health data | a special category under GDPR art. 9 |
| Data about other people | the data subject is not the user |
| Passwords and keys | obviously |
| Sensitive data about children | stricter requirements |
Openness in practice. The user should be able to see a list of what the system remembers, in readable form, and be able to remove individual entries. That is both a GDPR requirement and good design: a memory that cannot be corrected becomes unpleasant as soon as it contains something wrong.
Consent. A memory saved across sessions should be an active choice, not a default — particularly for children. And it should be possible to switch off without the service becoming unusable.
Code
import re, time, uuid
from dataclasses import dataclass, field
from enum import Enum
class Type(Enum):
PREFERENCE = ("preference", 365)
GOAL = ("goal", 365)
DIFFICULTY = ("difficulty", 180)
FACT = ("fact", 90)
SESSION = ("session", 1)
def __init__(self, label, retention_days):
self.label = label
self.retention_days = retention_days
# The patterns match Swedish formats — adapt them to your own locale
FORBIDDEN = {
"personal id number": r"\b(19|20)?\d{6}[-+]?\d{4}\b",
"email": r"\b[\w.+-]+@[\w-]+\.[\w.]+\b",
"telephone": r"\b0[\d\s-]{7,12}\b",
"address": r"\b\w+(gatan|vägen|gränd)\s+\d+\b",
}
def may_be_saved(text: str) -> tuple[bool, list[str]]:
hits = [name for name, m in FORBIDDEN.items() if re.search(m, text, re.I)]
return (not hits), hits
@dataclass
class Memory:
id: str
user: str
text: str
type: Type
created: float
sources: list[str] = field(default_factory=list) # the ids this was derived from
def expired(self, now=None):
now = now or time.time()
return now - self.created > self.type.retention_days * 86400
class MemoryBank:
def __init__(self, vector_index):
self.memories: dict[str, Memory] = {}
self.index = vector_index
def write(self, user, text, type: Type, sources=None):
ok, hits = may_be_saved(text)
if not ok:
return {"saved": False, "reason": f"contains {', '.join(hits)}"}
mid = str(uuid.uuid4())
self.memories[mid] = Memory(mid, user, text, type, time.time(), sources or [])
self.index.upsert(id=mid, vector=embed(text),
payload={"user": user, "type": type.label})
return {"saved": True, "id": mid}
def delete(self, mid: str) -> dict:
"""Deletes the entry AND everything derived from it."""
removed = []
queue = [mid]
while queue:
m = queue.pop()
if m not in self.memories:
continue
# everything pointing at m also has to go, or be regenerated
queue += [k for k, v in self.memories.items() if m in v.sources]
del self.memories[m]
self.index.delete(id=m)
removed.append(m)
return {"removed": removed, "count": len(removed)}
def delete_all(self, user: str) -> dict:
ids = [k for k, v in self.memories.items() if v.user == user]
for i in ids:
self.memories.pop(i, None)
self.index.delete(id=i)
return {"count": len(ids)}
def prune(self) -> dict:
now = time.time()
expired = [k for k, v in self.memories.items() if v.expired(now)]
for i in expired:
del self.memories[i]
self.index.delete(id=i)
return {"pruned": len(expired)}
def show_to_user(self, user: str):
"""A readable list — the user should be able to see and remove individual entries."""
return sorted(
[{"id": m.id, "text": m.text, "type": m.type.label,
"saved": time.strftime("%Y-%m-%d", time.localtime(m.created)),
"pruned_on": time.strftime("%Y-%m-%d", time.localtime(
m.created + m.type.retention_days * 86400))}
for m in self.memories.values() if m.user == user],
key=lambda d: d["saved"], reverse=True)
# Verify the deletion — otherwise it is a promise without backing
def verify_deletion(bank, user):
memories_left = [m for m in bank.memories.values() if m.user == user]
index_left = bank.index.count(filter={"user": user})
return {"memories_left": len(memories_left), "index_left": index_left,
"ok": len(memories_left) == 0 and index_left == 0}
The sources field is what makes deletion feasible. Without tracking what a memory was derived from, a deleted piece of data can be found again in a summary, and the deletion is not real.
Mastery means
- Designs memories with retention and deletion
- Applies data minimisation to what is saved
- Makes the memory visible and editable for the user
Sign in to do the exercises and build your mastery up.
Sources
- GDPR — Regulation (EU) 2016/679 — EU legal act
- IMY — Integritetsskyddsmyndigheten — myndighetsmaterial
- arXiv — MemGPT: Towards LLMs as Operating Systems — arXiv (open access; licence per article)