store_memory
Write-behind replication — the local commit returns in milliseconds; ciphertext sync happens in the background.
store_memory writes a record to the local LanceDB index and returns. It does not
wait for the replication endpoint. The contract is "durable locally, eventually
durable remotely" — a write-behind pattern with a transactional local commit.
This is the single most important operational property of sovseal: no tool call ever blocks on I/O.
The contract
| Property | Guarantee |
|---|---|
| Local durability on return | Yes — fsync'd to disk before the call returns |
| Remote durability on return | No — replication is asynchronous |
| Latency p50 | 3.8 ms |
| Latency p95 | 7.2 ms |
| Latency p99 | 12.5 ms |
| Network behavior | 0 RTT on the call path — replication is a background job |
| Failure mode if replication is offline | Buffered locally; flushed when the network returns |
The write pipeline, end to end
┌───────────────────────────────────────────────────────────────────┐
│ Application │
│ │
│ await memory.store("user.editor", { value: "neovim" }) │
│ │ │
└──────────────────────────────┼────────────────────────────────────┘
▼
┌───────────────────────────────────────────────────────────────────┐
│ sovseal SDK │
│ │
│ 1. canonicalize(payload) ─► stable JSON bytes │
│ 2. embed(payload) ─► 384-d vector │
│ 3. encrypt(payload, key, n) ─► ciphertext + auth_tag │
│ 4. snapshot_id = │
│ sha256(canonicalize(payload) ‖ parent_id) │
│ 5. lancedb.upsert({ id, embedding, ciphertext, … }) │
│ 6. fsync │
│ 7. enqueue(replication_job) │
│ 8. return ◄────────────────────────────── caller resumes here │
│ │
└──────────────────────────────┬────────────────────────────────────┘
│ (background, write-behind)
▼
┌───────────────────────────────────────────────────────────────────┐
│ Replication worker (in-process) │
│ │
│ • Batch up to N jobs (default 32 or 250ms) │
│ • POST /v2/replicate/push { records: [ … ] } │
│ • On 2xx: mark records as replicated │
│ • On 4xx: surface to telemetry, do not retry │
│ • On 5xx / network error: exponential backoff, infinite retry │
│ │
└───────────────────────────────────────────────────────────────────┘PII redaction (before embedding)
Every write funnels through one client-side chokepoint that masks high-risk PII before the embedding and encryption steps above. The redaction happens on the plaintext, so the secret never reaches the 384-d vector, the at-rest LanceDB row, or the ciphertext that replication pushes. This is pure local middleware — there is no cloud surface for redaction.
| Rule | Default | Detects |
|---|---|---|
ssn | on | US Social Security numbers (123-45-6789) |
credit_card | on | 13–19 digit PANs, Luhn-validated to suppress false positives |
api_key | on | sk_live_ / sk_test_ / AKIA / ghp_ / gho_ / glpat- / xoxb- / xoxp- |
email | off | Email addresses (opt-in) |
phone | off | Phone numbers (opt-in) |
aws_secret | off | 40-char AWS secret access keys (opt-in) |
Matches are replaced with [REDACTED:…] tokens. The store_memory response
reports redacted (a count) and redactedRules; the matched values are never
echoed back or logged.
Configuration & plan gating
Rules are configured under the redaction key in ~/.sovseal/config.json:
{
"plan": "growth",
"redaction": {
"rules": { "email": true, "ssn": false },
"custom": [{ "name": "employee_id", "pattern": "EMP-\\d{6}" }],
"auditLog": false
}
}| Plan | Redaction capability |
|---|---|
| Hobby / Starter | Default trio only. Disable attempts, opt-in enables, and custom regex are ignored — the trio cannot be turned off. |
| Growth | Full control: enable/disable any rule and add custom regex rules. |
| Pro / Enterprise | Full control plus a persistent redaction audit log at ~/.sovseal/redaction-audit.log (PII-free: rule names + counts only). |
The active plan is read from the plan field or the SOVSEAL_PLAN environment
override. A redaction.applied event (rule names + counts, never raw values) is
emitted to the local log on every redaction.
Retention & TTL
By default a memory persists forever. A plan-gated retention engine can stamp a time-to-live on each write so memories are automatically purged once they expire — the foundation for GDPR "right to erasure" workflows.
- On write, each memory is stamped with an
expires_atdeadline (0= never) derived from the active namespace's TTL. Reinforcing a memory resets its TTL window, so a fact you keep restating is not purged on its original deadline. - A local sweep runs every 24h, hard-deleting expired on-device vectors.
- For memories that were synced, a daily cloud cron purges the replicated
(still-encrypted) blob server-side, but only after a mandatory 7-day grace
period past the deadline — a recovery window before deletion is irreversible.
Each purge emits a PII-free
retention.purgedwebhook.
Configure it under the retention key in ~/.sovseal/config.json:
{
"plan": "growth",
"retention": {
"defaultTtlDays": 90,
"namespaces": { "ephemeral": 7 },
"auditLog": false
}
}TTLs are in days and clamped to [7, 365]. A per-namespace value overrides the
default; the active namespace comes from SOVSEAL_NAMESPACE (default "default").
| Plan | Retention capability |
|---|---|
| Hobby / Starter | No retention. Any TTL config is ignored — memories are kept forever (fails closed: never deletes without an entitlement). |
| Growth | Namespace TTL of 7–365 days. |
| Pro / Enterprise | TTL plus a persistent retention audit log / purge reports at ~/.sovseal/retention-audit.log. |
Calling it
import { sovseal } from "@sovseal/sdk";
const memory = new sovseal({ apiKey: process.env.SOVSEAL_API_KEY });
await memory.ready();
const result = await memory.store("user.preferences.testing", {
framework: "vitest",
reason: "ESM-native; faster cold start than Jest",
});
console.log(result.snapshotId); // e.g. "snap_4f3ad8…"
console.log(result.replicated); // false — write-behind, expected// Tool call payload sent by the AI client
{
"name": "store_memory",
"arguments": {
"path": "user.preferences.testing",
"payload": {
"framework": "vitest",
"reason": "ESM-native; faster cold start than Jest"
}
}
}
// Response
{
"success": true,
"data": {
"snapshot_id": "snap_4f3ad8…",
"replicated": false
},
"timestamp": 1716301928117
}# This is what the SDK does under the hood, in background batches.
# You almost never call this directly.
curl -X POST https://your-endpoint/v2/replicate/push \
-H "Authorization: Bearer sov_live_…" \
-H "Content-Type: application/json" \
-d '{
"records": [{
"path_hash": "4f3ad801…",
"ciphertext": "base64…",
"nonce": "base64…",
"auth_tag": "base64…",
"parent_id": "snap_1a2bc9d0…",
"snapshot_id": "snap_4f3ad8…"
}]
}'Parameters
| Param | Type | Required | Notes |
|---|---|---|---|
path | string | ✓ | Logical key. Never sent to the server (only sha256(path) is). |
payload | object or string | ✓ | Canonicalized to JSON before hashing & encryption. |
metadata | object | Searchable, encrypted alongside the payload. | |
parent | string (snapshot id) | Override HEAD as the parent (used by fork). |
Failure modes — and what happens
| Failure | What the SDK does |
|---|---|
| Local disk is full | store rejects before fsync; the snapshot is not created |
| Embedder is still warming up | await memory.ready() resolves first; the call queues briefly |
| Replication endpoint returns 401 | Surfaced via memory.on("replication-error", …); local write is unaffected |
| Replication endpoint returns 5xx | Exponential backoff, infinite retry; records remain in the local outbox |
| Network is offline | Same as 5xx — buffered locally, flushed on reconnect |
| Process crashes between commit & ack | Records survive in the outbox; next process start re-flushes |
Backpressure
If replication falls far enough behind that the local outbox grows past
maxOutboxBytes (default 256 MB), store begins to block until the worker drains.
This is the only condition under which a write can stall.
What gets sent to the server
POST /v2/replicate/push
{
"records": [
{
"path_hash": "sha256(path)", ← NOT the path itself
"ciphertext": "AES-256-GCM(payload, key)", ← server cannot decrypt
"nonce": "...",
"auth_tag": "...",
"parent_id": "...",
"snapshot_id": "...",
"created_at": "..."
}
]
}What is NOT sent:
- The path itself
- The plaintext payload
- The encryption key
- The embedding vector
- Any metadata not explicitly serialized into the payload
This is enforced at the SDK boundary. If you want to verify, run with
SOVSEAL_LOG_REPLICATION_BODIES=1 and inspect the requests.
Next
- recall_memory — how stored records become query results.
- Verified Semantic Recall — what prevents a malicious server from substituting your ciphertext on restore.
- Replication & Sync — deep dive on the background worker.