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 --jsonPUT /api/docs/:id).gno capture, gno_capture, /api/capture.gno remember, gno_remember, /api/memory/remember.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 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: trueremember 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.
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.
contentHash is the SHA-256 of the normalized text (NFC, whitespace collapsed, trimmed). It is also the span hash that appears in recall receipts.relations.supersedes is the existing typed-edge mechanism; ingestion projects it into the graph like any other relation. There is no separate memory store.gno status and gno audit. It stays visible to ordinary search.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.
. _ : / @ -; at most 64 characters. Examples: project:gno, family, client/acme, user@host.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.
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 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:
existing, the record. Nothing is written (idempotent).candidates, likely and weak matches. Nothing is written.--add / decision: "add": added, one fact file.--supersede <uri> --predecessor-hash <hash> / decision: "supersede": superseded, one fact file with a supersedes edge.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.
A fact is replaced, never edited in place:
uri and contentHash.remember the new text with --supersede <uri> --predecessor-hash <hash>.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 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.
--max-facts, --max-tokens). Selection reuses the Context Capsule budget logic; budget.omitted counts facts that matched but did not fit.uri, text, scopes, caller, session, createdAt, contentHash, spanHash, supersedes, score, and its egressLineage. The response-level lineage is the strictest policy across the returned facts; derived output inherits it.facts list and a hint naming the write path, verbatim on every surface, so a fresh agent learns remember from the empty read.Cite recalled facts by their gno:// URI, exactly as for any retrieved document.
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.
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.
gno index, gno update, and MCP writes, so an MCP gno_remember and a CLI gno remember serialise on one lease. A caller that cannot obtain it within the wait window gets MEMORY_WRITE_LEASE_BUSY.MEMORY_SUPERSEDE_PROJECTION_FAILED, the predecessor still reads as current, and gno update retries. Vector embeddings for new facts arrive with the next gno embed; recall’s lexical leg finds the fact before that.gno update; GNO does not coordinate memory across machines.These are exclusions, not gaps. Each one is a decision.
remember call.gno update.remember.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.
integrations/hermes-gno-memory, verified against Hermes v0.20.5): prefetch runs gno recall --json with the turn’s message and the configured scopes; a model-invoked gno_remember tool runs gno remember --json with propose, add, or supersede and presents the session’s latest recall receipt; Hermes’s own after-turn persistence never writes to GNO. Scopes come from the provider config only. Below the GNO version pin, or when gno is missing or returns malformed JSON, the provider reports memory unavailable and the session continues without it.integrations/openclaw-gno-memory, verified against OpenClaw 2026.8.1): OpenClaw keeps writing its own memory files; the plugin retrieves. memory_search runs gno search scoped to the memory collection, memory_get runs gno get, and every search syncs the collection first so a file written a moment ago is retrievable. A failed sync marks the index stale and the tool response says so.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.