Skip to main content
Memory persists state across runs, unlike per-run task outputs, and can seed a new agent task with relevant context.
API reference: Memory lists every memory export and type, its options, and links to source and tests.

Four layers

Local SQLite or Hindsight

The <Memory> component has two modes: without HINDSIGHT_URL it uses local SQLite facts and keyword recall; with it, Hindsight adds semantic recall, mental-model primers, and retention. Hindsight itself runs on Postgres 15+ with pgvector, so it’s documented and deployed as a Postgres-family feature. HINDSIGHT_URL points at a Hindsight deployment the human has set up (Hindsight Cloud signup or a self-hosted Docker/Helm install); that human step is covered in Set Up Semantic Memory.
Use one HindsightMemoryStore writer instance per transactional contract store: it serializes same-document projections only within that instance. Separate instances share no queue or durable version fence, so they can project competing mutations out of order.
The fallback is deliberate: skipping Hindsight keeps the existing SQLite facts behavior, with no Postgres dependency. For PGlite/Postgres workflow-state backends, fallback facts still use the local .smithers/smithers.db sidecar while workflow state stays on the selected backend. HindsightMemoryStore keeps MemoryStore’s identity and concurrency contract in its authoritative rows: fact identity is namespace plus key; message/note ids stay global. A failed projection logs and requeues while the process lives, never rejecting an already-committed mutation. The retry queue isn’t a durable outbox: an interrupted process can leave Hindsight stale until the record is rewritten or deleted. Same-instance projections to one remote document run in mutation order; that ordering doesn’t cross instances sharing a contract store. Exact facts get a stable document id from namespace+key and updateMode: "replace"; threads, messages, and notes use typed retained documents. searchNotes and task recall call Hindsight recall; exact reads and supersession filtering use the contract rows. tag_groups compound filters cover only stable tags (branch, stream, source, scope); session/run identity stays volatile, in metadata and append document ids. With a user bank plus a project bank, Smithers recalls the user bank without project filters and the project bank through (scope:main OR branch:current) plus stream tags as constraints. Primer ids are searched across configured banks; a missing bank/id pair doesn’t discard primers found elsewhere. The local SQLite runtime applies the same strict tag scope before keyword ranking. Every method has an Effect-returning twin (store.listThreadsEffect, store.deleteMessagesEffect, etc.) for use inside an Effect pipeline.

Namespaces

Pick the kind to match the lifetime: workflow is scoped to a workflow definition; agent to an agent identity; user to an end user; global is shared across everything.

Declarative task memory

Provider config and the per-task memory={...} prop both converge on TaskDescriptor.memoryConfig; a task prop replaces the inherited one. Before the task runs, the engine fetches primers and recall results, caps them conservatively (covering primers, recall rows, labels, framing, and serialized recall tool results), and prepends the block to the prompt. Recall failures degrade to the original prompt; a successful task with retain="on-complete" starts a non-blocking retain. Tasks with an active bank/banks config bypass task-output caching: recall is mutable input, and a cache hit would otherwise skip it and return an older snapshot’s output. Legacy-only memory metadata is inert and keeps the task’s existing cache semantics; Smithers still freezes one fetched snapshot across retries of the same execution. Use stable tags for branches, streams, source, and scope: scope:main belongs only on branch:main; branch-local writes use scope:branch. Given one branch tag and no scope tag, Smithers derives the correct scope; a tagless project-bank write defaults to scope:main so later canonical recall can see it. Conflicting scope/branch tags are rejected, and the 16-tag limit is checked after merging configured, tool, and automatic source/scope tags. Run and session identities instead belong in retention metadata and document ids, preserving provenance without fragmenting consolidation. Tags supplied to the mid-task recall tool only narrow the configured recall scope; they cannot replace the base project branch or stream filters. See <Memory> for the complete prop table, multi-bank behavior, tool mode, and deployment configuration. The legacy task shape remains valid:
Legacy namespace, object-form recall, remember, and threadId fields remain accepted and preserved on TaskDescriptor.memoryConfig, but the engine ignores them: no fact recall, output retention, or message-history appends. Use the bank-based fields above for runtime memory, or call createMemoryStore directly for exact local facts, threads, and messages.

Imperative get/set/delete inside a workflow

The memory={{ recall, remember }} legacy block is inert compatibility metadata. Get, set, or delete an exact fact from a compute <Task> by building a store with createMemoryStore(db) and calling it directly: the compute callback receives deps only, so there is no injected store.
Create the store once at module scope (outside the workflow) and reuse it across tasks rather than re-opening the SQLite handle in every task body. Under Effect, the same operations are available as store.setFactEffect(ns, key, value, ttlMs?), store.getFactEffect(ns, key), etc., or via MemoryService (MemoryServiceApi), which also exposes the underlying .store.

Durable notes

Facts are a mutable scratch lane; notes are the durable knowledge lane. A note’s body, labels, and provenance never change after insert; knowledge evolves by supersession instead: a new note lists the ids it replaces, and the superseded notes drop out of default reads only once the superseder is accepted. status is the one mutable field: a human or workflow gate flips it with setNoteStatus, so propose-then-reject leaves the original knowledge untouched. The default read contract (no filter) is a stability contract: reads return notes that are accepted and not superseded by an accepted note. Filters (status, includeSuperseded, kind, namespace) widen or narrow.
On SQLite, full-text search is lazy and opt-in per namespace kind: nothing is indexed, and note writes pay nothing, until enableNoteSearch(kind) creates the FTS index and backfills. On Hindsight, the transactional note row is projected and indexed on write, so enableNoteSearch is a no-op and searchNotes uses semantic recall before applying the contract store’s status and supersession rules. Search spans every namespace of the kind; pass { namespace } to stay namespace-local on a shared backend. See examples/incident-runbook-memory.jsx for the full recall → triage → bank → distill → ratify loop. Notes are a SQLite-only capability: the note + supersession-edge write needs one synchronous transaction and search needs FTS5, neither of which the Postgres path provides. saveNote, enableNoteSearch, searchNotes, and the transactional deleteThread fail loud against a Postgres/PGlite handle rather than degrading. The Postgres schema still carries _smithers_memory_notes and _smithers_memory_note_supersessions for dialect parity, labelled staged, not served by migration 0043_memory_notes_postgres_staged. Under Bun, a Postgres or PGlite workspace still gets notes: openSmithersBackend keeps memory in the .smithers/smithers.db sqlite sidecar it opens alongside the main database. Under Node there is no sqlite at all. See Node runtime.

Processors

Maintenance jobs you run periodically:

Inspect from the CLI

The CLI currently exposes fact listing. Use the store API for writes, deletes, threads, messages, and TTL cleanup.

Notes

  • Memory is not transactional with the workflow’s frame commits: don’t use it for run-scoped state.
  • Working-memory writes are unordered. Use message history when sequence matters.