Memory model
Engram stores every memory — short-term and long-term — in a single Prisma
model, Memory (prisma/schema.prisma). This is a deliberate choice: the
two tiers differ in lifecycle, not in shape. A short-term memory that gets
promoted to long-term keeps its id, content, tags, and metadata; only its
type and expiry change. One model means promotion is a field update, not a
cross-table copy, and every read path (recall, list, export, audit) works on
one shape.
The Memory row
Section titled “The Memory row”| Field | Type | Why it exists |
|---|---|---|
id |
String @id @default(cuid(2)) |
Stable identity across promotion, reindex, export/import |
userId |
String |
The tenant boundary — every query filters on it |
organizationId |
String? |
Optional org scope for multi-org deployments |
scope |
String? |
Namespace within a tenant (project:<slug>, session:<id>, …) |
content |
String @db.Text |
The memory text itself |
metadata |
Json? |
Open extension point — lifecycle status, importance, dedup/contradiction markers (below) |
tags |
String[] |
Filterable labels |
type |
String |
'short-term' or 'long-term' — the tier switch |
version |
Int @default(1) |
Optimistic-concurrency counter; bumped on every content update |
expiresAt |
DateTime? |
Set only for short-term memories; the TTL contract |
embedding |
Float[] |
Embedding as a float array — the vector source of truth the pgvector index is derived from |
createdAt / updatedAt |
DateTime |
Recency signals for ranking and decay |
Indexes back the hot paths: (userId), (userId, type), (userId, scope),
(organizationId), (organizationId, type), and (expiresAt).
Why the embedding is a Float[]
Section titled “Why the embedding is a Float[]”The vector store is a derived index
(vector store). The schema keeps only
embedding Float[] — the portable source of truth the reindex pipeline reads
back from Postgres. The pgvector-native column that Postgres indexes with HNSW,
embedding_vec, is not in schema.prisma: it is runtime-managed,
provisioned by PgVectorStore.ensureReady at the observed dimensionality and
dropped/rebuilt on reset(). Keeping the float array in Postgres is what makes
reindex able to rebuild a lost vector
index without calling the embeddings provider again.
The metadata JSON contract
Section titled “The metadata JSON contract”Rather than adding a column per lifecycle feature, lifecycle state lives in
metadata. The important keys (all written by services, not clients):
importance— blended score used by ranking and decay.accessCount,lastAccessedAt,pinned— access bookkeeping and the decay-protection flag.status—active/stale/superseded/contradicted; recomputed by the decay pass.supersededBy,supersededReason,supersededAt— durable markers written when a memory is hidden by contradiction policy or corpus consolidation. Recall filters onsupersededBy(durable), not onstatusalone, because the decay pass rewritesstatusand would otherwise un-hide a superseded row.contradictionWith,contradictionReason,contradictedAt— symmetric markers under the defaultflagcontradiction policy.duplicateMatches— annotations appended when a near-identical write is collapsed into this row.
The semantics of these keys are explained in Consolidation & decay.
version: the concurrency backbone
Section titled “version: the concurrency backbone”Every update folds expectedVersion into the SQL WHERE clause and bumps
version in the same statement — a compare-and-set. The update_memory MCP
tool requires expectedVersion; background lifecycle jobs use the same
mechanism internally so they can never clobber a concurrent edit. Full rules:
Concurrent-writer policy.
Supporting models
Section titled “Supporting models”The schema has eight further models; each exists to keep the Memory row
simple:
| Model | Purpose |
|---|---|
User, Organization, Membership |
Identity and optional org multi-tenancy |
ApiKey |
Hashed per-agent credentials with scopes — see Auth & multi-tenancy |
MemoryAudit |
Append-only trail for destructive ops (update/delete/promote/reembed/restore), with before/after snapshots — this is what restore_memory restores from |
MemoryLink |
First-class typed edges between memories (duplicate-of, contradicts, wikilink-derived relations) |
MemoryImportSource |
Idempotency ledger for import_agent_memory, including the lastWrittenVersion CAS baseline |
MigrationCheckpoint |
Legacy table retained from the retired profile-migration tooling; unused by the runtime |
Notably absent: a soft-delete column. Deletes are hard, but every delete
writes a MemoryAudit snapshot first, so restore_memory can recreate the
row (original id included) from the audit trail. This keeps every read path
free of deletedAt filters while still making deletion reversible.