Memory Model
The shape of a record, the role of lineage, and how snapshots compose into agent state.
A sovseal record is the unit of memory. A snapshot is the unit of state. Records compose into snapshots; snapshots chain into lineage. Understanding these three layers is enough to predict every other behavior in the system.
A record, in full
┌────────────────────────────── Record ──────────────────────────────┐
│ │
│ path : "user.preferences.testing" ← never sent │
│ to the server │
│ path_hash : sha256(path) = 0x4f3a…d801 ← what the server │
│ sees instead │
│ │
│ payload : { framework: "vitest", … } ← canonicalized, │
│ then encrypted │
│ embedding : [0.12, -0.44, … 384 dims] ← local only │
│ │
│ ciphertext : AES-256-GCM(payload, key, n) ← what replicates │
│ nonce : 12 random bytes │
│ auth_tag : 16 bytes (GCM) │
│ │
│ parent_id : 0x1a2b…c9d0 ← lineage pointer │
│ snapshot_id : sha256(canonicalize(payload │
│ ‖ parent_id)) │
│ created_at : 2026-05-21T14:32:08.117Z │
└────────────────────────────────────────────────────────────────────┘Three things are worth highlighting:
pathis local only. The replication endpoint seespath_hashand cannot reverse it. If you store"strategy.long_btc.v3", the server cannot tell that you store anything about BTC, longs, or strategies.- The embedding never leaves the device. Even with self-hosted Postgres+pgvector, the remote index stores encrypted vectors only.
snapshot_idis a content hash that includes the parent pointer. This is what makes lineage deterministic and tamper-evident.
Snapshots and lineage
A snapshot is an immutable state of the world at a point in time. Every snapshot points to its parent. The HEAD pointer names the current snapshot.
snapshot₀ (genesis)
│
▼
snapshot₁ ─── store_memory("user.testing.framework", "vitest")
│
▼
snapshot₂ ─── store_memory("user.editor", "neovim")
│
▼
snapshot₃ ─── store_memory("user.testing.framework", "vitest+playwright")
│ (overwrites; the v₁ record is still in the lineage)
▼
HEADWhy parent-pointers, not version numbers
Three reasons:
- Crash recovery is replay. On restart, the SDK walks the lineage from the last known durable HEAD and reconstructs in-memory state. No "is this corrupt?" branch.
- Multi-agent branching is free. Fork a snapshot, give the branch to another agent, merge later. Parent-pointers handle the DAG without coordination.
- Tamper detection is structural. Because
snapshot_idincludes the parent's ID, you can't substitute a snapshot in the middle of the chain without invalidating every descendant.
What composes into "state"
state(HEAD) = fold(lineage from genesis → HEAD)
= latest record per (path) seen along the chainSo if you stored "user.editor" = "vim" at snapshot₁ and "user.editor" = "neovim"
at snapshot₅, then state(snapshot₇) shows "user.editor" = "neovim". The older
record is not deleted from lineage — it's just shadowed by the newer one. This is
what lets you rollback(snapshot₃) and get back the world as it looked at snapshot₃,
byte-for-byte.
Rollback ≠ destruction
rollback(snapshot_n) only moves the HEAD pointer back to snapshot_n. The snapshots
between snapshot_n and the old HEAD remain in lineage and can be reached by forking
or restoring the HEAD again. Nothing is destroyed.
How the model maps to the SDK
| SDK call | What it does to the model |
|---|---|
store(path, payload) | Creates a new record; appends a snapshot to HEAD |
recall(query, opts) | Vector-search across records reachable from HEAD |
delete(path) | Writes a tombstone record; the path stops appearing in recall |
snapshot() | Returns the current HEAD snapshot ID |
fork(snapshot_id) | Creates a branch with snapshot_id as the parent |
rollback(snapshot_id) | Moves HEAD pointer back to snapshot_id |
Constraints you should design around
- Records are not blobs. Payloads are canonicalized JSON before hashing and encryption. If you want to store a 200 MB file, use object storage and put the reference in sovseal.
- Paths are conceptual, not enforced as a tree.
"a.b.c"and"a.b.d"are unrelated keys — there is no parent/child relationship in the path itself. Lineage is the only graph the system tracks. - Embeddings are an index, not the source of truth. If you upgrade the embedder model, the index must be rebuilt. The encrypted records are untouched.
Typing, reinforcement & provenance (schema v2, 0.3.5)
As of the local schema v2, every memory carries metadata that shapes how it is recalled. (The v1→v2 migration is idempotent and lossless — it keeps a memories.bak.lance backup and leaves the replication block byte-identical, so the server stays ciphertext-blind.)
| Field | Values | Role |
|---|---|---|
type | episodic · semantic · procedural | Sets the recall decay half-life (14d / 90d / 180d) and which memories surface in sovseal mind. |
reinforce_count | integer | Incremented when the same fact is stored again (see below). Boosts recall ranking. |
provenance | explicit · observed | Whether the user/agent asked to remember it, vs. it was inferred from context. |
last_reinforced | timestamp | Drives temporal decay. |
Reinforcement (storing the same thing twice)
store_memory deduplicates by content: storing an identical fact does not append a new row — it increments that memory's reinforce_count and refreshes last_reinforced. Recall then rewards reinforcement (score = similarity × decay × reinforcement), so a fact you keep restating becomes "stickier" and out-ranks one-off entries. This is distinct from lineage above: lineage tracks state over time; reinforcement tracks how strongly a memory is held.
See it
sovseal mind surfaces reinforced memories under "Recurring Patterns," and recall_memory factors reinforce_count into ranking. See recall_memory → Reinforcement-Aware Ranking.
Next
- store_memory — how a write becomes a snapshot.
- recall_memory — how a query becomes a result.
- Verified Semantic Recall — the cryptographic check on every read.
- Deterministic lineage — rollback, fork, and crash recovery in detail.