# 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.

Section: Guides  
Canonical: https://gno.sh/docs/memory  
Markdown: https://gno.sh/docs/memory.md

`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

A deleted memory file restored at the same path with identical bytes becomes active on the next successful sync. Activation and one `reactivate` journal event commit together; unchanged repeat syncs add no restoration events. Proven unchanged formatted inputs retain embeddings, while title, content, or model/runtime changes require matching coverage. Sync restoration does not broaden scopes, remove receipt fencing, alter supersession, or change the default 8-fact/512-token recall budget. Natural-language recall matching remains unchanged. Sync source Markdown between hosts, never the derived SQLite database.

## Three write paths, one taxonomy

- **edit** updates an existing canonical note: a document, changed in its file (editor, Web UI, `PUT /api/docs/:id`).
- **capture** creates a genuinely new document with provenance: `gno capture`, `gno_capture`, `/api/capture`.
- **remember** upserts one fact with supersession and current-state reduction: `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 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.

- `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.
- Files are canonical. You may hand-edit them, but a record that no longer satisfies the contract (missing frontmatter, bad hash, empty body, invalid scopes) is excluded from managed recall and reported by `gno status` and `gno audit`. It stays visible to ordinary search.

## 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.

- 1 to 8 scopes per call. Each is trimmed, lowercased, NFC-normalized, and deduplicated. Allowed characters: letters and digits, then `. _ : / @ -`; at most 64 characters. Examples: `project:gno`, `family`, `client/acme`, `user@host`.
- Visibility is any-intersection: a fact is visible to a call when at least one of the call’s scopes appears in the fact’s scope list.
- Scope filtering runs inside the retrieval query, not as a post-filter over a bounded candidate window, so a scope with few facts never comes back falsely empty behind a busier scope.

Scopes are a visibility partition, not an access-control boundary. Anyone who can read the collection’s files can read every fact; [egress policy](https://gno.sh/docs/collection-egress), 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:

- A fact with the same normalized text exists: `existing`, the record. Nothing is written (idempotent).
- No decision given: `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.

### 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.

- Only current facts come back; superseded records are excluded.
- Budget: at most 8 facts under 512 estimated tokens by default (`--max-facts`, `--max-tokens`). Selection reuses the Context Capsule budget logic; `budget.omitted` counts facts that matched but did not fit.
- Each fact carries `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.
- With nothing in scope the response has an empty `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.

## 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

- Every memory write runs under the same shared write lease as `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`.
- A fact is current only after write plus lexical sync succeed. A supersede additionally requires its edge to be projected; if the projection fails the write reports `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.
- Synced vaults (iCloud, Syncthing, git) replicate the files. The index on another machine sees a new fact after its own `gno update`; GNO does not coordinate memory across machines.

## What memory does not do

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

- No automatic capture. Nothing observes an agent’s turns and stores facts on its own. Every fact is an explicit `remember` call.
- No model in the write path. GNO never extracts facts from prose, never decides whether a likely match is the same fact, and never merges records. Embeddings only rank candidates; the caller decides.
- No consolidation or dedup jobs. Supersession is the only reduction.
- No delete or forget. Facts are superseded, never removed by the contract. To remove one, delete the file yourself and run `gno update`.
- No memory Web UI. Memory files are ordinary documents in the Web UI; there is no dedicated memory screen.
- No implicit global scope. Every call names its scopes.
- No cross-machine coordination. Files replicate through your vault sync; each index catches up on its own.
- No write path outside the contract. The adapters below map harness slots onto the four surfaces; none of them adds a way to store a fact that bypasses `remember`.

## 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`.

- **Hermes Agent provider** (`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.
- **OpenClaw plugin** (`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.

## Binding defaults

- Scopes per call: 1 to 8; scope length: 64 characters.
- Fact size: 4096 bytes.
- Candidate pool (BM25): 16; semantic likely-match threshold: cosine 0.83; lexical likely-match threshold: Jaccard 0.5.
- Recall budget: 8 facts / 512 tokens.

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`.
