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 the memory slice deliberately 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

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.

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. Success means more than a file on disk: the write and the lexical index sync complete under the shared write lease before the call returns, so the fact is retrievable the moment 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 verifies, under the write lease, 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.

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. The MCP adapter runs the lexical leg only, so a resident gateway does not load a model per call.

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

These are exclusions, not gaps. Each one is a decision.

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 retrieval ladder gains gno recall "<query>" --scope <scope> after exact search and before the document rungs; the writing contract states that remember proposes and the agent decides --add or --supersede from a recall, passing the 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.

Binding defaults

The constants live in src/core/memory-types.ts in the GNO repository; the full error-code table with CLI exit codes and HTTP statuses is in the repository’s docs/MEMORY.md.