What My Obsidian Dashboards Grew Into
One Node process, five vaults, a local vector index, a persona agent with a write protocol, and an issue tracker that AI agents share. Seven diagrams of a Personal OS, and what keeping it local actually costs.
The boring (or the useful) companion to Accept All, Then Buy a Beehive. That one has opinions. This one has diagrams.
In April this was a dashboard over an Obsidian vault. A few charts, a daily note, a weekly review. Today it is one Node process, five vaults, a local vector index, a persona agent with a write protocol, and an issue tracker that several AI agents share. I did not plan most of it. It grew the way gardens and technical debt grow.
Every number below was read from the code and the live index, not from older docs. The older docs were already wrong, which is its own finding.
One machine, two doors
Everything runs on one Mac. There are two ways in: the browser talks to an HTTP API, and an AI agent talks to an MCP server over stdio. Both call the same library functions, so they cannot drift apart in logic. They can still differ in what they expose: the MCP side ships with mutations switched off. Adding a capability means writing one function and exposing it twice.

Five vaults sit behind it. Two are writable: the operational vault that runs my weeks, and the persona vault. Three are read-only inputs for retrieval: a typed self-model kept in two languages, each copy a vault of its own, and a pointer vault for twenty-plus years of handwritten notes. Retrieval inputs never receive writes. That single rule prevents most of the ways a system like this poisons itself.
The parser never rebuilds your markdown
This is the decision I would defend hardest. Reads return items tagged with their source line number. A mutation flips exactly that line in the original file and writes the whole file back through a temp file and a rename. If the client is stale and the line no longer matches, it gets a 409 instead of a checkbox flipped in the wrong place.

A reader sees either the old file or the complete new one. Never half. When your notes are your memory, “never half” is the whole job.
Retrieval stays home
All five vaults are chunked and embedded on-device, including 1,190 OCR’d handwritten pages. Everything is embedded with a multilingual model, because I write in two languages and refuse to pick. Notes are cut into pieces of about 128 tokens, because that is as much as this embedding model actually reads at once. The index is one SQLite file with a vector table and a full-text table side by side.

A query can run vector search and BM25 together, fuse them with reciprocal rank fusion, filter by vault and author, and rerank for diversity. The persona and the consultant use the full pipeline. Plain vault search and history notes search still run on vectors alone. No remote call is made for retrieval. Not as an optimisation. As the requirement.
One turn of the persona
The persona agent gets a prompt assembled in a fixed order: a cached identity block, a temporal block with today and both of my week-numbering systems (yes, two: ISO weeks for everything, plus one personal project that counts weeks from its own day zero, and a model that mixes them up plans your Tuesday into the wrong month), retrieved chunks with their sources, behaviour rules, then the conversation.

Writing back has two trust levels. The agent’s own observations land in its own vault with no review. Anything that touches my operational notes is proposed, shown to me as a diff, and applied only after I confirm. Its identity files are read-only. Three memory stores, three trust levels. An agent that can quietly edit your life notes is not an assistant, it is a roommate with your diary.
How a fact becomes searchable
Nothing is pushed into the index directly. Integrations write plain markdown into the vault. A watcher notices the change, asks the server to reindex, and the server runs the indexer on changed files only.

It is slower than a direct insert and I like it that way. Everything the system knows exists as a file I can open, read and delete.
Agents need a standup too
Several AI agent sessions work on the repo in parallel, each in its own git worktree. An embedded issue tracker is the source of truth for who is doing what, and its memory table carries context between sessions. It is local-only by design: no remote, no push.

Yes, I built a project management layer for software that did not exist two years ago. No, I do not want to talk about it.
The boundary
One caveat first, because it is the honest one: a local MCP client can still forward whatever it reads to its own cloud model. That boundary belongs to the client, not to my server. Everything below is about the part I control.
After a hardening pass, everything that reads notes or generates text stays on the machine. The HTTP listeners bind to loopback. The MCP server runs over stdio, so it has no network surface at all, and it ships with mutations off and fails closed on unknown tools. Generation goes to a local model: T-lite-it-2.1, 8B, 4-bit, through Ollama. The cloud provider is a per-consumer opt-in, and it is switched off. A key alone does not enable it.

What still crosses the boundary: file sync, my calendar, a wearable import that only flows inward, and a couple of public lookups.
And the price. A free-form turn with 23K tokens of context takes about two minutes cold and about thirty seconds with the prefix cached. With the current week files injected, the answer is grounded. Without them, the model confidently invents numbers.
The real ceiling is not speed. An 8B model in 4 bits takes about 5 GB and fits comfortably. What 18 GB of memory does not fit is the next class up, 27B to 32B, and 8B is simply too weak for my use cases. A new laptop would not make this setup faster. It would allow a different model. That is what privacy costs right now, and it is why the essay ends with me shopping for a laptop.
The numbers
Read from the code and the live index, not from memory.
| What | Value |
|---|---|
| Indexed files | 4,813 |
| Chunks | 30,388, about 128 tokens each |
| Index size | 139 MB, one SQLite file |
| Embedding model | paraphrase-multilingual-mpnet-base-v2, 768 dimensions, runs on-device |
| Retrieval latency | 0.04 to 0.09 s per query once the embedding model is warm, 0.8 s for the first query after a restart |
| Full rebuild | about 7 minutes |
| Surface | 224 HTTP routes, 60 MCP tools, 44 library modules |
Index counts and latency were measured on the live system on 2026-09-20, the code counts on 2026-09-06.
What the design depends on
- One library function, exposed twice. HTTP and MCP cannot disagree.
- Line-based parsing. Never reconstruct a file you did not write.
- Atomic writes only.
- Everything a tab saves becomes a markdown file.
- Retrieval inputs are read-only.
- Reads degrade to empty, they never throw. A file evicted by cloud sync renders as no data, not as a crash.
- Agent writes are proposed, previewed, applied.