---
title: Memory API
description: Cross-run facts, message threads, namespaces, and the Effect store layer.
icon: "brain"
---

Memory persists state **across runs**. Task outputs are per-run; memory is
per-namespace and survives every workflow execution. The store keeps three
kinds of state: namespaced **facts** (JSON values with optional TTL), ordered
**message threads**, and append-only **notes** (durable knowledge with
supersession and a status gate).

All memory values and non-generic types are re-exported from the
`smithers-orchestrator` facade, which is canonical. The
`smithers-orchestrator/memory` subpath exports the same surface.

```ts
import {
  createMemoryStore,
  createMemoryLayer,
  MemoryService,
  Summarizer,
  TokenLimiter,
  TtlGarbageCollector,
  namespaceToString,
  parseNamespace,
} from "smithers-orchestrator";
import type {
  MemoryNamespace,
  MemoryNamespaceKind,
  MemoryFact,
  MemoryThread,
  MemoryMessage,
  MemoryStore,
  MemoryServiceApi,
  MemoryProcessor,
  MemoryProcessorConfig,
  MemoryLayerConfig,
  TaskMemoryConfig,
  SemanticRecallConfig,
  MessageHistoryConfig,
} from "smithers-orchestrator";
```

<Note>
The note types (`MemoryNote`, `SaveNoteInput`, `NoteReadFilter`, and
`MemoryProvenance`) are exported from the `smithers-orchestrator/memory`
subpath. `WorkingMemoryConfig<T>` is generic over a Zod schema and is documented inline
below rather than imported. The `memory` prop on `Task` is **returned by the
factory**, not imported; see [Memory](/concepts/memory) for the declarative
`<Task memory={...}>` metadata.
</Note>

## Concepts

A `MemoryNamespace` scopes everything you store. Pick the `kind` to match the
lifetime of the data, and the `id` to identify the specific workflow, agent, or
user.

```ts
type MemoryNamespace = { kind: MemoryNamespaceKind; id: string };
type MemoryNamespaceKind = "workflow" | "agent" | "user" | "global";
```

<ParamField path="kind" type='"workflow" | "agent" | "user" | "global"' required>
  Lifetime scope. `workflow` is per workflow definition, `agent` per agent
  identity, `user` per end user, `global` shared across everything.
</ParamField>
<ParamField path="id" type="string" required>
  Identifier within the kind.
</ParamField>

A **fact** is a namespaced JSON value, last-write-wins, with an optional TTL.

<ResponseField name="MemoryFact" type="object">
  <Expandable title="MemoryFact">
    <ResponseField name="namespace" type="string" required>
      The namespace, serialized via `namespaceToString`.
    </ResponseField>
    <ResponseField name="key" type="string" required>
      Fact key, unique within the namespace.
    </ResponseField>
    <ResponseField name="valueJson" type="string" required>
      The value, JSON-stringified.
    </ResponseField>
    <ResponseField name="schemaSig" type="string | null">
      Optional schema signature for the stored value.
    </ResponseField>
    <ResponseField name="createdAtMs" type="number" required>
      Creation time, epoch milliseconds.
    </ResponseField>
    <ResponseField name="updatedAtMs" type="number" required>
      Last-write time, epoch milliseconds.
    </ResponseField>
    <ResponseField name="ttlMs" type="number | null">
      Time-to-live in milliseconds. Expired facts are removed by
      `deleteExpiredFacts` and `TtlGarbageCollector`.
    </ResponseField>
  </Expandable>
</ResponseField>

A **thread** groups ordered **messages**.

<ResponseField name="MemoryThread" type="object">
  <Expandable title="MemoryThread">
    <ResponseField name="threadId" type="string" required>
      Thread identifier.
    </ResponseField>
    <ResponseField name="namespace" type="string" required>
      Owning namespace, serialized.
    </ResponseField>
    <ResponseField name="title" type="string | null">
      Optional human-readable title.
    </ResponseField>
    <ResponseField name="metadataJson" type="string | null">
      Optional JSON metadata.
    </ResponseField>
    <ResponseField name="createdAtMs" type="number" required />
    <ResponseField name="updatedAtMs" type="number" required />
  </Expandable>
</ResponseField>

<ResponseField name="MemoryMessage" type="object">
  <Expandable title="MemoryMessage">
    <ResponseField name="id" type="string" required>
      Message identifier.
    </ResponseField>
    <ResponseField name="threadId" type="string" required>
      Owning thread.
    </ResponseField>
    <ResponseField name="role" type="string" required>
      Message role, e.g. `"user"`, `"assistant"`, `"system"`.
    </ResponseField>
    <ResponseField name="contentJson" type="string" required>
      Message content, JSON-stringified.
    </ResponseField>
    <ResponseField name="runId" type="string | null">
      Run that produced the message, if any. Use `RUN_ID`.
    </ResponseField>
    <ResponseField name="nodeId" type="string | null">
      Node that produced the message, if any. Use `NODE_ID`.
    </ResponseField>
    <ResponseField name="createdAtMs" type="number" required>
      Creation time, epoch milliseconds. Optional on `saveMessage` input
      (defaults to now).
    </ResponseField>
  </Expandable>
</ResponseField>

A **note** is append-only knowledge. Facts are mutable KV (upsert semantics);
notes are immutable rows: body, labels, and provenance never change after
insert. `status` is the one deliberate exception: `setNoteStatus` lets a
human or workflow gate write an answer about an existing note without churning
its id. Notes carry no TTL: knowledge dies by supersession or rejection, not
by clock.

<ResponseField name="MemoryNote" type="object">
  <Expandable title="MemoryNote">
    <ResponseField name="id" type="string" required>
      Note identifier. Pass your own on `saveNote` to make retries idempotent.
    </ResponseField>
    <ResponseField name="namespace" type="string" required>
      Owning namespace, serialized via `namespaceToString`.
    </ResponseField>
    <ResponseField name="body" type="string" required>
      The knowledge itself. Immutable after insert.
    </ResponseField>
    <ResponseField name="kind" type="string | null">
      Optional, policy-free label (e.g. `"lesson"`, `"runbook-rule"`). The
      engine never interprets it.
    </ResponseField>
    <ResponseField name="tagsJson" type="string | null">
      JSON-encoded string array; null when the note has no tags.
    </ResponseField>
    <ResponseField name="author" type="string | null">
      Optional, policy-free author label.
    </ResponseField>
    <ResponseField name="status" type="string" required>
      Free-form; defaults to `"accepted"`. Conventionally
      `pending | accepted | rejected`. The one mutable field.
    </ResponseField>
    <ResponseField name="statusChangedAtMs" type="number | null">
      When `setNoteStatus` last flipped the status; null until then.
    </ResponseField>
    <ResponseField name="createdAtMs" type="number" required>
      Creation time, epoch milliseconds.
    </ResponseField>
    <ResponseField name="runId / nodeId / iteration" type="string | string | number | null">
      Provenance: the run coordinate that wrote the note (see
      `MemoryProvenance`).
    </ResponseField>
  </Expandable>
</ResponseField>

`MemoryProvenance` is the run coordinate a memory write was made from. It is
passed **explicitly** by the caller on `setFact` and `saveNote`, never
inferred from ambient context, which does not survive agent/tool boundaries.

```ts
type MemoryProvenance = {
  runId?: string | null;
  nodeId?: string | null;
  iteration?: number | null;
};
```

`SaveNoteInput` is the `saveNote` argument: the namespace as the structured
object, tags as an array, plus optional `kind`, `author`, `status`,
`provenance`, `supersedes` (note ids this note replaces; junction rows are
written atomically with the note), and `id` (pass one to make retries
idempotent).

`NoteReadFilter` shapes `listNotes`/`searchNotes` reads. The **default read
contract** (no filter) is a stability contract: it returns notes that are
(a) not superseded by an **accepted** note and (b) `status = "accepted"`.
A pending or rejected superseder hides nothing. Filters widen or narrow:

```ts
type NoteReadFilter = {
  status?: string | string[] | "any";
  includeSuperseded?: boolean;
  kind?: string;
  namespace?: MemoryNamespace; // scope searchNotes to one namespace of the kind
};
```

## createMemoryStore

Build a `MemoryStore` over a Drizzle SQLite handle. Synchronous. Create one at
module scope and reuse it across tasks rather than re-opening the database in
every task body.

```ts
function createMemoryStore(db: BunSQLiteDatabase<any>): MemoryStore;
```

<ParamField path="db" type="BunSQLiteDatabase" required>
  A Drizzle `bun-sqlite` database handle. The memory tables are created on the
  same database your workflow uses.
</ParamField>

```ts
import { createMemoryStore } from "smithers-orchestrator";
import { Database } from "bun:sqlite";
import { drizzle } from "drizzle-orm/bun-sqlite";

const store = createMemoryStore(drizzle(new Database("smithers.db")));
const ns = { kind: "workflow" as const, id: "code-review" };

await store.setFact(ns, "model", "gpt-4");
const fact = await store.getFact(ns, "model"); // MemoryFact | undefined
const all = await store.listFacts(ns);
```

## MemoryStore

The Promise-based read/write surface. Every method has an Effect-returning twin
(`getFactEffect`, `setFactEffect`, `listThreadsEffect`, `deleteMessagesEffect`,
and so on) with the same arguments, for use inside an Effect pipeline.

<ResponseField name="Facts" type="methods">
  <Expandable title="fact methods">
    <ResponseField name="getFact" type="(ns, key) => Promise<MemoryFact | undefined>" />
    <ResponseField name="setFact" type="(ns, key, value, ttlMs?) => Promise<void>">
      Last-write-wins. `value` is any JSON-serializable value; `ttlMs` sets an
      optional expiry.
    </ResponseField>
    <ResponseField name="deleteFact" type="(ns, key) => Promise<void>" />
    <ResponseField name="listFacts" type="(ns) => Promise<MemoryFact[]>" />
    <ResponseField name="deleteExpiredFacts" type="() => Promise<number>">
      Removes facts whose TTL has elapsed. Returns the count deleted.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="Notes" type="methods">
  <Expandable title="note methods">
    <ResponseField name="saveNote" type="(input: SaveNoteInput) => Promise<MemoryNote>">
      Appends a note and its supersession edges atomically. Idempotent on
      `id`: a re-save with the same id is a no-op (never an upsert, so history
      cannot be destroyed), and the returned note is the row the database
      holds, not the ignored input.
    </ResponseField>
    <ResponseField name="getNote" type="(id) => Promise<MemoryNote | undefined>" />
    <ResponseField name="listNotes" type="(ns, filter?) => Promise<MemoryNote[]>">
      Notes in one namespace, oldest first, under the default read contract
      (accepted, not superseded by an accepted note) unless `filter` widens.
    </ResponseField>
    <ResponseField name="setNoteStatus" type="(id, status) => Promise<void>">
      The one mutable write: flips `status` and stamps `statusChangedAtMs`.
      Fails loud when the note does not exist.
    </ResponseField>
    <ResponseField name="enableNoteSearch" type="(kind) => Promise<void>">
      Opts a namespace **kind** into FTS note search. Creates the FTS5
      artifacts lazily on first call and backfills existing notes of that
      kind; until then note writes pay nothing per write. Idempotent.
    </ResponseField>
    <ResponseField name="searchNotes" type="(kind, query, limit?, filter?) => Promise<MemoryNote[]>">
      Full-text search over note bodies, rank order, honoring the same read
      contract as `listNotes`. Fails loud, not silently empty, when the
      kind was never enabled. The index spans **every namespace of the
      kind**; pass `filter.namespace` to keep results namespace-local on a
      shared database. The read contract is applied to the top `limit * 5`
      FTS matches, so a heavily superseded corpus can return fewer than
      `limit` even when more live matches exist further down the rank order.
    </ResponseField>
  </Expandable>
</ResponseField>

<Note>
Notes require the SQLite backend: note writes and search ride the synchronous
sqlite driver (transactions for note+edge atomicity, FTS5 for search). On a
Postgres/PGlite-backed workspace `saveNote`, `enableNoteSearch`, and
`searchNotes` fail loud with an explicit error.
</Note>

<ResponseField name="Threads & messages" type="methods">
  <Expandable title="thread and message methods">
    <ResponseField name="createThread" type="(ns, title?) => Promise<MemoryThread>" />
    <ResponseField name="getThread" type="(threadId) => Promise<MemoryThread | undefined>" />
    <ResponseField name="listThreads" type="() => Promise<MemoryThread[]>" />
    <ResponseField name="deleteThread" type="(threadId) => Promise<void>" />
    <ResponseField name="saveMessage" type="(msg) => Promise<void>">
      `msg` is a `MemoryMessage` with an optional `createdAtMs` (defaults to
      now).
    </ResponseField>
    <ResponseField name="listMessages" type="(threadId, limit?) => Promise<MemoryMessage[]>" />
    <ResponseField name="countMessages" type="(threadId) => Promise<number>" />
    <ResponseField name="deleteMessages" type="(threadId, messageIds) => Promise<number>">
      Deletes the named messages. Returns the count deleted. Used by processors
      to compact history.
    </ResponseField>
  </Expandable>
</ResponseField>

## MemoryService

An Effect `Context.Tag` whose service value is a `MemoryServiceApi`. It exposes
the same operations as the store, but every method returns
`Effect.Effect<T, SmithersError>` instead of a `Promise`. The underlying store
is reachable via `.store`.

<ResponseField name="MemoryServiceApi" type="object">
  <Expandable title="members">
    <ResponseField name="getFact / setFact / deleteFact / listFacts" type="Effect">
      Fact operations, Effect-returning. `setFact(ns, key, value, ttlMs?)`.
    </ResponseField>
    <ResponseField name="createThread / getThread / deleteThread" type="Effect">
      Thread lifecycle, Effect-returning.
    </ResponseField>
    <ResponseField name="saveMessage / listMessages / countMessages" type="Effect">
      Message operations, Effect-returning.
    </ResponseField>
    <ResponseField name="saveNote / getNote / listNotes / setNoteStatus / enableNoteSearch / searchNotes" type="Effect">
      Note operations, Effect-returning, with the same semantics as the store
      methods above.
    </ResponseField>
    <ResponseField name="deleteExpiredFacts" type="() => Effect<number, SmithersError>" />
    <ResponseField name="store" type="MemoryStore">
      The underlying Promise-and-Effect store, for the few methods the service
      does not surface directly (e.g. `listThreads`, `deleteMessages`).
    </ResponseField>
  </Expandable>
</ResponseField>

## createMemoryLayer

Build the Effect `Layer` that provides `MemoryService`. Pass it a
`MemoryLayerConfig` carrying the Drizzle database; provide the resulting layer
to any Effect that depends on `MemoryService`.

```ts
function createMemoryLayer(
  config: MemoryLayerConfig,
): Layer.Layer<MemoryService, never, never>;
```

<ParamField path="config" type="MemoryLayerConfig" required>
  <Expandable title="MemoryLayerConfig">
    <ParamField path="db" type="BunSQLiteDatabase" required>
      The Drizzle `bun-sqlite` handle the memory tables live on.
    </ParamField>
  </Expandable>
</ParamField>

```ts
import { Effect } from "effect";
import { createMemoryLayer, MemoryService } from "smithers-orchestrator";

const program = Effect.gen(function* () {
  const memory = yield* MemoryService;
  yield* memory.setFact({ kind: "user", id: "u1" }, "tier", "pro");
});

Effect.runPromise(program.pipe(Effect.provide(createMemoryLayer({ db }))));
```

## Processors

A `MemoryProcessor` is a named maintenance pass over a `MemoryStore`. Each
exposes a Promise `process(store)` and an Effect `processEffect(store)`.

```ts
type MemoryProcessor = {
  name: string;
  process: (store: MemoryStore) => Promise<void>;
  processEffect: (store: MemoryStore) => Effect.Effect<void, SmithersError>;
};
```

<ResponseField name="Summarizer" type="(agent) => MemoryProcessor">
  Compresses older messages in each thread into a single `system` summary
  message, keeping the two most recent. `agent` is any
  `{ run: (prompt: string) => Promise<unknown> }`; its output text becomes the
  summary.
</ResponseField>

<ResponseField name="TokenLimiter" type="(maxTokens) => MemoryProcessor">
  Trims oldest messages per thread until each thread fits a rough token budget
  (approximated as `maxTokens * 4` characters).
</ResponseField>

<ResponseField name="TtlGarbageCollector" type="() => MemoryProcessor">
  Deletes expired facts across all namespaces by calling
  `deleteExpiredFacts`.
</ResponseField>

`MemoryProcessorConfig` selects which named processors to run.

<ResponseField name="MemoryProcessorConfig" type="object">
  <Expandable title="MemoryProcessorConfig">
    <ResponseField name="processors" type="string[]">
      Names of the processors to run, e.g. `["Summarizer", "TtlGarbageCollector"]`.
    </ResponseField>
  </Expandable>
</ResponseField>

```ts
import { TtlGarbageCollector, Summarizer } from "smithers-orchestrator";

await TtlGarbageCollector().process(store);
await Summarizer(myAgent).process(store);
```

## Task-level config

`TaskMemoryConfig` is the shape of the `memory` prop on `Task`. It is preserved
as task metadata for runtimes and integrations that layer memory behavior onto
task execution; the store APIs above are the current public read/write surface.

<ParamField path="memory" type="TaskMemoryConfig">
  <Expandable title="TaskMemoryConfig">
    <ParamField path="namespace" type="string | MemoryNamespace">
      Default namespace for this task's memory.
    </ParamField>
    <ParamField path="recall" type="{ namespace?; query?; topK? }">
      Inject relevant past facts into the prompt. `namespace` overrides the
      default, `query` is the recall query, `topK` caps results.
    </ParamField>
    <ParamField path="remember" type="{ namespace?; key? }">
      Persist this task's output as a fact under `key` in `namespace`.
    </ParamField>
    <ParamField path="threadId" type="string">
      Thread this task's messages are appended to.
    </ParamField>
  </Expandable>
</ParamField>

`SemanticRecallConfig` and `MessageHistoryConfig` describe the two recall
strategies a memory-aware runtime can apply.

<ResponseField name="SemanticRecallConfig" type="object">
  <Expandable title="SemanticRecallConfig">
    <ResponseField name="topK" type="number">
      Maximum number of facts to recall.
    </ResponseField>
    <ResponseField name="namespace" type="MemoryNamespace">
      Namespace to recall from.
    </ResponseField>
    <ResponseField name="similarityThreshold" type="number">
      Minimum similarity score for a match.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="MessageHistoryConfig" type="object">
  <Expandable title="MessageHistoryConfig">
    <ResponseField name="lastMessages" type="number">
      How many recent messages to include.
    </ResponseField>
    <ResponseField name="threadId" type="string">
      Thread to read history from.
    </ResponseField>
  </Expandable>
</ResponseField>

Working memory is a typed, single-document fact validated against a Zod schema.
Its config is generic over the schema type:

```ts
type WorkingMemoryConfig<
  T extends import("zod").ZodObject<import("zod").ZodRawShape> = import("zod").ZodObject<import("zod").ZodRawShape>,
> = {
  schema?: T;
  namespace: MemoryNamespace;
  ttlMs?: number;
};
```

## Helpers

Namespaces are stored as strings. These two helpers round-trip a
`MemoryNamespace` to and from its canonical `kind:id` form, percent-encoding
`:` and `%` in the id.

<ResponseField name="namespaceToString" type="(ns: MemoryNamespace) => string">
  Serializes a namespace, e.g. `{ kind: "user", id: "u1" }` becomes
  `"user:u1"`.
</ResponseField>

<ResponseField name="parseNamespace" type="(str: string) => MemoryNamespace">
  Parses a serialized namespace back. Strings without a known kind prefix parse
  as `{ kind: "global", id: str }`.
</ResponseField>

```ts
import { namespaceToString, parseNamespace } from "smithers-orchestrator";

namespaceToString({ kind: "user", id: "u1" }); // "user:u1"
parseNamespace("workflow:code-review"); // { kind: "workflow", id: "code-review" }
```

Cross-run facts are also viewable from the CLI:

```bash
bunx smithers-orchestrator memory
```

**Source** [`store/MemoryStore.ts`](https://github.com/smithersai/smithers/blob/main/packages/memory/src/store/MemoryStore.ts) · [`MemoryServiceApi.ts`](https://github.com/smithersai/smithers/blob/main/packages/memory/src/MemoryServiceApi.ts) · [`MemoryNote.ts`](https://github.com/smithersai/smithers/blob/main/packages/memory/src/MemoryNote.ts) · [`createMemoryLayer.js`](https://github.com/smithersai/smithers/blob/main/packages/memory/src/createMemoryLayer.js) · [`processors.js`](https://github.com/smithersai/smithers/blob/main/packages/memory/src/processors.js) · **Tests** [`store.test.js`](https://github.com/smithersai/smithers/blob/main/packages/memory/tests/store.test.js) · [`notes.test.js`](https://github.com/smithersai/smithers/blob/main/packages/memory/tests/notes.test.js) · [`service.test.js`](https://github.com/smithersai/smithers/blob/main/packages/memory/tests/service.test.js) · [`processors.test.js`](https://github.com/smithersai/smithers/blob/main/packages/memory/tests/processors.test.js) · **See also** [Memory](/concepts/memory), [Types reference](/reference/types)
