> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sensecode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Memory

> A key/value store distinct from an ordinary variable — session-scoped by default, durable and versioned with `persistent`.

## The shape of it

```sns theme={null}
memory notes: "working"

notes.remember("topic", "sense keywords")
print(notes.recall("topic"))     # "sense keywords"
print(notes.recall("missing"))   # nil -- unlike a bare variable, no NameError
notes.forget("topic")
print(notes.keys())              # []
```

Memory is distinct from an ordinary variable: `memory x` binds `x` in
whatever scope it's declared in — a function,
[session](/ai-native/sessions), or [agent](/ai-native/agents) — exactly
like [`model`](/ai-native/model-ask-answer) already does. No new scoping
mechanism was needed.

## Members

<Steps>
  <Step title="remember(key, value)">
    Stores `value` under `key`. `key` must be a `String` — a `SenseRuntimeError`
    otherwise.
  </Step>

  <Step title="recall(key)">
    Returns the stored value, or `nil` if the key was never remembered (or
    was forgotten). Unlike an undefined variable, a missing key is not an
    error — memory is meant to be probed speculatively.
  </Step>

  <Step title="forget(key)">
    Removes a key. Forgetting a key that was never there is a no-op, not an
    error.
  </Step>

  <Step title="keys()">
    Returns an `Array<String>` of everything currently remembered.
  </Step>
</Steps>

## `kind` is a hint, not an enum

```sns theme={null}
memory short_term: "short-term"
memory untagged

print(short_term.kind)   # "short-term"
print(untagged.kind)     # nil
```

Short-term/long-term/episodic/semantic/working are *potential*
categories, not a closed set. The optional `: "kind"` string is
stored on `.kind` for introspection — nothing validates it against a fixed
list, so any string is accepted.

<Warning>
  Backed by an in-process Python `dict` — **not persisted** across runs, the
  same "in-memory only" honesty as [`audit_log()`](/safety/actions). See
  "Persistence and versioning" below for the durable counterpart.
</Warning>

## Persistence and versioning: `persistent memory`

`persistent` in front of `memory` swaps the in-process dict for a durable,
versioned store:

```sns theme={null}
persistent memory ledger: "notes.db"   # or just `persistent memory ledger`
                                        # for the default location, below

ledger.remember("name", "ada")
ledger.remember("name", "ada lovelace")   # a new row, not an overwrite
print(ledger.recall("name"))              # "ada lovelace" -- the latest
for line in ledger.history("name"):
    print(line)                            # both versions, in order
print(ledger.as_of("name", 1))            # "ada" -- time travel to version 1
```

<Info>
  Scoped down from "the delta lake model" — Delta Lake itself is a
  distributed engine over Parquet/object storage built for Spark-scale
  analytics, a mismatch for an embedded language's key/value memory. What's
  worth keeping from that idea is the mechanism: **`remember`/`forget`
  always `INSERT`, never `UPDATE`/`DELETE`.** That one rule is what makes
  versioning and auditing the *same* log, for free — `forget(key)` writes a
  tombstone row rather than deleting anything, so `history(key)` still shows
  "this existed and was removed at version N."
</Info>

<Steps>
  <Step title="The optional string means a file path here, not a category hint">
    The `persistent` keyword is what disambiguates which meaning applies.
    An explicit path always wins; see below for the default when it's
    omitted.
  </Step>

  <Step title="Backed by stdlib sqlite3 — no new dependency">
    Opened with `PRAGMA journal_mode=WAL`. Values must be
    JSON-serializable (`Int`/`Float`/`String`/`Bool`/`Nil`/`Array` of
    those — Sense's own types already map onto JSON's) — a `PythonValue`
    or anything else non-serializable raises `SenseRuntimeError` at
    `remember()` time, not silently coerced or dropped.
  </Step>

  <Step title="history(key) and as_of(key, version)">
    `history(key)` returns an `Array<String>` of human-readable version
    lines (same convention as `audit_log()`'s simplified view).
    `as_of(key, version)` returns the value as of that version, or `nil`
    if the key didn't exist yet. `.path` is readable instead of `.kind`.
  </Step>
</Steps>

## Default location, when no path is given

The default is anchored to the `.sns` file itself, not to whichever
directory your shell happens to be in when you run `sense run` — so the
same script always finds the same database, regardless of where you
invoke it from:

<Steps>
  <Step title="SENSE_MEMORY_DIR, if set">
    An explicit, global override for anyone who wants every
    default-location memory in one place they chose — flat under that
    directory (`<dir>/<name>.sense_memory.db`). Setting this is an
    explicit opt-in to one shared location, so a same-named collision
    across two projects is a tradeoff you chose, not a surprise.
  </Step>

  <Step title="Otherwise: next to the declaring file, in .sense_memory/">
    `<the .sns file's own directory>/.sense_memory/<name>.sense_memory.db`
    — consistent no matter which directory `sense run` was invoked from,
    and scoped per file so two unrelated projects both declaring
    `persistent memory ledger` can never collide.
  </Step>

  <Step title="Last resort: the old CWD-relative bare filename">
    Only when there's no real file to anchor to at all — the REPL, or
    `run_source()` called directly with no file path.
  </Step>
</Steps>

<Info>
  An explicit `persistent memory name: "path"` always wins over all three
  and is completely unaffected by any of this. The `.sense_memory/` folder
  (or an explicit path's own missing parent directory) is created
  automatically if it doesn't exist yet.
</Info>

<Warning>
  Every operation on a `persistent memory` is internally locked, so two
  [`async agent`](/ai-native/agents#concurrency-async-agent)s can safely
  write to the same one concurrently. That's an implementation detail, not
  a Sense-level synchronization primitive — none exist for your own code;
  same concurrency caveats `async agent`/`async tool` carry generally.
</Warning>

**What this deliberately doesn't do:** no compaction/`VACUUM` (an
append-only log grows unbounded over a long-running process); no
Parquet/columnar storage (SQLite/JSON-text trades compression for zero new
dependency and real transactions); no cross-memory transactions (two
`persistent memory` declarations are two independent files); no TTL
(silent expiry fights the whole point of an auditable log — it would only
make sense on *session* memory as a cache-eviction feature, and wasn't
built speculatively).

## Reasoning with memory: `ask_with_memory`

`ask_with_memory(prompt, memory)` gives a model what's already in a
`memory` — without a tool call, and without the model needing to ask for
anything. It works with **any** provider, `inference("mock", ...)`
included, since it's plain string composition ahead of an ordinary
`ask()` call:

```sns theme={null}
memory profile
profile.remember("favorite_color", "blue")

set delegation = claude
answer = ask_with_memory("what is my favorite color?", profile)
print(answer.value)
```

**Use it when** you want a model to have context from memory available
by default, with no chance it forgets to look — the memory's entire
current contents are injected ahead of the prompt as a "Known information
from memory:" block (an empty memory leaves the prompt untouched).
Everything else — capability gate, error wrapping, audit behavior — is
identical to a plain `ask()` call. It works with a
[`persistent memory`](#persistence-and-versioning-persistent-memory) the
same way.

This is retrieval only — for letting a model *write* to memory (deciding
on its own what's worth remembering), see
[Tool-Calling: letting a model manage its own memory](/ai-native/tool-calling#letting-a-model-manage-its-own-memory).

## Continue

<CardGroup cols={2}>
  <Card title="Sessions" icon="layer-group" href="/ai-native/sessions">
    The same scoped-binding mechanism `memory` reuses.
  </Card>

  <Card title="Agents" icon="robot" href="/ai-native/agents#concurrency-async-agent">
    The concurrency model that motivated persistent memory's internal lock.
  </Card>

  <Card title="Tool-Calling" icon="hand-pointer" href="/ai-native/tool-calling">
    Letting a model write to memory itself, via a `tool` wrapper.
  </Card>
</CardGroup>
