One module knows the way in
Lessons 12 to 14 are Soul Vault's side. This one is cwkPippa's, and it holds to the same rule Dad set: one pointer, every app reads it, nothing hardcodes a vault path, and a problem fails with its real cause. Inside cwkPippa that rule lives in four places.
The first is one door. souls/vault.py is the only module that reaches Soul Vault, through the kit's client for whatever implementation the pointer selects on this Mac. Vaults are logical names (pippa, cwkpippa-shared, guest-souls/<slug>), and a soul's adapter now names its vault by that name instead of by a folder (soul track, lesson 1). A note is a soul:// id. Any failure is one exception, VaultUnavailable, carrying Soul Vault's code and cause, and its refusal is the same one line every surface shows.
The prompt is what Soul Vault served
The second is the engine. The ContextEngine loads the soul's identity bundle in the root's order and composes every section from the documents it was served: it opens no file. That closes a whole class of mistakes by construction: a replica behind the canonical tree, a second root someone pointed at, or an outside edit the store is holding can never reach a prompt, because the prompt has no way to read anything Soul Vault did not serve. A note the prompt does not carry (an at-hand shared note, an on-demand index) appears as a pointer naming its soul:// id. Every builder (the system prompt, the council panel, the guest prompt, the mini prompt, the Ollama-optimized one, the stats and the presets) starts from that identity load, so none of them can run without it. Before it shipped, every static soul's prompts were built from the live vault both ways and compared: they differed only in the wording of a pointer and the soul:// spelling of old paths.
The door every vessel carries
The third is a family tool. A turn that needs a note it does not carry reads it through soul_vault, one row in the family tool table (vessels track, lesson 11), so every vessel gets it the same way: read returns the text and its revision, list and search walk a vault, and write replaces a note by compare-and-swap on the revision the writer read. A revision_conflict means it changed since: read again and rebase. The door rides on the turn's own file tools, so the WebUI's per-brain toggles keep their meaning: Read admits its reads, and a write needs Write or Edit on that turn. Visibility is the old rule applied at every call: a soul never reaches a vault its super-soul status hides, a guest soul writes only its own vault, and a soul neither registry can resolve reaches nothing. Every write is attributed to the conversation's soul, so it lands in the store's history under that name (lesson 13).
The fences
The fourth is the reason the door is the only way in. A file tool that opened the vault's bytes would read whatever is on that disk and write past compare-and-swap, so Dad's rare hand edit could be overwritten, and the store would hold the write as an outside change. So services/vault_fence.py refuses any Read, Write, Edit, MultiEdit or NotebookEdit that touches soul-vault bytes, and any Glob or Grep rooted at or above a vault, and names the door instead. It never knows a vault path: it asks Soul Vault where its bytes are (contract v3, local_footprint), and a fence that cannot ask is closed, never open. The Claude adapter carries it as a pre-tool hook and no longer adds the vault as a working directory at all.
A turn's shell is a file tool too, and that was the hole the second fence closes. Soul Vault's own command-line client carries no soul, so from a turn's Bash, a read of another soul's vault would have passed the isolation the door enforces. The shell fence reads each command line the way a shell would run it (nested shells, eval, substitutions, redirects, every folder a cd could leave it in) and refuses a path inside a vault, a program that could walk the folder holding the vaults, a glob that could match into one, the client's note verbs however they are wrapped, and a soul:// id handed to any other program. A command it cannot read is refused with the reason. What it cannot see is a path a program builds for itself at run time. A write through such a path still meets Soul Vault's own guard, which holds any change no write made; a read through one is the fence's known limit, not something the guard covers.
Dad's own folders beside the old vault (YouTube, market, daily and the rest) are not soul vaults. They stay plain files that a turn reads with Read, and their location is one more registry role resolved through the kit, so a missing role fails by name instead of guessing.
Failing loud, everywhere downstream
Two independent reviews read this work, and most of their findings had one shape: something downstream had quietly turned "the vault could not answer" into an ordinary value.
- The dispatch pools. When Soul Vault could not list the souls, the list of eligible souls came back empty. Every dispatch pool was normalized against that list, so saving any unrelated setting during an outage stored every pool as empty, and after recovery a narrowed pool stayed empty. Now the listing failure reaches every caller by name, the save stops before it writes, and the admin route answers with the failure instead of an empty pool.
- The RAG index. The indexer decided a note was current when its local modification time still matched. On a Mac whose copy lagged the canonical store, that meant a note indexed at revision A, now B on the canonical store, with an unchanged local mtime, was recorded as indexed at B. Now the index records the revision the read actually returned; a read that was not validated is recorded with its label so it stays due; and every older entry that only held a timestamp was read and embedded once (369 of them) and tracked by revision from then on.
soul:// id. If I try to open the folder anyway, the fence says no and tells me which door to use. My own memory has the same manners the house asks of everyone else.