Skip to content

MemoryPolicy CRD

MemoryPolicy defines how the agentic memory store behaves across the institutional, agent, and user tiers: when memories expire, how the read path ranks and decays results, how the write path deduplicates, and how the shared-write gate publishes to the shared tiers. Policies are reusable documents — a single policy can be referenced by many workspaces.

Expiry (tiers), the write path (dedup, schema) and the recall response shape (recall.inlineThresholdBytes, recall.maxRelatedPerMemory) apply in the Core edition. The rest is Enterprise-gated: the recall ranking settings (recall.halfLife, recall.graphExpansion, tierPrecedence), the institutional tier and document ingestion, the shared-write gate, and per-user consent revocation (consentRevocation.action). Those sections are marked below.

apiVersion: omnia.altairalabs.ai/v1alpha1
kind: MemoryPolicy

Short name: mp (e.g. kubectl get mp).

MemoryPolicy is cluster-scoped. There is no metadata.namespace.

A MemoryPolicy is bound to a service group through Workspace.spec.services[].memory.policyRef. See Configure workspace memory for the field in context.

Each memory-api resolves the policy through its own Workspace and service group, and re-reads it at most every 30 seconds, so most spec changes reach the read and write paths without a restart.

The dashboard lists the policies under Settings → Memory policies, with each one’s phase and the service groups that bind it, and opens a form over its spec. A platform admin sees every policy; anyone else sees only the policies bound in a workspace they can see. Only a platform admin may create or change one, because a change reaches every workspace that binds the policy. Fields the form does not show (the schema, allowed terms, graph expansion) are saved back unchanged. What retention removed (expired, purged from the trash, deleted on a consent revocation; all permanent deletes) shows on the Memories page, read from memory-api’s GET /api/v1/memories/retention.

A service group with no policyRef (or one naming a policy that does not exist) runs with no policy: the read and write paths use their built-in defaults, and a memory expires only when its writer or memory-api’s --default-ttl gives it an expiry.

Per-tier expiry. The institutional tier is Enterprise; Core deployments use user and agent (see Installing a License).

spec:
tiers:
agent:
ttl: { default: "365d" }
user:
ttl: { default: "180d", maxAge: "365d" }

Each tier (institutional, agent, user) takes these fields:

FieldTypeDefaultDescription
ttlobject—Write-time expiry (see below).
perCategorymap—Per-consent-category expiry overrides (see below).

A tier left out of tiers, or with no ttl, sets no expiry. User-for-agent memories follow the user tier’s config.

An expired memory is not recalled, listed or opened. memory-api’s maintenance worker deletes expired memories on every tier on its schedule (--maintenance-schedule, hourly by default). Other memories leave by an explicit memory__forget, a user “forget everything”, a consent revocation or a DSAR delete.

FieldTypeDefaultDescription
defaultduration—Applied at write time when the caller sets no explicit expires_at. Empty falls back to memory-api’s global --default-ttl; when that is also unset, the memory does not expire.
maxAgeduration—Caps any expires_at (explicit or defaulted) at write time, and caps the expiry a merge of near-duplicates reaches. Empty means no cap.

The controller rejects a default longer than maxAge.

Durations are Go-duration-style strings matching ^([0-9]+d)?([0-9]+h)?([0-9]+m)?([0-9]+s)?$, e.g. 180d, 720h.

Overrides the tier’s expiry for specific consent categories (for example memory:health). Keys are consent-category strings. A ttl block on the override replaces the tier’s for write-time expiry; without one the tier’s applies:

spec:
tiers:
user:
ttl: { default: "180d" }
perCategory:
memory:health:
ttl: { default: "30d" } # health memories expire sooner

Tunes the read path. Optional — defaults baked into the recall SQL apply when omitted.

FieldTypeDefaultDescription
halfLifeobjectuser 30d, agent 90d, institutional nonePer-tier recency-decay half-life: user, agent, institutional, each a Go duration (720h = 30 days). Enterprise.
inlineThresholdBytesint328192Body-size cutoff above which a recall request that sets preview returns title + summary + content_preview instead of the full body. The agent runtime does not set preview, so agents receive full bodies. Range 0–1048576.
maxRelatedPerMemoryint323Caps the per-memory related[] slice in the recall response. Range 0–50.
graphExpansionobject—Structural graph walk from direct recall hits (see below). Enterprise.

See Memory for how half-life folds into the fused retrieval score.

FieldTypeDefaultDescription
enabledboolfalseExpand direct hits over structural relations. A recall request can override this.
hopsint321Maximum hops. Range 1–2.
relationTypesstring[]allRelation types to follow.
maxAddedint3210Cap on expanded rows appended to the direct results. Range 1–100.
weightstring (0–1)"0.6"Score multiplier applied to expanded candidates before merging.

Tunes the write path’s two dedup mechanisms. Optional.

FieldTypeDescription
requireAboutForKindsstring[]Kinds (e.g. fact, preference) that must carry an about={kind, key} hint on save. Unioned with memory-api’s --require-about-for-kinds flag; empty in both disables the check.
embeddingSimilarityobjectCosine-similarity dedup config (see below). Auto-disabled when no embedding provider is configured.
FieldTypeDefaultDescription
enabledbooltrueWhether cosine-similarity dedup runs. false: every save inserts a new memory, nothing is merged and no potential_duplicates are returned.
autoSupersedeAbovestring (0–1)0.95Cosine ≥ this merges the save into its closest match. Lower merges more near-duplicates automatically. Must be above 0.5.
surfaceDuplicatesAbovestring (0–1)0.85Cosine ≥ this surfaces the match as potential_duplicates for the agent to consider later.
candidateLimitint325Caps the matches compared and returned in potential_duplicates. Range 0–50; 0 uses the default.

CEL rules at admission:

  • surfaceDuplicatesAbove must be strictly less than autoSupersedeAbove when both are set.
  • autoSupersedeAbove must be above 0.5. The rule checks a new policy and any update that changes the field. On Kubernetes 1.30 and later, CRD validation ratcheting lets you keep updating a policy that already holds a lower value while that value is unchanged. On older clusters, raise the value in the same update. memory-api ignores a value of 0.5 or less and merges at 0.95.

An autoSupersedeAbove below the effective surfaceDuplicatesAbove merges every match it reads, so none is only surfaced.

A merge reinforces the existing memory: the newer wording replaces the body, a stated confidence replaces the memory’s, and otherwise confidence combines as 1 − (1 − old)(1 − 0.7) (capped at 0.99, never lowered), the expiry moves to the later of the two, and evidence_count goes up by one, at most once an hour per memory. The expiry is capped at the tier’s ttl.maxAge. The comparison covers one user’s memories with the same container, agent scope and evidence class. memory-api reads these fields on every save through its policy cache, so a change applies within 30 seconds without a restart. Merging happens at save time only; pairs already stored are not merged when the threshold drops. See Near-duplicates merge automatically.

Bounds what one memory write may carry. Optional.

FieldTypeDefaultDescription
maxBodyBytesint328192Largest body, in bytes, one memory may hold. A write above it is rejected, never truncated; store the content elsewhere and attach it as a reference. Document ingest is exempt, because it is already chunked. Range 0–1048576; 0 uses the default.

Declares the vocabulary of the memory graph: the types a memory node may have, the tiers each type may be written to, and the relations that may connect two nodes. memory-api enforces it on every write path and serves it at GET /api/v1/memory/schema so agents, the MCP gateway and the dashboard all see the same vocabulary. It also renders the types and relations into the agent’s memory tool descriptions (GET /api/v1/memory/tool-guidance), together with dedup.requireAboutForKinds — see Memory tools.

spec:
schema:
version: "policies.v2"
types:
- name: policy
displayName: Policy
description: A governing policy document.
allowedTiers: [institutional]
- name: control
displayName: Control
allowedTiers: [institutional]
- name: fact
allowedTiers: [institutional, agent, user, user_for_agent]
relations:
- name: implements
kind: logical
inverse: implemented_by
sourceTypes: [control]
targetTypes: [policy]
- name: related_to
kind: association
directed: false
sourceTypes: ["*"]
targetTypes: ["*"]
FieldTypeRequiredDescription
namestringyesThe value carried in a memory’s type (and a batch item’s kind).
displayNamestringnoLabel the dashboard shows for the type.
descriptionstringnoExplains the type. Agents see it in the memory__remember tool’s type parameter.
allowedTiersstring[]noTiers the type may be written to: institutional, agent, user, user_for_agent. Empty means every tier.
FieldTypeRequiredDefaultDescription
namestringyes—The relation type stored on the edge (relation_type). This is the value callers pass when creating an edge and the value recall.graphExpansion.relationTypes and a recall request’s relation_types filter on.
kindenumyes—The relation’s category: containment, sequence, reference, provenance, logical, association or equivalence. Served with the schema for consumers to interpret.
inversestringno—Name of the reverse reading (for example part_of / contains). Served with the schema; no edge is stored for it.
sourceTypesstring[]noanyTypes allowed at the edge’s source. "*" or an empty list matches any type.
targetTypesstring[]noanyTypes allowed at the edge’s target. Same matching as sourceTypes.
directedboolnotrueWhether source → target order is meaningful. Served with the schema. Graph expansion walks edges in both directions regardless.
Write pathWith a policy schemaWithout one
POST /api/v1/memories, agent-scoped savestype must be declared, and its allowedTiers must include the tier the write’s scope resolves to. The tier is user_for_agent when the scope has a user and an agent, user when it has only a user, agent when it has only an agent, and institutional otherwise.Validated against the built-in schema (below).
Single institutional write (POST /api/v1/institutional/memories)type must be declared and allowed in institutional.Not validated.
Batch ingest (POST /api/v1/institutional/ingest/batch)Every document must be allowed as type document. One violation rejects the whole batch.Not validated.

A violation answers 400 with a message naming the rule.

Declaring a schema replaces the built-in one; nothing is merged. A policy that declares only institutional types therefore rejects every user-tier save, including the general, fact, preference and observation types the agent memory tools produce. The SchemaCoverage condition warns when a configured tier has no type that allows writes to it.

Without a policy schema, GET /api/v1/memory/schema returns version builtin.v1:

TypeAllowed tiers
document, section, chunkinstitutional
general, factall tiers
preference, observation, temporal_summaryuser, user_for_agent, agent

It also lists two relations:

  • part_of (containment, inverse contains): from a section or chunk to a document or section.
  • next (sequence, inverse previous): between section and chunk nodes, in reading order.

See Memory for where the schema reaches agents, the MCP gateway, and recall.

Applies per-tier ranking multipliers to the fused retrieval score. multiplicative is the only ranking strategy and must be set when tierPrecedence is present — enforced by CEL at admission.

FieldTypeDefaultDescription
multiplicative.institutionalstring (0–10)"1.0"Weight applied to institutional-tier scores.
multiplicative.agentstring (0–10)"1.0"Weight applied to agent-tier scores.
multiplicative.userstring (0–10)"1.0"Weight applied to user-tier scores. user_for_agent inherits this weight.
spec:
tierPrecedence:
multiplicative:
institutional: "2.0"
agent: "1.5"
user: "1.0"

Configures the shared-write gate, which decides what reaches the shared agent and institutional tiers.

FieldTypeDefaultDescription
dedupThresholdstring (0–1)"0.95"Cosine similarity at or above which a candidate counts as the same fact as an existing shared item (added to it as evidence) or as a held candidate (counted towards corroboration).
allowedTerms[]string—The workspace’s own product, team and system names that de-identification treats as non-personal. Never a person’s name.
minDistinctSubjectsint32 (2–1000)2How many distinct users must stand behind a fact before it is published.
corroborationWindowDaysint32 (1–365)30How long a held fact waits for corroboration before it is rejected as uncorroborated and its text deleted.
accept.userSharesbooltrueAdmit users’ shares (memory__remember with share: true). When false they are rejected as share_disabled, and the user’s private copy stays.
accept.dreamingbooltrueAdmit Dreaming runs’ proposals. When false they are rejected as dreaming_disabled.
rateLimits.maxHeldPerSubjectint32 (1–100000)200How many shares one user may have waiting for corroboration. A share beyond it is rejected as rate_limited.
rateLimits.maxPublishesPerHourint32 (1–100000)100How many items the gate may publish in any hour. Beyond it, candidates stay pending until the hour rolls over.
canaries.onHitfreeze | rejectfreezeWhat a canary hit does (below).
canaries.clearedAttimestamp—Clears a freeze that happened before it.

Corroboration. One user can never put a fact into shared memory alone. A user’s shared memory stays in their own tier, which they see at once. The gate holds the shared copy until enough other users have shared the same fact, matched at dedupThreshold, and then publishes one de-identified item with all of them as evidence. A Dreaming proposal must cite items from at least minDistinctSubjects users. Three cases need no corroboration:

  • knowledge derived only from items that are already shared;
  • items the brain migration re-queues, which were shared before it ran;
  • a near-duplicate of an item already published, which only adds evidence.

A held fact keeps its raw text until it is published or expires. Erasing the user deletes it.

Canaries. The gate plants three made-up facts per workspace, such as “the amber vault override code is EMBER-0042”. They are private memories of a hidden subject that no person or agent can act as. Nothing legitimate can carry one to the gate. Only something that reads one user’s memories and promotes them can, such as a misbehaving Dreaming run or a leak in a cross-user path. The gate checks every candidate in two ways:

  • Certain match: the canary subject is among its sources, or it contains a canary’s text or code. The candidate is rejected as canary. With onHit: freeze the gate also stops deciding anything until an admin clears it. With onHit: reject it only rejects and alerts.
  • Near match: only its vector is as close to a canary as dedupThreshold. The candidate is rejected and alerted, but the gate never freezes.

omnia_memory_gate_canary_hits_total and omnia_memory_gate_frozen carry the alert. Canaries never appear in recall, Galaxy, export or erasure, and no caller or Dreaming run can act as their subject. Codes are matched as whole words, so member-0042 does not match EMBER-0042.

To clear a freeze after investigating it, set clearedAt to the current time. A clear up to two minutes behind the database’s clock still counts. Each queued candidate that carries a canary freezes the gate again when it is reached, which is intended: each one is evidence.

Terminal window
kubectl --context <ctx> -n <workspace-namespace> patch memorypolicy <name> --type merge \
-p '{"spec":{"gate":{"canaries":{"clearedAt":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}}}}'
spec:
gate:
minDistinctSubjects: 3
corroborationWindowDays: 14
accept:
userShares: true
dreaming: true
rateLimits:
maxHeldPerSubject: 200
maxPublishesPerHour: 100
canaries:
onHit: freeze

How memory-api reacts when a user toggles off a consent category. When a user revokes a category, privacy-api posts the event to each memory-api’s POST /api/v1/memories/consent-events, and memory-api applies action to that user’s memories in that category. With no policy bound the action is SoftDelete.

FieldTypeDefaultDescription
actionenumSoftDeleteOne of SoftDelete, HardDelete, Stop. Enterprise.
ActionBehaviour
SoftDelete, HardDeleteDelete the user’s memories in the category at once.
StopLeave existing memories alone. Not GDPR-compliant in most jurisdictions.

Whatever the action, new writes in a revoked category are refused: memory-api checks each write’s category against privacy-api’s consent grants.

Configures how source documents become index items at /institutional/ingest and /institutional/ingest/batch. Read live by memory-api at ingest time; the --ingest-* binary flags are the fallback when a field is unset.

FieldTypeDefaultDescription
strategyenumchunkOne of chunk (RAG-chunk the raw text), summary (one condensed summary per document), summaryThenChunk (summarize, then RAG-chunk the summary).
summarizerenumextractiveextractive (in-process lead-sentence, no LLM) is the only backend. Ignored when strategy: chunk.
chunkobject—RAG chunk-splitter geometry: size (words, default 200) and overlap (words, default 40). overlap must be less than size (CEL-enforced).

Adds workspace advice to the instructions the model reads for the memory tools. memory-api derives those instructions from the rest of this policy (schema, dedup, recall) and serves them at GET /api/v1/memory/tool-guidance; see Memory tools. Notes are appended after the derived text under “Notes for this workspace:” and never replace it, so a note cannot contradict how the store behaves.

FieldTypeDefaultDescription
notesstring—Appended to every memory tool’s description. At most 2000 characters.
tools.rememberstring—Appended to memory__remember’s description, after notes. At most 2000 characters.
tools.recallstring—Appended to memory__recall’s description. At most 2000 characters.
tools.liststring—Appended to memory__list’s description. At most 2000 characters.
tools.forgetstring—Appended to memory__forget’s description. At most 2000 characters.
tools.openstring—Appended to memory__open’s description. At most 2000 characters.
spec:
toolGuidance:
notes: "Never store payment card numbers or passwords."
tools:
remember: "Record support context with purpose: support_context."

The notes are sent to the model in every conversation that has memory tools, so keep them short. An agent can add its own advice with AgentRuntime.spec.memory.tools.notes, which follows these.

FieldTypeDescription
status.phasestringActive (spec validated) or Error (validation failed — see conditions).
status.observedGenerationint64Spec generation last reconciled.
status.conditionsCondition[]See below.
ConditionMeaning
PolicyValidTrue when the spec passes the controller’s checks (durations, tierPrecedence weights in 0–10, ttl.default ≤ ttl.maxAge). False sets phase: Error.
SchemaCoverageFalse (a warning, not an error) when a configured tier has no schema type that allows writes to it — writes to that tier will be rejected.
WorkspacesResolvedAlways True with reason NotApplicable.
ReadyTrue when the policy validated.

Minimal policy — expiry on the user tier

Section titled “Minimal policy — expiry on the user tier”
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: MemoryPolicy
metadata:
name: basic
spec:
tiers:
user:
ttl:
default: "180d"
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: MemoryPolicy
metadata:
name: research
spec:
tiers:
agent:
ttl:
default: "365d"
user:
ttl:
default: "180d"
maxAge: "365d"
perCategory:
memory:health:
ttl:
default: "30d"
recall:
halfLife: # Enterprise
user: "720h"
agent: "1440h"
institutional: "8760h"
inlineThresholdBytes: 2048
maxRelatedPerMemory: 3
dedup:
requireAboutForKinds:
- fact
- preference
embeddingSimilarity:
enabled: true
autoSupersedeAbove: "0.95"
surfaceDuplicatesAbove: "0.85"
tierPrecedence: # Enterprise
multiplicative:
institutional: "2.0"
agent: "1.5"
user: "1.0"
consentRevocation:
action: HardDelete
ingestion: # Enterprise
strategy: summaryThenChunk
summarizer: extractive
chunk:
size: 200
overlap: 40