Seven Tables, Each With One Job
The bootstrap started with four tables. Within three months the ledger had settled into seven, and the engine that owns the store today still names those seven as its legacy contract — the shape every older reader (Almanac, Vestige, the command-line tools) depends on. Each one exists because a specific kind of record kept turning up and did not fit anywhere else.
| Table | What a row is | The detail that matters |
|---|---|---|
todos | A unit of work in a scope | creator, assignee, status, plus result_summary and commit_sha written when it ends |
requests | A contract from one brain to another | sender and receiver; optional linked_todo_id |
letters | A note from a session to the next session of the same brain | Its own six-state machine (Lesson 4) |
messages | One reply in a thread | A CHECK forces exactly one of request_id or todo_id |
coop_events | One state change on a todo, request or letter | Append-only; never deleted |
history_entries | A work record: what was done, by whom, with which commit | Unique on (scope, slug) |
learning_entries | A gotcha a framework taught someone | problem, hypothesis, resolution, source, and superseded_by |
The order they arrived in tells you what the early system was missing. Letters got their own table the day after the bootstrap, 2026-05-13. Learning entries came on 2026-05-24, decided in a family council round for a reason that should sound familiar from Lesson 1: several sisters writing concurrently across the fleet would race on a markdown vault, and the ledger's SQLite with WAL already solved that. coop_events arrived on 2026-08-10 with the taken state, so a second session could see a claim instead of guessing at one.
History Is What Happened; Learning Is What It Taught
The two record tables look alike and are not. A history entry is per event: what I did this session, which commit, which todos it closed. It is the replacement for the history/*.md folder. Its scope is the repository the work happened in.
A learning entry is per trouble: what the framework taught me when I was stuck. It is row-shaped on purpose, because troubleshooting really is symptom, what I thought, what actually fixed it, where I found it — not an essay. Its scope is the framework or language (sqlite, launchd, react-19), not the repository, because the next sister to hit the same symptom will search by the tool, not by the project. And when a belief turns out wrong, the old row is not overwritten: superseded_by points at the corrected one, so "what we used to think" stays walkable.
The Eighth Table
Count the tables in the schema file today and you get eight. todo_reply_reviews arrived on 2026-10-05: one row per worker per reply it has actually acknowledged, so that a worker cannot take or finish a todo while a reply it never read sits underneath it. The engine tracks it with everything else, but it is not part of the legacy seven, which is why the precise sentence is seven legacy tables. Track 5 is where that table earns its place.
A Column That Meant Nothing
Requests once had a kind column: help or delegation. It was dropped on 2026-05-23. Nothing processed the two kinds differently — same inbox, same reply, same close — and 17 rows of real usage showed senders picking the label by feel rather than by meaning. A field without a handler is not neutral. It invites every writer to guess, and the guesses look like data.
Who Wrote This Row?
Every identity column in the ledger — creator, assignee, sender, receiver, a history entry's brain, an event's actor — is plain TEXT. The schema cannot catch a misspelling. For the ledger's first three months, who-wrote-this held by convention alone, and a census on 2026-08-10 measured what convention was worth: 26 distinct identity strings, and 527 rows written as unknown, the silent default for a session that never set its identity.
The first fix refused unknown at every write that records an actor. It closed that fountain and nothing else. A later census of rows written after 2026-08-20 still found app names (firelink, 43 rows of release records; coop-chief), bare model-registry ids (claude, codex, kimi, grok), a misspelling (grok-code-pippa) and the retired wildcard any.
So on 2026-09-30, with Dad's approval, commit 673ab6b turned the roster into a write-door guard. CANONICAL_BRAIN_IDS lists seventeen identities: dad, seven WebUI vessels (one per brain line), and nine coding surfaces. require_canonical_identity is the check, and the commit promised it for every identity a write stores: its actor, and any assignee, receiver or corrected brain it names, with a refusal that writes nothing at all. The code keeps that promise for every write that creates a record or hands one off: todos, requests, replies, history, learning, and a letter's first write. It does not yet keep it everywhere. A letter's later steps (read, start, ack, abandon) still resolve the caller through the lenient get_identity, so a session that acks a letter under a name off the roster gets its event recorded with that name as the actor. Read a commit message as the intent and the call sites as the fact. The derivation rule is what keeps the list from growing ad hoc: one ID per surface, named after that surface's own product, suffixed -pippa. A new surface joins by being added to the roster first.
Two details make the guard livable. Reads stay lenient, so the historical spellings still list and filter; only new writes are held to the roster. And before the guard went live, the live writers that were sending wrong names — Firelink and a shared kit — were fixed first, so that turning it on refused nothing that was still in use.