Skip to main content

Memories

The individual facts inside a memory store — embedded, deduplicated, and superseded rather than deleted.

Overview​

A memory is one fact: a sentence of content, a source_type saying whether there is a source to point at, and tags. Writing one embeds it, so knowledge search can rank it against a query alongside the project's documents.

Memories are not a log. A near-duplicate write is skipped rather than appended, and a memory that gets contradicted is invalidated and pointed at its replacement instead of being removed — so what an agent recalls stays coherent while the history of how it got there stays readable.

This module is a verbatim mirror of the runtime: every field, method, status code and error shape is the runtime's own, re-rooted under the project in the path.

A plan caps how much your account stores

Free stores 1 GB, Pro 30 GB, Business 100 GB; an Enterprise ceiling is set by contract. Creating a memory past that answers 403 plan_limit_reached with resource: "storage", and the plan, the limit and what your account is holding as storage_gb in details.

The ceiling is your account's, not each project's: every project the account pays for draws on the same figure, so one project may hold all of it. Read where you stand from storage_gb and storage_limit_gb on GET /v1/users/me/billing; the usage route below reports one project at a time.

A memory counts for its text plus its embedding vector, which is about 4 kB whatever the text — so many small memories cost far more than their words suggest. The same ceiling covers files and documents; read your current figure from the gb_day component of GET /v1/projects/{project_id}/usage under meter_type=storage.

Reading, updating and deleting stay open at the ceiling; only creates are refused.

See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Memories.

Data Model​

Memory​

FieldTypeDescription
idstringPublic memory ID (mem_ prefix).
memory_store_idstringThe memory store holding it. Required on create.
contentstringThe fact. Required on create.
source_typestringmanual or conversation — see Where a fact came from.
tagsobject, nullablePer-memory key-value tag map.
metadataobject, nullableArbitrary structured metadata, stored verbatim.
source_idstring, nullablePublic ID of the conversation the fact was learned in, when source_type is conversation. Null for manual.
invalidated_atstring (date-time), nullableNon-null once superseded — see Superseded, not deleted.
superseded_by_memory_idstring, nullableThe memory that replaced this one.
created_atstring (date-time)
updated_atstring (date-time)

A write response adds one field:

FieldTypeDescription
actionstringcreated, skipped or superseded — see What a write returns.

Key Concepts​

Listing is always per store​

GET /v1/projects/{project_id}/memories requires memory_store_id; there is no cross-store listing. That follows from what a store is — a scope — and it means a caller cannot accidentally read one customer's facts while paging through another's. Give an agent long-term memory lists its store this way.

Invalidated memories are left out by default. Pass include_invalidated=true to see the full history, which is what you want when answering "why does the agent think that?".

Where a fact came from​

source_type says whether there is a source to point at. conversation means source_id names the conversation the fact was learned in, and is required with it; manual means there is nothing to point at — a direct API write, or an agent write made outside a conversation — and rejects source_id.

source_id is a loose pointer rather than a foreign key: deleting the conversation leaves the id in place, so the record of where a fact came from outlives its source.

What a write returns​

Every write is resolved against what the store already holds, by cosine similarity to the closest valid memory:

SimilarityactionStatusWhat happens
≥ duplicate_thresholdskipped200The fact is already known; the existing memory is returned unchanged.
≥ supersede_thresholdsuperseded200The same fact, changed. The match is invalidated and the replacement is returned.
below bothcreated201A new memory.

Below supersede_threshold, cosine cannot tell "same fact, changed" from "related but distinct" (prefers email vs prefers Portuguese), and embeddings sit close on negations — so created is the outcome there. A near-duplicate stays searchable; a wrongly retired fact does not.

On a supersede the retired memory's tags and metadata are shallow-merged onto the replacement (incoming keys win), so a replacement stays inside every tag-scoped search and policy the original satisfied.

Where the thresholds come from​

Both cutoffs resolve per request, most specific first:

LevelSet onScope
Per requestduplicate_threshold / supersede_threshold on the create bodythat one call
Store defaultthe same two fields on the memory storethe corpus's dedup policy
Algorithmnull at both levelsthe built-in constants

The effective pair must satisfy supersede_threshold < duplicate_threshold. Equal makes superseded unreachable; inverted swallows skipped. Both are refused with 400, and the check runs against the effective pair — so a body overriding only one value cannot invert it against the store's other one.

Writing by hand versus by agent​

Every door consolidates, and they differ only in what supplies the fact. This endpoint takes one you wrote. When an agent has knowledge_config.write_memory_store_id set, the write_memory tool becomes available to it, and the agent spends it mid-turn at its own discretion (Limit what an agent may do sets it up). A memory rule on the store is the third: it reads completed turns and writes what its handler proposes, as in Give an agent long-term memory.

Which door a write came through is recorded on the assertion, not inferred from the result.

Every write is recorded​

Every write attempt is appended to a ledger, whatever its outcome — a skip that left no trace would make "why is this fact not in the store?" unanswerable.

FieldTypeDescription
idstringPublic assertion ID (massert_ prefix).
memory_store_idstringThe store written to.
memory_idstring, nullableWhat the write resolved into: the new memory for created and superseded, the memory that matched for skipped.
superseded_memory_idstring, nullableThe memory this assertion retired, on a superseded outcome.
generation_idstring, nullableThe generation that made the write, when an agent made it.
mechanismstringWhich door the write came through.
outcomestringcreated, superseded or skipped.
similaritynumber, nullableCosine similarity to the closest match.
created_atstring (date-time)

Read it from either end — one memory's history, or a store's whole write log:

Validity lives on the memory (invalidated_at, superseded_by_memory_id), not on the assertion: it is the filter on every read, and an invalidation with no replacement has no assertion to carry it.

Superseded, not deleted​

An invalidated memory is excluded from listing, from deduplication and from search — it stops affecting answers immediately — but the row stays, stamped with invalidated_at and superseded_by_memory_id. Following that pointer forward, or listing with include_invalidated=true, reconstructs the chain of what the agent believed and when it changed.

Deleting a memory, by contrast, removes it.

Who may do what​

Every route needs any project member.

Examples​

Seed a store with a fact​

naturali create-memory \
--project-id proj_V1StGXR8Z5jdHi6B \
--memory-store-id mstore_V1StGXR8Z5jdHi6B \
--content "Acme is on the enterprise plan and has a 4-hour response SLA." \
--source-type manual \
--tags '{"account":"acme"}'

Read what a store holds​

naturali list-memories \
--project-id proj_V1StGXR8Z5jdHi6B \
--memory-store-id mstore_V1StGXR8Z5jdHi6B \
--limit 20

Trace why the agent believes something​

naturali list-memories \
--project-id proj_V1StGXR8Z5jdHi6B \
--memory-store-id mstore_V1StGXR8Z5jdHi6B \
--include-invalidated true

Correct a fact​

naturali update-memory \
--project-id proj_V1StGXR8Z5jdHi6B \
--memory-id mem_V1StGXR8Z5jdHi6B \
--content "Acme is on the enterprise plan and has a 2-hour response SLA."