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:
- per failed tool result, an
erroror anenvironmentepisode bytranscripts.error_class():environmentwhen the failure text matchesENVIRONMENT(command not found, a password, a permission denial, the network, exit 137/143/144) or the call's description is a probe (PROBE: check, verify, list, read, find, …); titled with the tool, its description and the first meaningful line of the failure; - per tool call the person refused (
REJECTED), acorrectiontitledrefused: <call>, with what they said; - per agent turn whose text has a first-person admission (
STATED_SURPRISE: I was wrong, my mistake, it turns out the…, found a bug), asurpriseepisode; - per person's message for which
transcripts.correction_cue()finds a cue (CORRECTION_OPENINGat the start, orCORRECTION_ANYWHERE, pasted blocks removed,NOT_A_CORRECTIONexcluded), acorrectionwhose detail isingest.what_the_agent_did(): how the agent's reply began, how it ended, and the calls it made.
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.