# Smithers Memory

> Smithers cross-run memory: working memory, message history, semantic recall, processors.

---

## Memory

> Cross-run facts, message history, namespaces, and task memory metadata.

Memory persists state **across runs**. Task outputs are per-run; memory is per-namespace and survives every workflow execution.

<Note>API reference: [Memory](/reference/memory) lists every memory export and type, its options, and links to source and tests.</Note>

```mermaid
flowchart LR
  W[workflow] --> ST
  AG[agent] --> ST
  U[user] --> ST
  GL[global] --> ST
  ST[(Memory store<br/>one SQLite db)] --> F[Facts · namespaced JSON, optional TTL]
  ST --> H[Message history · ordered threads]
  ST --> N[Notes · append-only knowledge, supersession + status gate]
  style ST fill:#def,stroke:#36c
```

## Four layers

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

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

| Layer | API | Use for |
|---|---|---|
| Facts | `store.setFact(ns, key, value, ttlMs?)` / `store.getFact(ns, key)` / `store.listFacts(ns)` / `store.listAllFacts()` | Namespaced JSON facts. Optional TTL. Last-write-wins. `listAllFacts()` returns every fact across all namespaces (what `memory list` prints with no namespace). |
| Message history | `store.createThread(ns, title?)`, `store.listThreads()`, `store.saveMessage(msg)`, `store.listMessages(threadId, limit)`, `store.deleteMessages(threadId, messageIds)` | Ordered chat threads per agent or user; list/delete to compact history. |
| Notes | `store.saveNote(input)` / `store.getNote(id)` / `store.listNotes(ns, filter?)` / `store.setNoteStatus(id, status)` / `store.enableNoteSearch(kind)` / `store.searchNotes(kind, query, limit?, filter?)` | Append-only knowledge (lessons, decisions, runbook rules). Notes never mutate; they die by supersession or rejection, not TTL. SQLite backend only. |
| Maintenance | `store.deleteExpiredFacts()` plus processors | TTL cleanup and history compaction. |

Every method has an Effect-returning twin (`store.listThreadsEffect`, `store.deleteMessagesEffect`, etc.) for use inside an Effect pipeline.

## Namespaces

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

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.

## Task Memory Metadata

```tsx
<Task
  id="review"
  output={outputs.review}
  agent={reviewer}
  memory={{
    recall:   { namespace: ns, topK: 3 },               // inject relevant past facts into prompt
    remember: { namespace: ns, key: "last-review" },    // persist this output as a fact
    threadId: `${ctx.input.repo}:reviews`,              // append messages to this thread
  }}
>
  Review the latest PR.
</Task>
```

`memory` is preserved on the task descriptor as metadata for runtimes and integrations that layer memory behavior onto task execution. The direct store APIs above are the current public memory read/write surface.
## Imperative get/set/delete inside a workflow

The `memory={{ recall, remember }}` block above is **declarative metadata**; it is preserved on the task descriptor for runtimes that layer memory onto task execution, not an imperative call. To actually get, set, or delete a fact **while a run is executing**, build a store with `createMemoryStore(db)` and call it from inside a compute `<Task>` (a function task with no `agent`). The compute callback receives `deps` only; there is no injected `store`, so you create one:

```tsx
import { createMemoryStore } from "smithers-orchestrator/memory";
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" };

<Task id="remember-model" output={outputs.done}>
  {async () => {
    await store.setFact(ns, "model", "gpt-4");           // set
    const fact = await store.getFact(ns, "model");        // get (MemoryFact | undefined)
    const all  = await store.listFacts(ns);               // list
    await store.deleteFact(ns, "stale-key");              // delete
    return { done: true };
  }}
</Task>
```

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**: 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.

```ts
const ops = { kind: "user" as const, id: "ops-team" };

// Bank a lesson (append-only; provenance records which run learned it).
const lesson = await store.saveNote({
  namespace: ops,
  body: "Warm the cache tier before scaling deploys back up.",
  kind: "lesson",
  provenance: { runId: ctx.runId, nodeId: "bank" },
});

// Propose ONE rule that replaces piled-up lessons; pending hides nothing.
const rule = await store.saveNote({
  namespace: ops,
  body: "Always pre-warm caches after scale-down.",
  kind: "runbook-rule",
  status: "pending",
  supersedes: [lesson.id],
});

// The human gate: accepting the rule hides the lessons it replaces.
await store.setNoteStatus(rule.id, "accepted");

// Recall live knowledge only (accepted, not superseded).
const live = await store.listNotes(ops);
```

Full-text search is **lazy and opt-in per namespace kind**: nothing is
indexed (and note writes pay nothing) until `enableNoteSearch(kind)`, which
creates the FTS index and backfills. `searchNotes(kind, query)` spans every
namespace of the kind; pass `{ namespace }` in the filter to stay
namespace-local on a shared database. Notes require the SQLite backend and
fail loud on Postgres/PGlite. See
[`examples/incident-runbook-memory.jsx`](https://github.com/smithersai/smithers/blob/main/examples/incident-runbook-memory.jsx)
for the full recall → triage → bank → distill → ratify loop.

## Processors

Maintenance jobs you run periodically:

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

const gc         = TtlGarbageCollector();   // expire facts past their TTL
const limiter    = TokenLimiter(4000);      // keep history under token budget
const summarizer = Summarizer(myAgent);     // compress old messages with an LLM

await gc.process(store);
await limiter.process(store);
await summarizer.process(store);
```

```mermaid
flowchart LR
  M[Facts + messages accumulate] --> GC[TtlGarbageCollector<br/>drop expired facts]
  GC --> TL[TokenLimiter<br/>trim history to budget]
  TL --> SM[Summarizer<br/>compress old messages with an LLM]
  SM --> K[Compact, in-budget memory]
  style K fill:#dfe,stroke:#3a3
```

## Inspect from the CLI

```bash
bunx smithers-orchestrator memory list workflow:code-review -w workflow.tsx
```

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

## Notes

- Memory and task outputs are **distinct stores**. Don't use memory for run-scoped state; it's not transactional with the workflow's frame commits.
- Working-memory writes are unordered. Use message history when sequence matters.

---

## Memory Quickstart

> Folded into the opt-in memory fragment.

This material is now in the opt-in [`/llms-memory.txt`](/llms-memory.txt) fragment.
