Every serious AI tool I use has memory now. Cursor has rules files. Claude Code has a memory file. The browser chats have their own stores. None of them can read each other, so an architecture decision I explained in one tool is a stranger to the next one I open. Memorix, another MCP memory project, states the problem in its README without hedging: "Your architecture decisions in Cursor are invisible to Claude Code. Bug fix knowledge in Windsurf doesn't transfer to Codex. No one does cross-agent memory."
I build Vodou, a local-first AI system with a memory store, retrieval, MCP orchestration and autonomous agents, and I hit this from the operator's side. One vault on disk. Six clients, each with a different idea of where memory comes from. This is what it took to make them read the same thing, including the day and a half I spent building the wrong integration and the grader that lied to me for 709 messages.
One vault on disk, and three different doors into it
What a person gets is unremarkable to describe and that is the point. You install a host, and your memory is in the prompt. Hooks for Claude Code, Cursor, Codex, Gemini CLI and Aider inject the vault digest before the model sees your message. For anything that speaks MCP, MCP-servers/vodou-memory is a stdio server exposing three tools: memory_search, memory_context and remember. Your editor and your browser chats read one store, and a correction you make in one is true in the other on the next turn.
The server has no dependencies. It is hand-rolled newline-delimited JSON-RPC over stdio: initialize, tools/list, tools/call, and nothing else. An IDE spawns it as a child process, so every dependency is a thing that can break in someone else's editor at startup, and a node_modules tree is a thing I would have to vendor into an open-source repo forever. The two read tools shell out to the same CLI the hooks use, which means the formatting and the scoping cannot drift between the MCP path and the hook path. remember posts to the gateway's POST /api/capture/remember, which lands in the same manual-capture lane as the right-click save in the browser extension: same trust tier, same extractor, same loop-guard strip. One write path, three front doors.
The third door is the rules file. mem rules renders the vault digest, grouped by tag, into a marker-fenced block in .cursor/rules or the Copilot or Windsurf equivalent. Regeneration replaces only what is inside the fence. Everything you wrote around it survives, which I verified as an idempotency test because the first thing a generator like this does in the wild is eat somebody's hand-written instructions.
Three doors, one store
The DeepSeek Harness plugin I designed for a day and a half did not need to exist
The plan for attaching the DeepSeek Harness was an npm package called @vodou/dsh-plugin-memory, a GitHub repo carrying the dsh-plugin discovery tag, and plugin code hooking the harness's agent/pre-step event. That was scoped, estimated and about to be built.
The rule that saved it is boring: re-reading the target repo is a blocking gate before any integration work starts. The harness does not want a plugin. It attaches an MCP server as ordinary configuration through its own MCP client, and it already documented three memory systems that way, with the slot sitting empty. So mcp install dsh writes a patch block into a config file in the harness's home, and that is the entire integration. The package, the repo and the discovery tag all disappeared, and with them the exact failure the gate was written to prevent: publishing under a tag nobody reads. A day and a half became half a day. The result is also better than the plugin would have been, because the harness's MCP client registers the server's tools into its own registry, so they reach anything built on the framework rather than just the CLI.
10 board tasks, 4,507 graph runs, 0 turn records
Everything I had built for memory covered turns a human had a conversation for. Then I counted what the system does on its own.
Ten completed board tasks with seventy events between them produced zero turn records. Four thousand five hundred and seven graph runs produced zero. The autonomous work, the part you are least able to watch, was the part with no record at all. A board worker calls a model with a bootstrap, tools and memory, and none of it was written down.
The fix was to stop inventing a second identity. The chat path already carries a turn id to a CLI child through an environment variable that the child's hook reads, so the board spawner sets the same variable with a synthetic board:<task_id> value. The child's hook picks it up, the daemon adopts it, and the run lands as a partial turn: what the system contributed, which is the honest claim when the rest of the request is assembled inside somebody else's CLI child. The other backend was worse than silent. It passed a turn id, but a fresh random one per run, so it logged a turn that was connected to nothing.
The same counting exercise produced the number that still bothers me most: of 41,428 stored turns, 67 carry a memory trace. That is not 67 turns that used memory. Memory injection happens at prompt time, before the turn record exists, so the trace is only written when the path happens to run late enough to attach it. The measurement I want is "did this host read the vault on this turn," and for most of history I cannot answer it from rows.
Turns that can prove they read memory
chatgpt-web read unsupported for 709 live messages
The host grader is the thing that is supposed to tell me which of these integrations actually works. It graded five hosts unsupported, and it graded them in a way no evidence could contradict: for those hosts it forced "surface seen" to false and every verb to not-applicable before looking at anything. The reason printed in the stanza was "no pre-LLM hook," which was true when it was written and had been false since the browser extension shipped.
So the grader was reporting a stanza, not a measurement. chatgpt-web read unsupported across 709 live messages sitting right there in the rows. The placeholder surface name in hosts.toml was web, which substring-matched half the log, which is why nobody trusted the number enough to look at it.
The repair was to make the lookup explicit instead of clever. Hosts declare evidence ids and are matched prefix-anchored, never with a leading wildcard. Hosts with no ids get a measured zero rather than a log fallback, because a wildcard that finds something is worse than a zero that finds nothing. Unsupported stopped being a short circuit: rows behind a host marked unsupported now render as a contradiction of the stanza. The registry went from schema 1 to schema 2, additively, so an old file still loads.
The doc I wrote about the memory system leaked the memory system
On the day the release gate ran against fresh archives, it failed them on real findings. The worst was mine. docs/memory-follows-you.md used family names as illustrative examples, because the fastest way to write an example of a personal memory query is to look at your own vault and copy one out. The same pattern had reached a tool description in another server, where the example correction was a real correction about a real person.
That is the tax on building a memory product with your own memory in it, and it is a general one: anything you can read in your vault, you will eventually paste into a file that ships. Two other things in that same gate run are worth stealing. Live database sidecars (*.db-shm, *.db-wal, corrupt-FTS backups) were being staged into the release tarball, because the strip pass only knew .bak shapes. And the pattern scan false-positived on tokenizer vocabulary blobs, which contain every common English name in the language, so the model cache had to be excluded from the text scan while staying in the binary one.
Scope belongs to whoever starts the server, never to whoever calls the tool
Here is the property, and it is either true or false of your codebase.
A memory tool exposed over MCP must take its scope from the process launch, not from its arguments. In vodou-memory the vault is fixed by a launch flag or an environment setting at spawn time and is not a field in any tool's input schema. A model can ask for anything it likes; it cannot ask for a different vault, because there is no parameter to put that in.
This matters because of who is talking. The text arriving at your MCP server passed through a model that read a web page, a repo issue, a PDF or a teammate's commit message five minutes ago. If namespace, vault, collection, user_id or project is a tool argument, then the widening of scope is one convincing sentence away and it will look exactly like a normal tool call in your logs. The disclosure boundary has to live somewhere the model cannot reach, and the only such place is the process the operator started.
Dump your own tools/list and grep the parameter names
You can check this against your own MCP servers right now, with nothing of mine. Any stdio MCP server will answer a handshake on a pipe:
{
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"audit","version":"0"}}}'
printf '%s\n' '{"jsonrpc":"2.0","method":"notifications/initialized"}'
printf '%s\n' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
sleep 1
} | your-server-command 2>/dev/null \
| jq -r 'select(.id==2) | .result.tools[] | .name as $t | (.inputSchema.properties // {}) | keys[] | "\($t)\t\(.)"' \
| grep -Ei 'scope|vault|namespace|collection|tenant|project|user|index|path|db|table'
Passing output is nothing. Your read tools take a query and maybe a limit, and every boundary word comes back empty. Failing output looks like memory_search\tnamespace or recall\tuser_id: one line per way a model can point your retrieval somewhere it was not meant to go. A hit is not automatically a bug, but each one now owes you an answer to "what stops injected text from setting this," and "the model is usually sensible" is not that answer.
The second half takes about as long and is the 67-of-41,428 check on your own stack. If you log turns and log retrievals, ask how many turns can prove they read memory:
SELECT
COUNT(*) AS turns,
SUM(CASE WHEN r.turn_id IS NULL THEN 0 ELSE 1 END) AS with_memory_trace
FROM turns t
LEFT JOIN memory_reads r ON r.turn_id = t.id
WHERE t.created_at > datetime('now', '-30 days');
Then run it again restricted to work your system started by itself: scheduled jobs, background agents, graph or workflow runs. The failing shape is not a low ratio. It is a healthy ratio for human-started turns and an exact zero for agent-started ones, which is what I found, and it means your autonomous lane is writing its records under a different identity or under none. If the join returns zero rows because you have no memory_reads equivalent at all, that is the same finding arriving faster.
Four memory taxonomies, and none of them count reads per host
The published advice on agent memory is good on structure and silent on distribution. Hidekazu Konishi's memory design guide is the best thing I have read on write paths, and its central observation (that working, episodic, semantic and procedural memory are each written by a different mechanism, and that only the procedural one should never be fully automatic) shaped how I think about the extractor. The Agentic engineering playbook maps the same three long-term flavours onto storage choices cleanly. Pockit's production guide compares the frameworks and is honest about where each pattern breaks.
All three assume one agent reading its own store. The moment the same vault is read by an editor hook, an MCP client and a browser extension, the interesting question stops being "what do I store" and becomes "which host actually read it on which turn," and none of them have a shape for that. The projects that do take the multi-client framing are building it as infrastructure: chalie-ai/ide-memory runs Postgres with pgvector, an MCP server and a web UI in a Docker container and has an agent-readable setup doc that detects your IDE and installs the plugin for you. That is a reasonable answer for a team. It is the wrong shape for a vault that is supposed to be a file on your own disk that no container has to be running for you to own. The Dark Memory RFC argues the opposite of my minimal surface, that every MCP tool should answer an intent rather than a database operation, and it is worth reading precisely because it disagrees. My three tools are closer to CRUD than its design allows; the reason is that the scope, not the verb, is what I refused to give the model.
Zed and JetBrains have a preset card and no hook behind them yet
Two things are still open. The preset covers Zed and JetBrains through the MCP server, but the prompt hooks for them are planned and not written, so in those editors memory arrives when a tool is called and not before the model reads your message. And there is no turn-level dedupe on any write path in the engine, so a turn that arrives both from live capture and from a host's own state file is stored twice. It is a dedupe guard keyed on provenance that I have not built, and until I do, counts in the memory store run slightly high.
If your editor and your browser chats keep separate memories of the same work, Vodou keeps one vault on your own machine and gives every MCP client and every hooked CLI the same read of it. You can start at vodou.ai.
Source: One vault, every MCP client, and a scope the model cannot widen by Chad Priest, from Building Vodou in Public.




Top comments (0)