Skip to content

Memory

Agents remember across runs through a memory special page: a running list of facts, episodes, and procedures, each an ordinary node you can read, link, and search. Agents propose items during runs, a nightly pass consolidates duplicates without ever deleting history, and every new run retrieves the relevant items into its system prompt. Because memory items are just nodes, they surface as backlinks on the entities they mention and get vector coverage for free.

The agent memory page

Memory items carry provenance and [[links]], are filtered by kind, and are consolidated nightly.

Memory items are ordinary nodes on the memory special page (ensured on boot), each carrying a props.memory block:

kind  'semantic' | 'episodic' | 'procedural'

semantic is a stable fact, episodic is a dated event, procedural is a how-to.

source  string

Where the item came from, for example agent:<runId>.

ts  timestamp

When the item was recorded.

supersededBy  nodeId?

Set by consolidation when a newer item replaces this one. The old node and its history are kept, never deleted.

[[links]] in an item’s text are plain link rows, so an item that mentions [[Rachel Kim]] shows up as a backlink on that CRM page and is retrievable by search like any other node. There is no separate memory index.

During a run an agent calls memory.propose {kind, text}, which files a new item under props.memory = {kind, source: 'agent:<runId>', ts}. Proposing is deliberately the only thing a run does to memory: curation (dedup, supersession) belongs to the consolidation pass, so a chatty run cannot quietly rewrite what you already know. See tools & approvals for the tool surface.

Consolidation is deterministic TypeScript, no LLM required. Walking active items in timestamp order:

  • An exact normalized-text duplicate merges: the newer copy is stamped superseded by the older.
  • A same-kind near-duplicate with differing content (character-bigram Dice similarity at or above 0.82) supersedes: the older item is stamped superseded by the newer, so memory stays current.

An optional LLM-assist seam refines borderline pairs (Dice between 0.6 and 0.82); under PROVIDERS=fake it is a no-op, so tests stay deterministic. Each run commits one transaction: all the supersededBy stamps plus a consolidation frame node appended to the memory page (recording how many items merged and how many were superseded). Re-runs are idempotent.

The pass runs as a normal agent run: the seeded memory-consolidate schedule (0 3 * * *) targets the seeded agents/memory-consolidation definition, so nightly consolidation appears in the task queue Scheduled section and streams in the run tree like any other run.

retrieveMemory {q?, pages?, limit=8} is a hybrid search filtered to the memory corpus plus any items [[linked]] to the given pages, deduped and active-only. It is wired into the runner: at every run start, Subspace retrieves on the instruction text and the invoking page, and when items come back, appends a Relevant memory: block to the run’s system message (the instruction stays verbatim; a retrieval failure is swallowed, never blocking the run). So an agent starts each run already knowing the relevant facts, without you pasting them in.

Open /p/memory to browse and curate:

  • Filter tabs: all / semantic / episodic / procedural, each with an active count, plus a show-superseded toggle.
  • Item rows: a kind chip, the text with [[links]] rendered as navigable links, and a source · ts meta line. Superseded items are struck through with a superseded chip.
  • The consolidation frame: shows the nightly schedule and the last run’s merged and superseded counts, with a consolidate now button (backed by memory.consolidate).

The memory.view {kind?, includeSuperseded?} endpoint returns the items, active counts per kind, and the last frame; memory.consolidate {} runs the pass on demand.