Recall architecture

How Polign Recall is built. The page covers the record it stores, the fold that turns records into answers, the registry that holds the correction rules, search, cached reads, the recall serve front door, agent resume, audit bundles, and what a storage backend has to provide. It describes recall 0.18.

Source

Recall is open source under Apache 2.0. The code paths this page names are in github.com/Polign/recall, and the same material is kept in the repository as docs/architecture.md. For what Recall does and why, start with the introduction.

The shape of the system

Recall is a library with two optional processes around it. The library holds every rule: what a statement must look like, how corrections work, how history folds into an answer. It stores nothing itself. A backend holds the records, and the backend is a vector store you run: polign_db in your own bucket, or a PostgreSQL, MongoDB or Qdrant you already have.

Callers on top, the three ways to run Recall in the middle, and the two backends underneath. Your code Go program imports the Go module Python client polign-recall on PyPI MCP host Claude Code, OpenClaw HTTP client LangGraph, LiveKit, your own Recall: rules, validation, fold, history Library in your process recall mcp one agent, one process, MCP over stdio recall serve many agents; keys, namespaces; HTTP and MCP stdio stdio recall connect HTTP Put, List, Search: the backend contract Backend: persistence, vectors, filters polign_db your S3, GCS, or Azure bucket; every capability PostgreSQL · MongoDB · Qdrant a database you already run
One deployment uses one backend. The library, recall mcp, and recall serve run the same code; they differ in who they answer and how.

A memory is an event

Recall writes one kind of record: an event. An event says that a subject stands in a predicate relation to a value, and when that was observed. It is never updated in place. A correction is a new event, and forgetting is a new event with the retraction flag set. The log of events for one subject and predicate is the complete record of what was ever said about that pair.

FieldWhat it holds
subject, predicate, valueThe statement. The subject is lowercased; the value is stored with the type the registry declares for the predicate (string, number, boolean; enums, dates, and references are strings in a canonical form).
observed_atWhen the statement was made. The fold orders by this, not by when the backend accepted the write, so a backdated statement lands where it belongs in history. It is stored twice: as a fixed-width RFC 3339 string that sorts lexically, and as milliseconds for numeric range filters.
retractionTrue for a forget. A retraction withdraws a value, or every value of the pair when it names none.
replaces, replaced_byWhich earlier beliefs this statement made untrue, under any predicate, and which later statement withdrew this one. Both are recorded on the events, and the fold derives answers from them.
evidence, evidence_idThe exact excerpt a statement was drawn from, and the id of the note holding the whole text. This is what explain reads back.
kind, source, confidenceFact or preference; who said it (user_stated, agent_inferred, tool_result); a number from 0 to 1.
idA hash of the statement and its instant. Content alone would make a re-assertion overwrite the retraction between the two, so the instant is part of the identity.

In the backend an event is one vector with this metadata attached. The vector is the embedding of the statement rendered as a sentence ("user prefers editor neovim"), so semantic search and exact filtered reads see the same statements. The metadata also carries that sentence, with the evidence appended, as one text field for a backend that indexes text.

Three families of record share the memory collection and are read by the same code:

Agent resume records and recall serve keys live in collections of their own, because every record in a memory collection must decode as an event or an export fails.

Reading folds the log

Beliefs are never stored. They are derived, every time, by a function the code calls the fold: given the events of a pair, the predicate's cardinality, and an instant, return the values believed at that instant. "What is believed now" and "what was believed on Tuesday" are the same call with a different ceiling. That is the whole design, and the diagram below shows one customer's shipping address as the fold sees it.

Two events on a time axis and the answers a read gets at two different instants. customer-4812 · shipping_address · holds one value observed_at Sep 12 · 12 Pine St assert · user_stated · evidence: "Ship it to 12 Pine St" Oct 2 · 48 Oak Ave assert · replaces 12 Pine St (Sep 12) recall as_of Sep 20 fold stops at Sep 20: 12 Pine St recall now 48 Oak Ave, replaced: 12 Pine St history: both events, oldest first. Nothing was edited or removed.
The log holds two events. The answer depends on where the fold stops, and every answer says what it replaced.

The fold walks the pair's events in observation order, stopping at the instant asked about, and applies each one:

Because the fold is a pure function of the events, a client crash between two writes cannot produce a state the fold reads wrongly. The earlier design this replaced wrote a new record and then rewrote each superseded one; a crash in the middle left two live values for a single-valued predicate. Here there is nothing to rewrite.

The write path

A remember is a read followed by at most one append, plus a retraction per correction it carries:

  1. Validate. The subject is normalized, the predicate must be registered (or definable, under an open vocabulary), the value must match the predicate's type, and confidence and source must be in range. Nothing reaches the backend until this passes.
  2. Read the pair's complete history and fold it at the statement's instant. A history that would exceed the per-pair limit is refused rather than truncated.
  3. Return early if the value already holds. Restating a belief is not new information, so no event is written. Idempotence lives here, not in the record id.
  4. Resolve the corrections. Each belief the statement names in replaces is checked at the same instant. One that is not held, was stated later, is a note, or is historical is reported back as unreplaced instead of failing the write: the statement is still true.
  5. Append one event, then one retraction per correction, each naming the event that caused it. If the process dies between the two, the next remember of the same statement finds the event present and the retractions missing, and completes them. The error message says so.

The write returns what it displaced, derived from the fold before and after, and a short list of related beliefs: current values about the same subject under other predicates that may contradict the new one. Nothing in Recall acts on that list. It is shown so that the agent, or the person, can confirm a correction the rules did not cover.

The read path

Two shapes of read exist, and the first is the one everything else builds on.

Every typed belief in an answer carries replaced, the values it displaced, and elsewhere, the same value held about the same subject under other predicates at that instant. The second is a read-time safety net for a correction nobody linked: the reader sees "likes mangoes" beside "dislikes mangoes" with both dates, instead of one slot alone. It is a bounded number of exact listings per answer, and nothing is decided by it.

Search without a model

Recall ships with a lexical embedder: signed feature hashing of the words in a sentence into a 256-dimensional vector. It needs no model, no key, and no network, and it retrieves overlapping words, not synonyms. Its space is versioned (recall-lexical-v1-256) and the default collection name carries that version, so a collection never mixes it with model embeddings.

On polign_db, every search runs twice: once as a vector query and once as a BM25 keyword query over the text field, and the two rankings are fused. With the lexical embedder the keyword ranking leads and the vector hits it lacks follow, since those are mostly events written after the text index last caught up. With a model embedder the two are merged by reciprocal rank fusion, which rewards a hit both rankings found. On a backend without text search, the vector ranking stands alone.

A time phrase in the question ("two weeks ago", "last Saturday", "in March") is read as a hint: the span it names is searched first, then everything. The hint reorders candidates and never drops any.

Bring your own embedding service with -embed-url for paraphrase matching. That is a per-collection decision: changing embedders means a new collection.

Cached reads

Reading a pair means listing its history and folding it, every time. For a backend that can say whether a collection changed, Recall keeps a bounded, process-local view of folded pairs and skips the listing when nothing did.

The registry

The registry is the set of predicates a store accepts, and for each one its cardinality (single or multi), its value type, the predicates it excludes, whether it is historical, and any former names. It is the only place a correction rule lives. Recall ships starter registries for coding, personal assistant, support, sales, and voice agents, and a store can load its own.

Two modes exist:

note is registered in every store and is the one predicate that can hold anything: multi-valued free text that never supersedes and ranks after typed beliefs. "Nothing matched" always has a place to be written. Once a predicate for that kind of fact exists, Promote files each note under it and keeps the note in history.

Changing a registry later does not change what was remembered before. Redefine, merge, and split are events; the fold reads each statement through the definition that held when it was made, and explain lists the definitions, oldest first, that decide an answer.

Text in, text out

Since 0.18 the MCP tools take plain text by default: remember(text), recall(question), forget(text). No predicate appears in a tool, an argument, or an answer. Underneath, the records are the same events, so a client that starts here loses nothing when it moves to typed statements.

  1. The whole passage is written first as a note, the episode, so that every statement drawn from it can name it as evidence. Restating text already kept returns the existing note.
  2. The model named by -extract-model proposes statements: subject, predicate, value, and an exact excerpt as evidence. Its proposals are untrusted input. Each is validated against the registry like any write, and the batch is folded as a whole so one passage may carry a correction. A proposal whose predicate is not registered is kept as a note, not dropped.
  3. The proposals are recorded beside the episode as an extraction record, so a retried or repeated remember replays them without a second model call.

The model decides how a statement is filed. It does not decide what to forget. Replacement follows the predicate's rule as before, every filing is recorded, and a wrong filing is corrected with one registry event that every answer then follows. Without a model configured, the text is kept as written and replaces nothing.

Serving many agents

recall serve puts a front door in front of the library. The server holds the backend's credentials; agents hold Recall keys. Each namespace gets its own collection, so nothing a caller sends can reach another namespace's records.

Three ways a caller proves who it is, the steps inside recall serve, and the three collections it uses in the backend. Caller proves who it is Recall key rcl_… bound to a namespace Kubernetes pod token projected, audience "recall" Keys file <key> <namespace> per line recall serve 1. Authenticate hash the key, read its record and its revocation tombstone; or TokenReview 2. Namespace from the credential never from the request body; <tenant>:<agent> for a pod 3. One server per namespace built on first use, kept for the process 4. The library, as everywhere fold, rules, registry, search, cache Backend collections <base>__keys key records, written once; tombstones, never overwritten <base>__<ns>_<hash> this namespace's events: statements, registry, extraction …_agents working state, turns, pointers, outputs, actions; leases Surfaces: POST /mcp (MCP over streamable HTTP) · POST /v1/{remember, recall, forget, history, diff} · /v1/keys · /v1/auth/kubernetes A key check is cached for a bounded time (30 s by default), so a revoked or expired key is refused within that time rather than at once.
The namespace comes from the credential, and the collection name comes from the namespace, so a request body can never choose where it reads or writes.

The contract every client is written against, tool by tool and field by field, is memory-api-v1.md in the repository. A test in the server checks the tool definitions against it, so a tool cannot change without the document changing.

Resume

An agent resumes from what it wrote down, not from a snapshot of its process. It keeps five kinds of record in a collection of its own, the memory collection's name with _agents appended:

Resume takes the agent's lease, reads those records back, adds memories that match its focus, and renders one briefing within a token budget. A lease is a fencing token: an epoch that only increases, granted and renewed by the backend, so a holder presenting an old epoch has been superseded and a second pod resuming the same agent cannot write over the first. On polign_db, PostgreSQL and MongoDB the backend also checks the epoch on every agent write, so a process that stalled past its lease is refused even when its own clock says the lease is fresh. Qdrant cannot grant leases; there, resume works only with Unleased: true, which is safe only when something else keeps one process per agent. Resume agents without snapshots walks through the measured behavior.

Actions

The records say what the agent was doing, not whether its last tool call happened. A harness brackets each call that touches the world: BeginAction before it runs, EndAction with its result when it returns. A crash between the two leaves the action open, and the next resume lists it first in the briefing. Each action has a class, which says what running it again would do:

ClassForOpen after a crash
pureReadsClosed as abandoned. The agent runs it again if it still needs the answer.
idempotentWrites that are safe to repeat with the same keyRun again by the harness with the action's key, which is fixed when the action begins.
externalAnything that could happen twice: send, post, charge, deletePassed to a resolver that checks the outside system. If nothing can tell, it is marked unknown and stays at the top of every briefing until it is ended.

Reconcile settles open actions before the model takes another step, and writes each settled outcome into the turns so the model sees what happened. Recall never runs a tool itself. Durable agent actions describes the kill test behind it.

Audit

ExportAudit produces a bundle: the complete event histories in scope, the registry used to interpret them, an explicit as-of time, and two version strings, recall-event-v3 for the record fields and recall-fold-v4 for the fold rules. A standalone verifier, recall-audit, reads a bundle and reproduces the beliefs offline, with no database and no embedding model. It rejects unknown fields, duplicate ids, invalid typed events, and unsupported versions, and checks an optional digest obtained through a separate channel.

Replay establishes what the supplied events imply under the supplied rules at the recorded time. It cannot prove the exporter included every event, and the format does not sign data. Export refuses to truncate: a scope larger than the export limit fails rather than producing a partial bundle.

The backend contract

A backend is a vector store with filtered listing. Three methods are required and the rest unlock capabilities. Backends register themselves by name at init, the way database drivers do, and a program picks one by importing it; the conformance suite is what a new backend has to pass.

MethodUsed forpolign_dbPostgreSQLMongoDBQdrant
PutAppend an event, a registry change, a key record, an agent recordyesyesyesyes
List (filtered, with a total)Complete history of a pair; exports; detecting a truncated readyesyesyesyes
Search (vector, filtered)Candidate pairs for a questionyesyesyes, exactyes
Get by idEvidence lookups for explain; the key storeyesyesyesyes
SearchTextKeyword ranking, fused with the vector rankingyes, BM25yes, full-text searchyes, text indexno
WatermarkCached readsyesyesyesno
Leases (acquire, renew, release)One process per agent on resumeyesyesyesno
PutFencedAn agent write checked against its lease by the backendyesyes, in the write's transactionyes, in a transaction on a replica setno

Recall provides the memory rules, validation, and history; the backend provides persistence and access. Concurrent reads and writes follow the backend's consistency guarantees. Recall adds no transaction isolation and no replication. Two clients that write the same pair at the same instant are ordered by observation time, with a tie broken deterministically, and both events survive in history.

Limits, stated plainly

Go deeper