---
slug: 61-memory
title: Memory & state
description: Declared persistent agent memory (require memory + remember/recall), per-agent namespaces, crash-resumable progress, owner rules, and per-request serve state.
example_ids: [memory]
---

# Memory & state

Persistent agent state is **declared, not implicit**. One line at the top of the program names the memory and unlocks the whole family — memory, owner rules, and progress:

```synsema
require memory("support-agent")
```

The declared name **is** the identity: state persists in `<program-dir>/.synsema/state/<name>.db` (gitignore `.synsema/`), keyed by that name — not by the filename. `remember()` in one run is found by `recall()` in the next; renaming the `.syn` file changes nothing; two entry points that declare the **same** name share one memory.

```synsema
-- Doc example: DECLARED agent memory, progress, and owner rules (persist to SQLite).
-- The declared name IS the identity: state lives in .synsema/state/doc-memory.db,
-- keyed by the name below — not by this file's name. Without the declaration the
-- whole family fails with `Capability not granted: memory` and creates no files.
intent: "doc example: declared memory, namespaces, progress, rules"
require memory("doc-memory")

remember("learning", "demo note from run", ["demo"])
print("recalled → " + text((recall("learning", ["demo"])[0])["content"]))

test "remember then recall — newest first ([0] is the most recent)"
    remember("learning", "API is slow on Mondays", ["api"])
    let notes be recall("learning", ["api"])
    assert_eq((notes[0])["content"], "API is slow on Mondays")

test "recall named args: limit caps results; from filters by writer namespace"
    remember("context", "note-a", ["ns"])
    remember("context", "note-b", ["ns"])
    assert_eq(length(recall("context", ["ns"], limit = 1)), 1)
    -- top-level writes source = "main"; agents write under their own name
    assert(length(recall("context", ["ns"], from = "main")) >= 2)
    assert_eq(length(recall("context", ["ns"], from = "nobody")), 0)

test "progress: resume_point returns the step to resume from"
    create_progress("import", ["ingest", "validate", "load"])
    start_step("import", "ingest")
    complete_step("import", "ingest", "100 rows")
    assert_eq(resume_point("import"), "validate")

test "owner rules: a numeric must-rule flags a violation"
    add_rule("max_discount", "must", "discount <= 0.20", "pricing")
    assert(length(check_rules("pricing", {"discount": 0.25})) > 0)
```

## The declaration (know this before anything else)

- **Without it, nothing persists** — `remember`, `recall`, `add_rule`, `create_progress`, and the rest of the family fail with `Capability not granted: memory` (which names the exact line to add), and **no file or directory is created**. `memory` is never auto-granted, not even under `run`: it writes files to disk.
- **One name per program.** Two `require memory` with different names is a startup error. Names match `[a-zA-Z0-9_-]+` — no paths, no empty string; invalid names fail at the declaration. Bare `require memory` (no name) is a parse error.
- **Every execution context shares it**: `run`, `test`, `serve` handlers, cron ticks, `parallel_map` workers, and spawned agents all see the program's one declared memory, saved on every write (a crash loses nothing already written).
- **Capability semantics like any other**: denied inside `sandbox`; `call_tool` intersects it (a tool must declare `require memory("<name>")` itself); the host ceiling gates it (`--cap-set` without `memory` denies the family and creates no file; `--cap-set "memory=shop-*"` allows names under a prefix).
- **Env vars:** `SYNSEMA_STATE_DIR` relocates the state directory (handy in tests). `SYNSEMA_STATE_NAME` is deprecated and ignored with a warning — the declaration replaced it.
- **Upgrading:** if an older engine left a `<file-stem>.db` behind, running without a declaration prints a warning with the exact `require memory("<stem>")` line to keep it; the schema is unchanged (renaming the `.db` is the whole migration). In the REPL, typing `require memory("x")` enables a session-only in-memory store.

## Persistent memory

```synsema
require memory("assistant")
remember("learning", "API slow on Mondays", ["api", "performance"])
let notes be recall("learning", ["api"])        -- newest first ([0] = most recent)
let hits  be recall(nothing, nothing, "Monday") -- free-text search (skip args with nothing)
forget_memory(entry_id)
```

A recall entry is a map with `id, category, content, source, tags`.

**`recall` takes 6 args** — `recall(category, tags, search, mode, limit, from)` — all optional, all usable as named args (`recall("learning", limit = 10)`):

| Arg | Meaning |
|---|---|
| `category` | one of the fixed categories (below) |
| `tags` | list; **OR** by default |
| `search` | free-text substring match |
| `mode` | `"all"` = every tag must match (AND) |
| `limit` | max entries, default 200 |
| `from` | `source` namespace to read (see next section) |

> **Categories are a fixed English set:** `preference`, `rule`, `learning`, `decision`, `context`. Any other string (e.g. `"preferencia"`) raises an error.

## Per-agent namespaces (`source` / `from`)

Each entry records its writer in `source`: inside `agent X` a `remember` writes `source = "X"`; top-level code (and serve handlers) write `"main"`. Reads are namespaced by default, so agents sharing one memory don't confuse each other's notes:

```synsema
require memory("newsroom")

agent Writer
    remember("context", "draft done")        -- source = "Writer"

agent Analyzer
    let mine   be recall()                   -- only Analyzer's own entries (default)
    let theirs be recall(from = "Writer")    -- explicit cross-namespace read
    let all    be recall(from = "*")         -- everything

spawn Writer
spawn Analyzer
let everything be recall()                   -- top-level sees ALL by default
```

Rules and progress are **not** namespaced — one rulebook, one plan board per program.

## Progress (crash-resumable)

```synsema
require memory("import-agent")
create_progress("import", ["ingest", "validate", "load"])
start_step("import", "ingest")
complete_step("import", "ingest", "100 rows")     -- or fail_step(...)
let where be resume_point("import")               -- "validate" — where to resume after a restart
```

## Owner rules

```synsema
require memory("pricing-agent")
add_rule("max_discount", "must", "discount <= 0.20", "pricing")
let violations be check_rules("pricing", {"discount": 0.25})   -- non-empty → violated
```

Levels: `must` (hard block), `should` (warning), `avoid` / `prefer` (preferences). Numeric conditions are evaluated against the context map. `get_rules(category?)` lists them; `memory_summary()` prints an overview of everything stored.

## Serve state (per-process, in-memory)

Under `serve`, `state_*` builtins (e.g. `state_incr("visits")`) share **in-memory** state across requests — see **[HTTP server](/en/0.6.x/40-serve)**. (They're a serve feature, not available in plain `run`, and they need no `memory` declaration — they never touch disk.) For durable state, use the declared memory/progress builtins above or a database.
