YouMind
Entrar

Como criar um sistema de memória para o Opus 5.5 que sobrevive entre sessões: escrever, consolidar, recuperar

@cyrilXBT
INGLÊS13 de out. de 2026
130K
151
26
12
170

TL;DR

Um guia técnico sobre como implementar um sistema de memória de quatro etapas (escrever, consolidar, recuperar, aplicar) para agentes Claude Opus 5.5 usando arquivos Markdown locais e Python, garantindo a persistência do contexto entre sessões.

Every Opus 5.5 session starts from zero.

You spent an hour yesterday explaining your stack, your naming conventions, the client who hates bullet points, the reason you moved off Heroku. Today it's all gone. You explain it again. Tomorrow you'll explain it again.

The model isn't the problem. Opus 5.5 is excellent at using context. The problem is that nothing carries the context from one session to the next.

Most people try to fix this by pasting a giant "about me" document into every chat, or by stuffing old transcripts back into the prompt. Both break fast. The document goes stale. The transcripts get expensive, and they're full of noise: dead ends, bugs you already fixed, opinions you changed.

A real memory system is different. It has four jobs:

Write: after a session, save only the facts worth keeping. Consolidate: every so often, merge, correct and shrink what's saved. Recall: at the start of a request, load the right memories and nothing else. Apply: use a memory only when it actually changes the answer.

This article builds all four from scratch with Opus 5.5, plain files and about 200 lines of Python. Every prompt is copyable. Every piece of code runs.

Why Chat History Isn't Memory

Before we build, it's worth being precise about what goes wrong with the obvious approaches, because the design choices below all come from these failures.

Transcripts are mostly noise. A 40 minute session might contain three facts worth keeping and 20,000 tokens of everything else. Reload the transcript and you pay for all of it, every time, and the model has to dig the three facts out again.

Facts change. You said you use Postgres in March. You moved to SQLite in June. If both statements sit in your history, the model has to guess which one is current. It will sometimes guess wrong.

Stale context gets applied at the wrong time. An old half finished task in the history looks like an instruction. The model brings it up when you're asking about something else entirely.

Nobody can see or fix it. When memory lives inside a vector database or a pile of old chats, you can't open it, read it, correct it or delete one wrong line.

So the design goals are simple. Store facts, not transcripts. One place per fact, so updates replace instead of pile up. Make every memory readable and editable by a human. And make the model earn the right to use a memory by showing it changes the answer.

The Storage: Plain Files, One Subject Per File

I use a folder of markdown files. Not a vector database, not a graph store. Files.

That sounds too simple, and that's the point. Opus 5.5 reads markdown extremely well. You can open any memory in your editor, fix a wrong line in five seconds, diff it in git, and see exactly what the model knows about you. When you have tens of thousands of memories across thousands of users, add a search index on top. Until then, files win.

The layout:

memory/

profile.md who the user is: role, company, what they build

preferences.md how they want the model to work: format, tone, depth

topics/

tools.md editor, languages, services

writing.md style rules, banned words, audiences

projects/

checkout.md an ongoing project: decisions, constraints, status

people/

maya.md a colleague: role, how they work together

The rule that makes this work: one subject per file, and a fact lives only in its subject's file. A fact about the checkout project goes in projects/checkout.md, even if you learned it during a conversation about tools. That's what lets a new fact replace an old one instead of creating a second, conflicting copy somewhere else.

Every file starts with the same header, then one fact per line:

name: tools

description: Editor, languages, hosting and dev tools the user works with. Read for any coding or setup question.

updated: 2026/09/26

[stated 2026/09/12] uses Neovim, not VS Code

[stated 2026/09/20] deploys on Fly.io (previously Heroku, moved in August)

[stated 2026/09/20] prefers pnpm over npm

The description line matters more than anything else in the file. It's what the model reads when deciding whether to open the file at all. Write it as "what's in here, and when to read it."

The [stated date] tag records that the user said this directly, and when. Every line in the system carries one. That tag is how consolidation knows which fact is newer, and it's how you keep the model from saving its own guesses as facts.

Here's the store. It handles listing, reading and writing, and it uses a version token on every write so two processes can't silently overwrite each other:

import hashlib

import pathlib

def version(text):

return hashlib.sha1(text.encode()).hexdigest()[:12]

class MemoryStore:

def __init__(self, root="memory", max_chars=12000):

self.root = pathlib.Path(root)

self.root.mkdir(exist_ok=True)

self.max_chars = max_chars

def _path(self, name):

p = (self.root / name).resolve()

if self.root.resolve() not in p.parents:

raise ValueError("path escapes the memory folder")

return p

def list(self):

rows = []

for f in sorted(self.root.rglob("*.md")):

text = f.read_text()

desc = next((l[12:].strip() for l in text.splitlines() if l.startswith("description:")), "")

rows.append({"path": str(f.relative_to(self.root)), "description": desc, "chars": len(text)})

return rows

def read(self, name):

p = self._path(name)

if not p.exists():

return {"path": name, "exists": False, "version": "new"}

text = p.read_text()

return {"path": name, "exists": True, "version": version(text), "content": text}

def write(self, name, content, if_version):

p = self._path(name)

current = p.read_text() if p.exists() else None

expected = "new" if current is None else version(current)

if if_version != expected:

return {"ok": False, "error": "version conflict, read the file again", "current": self.read(name)}

if len(content) > self.max_chars:

return {"ok": False, "error": "file too long, condense it first"}

p.parent.mkdir(parents=True, exist_ok=True)

p.write_text(content)

return {"ok": True, "version": version(content)}

def delete(self, name):

self._path(name).unlink(missing_ok=True)

Three safety details are built in. Paths can't escape the memory folder. Writes fail if the file changed since you read it, so nothing gets overwritten blind. And files have a size cap, which forces consolidation instead of endless growth.

Stage 1: Write

Writing is where most memory systems go wrong. They save too much.

The instinct is to save everything the model learns, just in case. Do that and within a month your memory is full of one off details, half true guesses and things that stopped being true weeks ago. Recall gets noisy, the model starts applying irrelevant facts, and the whole system feels creepy instead of helpful.

So the write stage has one job: find the few durable facts in a session, and ignore everything else.

When to write: after the session ends, in a separate background pass. Not during the conversation. Deciding what's worth remembering mid task distracts the model from the task, and it can't see the whole conversation yet. After the session, with the full transcript in view, it can tell a passing remark from a real fact.

What to keep: a fact passes only if all three are true. The user said it directly. It will still be true in a month. And it would change how the model answers some future request.

Here's the extraction prompt:

You maintain long term memory for one user. Read the session

transcript and propose facts worth remembering in future sessions.

Keep a fact only if ALL of these are true:

  1. The user said it directly. Not your inference, not tool output, not something you suggested unless the user clearly confirmed it.
  2. It will still be true in a month.
  3. It would change how you answer some future request.

Skip: one off details (a branch name, today's bug, a temporary port),

your own conclusions, compliments, small talk.

Never store: passwords, API keys, card or account numbers, health

details, or anything the user asked you to forget.

Pick the file by subject:

profile.md for who they are

preferences.md for how they want you to work

topics/<subject>.md for facts by domain

projects/<name>.md for ongoing work

people/<name>.md for people in their life or work

Quote the user's exact words as evidence.

If a new fact contradicts an existing line, put that line in "replaces".

Most sessions produce zero to three memories. An empty list is normal.

That last line matters. Without it, the model feels obliged to find something every time, and it will.

Now the code. I use tool calling with a strict schema, so the output is always valid JSON I can act on:

import os

import datetime

import anthropic

client = anthropic.Anthropic()

OPUS = os.environ["OPUS_MODEL"] # your Opus 5.5 model id

PROPOSE = {

"name": "propose_memories",

"description": "Propose durable facts to save to long term memory.",

"input_schema": {

"type": "object",

"properties": {

"memories": {

"type": "array",

"items": {

"type": "object",

"properties": {

"file": {"type": "string"},

"fact": {"type": "string"},

"evidence": {"type": "string"},

"replaces": {"type": "string"},

},

"required": ["file", "fact", "evidence"],

},

}

},

"required": ["memories"],

},

}

def write_pass(store, transcript, extraction_prompt):

index = "
".join(f"{r['path']}: {r['description']}" for r in store.list())

resp = client.messages.create(

model=OPUS,

max_tokens=4000,

system=extraction_prompt,

tools=[PROPOSE],

tool_choice={"type": "tool", "name": "propose_memories"},

messages=[{"role": "user", "content": f"EXISTING FILES:
{index}

TRANSCRIPT:
{transcript}"}],

)

proposals = next(b.input for b in resp.content if b.type == "tool_use")["memories"]

today = datetime.date.today().strftime("%Y/%m/%d")

for m in proposals:

cur = store.read(m["file"])

text = cur.get("content") or f"name: {m['file'][:m['file'].rfind('.')]}
description:
updated: {today}
"

if m.get("replaces") and m["replaces"] in text:

text = text.replace(m["replaces"] + "
", "")

text = text.rstrip("
") + f"
[stated {today}] {m['fact']}
"

store.write(m["file"], text, cur["version"])

Two things to notice.

The model sees the list of existing files before it proposes anything. That's how it knows topics/tools.md already exists and puts the Neovim fact there, instead of inventing topics/editor.md.

And the code, not the model, does the actual file edit. The model proposes. Your code checks the version, applies the change, and enforces the size cap. That keeps a bad proposal from ever corrupting a file.

One more rule: when the user explicitly says "remember this," save it right away, during the session, without waiting for the background pass. An explicit request is the one case where the user has already decided it's worth keeping. The same goes for "forget that": delete the line immediately and confirm it's gone.

Stage 2: Consolidate

Even with a strict write stage, files drift. Two lines say almost the same thing. A fact from March contradicts one from September. A project finished and its status lines are dead weight. Three separate lines about favorite tools should be one.

Consolidation is the cleanup pass. Run it when a file crosses a size threshold, and on a schedule (weekly works well for most setups).

Here is one memory file. Rewrite it so that:

  1. Duplicate facts are merged into one line.
  2. Contradictions resolve to the newest dated line. Keep the old value as "(previously X)" only when the change itself is useful to know.
  3. Related lines are combined: "uses A, B and C" beats three lines.
  4. Anything that has clearly expired is removed: passed deadlines, finished projects, temporary situations.
  5. Every remaining line keeps its [stated date] tag. When lines are merged, keep the newest date.
  6. The description line still matches what the file now covers.

Do not add any fact that is not already in the file.

Return the complete file and nothing else.

And the code, with one guard I consider essential: consolidation must never grow the file or add facts. If the output is longer than the input, reject it.

def consolidate(store, name, consolidation_prompt):

cur = store.read(name)

if not cur["exists"]:

return

resp = client.messages.create(

model=OPUS,

max_tokens=8000,

system=consolidation_prompt,

messages=[{"role": "user", "content": cur["content"]}],

)

new_text = resp.content[0].text.strip() + "
"

if len(new_text) > len(cur["content"]):

return # consolidation should only ever shrink a file

store.write(name, new_text, cur["version"])

def consolidate_all(store, consolidation_prompt, threshold=6000):

for row in store.list():

if row["chars"] > threshold:

consolidate(store, row["path"], consolidation_prompt)

If you want an extra safety net, keep the memory folder in git and commit after every consolidation. Then every change is a diff you can review and roll back.

Consolidation is also where contradictions get settled properly. The write stage might add "[stated 2026/09/20] deploys on Fly.io" without catching the old Heroku line. Consolidation sees both, keeps the newer one, and folds in "(previously Heroku)" if that history is useful.

Stage 3: Recall

Recall is where cost and quality are decided. Load too little and the model forgets things it knows. Load too much and you pay for context that makes answers worse.

The design I use has two layers.

📷

Layer one is always loaded: the index of every memory file (path and description), plus the full contents of profile.md and preferences.md. These are small, they're relevant to almost every request, and they go at the very top of the system prompt, so they're cached across calls.

Layer two is loaded on demand: everything else. The model sees the index, decides which files matter for this request, and reads them with a tool.

TOOLS = [

{

"name": "memory_read",

"description": "Read one memory file. Check the index first and read any file whose description suggests it holds something this reply needs.",

"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]},

},

{

"name": "memory_write",

"description": "Only when the user explicitly asks you to remember, update or forget something. Pass the full new file content and the version from your last read.",

"input_schema": {

"type": "object",

"properties": {"path": {"type": "string"}, "content": {"type": "string"}, "if_version": {"type": "string"}},

"required": ["path", "content", "if_version"],

},

},

]

def build_system(store, base_prompt, memory_rules):

index = "
".join(f"{r['path']}: {r['description']}" for r in store.list())

profile = store.read("profile.md").get("content", "")

prefs = store.read("preferences.md").get("content", "")

memory_block = f"MEMORY INDEX
{index}

PROFILE
{profile}

PREFERENCES
{prefs}

{memory_rules}"

return [

{"type": "text", "text": memory_block, "cache_control": {"type": "ephemeral"}},

{"type": "text", "text": base_prompt},

]

Then a standard tool loop:

import json

def run_tool(store, name, args):

if name == "memory_read":

return store.read(args["path"])

if name == "memory_write":

return store.write(args["path"], args["content"], args["if_version"])

return {"error": "unknown tool"}

def chat(store, messages, base_prompt, memory_rules):

system = build_system(store, base_prompt, memory_rules)

while True:

resp = client.messages.create(model=OPUS, max_tokens=4000, system=system, tools=TOOLS, messages=messages)

messages.append({"role": "assistant", "content": resp.content})

if resp.stop_reason != "tool_use":

return resp

results = []

for block in resp.content:

if block.type == "tool_use":

out = run_tool(store, block.name, block.input)

results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(out)})

messages.append({"role": "user", "content": results})

Why let the model pick instead of doing automatic similarity search? Because the question "does this request need what's in projects/checkout.md?" is a judgment, and Opus 5.5 makes it well when the descriptions are good. A user asking "should I use Stripe or Paddle?" doesn't mention the checkout project by name, but a model reading the index sees "checkout: payment flow rebuild, decisions and constraints" and opens it. Keyword or vector matching often misses that.

If your index grows past a few hundred files, add a cheap pre filter: score each description against the request with a small model, and only show the top 30 in the index. Until then, the full index is cheap enough to show every time.

The single most important recall rule: never say "I don't know that about you" without checking. If the index shows people/maya.md and the user asks about Maya, the model reads the file before answering. A confident "I don't have that information" while the file sits unread is the most frustrating failure a memory system can have.

Stage 4: Apply

Loading a memory and using it are two different decisions. This is the stage most memory systems skip, and it's the one users actually feel.

Bad application looks like this: you ask for a pasta recipe, and the model opens with "Since you're a backend engineer at a fintech startup…" It remembered. It didn't need to say so. That reads as surveillance, not helpfulness.

Good application is invisible. The model knows you're vegetarian, so the recipe has no meat. It knows you hate long intros, so the answer starts with the answer. It never mentions how it knows.

These are the rules I put in the system prompt, right after the memory block:

MEMORY RULES

  1. The index above lists everything you remember about this user. Before saying you don't know something about them, check the index and read the file that would hold it.
  2. Use a memory only when it changes your answer: what you recommend, conclude or ask. A personal detail that changes nothing reads as surveillance. Leave it out.
  3. Never narrate memory. No "based on your memories", no "I remember that you". Just use what you know.
  4. The current request beats any stored preference. If they ask for bullet points today, give bullet points, even if preferences say prose.
  5. Open items in memory are context, not an agenda. Don't bring up an old unfinished task unless the user raises that subject.
  6. Facts about other people only come up when the user brings that person into the conversation.
  7. If the user asks you to remember something, save it now with memory_write. If they ask you to forget something, delete that line now and confirm it's gone.

Rule 2 is the heart of it. Every memory has to pass a simple test before it shows up in an answer: does it change what I'm saying? If not, it stays silent.

Rule 4 prevents a subtle failure: memory hardening into rules the user can't escape. Preferences are defaults, not laws.

Rule 5 prevents the other common failure: the model acting like a project manager, asking about a task you finished three weeks ago.

Forgetting, Privacy and Trust

A memory system people trust needs a delete button that actually works.

When the user says "forget that I told you about the lawsuit," three things happen. The line is removed from its file, immediately, in the same turn. Anything derived solely from it is removed too. And the write stage's prompt already says never to store anything the user asked to forget, so the background pass won't quietly save it again next session.

Beyond explicit requests, some things should never be stored at all, even if the user mentions them in passing: passwords and API keys, card and bank numbers, health details, and anything about other people that they'd be uncomfortable having written down. The extraction prompt blocks these. For anything that touches real user data in production, add a code level filter too: a regex pass for key and card number patterns before any write reaches disk.

The test I use is simple: would the user be uncomfortable seeing this line on a settings page? If yes, it doesn't get saved.

And give users a way to see their memory. Because it's plain files, this is trivial: a page that lists the files and lets them delete any line. Nothing builds trust faster than "here's everything I know about you, fix anything that's wrong."

What It Costs

Memory systems have a reputation for being expensive. This one isn't, and the reason is the always loaded layer sitting in the cache.

Here's the math at Opus 5.5 list prices ($4 per million input tokens, $20 per million output, $0.20 per million cached input).

Recall, every request. An index of 60 files at about 20 tokens each is 1,200 tokens. Profile and preferences add maybe 550. So roughly 1,750 tokens, read from cache after the first call:

1,750 tokens × $0.20 per million = $0.00035 per request

Reading one or two extra files on demand adds a few hundred fresh tokens, well under a cent.

Write, once per session. Running the extraction on a 20,000 token transcript:

input: 20,000 × $4.00 per million = $0.080

output: 500 × $20.00 per million = $0.010

total per session = $0.090

If that's too much at your volume, run the write pass on Haiku 4.5 instead ($1 input, $5 output per million) for about $0.02 per session. The extraction prompt is strict enough that a smaller model handles it well. Keep Opus for consolidation, where judgment about contradictions matters more.

Consolidation, weekly. A few files over the threshold, a few thousand tokens each. Cents per week.

For comparison: pasting a 20,000 token "context document" into every request costs $0.08 per request uncached, and it's still out of date.

How to Test It Before You Trust It

Run these five tests. Each one catches a different failure.

1. Remember. In session one, say "I deploy everything on Fly.io." End the session and run the write pass. In session two, ask "how should I deploy this new service?" The answer should assume Fly.io without asking, and without saying "you mentioned."

2. Update. Later, say "we moved everything to Railway." After the write pass and consolidation, the file should show Railway, with Heroku and Fly.io gone or marked as previous. Ask the deploy question again.

3. Ignore noise. Have a session full of debugging chatter: file paths, stack traces, a temporary port. The write pass should save nothing. If it saves the port, tighten the prompt.

4. Forget. Say "forget where I deploy." The line should be gone in the same turn. Run the next write pass on that transcript and confirm it doesn't come back.

5. Don't overapply. Store "user is vegetarian," then ask a question about Python packaging. The answer should contain no reference to food. Then ask for a dinner idea. It should be vegetarian, without announcing why.

If all five pass, your memory system is doing its job.

Mistakes to Avoid

Saving the model's conclusions as facts. "User seems stressed" is not a fact the user stated. Only save what they said.

Letting files grow forever. Without a size cap and consolidation, recall gets slower, noisier and more expensive every week.

Automatic similarity recall with bad descriptions. Whatever method picks the files, it's only as good as the description lines. Spend time on them.

Narrating memory. "As you mentioned before…" in every answer makes a helpful system feel like it's watching you.

No versioning on writes. Two sessions writing the same file at once will silently erase each other's changes. The version token prevents it.

No way for the user to see it. A memory the user can't read or edit is a memory they won't trust.

Build It This Week

  1. Create the memory/ folder with profile.md and preferences.md.
  2. Add the MemoryStore class and the two tools.
  3. Put the index, profile and preferences at the top of your system prompt with cache_control, then the memory rules.
  4. Run the write pass after every session.
  5. Run consolidation weekly, and whenever a file passes 6,000 characters.
  6. Run the five tests.
  7. Commit the memory folder to git so every change is reviewable.

The whole system is maybe 200 lines of code and three prompts. It turns Opus 5.5 from a brilliant stranger you brief every morning into something closer to a colleague who's been on the project for months.

Write carefully. Consolidate often. Recall on demand. Apply only when it matters.

If this helped, follow @cyrilXBT. I break down how to build real systems with the newest AI models every week.

Bookmark this for when you build yours.

Salvar com um clique

Faça leitura profunda de artigos virais com IA no YouMind

Salve a fonte, faça perguntas específicas, resuma o argumento e transforme um artigo viral em notas reutilizáveis em um único espaço de trabalho com IA.

Explorar o YouMind
Para criadores

Transforme seu Markdown em um artigo 𝕏 impecável

Quando você publica seus próprios textos longos, formatar imagens, tabelas e blocos de código para o 𝕏 é uma dor de cabeça. O YouMind transforma um rascunho completo em Markdown em um artigo 𝕏 impecável e pronto para publicar.

Experimente Markdown para 𝕏

Mais padrões para decifrar

Artigos virais recentes

Explorar mais artigos virais