The shape of it
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
: "kind" string is
stored on .kind for introspection — nothing validates it against a fixed
list, so any string is accepted.
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.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:
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.
