Skip to content

levain 0.5.10

Choose a tag to compare

@phillipclapham phillipclapham released this 04 Oct 17:35
· 5815 commits to main since this release

The seed now gives Claude Code's own auto-memory a job (the operator-facing layer) instead of displacing it, and a Claude Code install mirrors every native memory write into the entity's anneal store.

Changed

  • seed/memory.md now hands the harness's built-in memory the operator-facing layer. It used to call that memory "a scratchpad for harness-operational notes only ... subordinate", and in Claude Code runs with auto-memory on, a Levain entity wrote no native memory files at all. The new wording keeps anneal-memory authoritative for the work history, decisions, the self-model and anything that must be portable or governed, and tells the entity to save the operator's preferences, shorthand, working rules and corrections in the harness's memory, alongside operational notes. A rule that shapes a work decision still gets its own anneal episode. The tie-break is unchanged: on the self-model, anneal-memory wins. An unedited seed/memory.md is refreshed by levain update; an edited one is kept and the new version staged under .levain/pending/.
  • The trade, stated plainly: what the entity writes to native memory lives in Claude Code's store (~/.claude/projects/<folder>/memory/), which is not sovereign, not portable and not under anneal-memory's immune system. The mirror below is what brings it back under anneal.

Added

  • The auto-memory mirror (Claude Code adapter), ON by default. A new activation/hooks/automemory_mirror.py, wired as a PostToolUse hook on Write|Edit and swept at every session start, copies each change to this install's Claude Code auto-memory notes into this install's anneal store, one way. A new note becomes an episode carrying its text; an edit records an episode that supersedes the current one for that note (anneal's own --supersedes, so recall hides the old one); a deletion records a retraction that supersedes it; a note recreated after a deletion supersedes the retraction. Notes edited by hand or through Bash (rm, sed -i) are picked up at the next sweep, not at once. It never writes continuity and never writes back to native memory; the wrap decides what graduates. The hook does its work in a detached process and always exits 0; on a write under Claude Code's projects folder it may first run one git rev-parse (capped at 2 s).
    • What is superseded is read from the store, not from the mirror's own state file: a write supersedes every episode the store currently shows for that note. anneal accepts a second supersession of an episode that was already superseded, so an id remembered locally could otherwise leave an old note current.
    • No backfill. The first sweep that sees a memory folder records the hash of every note already in it and writes no episode: on a new install, on an existing one upgraded with levain update, and for an autoMemoryDirectory set later. A folder that drops out of view (a different CLAUDE_CONFIG_DIR, a removed setting) keeps its record, so it is not backfilled when it comes back. A note written after the session started is mirrored even when the session-start sweep runs after it.
    • Turn it off with "automemory_mirror": false in .levain/config.json (durable), or LEVAIN_AUTOMEMORY_MIRROR=off (per session; it wins over the config).
    • What a mirrored episode looks like: source automemory-mirror, tags automemory, mirror and amem-path-<16 hex> (one per note). A note whose frontmatter type is user or feedback is type decision, tagged operator-rule, with content starting OPERATOR RULE (auto-memory mirror): memory/<file>.md; any other note is type context, starting AUTO-MEMORY NOTE (auto-memory mirror): memory/<file>.md. An edit reads ..., REVISED (auto-memory mirror) and a deletion RETRACTED OPERATOR RULE / RETRACTED AUTO-MEMORY NOTE. anneal-memory --db .levain/memory.db episodes --source automemory-mirror lists the current ones (add --include-superseded for the history).
    • Which folders: Claude Code's per-project folder for the install directory, for the git work tree it sits in and for the repository of a linked worktree (Claude Code keys auto-memory by repository, so an install inside a repository shares that repository's notes), or $CLAUDE_CODE_PROJECT_DIR_NAME; plus the effective autoMemoryDirectory when it is set in the install's .claude/settings.local.json or .claude/settings.json (an absolute or ~/ path, as Claude Code requires). A user-scope autoMemoryDirectory is not mirrored: one folder shared by every project would pull other projects' notes into this entity. An autoMemoryDirectory given by claude --settings or by managed (policy) settings cannot be seen by a hook and is not mirrored. With scope: "global", sessions started outside the install write to other projects' folders, which are not mirrored. MEMORY.md (the index) is never mirrored.
    • If the state file is lost while the store already holds mirror episodes, the mirror stops (re-baselining then would leave an edited or deleted note current until its next edit) and writes .levain/automemory_mirror.lost; levain doctor reports it. Recovery is a person's: restore .levain/automemory_mirror.json from a backup (an older copy is safe) and delete the marker, or turn the mirror off. Deleting only the marker stops it again.
    • If the very first sweep cannot read the anneal store, it waits (it cannot tell a new install from lost state) and records when; once the store is readable, notes written since then are mirrored, not baselined. levain doctor reports the wait.
    • A memory folder that is missing or cannot be listed (an unmounted volume, a removed directory) is skipped, and nothing in it is retracted; levain doctor reports it when it holds mirrored notes.
  • levain doctor reports the mirror: wired, off, start-sweep only (advisory: run levain update), stopped, or a last sweep that could not read the store, left notes unmirrored or could not list a memory folder.

Known open issues

  • levain doctor reports the mirror wired when a PostToolUse command names automemory_mirror.py, without checking that its matcher covers Write/Edit or that it passes hook; only a hand-edited .claude/settings.json can differ from the rendered one.
  • The first-sweep wait records one time for all folders. A folder that cannot be listed when the store recovers is baselined, not mirrored, when it returns; a folder configured during the wait has notes written after the wait began mirrored.
  • A sweep that waits more than 60 seconds for another sweep's lock gives up, and the note it was started for is mirrored at the next session start or memory write, not at once.