cerveau docs site github
How it works

Memory

Five stores, each holding a different kind of thing, and none of them asking the model to remember to remember. Recall is system-owned: relevant facts are pulled into every turn whether the model thinks to look or not.

The five systems

MemoryStoreRole
Workingthe live windowWhat the model sees this turn. See Context window.
Episodicevents.jsonlAppend-only source of truth, crash-safe.
SemanticTypesense (managed)Curated cross-session facts, deduped, with provenance.
CodebaseSQLite graphSymbols and call edges. See Code intelligence.
Procedural~/.crv/skills/*.mdMarkdown skills, loaded on trigger. See Skills.

Distillation, compiled not prompted

At every turn close the summary pass runs against a grammar compiled from a JSON schema. The grammar is passed to the sampler, not asked for in the prompt — prose is not a shape the model can produce.

grammar, err := tools.SchemaToGBNF(turnCloseSchema)
reply, _, err := l.llm.Complete(ctx, messages, nil, grammar, 1024)

Four fields come back:

summary
One line.
decisions
Choices made this turn. May be empty.
promotion_candidates
Durable facts, preferences and decisions worth long-term memory. The prompt tells it to be conservative.
open_loops
Unresolved threads.

Asking politely for JSON fails eventually. A grammar cannot. Every memory is a typed record the moment it is written, so it can be searched, merged and superseded later without anything downstream parsing prose.

Memory never costs you the answer Grammar-constrained output can still be truncated at the token cap. If the distill fails to parse it degrades to an empty result and the turn still ships — the answer already reached you. A failed write logs an error event and carries on. Memory is subordinate to the work, never the other way round.

The curator

New knowledge that contradicts old does not pile up beside it. curator.Write() takes a candidate and decides:

The curator's write path A candidate is embedded and compared against what is already stored. Above 0.9 similarity it merges, between 0.7 and 0.9 it is created and linked for review, below 0.7 it is new. > 0.9 0.7 – 0.9 < 0.7 candidate from the distill dedup gate embed, then k-NN merge create + link queued for review new document typesense tag: semantic NEVER DELETES · CORRECTIONS SUPERSEDE · SEARCH FILTERS ON SUPERSEDED:=FALSE
Retrieval only ever sees what is still true, and every document carries the events it came from.

An explicit correction marks the previous document superseded and links its replacement by id. Search then filters on superseded:=false, so last month's mistake is not competing with this month's fact. The chain is preserved rather than deleted, which is what makes provenance answerable.

The dedup gate

Every candidate is embedded and run k-NN against existing documents. The thresholds are explicit:

SimilarityAction
> 0.9Merge into the existing document: bump confidence and last_seen, add a source reference.
0.7 – 0.9Create it, and link related_to. Surfaced in the panel for review.
< 0.7A new document.

Four properties that make it trustworthy

Single writer
The curator serialises every write. No races, by construction rather than by locking discipline.
Never deletes
Corrections supersede through a superseded_by chain, so the history of a belief survives.
Provenance on everything
Each document carries the source episodic event ids. "Why do you think this?" always has an answer.
Writes survive an outage
If Typesense is down, candidates park as pending_semantic episodic events and drain through the dedup gate on recovery. A write never dies because a dependency was briefly missing.

Recall is system-owned

The agent never has to remember to search. Before each turn the harness pulls relevant facts and past events and formats them:

## Recalled memory (auto, system-owned)
- [evt_004411] the retry cap matches the upstream timeout
- [evt_004583 (live tail)] working on the swarm trigger

Every pull carries its event id, so a claim can be traced back to the turn that produced it. Live-tail entries are recent events from the running session rather than the long-term store.

Recalled text is data, never instructions Pulls re-enter the conversation inside a <system-reminder> envelope, and any closing tag inside a stored document is escaped first. Memory holds whatever it was given, including text off a web page — the envelope keeps it usable without letting it speak as you. See Safety guard.

Hybrid search

Typesense does keyword search on its own. With the embedder sidecar running, search is hybrid — keyword plus vector — which is what finds a fact you described one way in June and another way in August. Without the embedder it degrades to keyword-only rather than failing.

The embedder belongs on the CPU Sharing a 24 GB card with the model left roughly 50 MiB free: a short string embedded in 15 ms while a realistic batch of code returned HTTP 500. That surfaces as "memory never retrieves anything useful", not as an error anyone would notice. On CPU it costs about 297 ms per chunk — invisible inside a thirty-second turn — and returns 2.6 GB to the card.

The episodic log

Append-only events.jsonl per session under ~/.crv/sessions/. It is the source of truth: every message, every tool call and its full output, every error. Pointer demotion works precisely because this exists — the window can throw text away safely when the disk still has it.

It is also what the compaction briefing is assembled from, and what GET /api/sessions/{id}/events replays. See HTTP API.

The write-behind indexer

An indexer keeps a cursor on the last indexed event. If Typesense goes down the cursor stalls while events keep appending; when it comes back, indexing replays from the cursor. Self-healing, because the file is the source of truth and the index is only ever a derived view of it.

The same recovery path drains any pending_semantic candidates that parked while the store was unreachable.

Typesense holds every memory type One instance, with each document tagged memory_type. Recall filters by tag or queries across all of them. For episodic it is a rebuildable indexevents.jsonl is the truth. For semantic it is the primary home, snapshotted to semantic.jsonl for git-friendly backup.

Instant sessions

Scratch sessions never promote to long-term semantic memory. They are swept after 24 hours idle, at boot and every 30 minutes thereafter. Use them for a question you do not want colouring the store.

Reviewing what it kept

Memory is inspectable, not a black box. The panel has a memory view backed by these routes:

GET /api/memory/list
Everything stored.
GET /api/memory/search
Search it the way the agent does.
GET /api/memory/graph
The relationship graph the panel renders.
GET /api/memory/provenance/{id}
Where a fact came from, and what it superseded.
GET /api/memory/review
Candidates awaiting a decision.
POST /api/memory/review/{id}
Accept or reject one.