MemorySyncMemorySync

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/add turns 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 created and from then on are addressable by their numeric id.
  • The classification is preserved on the record as extracted_type (e.g. fact, event, decision) plus a canonical extracted_key used for deduplication.

The three scopes every memory lives in

ScopeFieldBehaviour
Useruser_id (required, foreign key, cascade delete)Hard ownership. A query physically cannot return another user's memories — the vector namespace is per-user.
Projectproject_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.
Environmentenvironment (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

  • text and summary live on the row as text_ciphertext and summary_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_status moves past active, 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.