Skip to main content

The shape of it

Memory is distinct from an ordinary variable: memory x binds x in whatever scope it’s declared in — a function, session, or agent — exactly like model already does. No new scoping mechanism was needed.

Members

1

remember(key, value)

Stores value under key. key must be a String — a SenseRuntimeError otherwise.
2

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.
3

forget(key)

Removes a key. Forgetting a key that was never there is a no-op, not an error.
4

keys()

Returns an Array<String> of everything currently remembered.

kind is a hint, not an enum

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.
Backed by an in-process Python dictnot persisted across runs, the same “in-memory only” honesty as audit_log(). See “Persistence and versioning” below for the durable counterpart.

Persistence and versioning: persistent memory

persistent in front of memory swaps the in-process dict for a durable, versioned store:
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.”
1

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.
2

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.
3

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.

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:
1

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.
2

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.
3

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.
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.
Every operation on a persistent memory is internally locked, so two async agents 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.
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:
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 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.

Continue

Sessions

The same scoped-binding mechanism memory reuses.

Agents

The concurrency model that motivated persistent memory’s internal lock.

Tool-Calling

Letting a model write to memory itself, via a tool wrapper.