Memory

How gno remember and gno recall store and retrieve agent facts: the write-path taxonomy, explicit scopes, supersession, budgeted cited recall, context fencing and its limits, and what memory does not do.

gno remember stores one fact. gno recall returns the current facts that match a query, under a budget, with gno:// cites and a receipt. Both are core contracts exposed on every surface with one shared schema: the CLI, the MCP tools gno_recall (read set) and gno_remember (write set, --enable-write), the REST endpoints POST /api/memory/remember and POST /api/memory/recall, and the SDK methods client.remember() and client.recall(). Results are the same objects everywhere; error codes (MEMORY_*) are identical and each surface maps them onto its own envelope.

Memory lives in your own markdown files. The SQLite index stays derived and disposable, exactly as for every other collection.

gno remember "Prod deploys from main only" --scope project:gno
gno remember "Prod deploys from main only" --scope project:gno --add
gno recall "deploy branch" --scope project:gno
gno recall "kindergarten" --scope family --max-facts 3 --max-tokens 256 --json

Restoring a memory file

Restore a deleted memory file at the same path with identical content and it becomes active again on the next sync. Unchanged content keeps its embeddings. Restoring a file does not change its scopes, fencing, or supersession. Sync the Markdown files between machines, never the SQLite database.

Three write paths, one taxonomy

Remember is not a second capture. A fact is one sentence or two, at most 4096 bytes; anything longer is a document, and remember refuses it with MEMORY_TEXT_TOO_LARGE and points at gno capture. An existing note that is wrong: edit it. A new meeting, idea, or source: capture it. A standalone fact an agent should look up later: remember it.

A memory-managed collection

A collection accepts remember only when its config declares it memory managed. There is no CLI flag for it yet; edit the config:

collections:
  - name: memory
    path: /Users/you/notes/memory
    pattern: "**/*.md"
    memoryManaged: true

remember into any other collection fails with MEMORY_COLLECTION_UNMANAGED, and recall reads only memory-managed collections. With exactly one memory-managed collection configured the CLI defaults --collection to it. The flag changes nothing else: the collection is indexed, searched, and egress-governed like any other, so gno search and gno query see memory files as ordinary documents.

The fact file

One fact per markdown file, written by GNO at facts/<YYYY-MM-DD>/mem-<16 hex>.md inside the collection. The frontmatter carries a memory block with recordId, scopes, caller, session, createdAt, contentHash, and the optional free-text source given at write time; a successor also carries relations.supersedes with its predecessor’s URI. The body is the fact text.

A fact's content date is its createdAt record date: Context Capsule evidence, ask sources, recency sorting, and date filters all use it. After upgrading to GNO 2.12, the next gno update reprocesses existing documents once to pick this up; embeddings are reused.

Scopes

Every remember and recall call names its scopes explicitly. There is no implicit global scope: an unscoped call fails with MEMORY_SCOPES_REQUIRED on every surface. Shared visibility is something you configure by choosing a scope name that several callers agree on (--scope shared), never a default.

Scopes are a visibility partition, not an access-control boundary. Anyone who can read the collection’s files can read every fact; egress policy, not scope, decides where derived output may travel.

Identity

Every call carries a caller and a session, recorded in the fact frontmatter and bound into every recall receipt. CLI: --caller and --session, else $GNO_MEMORY_CALLER / $GNO_MEMORY_SESSION, else cli:<user> and ppid:<parent pid>. MCP: the client name from the initialize handshake and the Streamable HTTP session id (or the stdio server instance id). REST and SDK: the request’s caller and session fields. MCP tool arguments never carry identity; it is mapped from the connection, so a client cannot claim to be another one.

Remember

remember first searches the current facts in the same scopes for candidates: a BM25 pool of 16, then cosine similarity ≥ 0.83 when the collection’s embedding model is already cached, otherwise normalized-token Jaccard ≥ 0.5; the result’s matching block says which. Then:

The caller decides. GNO never adjudicates a likely match with a model; it returns the candidates and waits for an explicit add or supersede.remember writes the file and updates the keyword index before it returns, so the fact is retrievable as soon as sync.status reads completed. A failed sync is reported as such (the file exists, the index lags); rerun gno update for that collection.

Supersession

A fact is replaced, never edited in place:

  1. Recall the current fact and take its uri and contentHash.
  2. remember the new text with --supersede <uri> --predecessor-hash <hash>.
  3. GNO checks that the predecessor exists, that its hash still matches (MEMORY_PREDECESSOR_HASH_MISMATCH otherwise), and that nobody has superseded it yet. It then writes the successor carrying relations.supersedes.

Two writers racing to supersede the same predecessor get one successor and one MEMORY_SUPERSEDE_CONFLICT (HTTP 409, CLI exit 4). The loser recalls again and decides against the new current fact. Two current branches of one fact cannot exist. Superseded facts stay on disk and in ordinary search; recall excludes them inside the query. Nothing is deleted by the memory contract.

Everywhere else a superseded fact is marked. Search, vector search, hybrid query, get, multi-get, ask citations and Context Capsule evidence carry supersededBy with the URI of the current fact (the end of the chain), and text output prints a Superseded by: line (gno get prints it on stderr so piped content stays exact). Answers are told never to present a superseded fact as current, and verifying a saved Capsule that cites a fact superseded since reports that evidence as stale (superseded).

Retrying with a request ID

An add or supersede can carry an optional request ID (--request-id, or requestId on MCP, REST, and the SDK) so a retry after a lost response replays or finishes the same remember instead of adding or superseding twice. See Retry-safe writes.

Recall

recall is the fast path: BM25 over the memory collection, fused with the vector leg when the embedding model is already cached, with query expansion, graph expansion, and reranking off. It never downloads a model. The response retrieval.mode reports hybrid or lexical with the reason. Over MCP, recall uses keyword search only, so it never waits for a model to load.

Keyword recall works on the query's content words. Question words such as “which”, “does”, and “what do we know about” are dropped, so gno recall "which branch does the QA canary deploy from?" finds a fact about the canary without vectors. Facts that contain every content word come first; when none does, recall returns facts that share any of them, best match first, and leaves out facts that match only on a word most facts contain. A paraphrase that shares no word with the fact still needs vectors: run gno embed on the memory collection to match by meaning too.

Cite recalled facts by their gno:// URI, exactly as for any retrieved document.

Context fencing

Agents that recall and then remember in the same loop tend to feed GNO’s own output back in as a “new” fact. The fence stops that loop where it can be stopped honestly. Every recall response includes a receipt: caller, session, issuedAt, memoryIds, spanHashes (the contentHash of every returned fact), and a digest over those fields. It is content-free; it carries no fact text.

remember rejects, and writes nothing, when the normalized hash of the submitted text matches a spanHashes entry on the presented receipt (MEMORY_FENCED_REPLAY; CLI --receipt <path> pointing at a saved gno recall --json output, MCP/REST/SDK the receipt field), or when the submission declares a gno:// origin in derivedFrom (MEMORY_FENCED_DERIVED). Non-GNO origins are fine and are recorded as declared. Receipts are surface-independent: a receipt issued by gno_recall fences a gno remember --receipt, and the reverse.

What the fence cannot do

A paraphrase without lineage cannot be fenced. If an agent recalls “Deploys go out from the main branch only”, rewrites it as “only main is deployed”, presents no receipt, and declares no derivedFrom, GNO sees an original fact and stores it. The fence is exact-span plus declared origin. It is a guard against the accidental replay loop, not a proof of provenance, and it depends on the calling agent passing the receipt it was given and declaring what it derived from. Treat receipts as part of the agent’s contract, not as a security boundary.

Concurrency and consistency

What memory does not do

Memory leaves these out on purpose.

Adapters

The gno agents protocol block (v3) carries the memory contract into every harness’s global instruction file, and the gno skill carries the workflows. Neither is a runtime adapter: they tell an agent when to call recall and remember, and the agent calls them like any other command. The instructions tell an agent to try gno recall "<query>" --scope <scope> after exact search and before document search, and to choose --add or --supersede itself after a recall, passing the recall receipt back. gno skill install ships three memory recipes: recipes/memory-file-decision.md, recipes/memory-supersede-fact.md, and recipes/memory-scoped-recall.md.

Defaults

The full error-code table, with CLI exit codes and HTTP statuses, is in docs/MEMORY.md in the GNO repository.