Skip to content

Typed memories

Agent memory is not a vector store with extra steps. A memory is a typed record inside a profile database, and the type determines its lifecycle: facts and instructions supersede, events accumulate, tasks expire — the store enforces the semantics instead of your prompt.

FieldNotes
idcontent-addressed: mem_ + truncated SHA-256 of (type, topic_key, canonical content)
typefact · event · instruction · task
topic_keythe supersession key; valid only on fact and instruction (e.g. user.diet)
summaryone-line natural-language gist; FTS-indexed
contentJSON payload — the full memory
keywordsoptional space-separated terms; FTS-indexed alongside summary
embeddingoptional f32 vector; powers the vector recall channel. Bring-your-own by default; with auto-embedding the node embeds summary + keywords at ingest
session_idoptional grouping; required semantics only for tasks — see Sessions
sourceoptional provenance: which agent wrote this (e.g. claude-code, cursor). Free-form, returned on every memory, filterable at recall
superseded_by / superseded_atset when a newer memory replaces this one; history preserved
expires_attasks only; default 24 h TTL

Ingest results carry a status per memory: created, revived, or duplicate (see below).

TypeSupersessionEmbeddingsLifetime
factby (type, topic_key) — newer replaces older, old row keptyesdurable
instructionsame as factyesdurable
eventnever — events accumulateyesdurable
taskneverskipped (no vector channel)TTL, session-scoped, default 24 h

Supersession is a state machine, not a delete. Ingesting a fact whose topic_key already has an active memory marks the previous row superseded_by = <new id> in the same transaction.

Running example — the user’s diet changes:

Terminal window
# 1. initial fact
curl -X POST http://localhost:8080/v1/memory/acme/alice/memories \
-H 'content-type: application/json' \
-d '{"memories": [{"type": "fact", "topic_key": "user.diet",
"summary": "vegetarian since 2024", "content": {"diet": "vegetarian"},
"keywords": "food preference"}]}'
# → {"results": [{"id": "mem_a1…", "status": "created", "superseded": []}], "txid": 41}
# 2. the fact changes
curl -X POST http://localhost:8080/v1/memory/acme/alice/memories \
-H 'content-type: application/json' \
-d '{"memories": [{"type": "fact", "topic_key": "user.diet",
"summary": "vegan since 2026", "content": {"diet": "vegan"},
"keywords": "food preference"}]}'
# → {"results": [{"id": "mem_b2…", "status": "created", "superseded": ["mem_a1…"]}], "txid": 42}

Recall now returns only the vegan fact. The vegetarian row is preserved: GET /v1/memory/acme/alice/memories/mem_a1… returns it with superseded_by and superseded_at set, and fetching mem_b2… lists mem_a1… under supersedes — the full chain in both directions. Recall with "include_superseded": true exposes superseded rows in results.

Events never supersede: {"type": "event", "summary": "ordered the vegan tasting menu"} simply accumulates alongside every previous event.

The id is a content hash, so ingest is idempotent by construction:

  • Re-ingesting an already-active memory → status: "duplicate", no write.
  • Re-ingesting a memory that exists but was superseded → status: "revived": the old row becomes active again and the supersession pointer moves. Re-asserting an old fact reinstates it.
  • Anything else → status: "created".

Agents can replay extraction output without flooding the store with copies.

When several agents share one profile — a coding assistant, an IDE agent, a support bot, all acting for the same user — source records who wrote each memory:

{ "memories": [
{ "type": "fact", "topic_key": "user.indentation", "summary": "prefers tabs",
"content": {"indent": "tabs"}, "source": "claude-code" }
] }

Source is provenance, not identity. It is excluded from the content hash, so the same memory ingested by two agents still dedupes to one record — the first writer’s attribution sticks, and duplicate/revived outcomes never overwrite it. Supersession is likewise profile-wide: a fact from one agent supersedes the same topic_key from another, because shared memory is the point.

You rarely set source by hand. The MCP server defaults it from the MEMOTURN_SOURCE environment variable (set per agent in each tool’s MCP config), the TypeScript and Python SDKs take a client-level source applied to every ingest, and the CLI has --source.

POST /v1/memory/{ns}/{profile}/memories takes a batch; a batch is atomic and returns one txid (concurrent batches may group-commit and share a txid). Batches cap at 1,000 memories per request. The profile auto-creates on first ingest.

{ "memories": [
{ "type": "fact", "topic_key": "user.diet", "summary": "vegan since 2026",
"content": {"diet": "vegan"}, "keywords": "food preference" },
{ "type": "event", "summary": "deployed v2 to prod",
"content": {"version": "v2"}, "session_id": "s-417" },
{ "type": "task", "summary": "follow up on refund #88", "content": {}, "ttl": 86400 }
] }

Response:

{ "results": [
{"id": "mem_b2…", "status": "duplicate", "superseded": []},
{"id": "mem_c3…", "status": "created", "superseded": []},
{"id": "mem_d4…", "status": "created", "superseded": []}
],
"txid": 43 }

Typed-fact ingest with a 256-dim embedding and supersession measured 3.9 ms p50 on the prototype.

Deciding what is memorable — extraction — is the client’s job by default; the optional server-side extraction endpoint distills raw transcripts into typed memories through this same ingest path. See Extraction.

DELETE /v1/memory/{ns}/{profile}/memories/{id} is the only hard removal — it deletes the row, its FTS entry, and its vector. Everything else (supersession, task expiry) preserves or merely hides history.

Terminal window
curl -X DELETE http://localhost:8080/v1/memory/acme/alice/memories/mem_a1…

By default history is kept indefinitely. Namespaces that need bounded retention can set a governance policy: cap task TTLs (clamped at ingest), age out events, and bound superseded history by age or per-topic count — the node deletes aged-out rows automatically.

When forget isn’t enough — the data must also leave point-in-time-recovery history — verifiable erasure (POST .../erasures) hard-deletes now with secure_delete page zeroing, rewrites object-storage history below the erasure point after a grace window, and proves it with a signed receipt. It can target one memory, a topic’s whole supersession chain, or a session.

Memories live in reserved tables inside the profile database: __memoturn_memories (rows and supersession columns), __memoturn_memories_fts (FTS5 external-content index), __memoturn_memories_vec (vectors, created lazily at the client’s embedding dimension), and __memoturn_memory_sessions. Reserved tables are unreachable from user SQL, and all of them replicate, fork, and rewind with the database as one unit — see Branching.