Memory Record
Every piece of information stored in engram is a memory record. Records are stored as vectors in Qdrant and surfaced through the MCP tools. This page documents every field, its serialized JSON name, allowed values, and who sets it.
Field reference
Section titled “Field reference”| Field | JSON key | Type | Set by | Description |
|---|---|---|---|---|
| ID | id |
string (UUID) | server | Unique record identifier, generated on creation |
| Short ID | short_id |
string (Crockford base32) | server | Short, case-insensitive handle; accepted anywhere an id is accepted; minted on creation |
| Content | content |
string | client | The memory text; also the text that is embedded |
| Scope | scope |
string | client | run:tier:repo identifier, e.g. eval-2026-05:project:selfhosted-cluster |
| Repo | repo |
string | client | Repository name or URL (optional context) |
| Workspace | workspace |
string | client | Workspace identifier (optional context) |
| Worktree path | worktree_path |
string | client | Path to the git worktree (optional context) |
| Base dir | base_dir |
string | client | Base directory for the project (optional context) |
| Source | source |
string | client | How the memory was produced — see Source values |
| Category | category |
string | client | What kind of memory — see Category values |
| Tags | tags |
string[] | client | Free-form labels; list_tags reports exact, recall-visible per-tag counts for a scope or every readable scope |
| Summary | summary |
string | client/server | Short human-readable summary; omit or empty for none — see Summary fields |
| Summary Source | summary_source |
string | client/server | How the summary was produced: client (caller-authored), auto (offline-generated), or "" (none) |
| Summary Model | summary_model |
string | server | Name of the model used when summary_source=auto (e.g. gpt-4o-mini); empty when source is client or none |
| Summary Egress At | summary_egress_at |
string (RFC3339) | server | Durable audit stamp: when this record’s content was egressed to the summarizer model (summary_source=auto path only); zero if never egressed or the summary was client-authored/cleared |
| Actor | actor |
string | server | Verified caller identity extracted from the OIDC token (email, username, or subject); never client-supplied; empty when auth is disabled |
| Owner | owner |
string | server | Value of the configured owner claim (ENGRAM_OWNER_CLAIM, default email) — the authorization key; never client-supplied; empty string when auth is disabled (anonymous bucket) |
| Visibility | visibility |
string | client/server | "" (private, default) or "shared" — see Visibility |
| Created at | created_at |
string (RFC3339) | server | UTC timestamp of creation |
| Not before | not_before |
string (RFC3339, optional) | client | Deferred-reveal lower bound: the record is hidden from recall until now is at or past not_before. Stored as an epoch-second integer, the same convention archived_at uses — see The validity window |
| Not after | not_after |
string (RFC3339, optional) | client | Expiry upper bound: the record drops out of recall once now is at or past not_after. Stored as an epoch-second integer, the same convention archived_at uses — see The validity window |
| Supersedes | supersedes |
string[] (optional) | server | Set on a correcting record: the id(s) of the memory or memories it replaced — see Supersession |
| Superseded by | superseded_by |
string (optional) | server | Set on a corrected record: the id of the memory that replaced it; its presence soft-hides the record from recall |
| Archived at | archived_at |
string (RFC3339) | server | Set via engram spine-review archive; its presence soft-hides the record from recall but never from get_memory — see Archiving |
| Access count | access_count |
integer | server | Monotonic count of strong-signal touches (get_memory fetch + update_memory); never incremented by search/list result-set membership. Never read by the reranker or any recall filter |
| Last accessed at | last_accessed_at |
string (RFC3339, optional) | server | Timestamp of the most recent strong-signal touch; absent when the record has never been accessed |
| Schema version | schema_version |
integer | server | Schema-version discriminator; a record predating the key reads as 0 by absence, no backfill required; never participates in a recall or authorization filter — see Schema version |
| Kind | kind |
string (optional) | client | Discovery discriminator: map or fact; present only on discovery-category records, set via store_discovery — see Discovery fields |
| Citations | citations |
Citation[] | client | Optional structured source anchors on any category (required, min 1, only for discovery) — see Citation fields; never auto-populated |
| Score | score |
number (optional) | server (query-time) | Not a stored payload key. The Qdrant similarity score for this result on search_memory (higher = closer); zero or omitted on unranked list_memory/get_memory results |
| Relevance | relevance |
number (optional) | server (query-time) | Not a stored payload key. The Jev reranker’s per-hit probability that this record answers the query, on search_memory/search_discovery; present only when reranking ran and succeeded for this hit |
Supersession
Section titled “Supersession”supersede_memory corrects one or more records without losing history. It stores
a single new, correcting record carrying a supersedes link to every target, and
stamps superseded_by onto each target — both additive; a target’s content, tags,
and vector are untouched. One correcting record may replace several predecessors
in the same call; each predecessor still has exactly one successor.
A record carrying superseded_by is soft-hidden from recall (search_memory,
list_memory, search_discovery, list_scheduled) but remains fetchable by id
via get_memory, so the prior state is always auditable. Absent on every record
that has never participated in a supersession — pre-feature records are unaffected.
Chains still run forward (C supersedes B supersedes A) with exactly one live head
per chain: superseding an already-superseded record is rejected, which makes
cycles and self-supersession structurally impossible. validate_only=true
runs this same preflight and previews the resolved chain without writing
— no new record, no supersedes/superseded_by link on either side. See
supersede_memory for the full contract.
Archiving
Section titled “Archiving”engram spine-review archive --id <id> explicitly retires a record: it stamps
archived_at, an entirely new, orthogonal key — distinct from both not_after
expiry and superseded_by supersession. Archiving never writes not_after and
never writes superseded_by; a record can be archived, expired, and superseded
independently, and each state is cleared independently of the others. The MCP
archive_memory /
restore_memory tools and the Connect
ArchiveMemory / RestoreMemory RPCs reach the identical effect over a batch
of ids, owner-gated the same way the CLI verbs are.
A record carrying archived_at is soft-hidden from recall (search_memory,
list_memory, search_discovery, list_scheduled) but remains fetchable by id
via get_memory, exactly like superseded_by above. engram spine-review restore --id <id> reverses it by deleting the archived_at key outright — never a
delete, content erasure, or vector removal on either verb. Archiving an
already-archived record, or restoring a never-archived one, is a no-op success.
Internally, archived_at is stored in Qdrant as an epoch-second integer (matching
not_before/not_after’s stored form) but round-trips through get_memory as an
RFC3339 string like every other time.Time-typed field in this table. It is
visible on both lanes: get_memory returns the store.Memory struct directly
as structured output on the MCP lane, and the Connect Memory message carries
archived_at alongside superseded_by, supersedes, not_before, not_after,
and schema_version — the full record-state contract is the same on either lane.
archived_at is a Qdrant integer payload index, like not_before and
not_after, and superseded_by and visibility are keyword indexes. Together
they cover every condition of the recall gate, so Qdrant evaluates the gate
from its indexes instead of reading each record’s payload. Server startup creates
any missing index; no migration is needed. See engram spine-review scan’s
archived bucket (in
the CLI guide) for a spine-wide count of archived records,
separate from its expired bucket.
The validity window
Section titled “The validity window”not_before and not_after together define a record’s validity window — the same
mechanism schedule_memory writes and list_scheduled surfaces. Both are optional,
independent wire fields; a record carrying neither is unaffected, which is why the
feature needed no backfill.
not_beforegates deferred reveal with an inclusive lower bound: the record is hidden from recall until now is at or pastnot_before. A record whosenot_beforeequals the current instant is already active — the moment of the boundary belongs to the active side, not thescheduledside.not_aftergates expiry with an exclusive upper bound: the record drops out of recall once now is at or pastnot_after. A record whosenot_afterequals the current instant is already expired — the moment of the boundary belongs to theexpiredside, not the active side.
The window is therefore half-open, [not_before, not_after) — the same convention
search_memory/list_memory’s
created_after/created_before already use. A caller who has internalized one
has internalized both; this is the house convention applied consistently, not a
special case.
expired and scheduled are mutually exclusive: expired is evaluated first
and, when present, suppresses scheduled. This precedence lives in the
derivation rather than in write-time validation — not_before and not_after
are independent wire fields, so an inverted window can in principle arrive from a
legacy or future path even though writing one is rejected today. The canonical
order in which state words are emitted, on every surface that renders them, is
archived, superseded, expired, scheduled — descending by finality. See
get_memory for where a caller reads these
words back.
Schema version
Section titled “Schema version”Every write stamps the current schema version onto the record’s schema_version
key. A record written before the key existed has no such key at all and reads as
version 0 by absence — no backfill is required. The value is wire-visible on
both the MCP and Connect lanes; it is an observable field, not an internal audit
stamp, and it never appears in a recall or authorization filter.
The forward-compatibility guarantee has three parts and a boundary — read all of them before assuming a rollback is always safe:
- Reads and recall are unconditionally safe. A binary reading a record stamped newer than its own constant returns it normally — it never rejects, hides, or downgrades it. The version is not a filter key, so recall does not narrow on it either.
- The normal write path preserves the higher stamp. An ordinary write computes the stored version as the greater of the current constant and the record’s own decoded value, so a full write through the normal path cannot lower it.
- Editing a newer record with an older binary is lossy, and one write path sits outside the guarantee. An older binary that edits a newer record keeps the newer stamp but rebuilds the payload from its own older struct, so keys only a newer version knows about are dropped. What is lost is re-derivable, because migration steps are additive-only — the recovery is re-running the migration sweep. Separately, one lower-level write path replaces a record’s payload by id without reading the existing record first, so a caller holding a stale copy of a record’s fields can lower its stored version through that path; this is a real, narrower boundary on the guarantee, not an edge case to wave away.
See the engram migrate guide for the operator procedure
that advances a record’s schema version, and the rollback hazard in
guides/upgrade.md for what a version downgrade means in
practice — the two pages describe the same hazard and must not disagree.
Source values
Section titled “Source values”The source field describes how the memory was produced. Exactly two values are
accepted by the store:
| Value | Meaning |
|---|---|
user-said |
The user stated this explicitly |
agent-inferred |
The agent derived or inferred this |
Discovery records always have source set to agent-inferred by the server.
Category values
Section titled “Category values”The category field classifies what kind of memory is stored:
| Value | Meaning |
|---|---|
decision |
An architectural or design decision |
preference |
A stated user or team preference |
convention |
A coding or workflow convention |
gotcha |
A known pitfall or non-obvious behaviour |
discovery |
Agent-earned codebase understanding (see Discovery fields) |
The discovery category is set by the server for records created via
store_discovery; client callers use the other four values with store_memory.
Visibility
Section titled “Visibility”The visibility field controls cross-actor reads:
| Value | Meaning |
|---|---|
"" (empty string) |
Private — only the owner can read and write |
"shared" |
Readable by any authenticated caller; writable only by owner |
Toggle visibility with set_visibility or update_memory’s shared argument.
Sharing grants read only — another actor can never write a record they do not
own, even when it is shared.
Summary fields
Section titled “Summary fields”Summaries help agents work with stored memories efficiently. Recall (via
search_memory / list_memory) returns compact summaries by default, keeping
the spine bootstrap small; full content is always accessible via get_memory
(id fetch) or by passing full=true to recall tools.
-
summary: Short human-readable digest of the memory. Optional. -
summary_source: Describes how the summary was produced:"client"— caller-authored and explicit; important for content matching and stable, treat as authoritative"auto"— offline-generated byengram summarize-missing; lossy but useful for orientation; always verify against full content before acting on caveats""(empty string) — no summary present
-
summary_model: Name of the model used whensummary_source=auto(e.g.gpt-4o-mini). Set by the server; empty for client-authored or absent summaries.
Updating with a stale summary: If summary_source=client and you change the
memory’s content, you must address the summary: re-send it unchanged, update
it to reflect the new content, or clear it. Failing to address a stale
caller-authored summary causes the update to be rejected.
Discovery fields
Section titled “Discovery fields”Records in the discovery category carry one additional field that is absent
(or zero-valued) on regular memory records.
| Field | JSON key | Type | Required | Description |
|---|---|---|---|---|
| Kind | kind |
string | yes | map (orientation/structure) or fact (pinned checkable claim); discovery-only, not settable on store_memory |
| Summary | summary |
string | no | Short human-readable summary |
Discovery records live in scopes starting with discovery:, typically
discovery:repo:<repo>. They are recalled on demand via search_discovery and
are never returned by list_memory session bootstrap.
citations itself is not discovery-only — see the Citations
row in the main field reference above. The one asymmetry that remains: a
discovery record requires at least one citation, while a curated
memory-category record requires none (citations there are optional
provenance, added only when a claim benefits from a checkable anchor).
Citation fields
Section titled “Citation fields”Each citation anchors a claim to a verifiable source. The shape is identical
whether the citation lives on a discovery record or on any other category:
| Field | JSON key | Type | Required | Description |
|---|---|---|---|---|
| Kind | kind |
string | yes | file, commit, url, or repo |
| Ref | ref |
string | yes | Path, repo URL, or doc URL |
| Locator | locator |
string | no | E.g. 200-240 line range |
| Pin | pin |
string | no | Aging anchor: commit SHA, content-hash, @rev, or fetched-at |
| Excerpt | excerpt |
string | no | Cached substance from the source; max 16 KiB, soft cap ~50 lines |
Field name notes
Section titled “Field name notes”The serialized JSON keys match the Go struct tags in internal/store/store.go
exactly:
worktree_path(notworktree) — the struct field isWorktree string \json:“worktree_path”``base_dir(notbaseDir) — snake_case throughoutcreated_at— RFC3339 string in the Qdrant payload; deserialized astime.Time
These match the README’s description (repo/workspace/worktree_path/base_dir).
Isolation and ownership
Section titled “Isolation and ownership”actor and owner are always server-set. The actor is extracted from the
token’s UserID (email, username, or subject claim, in priority order). The
owner is the value of the configured owner claim (ENGRAM_OWNER_CLAIM,
default email); it changes only if the chosen claim is not stable across IdP
profile updates (e.g. email changes when the user renames their account; sub
never changes).
When authentication is disabled (no --oidc-issuer), both actor and owner
are empty strings, and all callers share one anonymous bucket.
Pre-isolation records — those written before per-actor ownership was added —
carry no owner key (distinct from an empty-string owner). They are invisible
to every owner-scoped read. See Auth and Isolation for
migration details.