memorypalace
An append-only log you can ask questions of.
./brain log "the opener matters more than the model"
./brain import history
./brain build
./brain search anneal
One rule
log/ is the brain. Everything else is derived and can be deleted.
log/YYYY-MM.jsonl append-only text. tracked in git.
brain.db sqlite + fts5. gitignored. ./brain build
site/ html. gitignored. ./brain render
The log is text and not the .db because two machines that both append to a
sqlite file produce two binaries that can never be merged. Two machines that
both append to a jsonl file produce a conflict whose resolution is keep both
lines, sort by ts.
Sharded by month so an append never rewrites an old file: git stores a small
delta per day instead of a growing blob, and a conflict can only ever land in
the current month.
Two tables
entry is dumb and never edited. mark is the judgment, kept apart from the
thing judged, and append-only as well — two marks on one entry is not a
conflict, it is a record of changing your mind.
That separation is what makes harvesting a query instead of a feature:
-- SFT data: only replies that were approved
SELECT e.body FROM entry e JOIN mark m ON m.target = e.uid
WHERE e.kind = 'chat' AND m.verdict = 'gold';
-- what happened in September
SELECT ts, body FROM entry WHERE ts LIKE '2026-09%' ORDER BY ts;
No dependencies, on purpose
stdlib only. sqlite3 and fts5 ship inside the interpreter; ./brain is a
two-line shell script. There is nothing to install and no venv.
Three earlier attempts at this died of the same cause — writing one note
required something else to be alive:
| attempt | needed |
|---|---|
ocean-chat-simple/conversation_memory.py | AWS credentials + a DynamoDB table |
melissakate.com/bookmarks/api.php | the PHP endpoint reachable |
| this repo, 2023 | a particular Linux machine |
None of them died of boredom. So: never put the network between you and your
own notes. Capture is a local file append, offline, always.
Hosted apps are the exception, because they are already online — those write
server-side and get pulled down.
Sources
| source | rows | kind | status |
|---|---|---|---|
~/.claude/history.jsonl | 1,749 | prompt | done |
~/.claude/projects/**/*.jsonl | 3,626 | chat | done |
~/.claude/projects/**/memory/*.md | 46 | note | done |
~/Code/**/*.md, git-tracked | 58 | note | done |
melissakate.com/bookmarks.json | 1,060 | link | |
prose/mkm_train/chat_logs/*.jsonl | 543 | chat | carries ckpt/temp/top_k |
Notes are chosen by git ls-files, not by a path rule. There are 4,710
markdown files under ~/Code and 4,651 of them are the Victorian corpus — one
file per novel, two copies, up to 1.9 MB each. No path pattern separatesprose/notes/corpus-design.md fromprose/data/conversations_v2/dickens/our_mutual_friend.md without a list of
exceptions that goes stale. git separates them exactly, and not by coincidence:
the corpus is generated and ignored, the notes were written and committed.
Notes and memories are uid'd by path plus content hash, so editing one
files a new version beside the old rather than replacing it. results.md in
August and results.md now are two different things to have said. --replace
collapses them back to current if that ever becomes noise.
history.jsonl went first because it is the clean version of what was typed —
no injected reminders, no tool output, and it covers sessions whose transcripts
no longer exist. The transcript importer then supplies only the assistant
side, so the two halves join on meta.session and sort by ts without
duplicating anything.
Together they reach back to 2026-06-22, two weeks before history.jsonl
begins: 5,375 entries, the whole of Jane.
The transcript importer takes 4.9 MB of a 119 MB file. The rest is
file-history snapshots (41 MB of editor undo buffer) and tool_use/tool_result
pairs — reconstructible from the repo, and enough to make the log too big to
keep in git, which is the one thing it must stay small enough to do.
Keeping it fed
Claude Code deletes transcripts after cleanupPeriodDays. The 2026-06 session
survived only because a continuously-resumed session never ages out — that is
luck, not a design. Two lines in ~/.claude/settings.json remove the luck:
{
"cleanupPeriodDays": 3650,
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "cd ~/Code/memorypalace && ./brain import transcript >> .hook.log 2>&1",
"timeout": 30
}
]
}
]
}
}
No matcher, so it fires on every ending. No path argument and no stdin
parsing either: importing every transcript takes 0.4 s and files only what is
new, so the simplest possible command is also the correct one.