Memory Model
A memory in MemorySync is a single durable record of something a person, agent, or integration wanted the system to remember. It owns content, metadata, a vector representation, a place in a relationship graph, and a lifecycle. This page lays out what the record is for, what it is not, and how to think about it.
A memory in one paragraph
A memory is an encrypted, vector-indexed, lifecycle-managed record. It carries the original text (encrypted at rest), an optional model-produced summary (also encrypted), an embedding vector for semantic recall, a structured metadata blob the caller chose, and a status row that tracks where it sits in its retention timeline. Every read filters by the owning user; every write commits to the durable store before any background work starts.
What gets stored and what gets discarded
- Not every
POST /memory/addturns into a row. The candidate runs through an extraction pipeline that classifies it and may reject it. - Discard reasons are recorded explicitly:
discarded_low_signal,discarded_redundant,discarded_transient,discarded_policy. - Accepted candidates are written with decision
createdand from then on are addressable by their numericid. - The classification is preserved on the record as
extracted_type(e.g.fact,event,decision) plus a canonicalextracted_keyused for deduplication.
The three scopes every memory lives in
| Scope | Field | Behaviour |
|---|---|---|
| User | user_id (required, foreign key, cascade delete) | Hard ownership. A query physically cannot return another user's memories — the vector namespace is per-user. |
| Project | project_id (optional, foreign key, set-null on delete) | Soft scope. If the caller has a project set, every read filters on it. Memories with project_id IS NULL are visible to all of the user's projects. |
| Environment | environment (default "production", check-constrained) | Allowed values: development, staging, production. API keys are scoped to one environment; cross-env reads require explicit multi-env keys. |
The encryption promise
textandsummarylive on the row astext_ciphertextandsummary_ciphertext— the database never sees plaintext.- Each tenant has its own data encryption key. Decryption happens on the read path using the tenant's key; cross-tenant decryption is structurally impossible.
- If a key has been rotated, old memories are flagged for re-encryption by a background pass. Old keys remain usable read-only for 30 days so reads keep working during the rollover.
The relationship graph
Memories are not isolated. A separate edge table records typed relationships between them: supports, extends, continuation, detail_of, summary_of, similar, references, derived_from, caused_by, contradiction. These edges power multi-hop recall and the explanation that ships back with each query result.
What a memory record is <em>not</em>
- It is not a chat message. Chat lives in sessions; memories are extracted from chat as a deliberate decision.
- It is not a document. Source documents are tracked separately as
external_objects; a memory is the distilled fact that came out of one. - It is not editable in place. An update produces a new vector with a new
vector_id; the index is upserted, not patched. - It is not visible after deletion. Once
retention_statusmoves pastactive, the record disappears from every recall path even before the row is removed.
How to reason about a memory in your code
Treat the id as the only handle. Fields like tags, importance, and metadata are advisory — the system uses them but does not require them. Fields like retention_status, vector_id, extracted_type, and extracted_key are owned by the platform — the API will reject writes that try to override them. If a memory needs to outlive the default 90-day policy, the platform's retention rules govern that decision; do not set retention_expires_at directly.