Implementation

Last verified against the code: 2026-10-21 (first commit)

How each goal in GOALS.md is built, and the gaps.

Goal 1: read the raw stream

transcripts.session_files() lists *.jsonl directly under each project folder of CLAUDE_PROJECTS_DIR. transcripts.parse() reads a file line by line: only user and assistant records with isSidechain false are conversation. User records: isMeta lines and tag-wrapped command records (TAG) are dropped; tool_result blocks are attached to the last assistant turn (Turn.errors when is_error). Assistant records: consecutive records are one turn (text joined, tool calls appended), because the model streams text and tool calls as separate lines. ingest.ingest() skips a session whose mtime the sessions table already has, otherwise replaces its episodes.

Gap: only Claude Code's format. Other agents (goal 15) need their own reader producing the same Session.

Goal 2: the unit is the surprise

ingest.episodes_of(): one error episode per distinct failed tool result in a turn, titled with the tool, its description (from the description, file_path, command, … input key) and the first meaningful line of the failure (transcripts._error_of() turns a bare Exit code N first line into a suffix). One correction episode per user turn whose opening words match transcripts.CORRECTION, with the agent's preceding statement as detail.

Gaps: corrections are detected by opening words only, so "that's wrong" mid-message is missed and "no problem" is a false positive; the agent's own stated surprises ("that's unexpected") are not yet episodes.

Goal 3: recall

recall.recall() embeds the query (embed.embed(): lowercase words of two or more characters minus stop words, plus adjacent pairs, hashed with BLAKE2b into 256 signed buckets, unit-normalized), scores every unarchived episode and every lesson with recall.score(), sorts, returns the top k as dicts and touches the returned episodes (Store.touch()). --cwd limits episodes to one project.

Gap: a hashed embedder has no notion of synonyms; a learned embedder is the first upgrade (paper §7.6).

Goal 4: sleep

sleep.sleep() runs decay(), consolidate(), verify() and predict() in that order and returns a report dict; sleep.morning_text() renders it. consolidate() first attaches each uncovered surprise to the nearest existing lesson above SAME_SURPRISE, then greedily clusters what is left by kind. verify() compares each open prediction's embedding with the episodes created after it was made. predict() embeds the lesson text itself as the prediction's embedding, so a repeat of the same surprise confirms it.

Gaps: thresholds are untuned first guesses; clustering is greedy and order-dependent; lessons never merge with each other once created.

Goal 5: observations, never instructions

Model-written lesson text goes through sleep._as_observation(), which prefixes "Observed:" to text that opens with an imperative (FORBIDDEN_STARTS). The model prompt (LESSON_SYSTEM) asks for an observation under 30 words. Deterministic lesson texts are of the form "Seen N×: <title>" or "The user corrected the agent: <words>".

Gap: the check is on the opening words only.

Goal 6: have it in mind

render.render() writes om-<id>.md per lesson, with the frontmatter Claude Code's memory files use (name, description, metadata.type of feedback for corrections and project otherwise, plus managed: openmemory, seen, strength, updated), and one section of MEMORY.md between <!-- openmemory:start --> and <!-- openmemory:end -->. The CLI renders into every project (cwd) that has episodes.

Gap: this is interpreted memory read at session start; compiled memory is goal 12.

Goal 7: see and veto

morning.md is written by openmemory sleep and shown by openmemory morning. render.render() keeps written.json; a lesson in it whose file is gone is added to vetoed.json and skipped from then on.

Goal 8: recall mid-task

mcp.serve() reads newline-delimited JSON-RPC from stdin. initialize answers protocol 2024-11-05 with a tools capability; tools/list returns TOOLS; tools/call runs recall (formatted text) or remember (a note episode with session_id mcp). Unknown methods and tools are -32601; any other exception is -32000.

Goal 9: no model, better with one

llm.model() returns the configured model or the first one GET /models lists, cached per process, None when OPENMEMORY_LLM=0 or nothing answers within two seconds. llm.complete() posts a chat completion and returns None on any failure. Callers (ingest.episodes_of(), sleep.lesson_text()) use the deterministic text then.

Goals 10 to 15

Not started. The store already records what goal 10 needs: surprises per session (episodes by session_id and kind), prediction outcomes (predictions.outcome), and repeats (lessons.seen). See ROADMAP.md.

Tests

tests/test_openmemory.py builds a transcript in a temporary project folder (an install that fails, a correction to use uv, a success), plus a subagent transcript that must be skipped, and runs: parsing (turns, tool pairing, error text, correction), ingest (three episodes; idempotent), recall, sleep (one lesson from the correction, none from the single error; a prediction), render (files, the managed section, the veto), a second session the next day (the prediction confirmed; the repeated error now a lesson), and the MCP server as a subprocess.