melissa kate
<< memorypalace

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:

attemptneeded
ocean-chat-simple/conversation_memory.pyAWS credentials + a DynamoDB table
melissakate.com/bookmarks/api.phpthe PHP endpoint reachable
this repo, 2023a 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

sourcerowskindstatus
~/.claude/history.jsonl1,749promptdone
~/.claude/projects/**/*.jsonl3,626chatdone
~/.claude/projects/**/memory/*.md46notedone
~/Code/**/*.md, git-tracked58notedone
melissakate.com/bookmarks.json1,060link
prose/mkm_train/chat_logs/*.jsonl543chatcarries 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 separates
prose/notes/corpus-design.md from
prose/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.