Implementation

Last verified against the code: 2026-11-13 (0.9.0)

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

transcripts.parse() reads user and assistant lines, plus queued_command attachments from a human (messages sent mid-turn; the same text delivered again as a user line is one turn), and skips user lines whose origin.kind is not human (task notifications). ingest.episodes_of() then yields, per turn:

Ids are ingest.episode_id() (SHA-1 of session, kind, title); Store.replace_session() upserts a transcript's episodes, keeps strength and touches, deletes what the transcript no longer yields, and renames old ids in lessons.

Measured (0.1.4): on 41 real exchanges from the owner's sessions, hand labelled (6 corrections, 4 borderline left out), the cues caught 6 of 6 with no false alarm. They were written while reading that set, so this is a fit, not a test; the next weeks' messages are the test. The local model as judge did worse (best prompt: 6 of 6 with 8 false alarms; a yes/no prompt: 4 of 6 with 8) and is not used.

Gaps: cue rules are English and tuned on one person; borderline pushback ("think of something revolutionary") is not counted; a probe is recognised by its description, so a call with none is never a probe.

Goal 3: recall

recall.recall() embeds the query (embed.embed(): with an embedding model, Ollama's /api/embed, unit-normalized and tagged {"s": model, "v": [...]}; else lowercase words of two or more characters minus stop words, plus adjacent pairs, hashed with BLAKE2b into 256 signed buckets, unit-normalized), rescales each cosine above the space's floor (embed.relevance()), 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.

embed.space() decides the space once per process: REMEM_EMBED, else the first Ollama model whose name says "embed", else hashed (always hashed when REMEM_LLM=0, so tests never touch the network). A model that fails mid-run gives hashed vectors for that call; cosine() returns 0 across spaces and sleep.reembed() heals them.

Measured (0.2.0): with qwen3-embedding:0.6b the owner's three "act, don't ask me" corrections sit at cosine 0.65 to 0.67 to each other and at most 0.59 to the other three; "should I ask before releasing?" ranks "do it always dont ask" first, where hashed words found nothing. Gap: six corrections set the 0.62 cut; it is a fit.

Goal 4: sleep

sleep.sleep() runs reembed(), decay(), consolidate(), merge(), verify() and predict() in that order and returns a report dict; sleep.morning_text() renders it. consolidate() first attaches each uncovered surprise to the existing lesson of its kind whose sources it resembles most (linkage(), the mean cosine to them, against embed.same()), then greedily clusters what is left by kind, every pair over the cut. merge() joins lessons of one kind whose sources pass the same cut by average linkage, keeping the strongest lesson's id and recording the others in aliases (Store.alias()). A lesson's vector is the centroid of its sources' (lesson_vector()), and its prediction carries that vector. verify() compares each open prediction with episodes of its lesson's kind that happened (_when(), the episode's own time in UTC) after it was made.

Gaps: clustering is greedy and order-dependent; a theme never splits again once merged.

Goal 10: measure

measure.measure() reads the store: per lesson, the sources that happened after Store.in_mind_since() (the lesson's or its first alias's creation) are repeats with the lesson in mind; sources after the first but before that are the baseline (the person repeating themselves with no memory). Errors likewise. measure.measure_text() prints it, each number with its episodes; remem measure --json gives the dict. Gap: "in mind" is assumed from the moment a lesson is made; the render is per project, so a lesson in one project's folder is counted as in mind everywhere.

Goal 5: observations, never instructions

A lesson from corrections is never written by the model: sleep.lesson_text() quotes the person's words and the sentence of the agent's reply they answered (sleep._agent_context(): its closing when it asked them something, else its opening): The user corrected the agent with "…" after the agent said "…". The model's paraphrases of the owner's six real corrections said the opposite of what was meant in two. A lesson from errors or stated surprises is worded by the model (LESSON_SYSTEM: an observation under 30 words, only what the surprises say, no guessed cause) and goes through sleep._as_observation(), which prefixes "Observed:" to text that opens with an imperative (FORBIDDEN_STARTS); without a model it is "Seen N×: <title>". sleep.rewrite() (sleep --rewrite) re-words every lesson from its sources and rewords its open prediction.

Gap: the check is on the opening words only.

Goal 6: have it in mind

render.render() writes remem-<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: remem, seen, strength, updated), and one section of MEMORY.md between <!-- remem:start --> and <!-- remem:end -->. The CLI renders into every project (cwd) that has episodes.

prime.prime() (0.2.0) is the other half: on each prompt, the lessons whose vector the prompt's embedding meets (prime.cut(), 0.60 in a model's space), at most MAX_LESSONS, each once per session (state in ~/.remem/primed/), logged to primed.jsonl. prime.from_hook() reads Claude Code's hook JSON; cli.cmd_prime() swallows every error so a prompt is never blocked.

prime.on_stop() (0.2.1) fires when transcripts.hands_back() finds a hand-back in the closing of the agent's reply and prime.handback_lessons() finds correction lessons most of whose sources' "Agent ended" (or, for a one-sentence reply, "Agent began") was one; prime.on_tool() compares a call's tool, description and command with error lessons. prime.held_out() puts half the sessions in the control arm; measure._experiment() compares the arms. prime.reminder() (0.8.0) is the one wording of the end-of-turn reminder, used by the hook, the proxy and the experiments; it names the boundary (ask for exactly what only the person has). prime.on_stop() stays silent when transcripts.needs_person() finds that the person's last message says only they have what is needed; prime.last_prompt() reads that message from the transcript the Stop hook names.

experiment.py has four experiments (behaviour, mechanism, loop, boundary), each kept by experiment.save() in ~/.remem/experiments; experiment.pairs() turns them into preference pairs (rejected, chosen) for scripts/adapter.py, which can also build pairs from the model it trains (--pairs own|both) and runs on the GPU when torch sees one (--device auto). goals.check() reads the kept runs, measure, compare and every ~/.remem/adapters/*/result.json in order, and gives each goal A1–A10 a status from its target (A8 is met only by a run on ≥200 pairs).

Gaps: the hand-back rule is English; "most sources" is a vote of very few; this is interpreted memory; compiled memory is goal 12.

delegation.evidence() (0.2.2) reads every correction: a refused call is "acted, stopped"; one whose context has "Agent handed back" (written by ingest.what_the_agent_did() from transcripts.handback_sentence()), or whose closing sentence hands back, is "asked, told to act"; delegation.action_kind() names the kind from what was offered. Since 0.8.0 the tally goes only to the owner (morning.md): render.render() removes an remem-delegation.md an earlier version wrote, and render.lessons_markdown() leaves it out, because shown to agents it raised hand-backs (paper §7.4). delegation.wants() words the same evidence as what the person wants; it was tested as an arm and is not shown either. ingest.offers_of() (0.2.3) walks every reply that precedes a person's message: transcripts.handback_act() calls its closing an offer or waiting (the last hand-back cue decides; a waiting cue at or after it means waiting), and transcripts.answer_to_offer() reads the answer (corrected, declined, approved, other). prime.on_stop() fires on offers only. Gaps: kinds are keyword rules; a clarifying question ("which name do you want?") counts as an offer; with no refusals or declines yet, the "stop" side is untested on real data.

Goal 7: see and veto

morning.md is written by remem sleep and shown by remem 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 REMEM_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.

Goal 15: any agent, any model

setup.setup() detects agents by their config folders and adds the MCP server (mcp_json(), codex()), Claude Code's hooks (claude_code()) and the cron line (cron()), backing each file up first. chats.parse_messages() turns OpenAI, Anthropic or Gemini messages into the Session/Turn shape ingest.store_session() already takes; a tool result without an error flag counts as failed when its first line says so (chats.FAILED). events.to_episode() maps an event onto an episode (a correction's gist quotes said; agent_did becomes the context, with an offer recognised). proxy.Handler injects proxy.context() into the system prompt (inject_openai, inject_anthropic), streams the upstream reply through while keeping it, and proxy.record() writes the conversation without the injected block. render.render_instruction_files() writes a marked section into ~/.codex/AGENTS.md and ~/.gemini/GEMINI.md. Gaps: no native readers for other agents' own transcript files; the proxy does not yet do the end-of-turn reminder; the /memory/* API has no authentication (it listens on 127.0.0.1 only).

Goal 11: two agents, and the start of goal 14

share.export() writes this memory's lessons of the shareable kinds with lesson_hash() and origin(), plus confirm records for others' lessons whose prediction came true here and revoke records. share.receive() checks each lesson (safe(): length, FORBIDDEN imperative openings, control characters, kind, the hash recomputed), corroborates a local lesson with the same hash or the same meaning (embed.link()), or adds a candidate with origin, hash and strength 0.5. sleep.predict() lets a candidate predict whatever its strength. share.sync() is the folder transport; sleep.sleep() runs it when REMEM_SHARE_DIR is set. Since 0.5.0 share.signed() adds key and an Ed25519 sig over _canonical() (sorted-key compact JSON) and share.authentic() checks the signature and that fingerprint(key) is the record's origin; share.trusted() narrows hearing to a team. Since 0.6.0 share.sync() also speaks HTTP to the commons (PUT this memory's wire() lines, GET index, GET each other file); src/commons.js signedBy() checks each line's key against the path's origin and the Ed25519 signature over the line with sig cut out. codex.parse() reads Codex sessions; ingest.gemini_files() finds Gemini CLI's chats. Gaps: keys are not rotated; a lost key cannot revoke; confirmations are counted per origin but the commons' common state is not computed; one person's two machines have not run it for real yet.

Goals 12 and 13

Not started. Where things stand and what comes next is in ROADMAP.md, "where we left off". 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_remem.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.