# Agentic Memory

Omnia gives agents durable, long-term memory that outlives a single
conversation: memories are tiered by who and what they are about, recalled by
rank, and decayed and expired over time.

Memory is distinct from [sessions](/explanation/agents/sessions/). A session is the
transient transcript of one conversation; memory is the distilled, cross-session
knowledge an agent accumulates about a user, about its own domain, and about the
organisation it serves. Memory is served by the per-workspace **memory-api**
service and reached by agents through the `memory__remember`, `memory__recall`,
`memory__list`, and `memory__forget` platform tools.

## What is a Memory?

A memory is a durable, typed observation: a fact, a preference, or a piece of
context. It is stored with metadata that lets it be ranked, deduplicated,
decayed, and (when a consent policy applies) classified. Each memory carries:

- **Content** — the observation text, plus an optional title and summary.
- **Kind** — e.g. `fact`, `preference`, `observation`; the accepted kinds come
  from the memory schema (built-in, or the MemoryPolicy's `schema`).
- **Confidence** — how strongly the agent believes the observation.
- **Embedding** — an optional vector, when an embedding Provider is configured,
  enabling semantic recall.
- **Consent category** — an optional privacy classification (Enterprise).
- **Scope** — which tier the memory belongs to (see below).

## The memory schema in use

A MemoryPolicy's `schema` declares the kinds, relation types, and display
names a workspace accepts; a workspace with no bound policy falls back to the
built-in kinds. That schema reaches these places:

- **Agents and the MCP gateway** — memory-api renders the schema into the
  memory tools' descriptions and input schemas
  (`GET /api/v1/memory/tool-guidance`). The agent runtime and the dashboard's
  memory MCP tools both use that one rendering. `memory__remember`'s `type`
  enum lists the types a user-tier write accepts; `memory__recall`'s `types`,
  `kinds` and `relation_types` enums list the policy's declared types and relations,
  and stay open when no policy declares them. See
  [Memory tools](/reference/platform/memory-tools/#where-the-tool-descriptions-come-from).
- **Recall** — graph expansion follows stored edges whose relation type is in
  `recall.graphExpansion.relationTypes`, or every relation type when that is
  empty.

See [MemoryPolicy](/reference/policies/memorypolicy/) for the schema's
field-by-field reference.

## Memory Tiers

Memories are organised into tiers that scope who and what a memory is about.
Recall queries fan out across the tiers and fuse the results into one ranked
list.

| Tier | Scope | Example |
|---|---|---|
| **User** | A specific end-user, across all agents | "Prefers metric units" |
| **Agent** | A specific agent, across all users | "This agent's booking flow requires a confirmation step" |
| **User-for-agent** | One user as seen by one agent | "This user asked this agent about refunds twice" |
| **Institutional** | The whole workspace / organisation | "Company return policy is 30 days" |

The `user_for_agent` tier is derived at recall time from the intersection of a
user and an agent — it inherits the user tier's ranking weight rather than
carrying its own.

User and agent memories are written by the agent-facing tools. Institutional
memory is authoritative workspace content, written by an operator or an
ingestion populator.

```mermaid
flowchart LR
    U[Identified user] --> UT[User memory]
    A[Agent] --> AT[Agent memory]
    U --> UA[User-for-agent view]
    A --> UA
    O[Operator or populator] --> IT[Institutional memory]
    UT --> R[Multi-tier recall]
    AT --> R
    UA --> R
    IT --> R
    R --> C[Agent context]
```

:::note[Enterprise Feature]
The **institutional** tier (organisation-wide shared memory, its ingestion
pipeline, and cross-tier ranking) is an enterprise feature. Core deployments use the user, agent, and
user-for-agent tiers. See [Installing a License](/how-to/operations/install-license/).
:::

## Containers

A memory can belong to a container: a project, a client or a matter. The
container is the memory's facet, and it keeps one piece of work's memories
out of another's recall inside the same tier.

A container is a relevance boundary, not a security boundary. The caller
chooses it on each request, and a caller that names no container sees every
container. Who may read a memory is decided by its tier and subject, as
before.

- A memory saved with a facet belongs to that container. A memory saved
  without one is cross-cutting.
- A recall with a container in scope returns that container's memories and the
  cross-cutting ones. It never returns another container's, in any tier.
  Graph expansion from the hits follows the same rule.
- A recall with no container in scope returns every container's memories.
- Deduplication, by `about` key or by similarity, collapses the same fact
  within one container only. The same fact saved under two containers is two
  memories.
- List, search, open and forget apply the same rule. Another container's
  memory is not found.

A facet is 1 to 128 ASCII letters, digits and `. _ : / -`, starting with a
letter or digit, such as `project:apollo` or `client/acme`. Facets are
compared exactly, so `Acme` and `acme` are two containers.

The dashboard's MCP gateway takes the container from its `project` connection
parameter: see [Memory tools](/reference/platform/memory-tools/#scoping-an-mcp-connection-to-a-project).
A service calling memory-api sends it as `facet`: see
[Memory API](/reference/platform/memory-api/#containers).
An agent's conversation takes its container from the `memory_facet`
parameter of the WebSocket connection it runs on: see
[Memory tools](/reference/platform/memory-tools/#scoping-an-agent-conversation-to-a-container).
The client sets it when it connects, and the model cannot change it. A
conversation without one saves cross-cutting memories and recalls every
container. A container the client chooses isolates relevance, not access: an
operator who must hold an agent to one container sets the AgentRuntime's
`spec.memory.facet`, which applies on every facade and refuses a connection
that names another.

Consolidation keeps containers apart too. A merge or a summary only combines
memories of one container.

## The Recall Model

When an agent calls `memory__recall`, memory-api runs a ranked retrieval across
all applicable tiers and returns a single fused, ranked list. How ranking works
depends on whether an embedding Provider is configured.

### Hybrid retrieval with Reciprocal Rank Fusion

With an embedding Provider configured and a non-empty query, recall is
**hybrid**: two independent rankings are computed and fused.

1. **Full-text search (FTS)** ranks memories lexically against the query.
2. **Vector search** ranks memories by pgvector cosine similarity between the
   query embedding and each memory's embedding.

The two ranked lists are combined with **Reciprocal Rank Fusion (RRF)** using a
constant **k = 60**. RRF scores each memory by `1 / (k + rank)` in each list and
sums the contributions, so a memory that ranks well in *either* list surfaces.
Semantic-only matches (no lexical overlap with the query) appear alongside
keyword matches.

```mermaid
graph LR
    Q[Query] --> FTS[FTS rank]
    Q --> VEC[pgvector cosine rank]
    FTS --> RRF["RRF fuse (k=60)"]
    VEC --> RRF
    RRF --> HL[Per-tier half-life decay]
    HL --> TP[Tier precedence weights]
    TP --> R[Ranked results]
```

On an embedding failure, or on an empty query, recall **falls back to FTS-only**
multi-tier retrieval. Semantic-only matches
are not available in that mode, but the tier fan-out and decay still apply.

### Half-life recency decay

After fusion, each memory's score is multiplied by a **per-tier recency-decay
multiplier** driven by a half-life. A memory whose age equals the tier's
half-life scores at 0.5× the recency multiplier; at 5× the half-life it is
effectively gone. The defaults are **30 days** for user memories, **90 days**
for agent memories, and no recency decay for institutional memories. With
Enterprise enabled, the half-life is configurable per tier via
`spec.recall.halfLife.{user,agent,institutional}`; Core deployments use the
defaults.

### Tier precedence

Finally, a per-tier multiplier biases the fused score across tiers — for example
to make institutional policy outrank a casual user preference. Ordering *within*
a tier is preserved; the weights bias *across* tiers. This is configured via
`spec.tierPrecedence.multiplicative`.

### Large memories: inline vs preview

Recall returns previews; `memory__open` returns the rest. A memory whose body
exceeds `spec.recall.inlineThresholdBytes` (default 8192 bytes) comes back from
`memory__recall` as a `content_preview` with `has_full_body: true` and an empty
`content`, so a recall that matches long documents does not flood the model's
context on every turn. When the preview is not enough, the agent calls
`memory__open` with the memory's id and reads the full body (and its title).

The two tools travel together. The agent runtime and the dashboard's MCP
gateway register `memory__open` only beside `memory__recall`, and recall asks
memory-api for previews only when open is there to read them. Open applies
recall's visibility to the id (the user's own memories and the shared tiers the
same scope recalls) and answers an id it cannot see exactly as it answers one
that does not exist, so an id is never a capability and open cannot probe which
ids exist. See [Memory tools](/reference/platform/memory-tools/#memory__open).

### Instruction text never reaches the model

A stored memory is read into later sessions, and an institutional-tier memory
reaches every conversation in the workspace. Text that instructs the AI, once
stored, would act in sessions its author never saw. The agent runtime scans
every memory before the model sees it, and drops one that reads as an
instruction to the AI rather than as knowledge.

The scan covers every path that returns stored text:

- ambient retrieval into `{{memory_context}}`, both the profile pull and the
  per-turn search;
- `memory__recall` and `memory__list` results;
- `memory__open`, which answers `not_found` for a flagged memory.

The rules match text that overrides the model's instructions, addresses future
agents or models, or carries a chat-template role marker such as
`<|im_start|>`. An ordinary imperative, such as "always escalate refunds over
£500", passes. The scan reads a memory's content and every text value in its
metadata, including values nested in lists and objects such as tags, because
recall returns the metadata, title and preview included.

The shared-tier write gate applies the same rules before a shared memory is
stored. The read-side scan also covers user-tier memories, which skip that
gate, and rows stored before a rule existed.

A dropped memory stays in memory-api. Each drop is logged as
`memory injection blocked` with the memory's id, tier, the path and the
matched rule, never its text, and counted in
`omnia_runtime_memory_injection_blocked_total`. A memory dropped again on a
later turn is counted again but logged at info level only the first time, so
the counter measures drops rather than distinct memories. The scan has no
setting and cannot be turned off.

## Expiry and deletion

A memory expires at its `expires_at`. The writer can set one, and otherwise the
service group's MemoryPolicy sets one at write time from the tier's
`ttl.default`, capped at the tier's `ttl.maxAge`. A tier with no `ttl` sets no
expiry. An expired memory is not recalled, listed or opened, and
memory-api's maintenance worker deletes it on its next run (hourly by default).

Deletion depends on the tier. Forgetting a user's memory deletes it at once.
Forgetting a shared (agent or institutional) memory moves it to a trash, from
which it can be restored for the trash window (seven days by default) before maintenance purges it. A
consent revocation deletes the user's memories in the revoked category at once,
unless the policy's `consentRevocation.action` is `Stop`. Full field-by-field
configuration lives in the [MemoryPolicy CRD reference](/reference/policies/memorypolicy/).

## Deduplication

Two mechanisms keep the store from filling with near-duplicates:

- **Structured-key dedup** — identity-class kinds (e.g. `fact`, `preference`)
  can be required to carry an `about={kind, key}` hint, so repeated observations
  about the same entity collapse onto one row.
- **Embedding-similarity dedup** — when embeddings are available, a new memory
  whose cosine similarity to an existing one is above `autoSupersedeAbove`
  (default 0.95) automatically supersedes it; matches above
  `surfaceDuplicatesAbove` (default 0.85) are surfaced to the agent as
  `potential_duplicates` to reconcile on a later turn.

### Near-duplicates merge automatically, and reinforce

A user-tier save that matches one of the same user's memories at or above the
merge threshold is merged into it at write time: it is the same fact confirmed again. The threshold is the
MemoryPolicy's `dedup.embeddingSimilarity.autoSupersedeAbove` (default 0.95).
Lower it to merge more near-duplicates automatically; it must stay above
0.5. Below `surfaceDuplicatesAbove`, every match merges and none is only
surfaced. With `dedup.embeddingSimilarity.enabled: false`, every save inserts
a new memory and nothing is merged or reported. The match compares one
user's memories of the same container, agent scope and evidence class,
scored exactly. Policy changes reach memory-api within 30 seconds, without a
restart. While memory-api cannot read the policy at all (a cold start with
the API server unreachable), it does not merge, because the policy might
disable merging.

A merge keeps the newer wording, and the previous body stays in the item's
revision history. A confidence the save states replaces the memory's, and
can lower it. Otherwise confidence and expiry are reinforced rather than
overwritten:

- **Confidence** becomes `1 − (1 − old)(1 − 0.7)`, capped at 0.99, where 0.7
  is the default a save without a confidence gets. Two such saves give 0.91.
  A casual repeat never demotes a fact stated firmly.
- **Expiry** becomes the later of the memory's and the tier's TTL default. A
  memory that never expires keeps not expiring, and with no TTL default the
  deadline lifts. The result is then capped at the tier's `ttl.maxAge`, so a
  memory without an expiry under a tier with `maxAge` gets one.
- **Evidence count** goes up by one. Reads return it as the
  `evidence_count` metadata key, so a client can show how many times a
  memory was confirmed.

A memory is reinforced at most once an hour, counted from when it was saved
or last reinforced. A save repeated within the hour, such as a retry, a loop
or the same fact said twice in one conversation, still updates the wording
and any stated confidence, but leaves the confidence, expiry and evidence
count as they were.

The save answers `auto_superseded` with the stored confidence, expiry and
count. Merging happens only when a memory is saved: lowering the threshold
does not merge pairs already stored below the old one. An explicit
correction, such as a save with the same `about` key or a Dreaming run's
rewrite, sets confidence and expiry to the values it carries.

### Corroboration reinforces shared memories

The shared-tier gate (Enterprise) adds a candidate that matches a published
agent or institutional memory to that memory as evidence instead of
publishing it again. When the candidate comes from a user the memory has not
counted yet, the memory is reinforced: its confidence combines with the
candidate's (0.7 when the candidate states none). Its expiry moves only to a
later expiry the candidate states; a candidate without one leaves the
expiry as it is. A repeat from a user already counted raises
`evidence_count` but leaves confidence and expiry alone, so one user saying
the same thing many times does not make shared knowledge more certain. The
same applies to held candidates that corroborate a candidate as it is
published.

Reverting a Dreaming run undoes the confidence and expiry its evidence
changed, as long as the memory still holds the values that evidence left.
When a later corroboration or edit has changed them since, the revert keeps
the memory as it is, so another user's reinforcement is not lost; memory-api
logs each memory it kept.

## Embedding providers

Semantic recall, embedding-similarity dedup, and consent-embedding
classification all depend on an embedding **Provider** (`spec.role: embedding`).
memory-api sizes its pgvector columns at startup to the Provider's declared
`spec.embedding.dimensions` (1–4000, the CRD's range and memory-api's) when it is set, and otherwise to the
dimension the provider implementation reports: Azure/OpenAI 1536, ollama
`nomic-embed-text` 768, `mxbai-embed-large` 1024, `all-minilm` 384. For
`type: openai` the reported value comes from the model name: 1536 for
`text-embedding-3-small` and `text-embedding-ada-002`, 3072 for
`text-embedding-3-large`. For any other model the provider reports none and the
schema uses 1536, so an OpenAI-compatible gateway serving another model must
declare `spec.embedding.dimensions` to match the model's output. The declared
value is also passed to the provider as the requested output size. `type:
openai` sends it as the request's `dimensions`, which `text-embedding-3-*`
honours by shortening the vector and a fixed-size model rejects. If the first
vectors the provider returns do not match the schema's dimension, memory-api
logs `embedding dimension mismatch` once, naming both sizes.
memory-api refuses to start when its Provider's model or dimension differs from
the one its database holds, and changing the field between two concrete values
produces an admission warning that says so. A different model is a change of
its own: memory-api embeds every memory with the next model beside the current
vectors, swaps recall over in one step, and keeps serving throughout. See
[Change the memory embedding model](/how-to/memory/change-memory-embedding-model/).

Memory requires an embedding Provider. A managed service group that
configures memory without `providerRef` gets no memory-api: its
`EmbeddingProviderReady` condition is `False` with reason
`ProviderNotConfigured`, and its session-api keeps running.

Embedding Providers use the provider types the PromptKit SDK supplies; Omnia
does not keep a second, Omnia-owned provider catalogue.

## Memory Galaxy

The **Memory Galaxy** is a clustering overview of a workspace's memories for
an operator view, drawn in 3D over the memory store's cluster tree. memory-api's
cluster worker builds the tree from the memories' vectors.

Every memory contributes a point so the cluster shape stays faithful, but points
whose consent category is PII-sensitive (`memory:identity`, `memory:location`,
`memory:health`) are **masked server-side before serialization**: their
identifying and content fields are stripped, leaving an anonymous,
non-interactive dot. Dropping the `id` is the security boundary — a masked dot
cannot be clicked through to the underlying memory. Masking is applied at
read time on every serve and is scope-independent, so an operator cannot
enumerate users to defeat it.

:::note[Enterprise Feature]
The Memory Galaxy is an enterprise feature. See [Installing a License](/how-to/operations/install-license/).
:::

## Consent Classification

When enterprise privacy enforcement is enabled, a consent-category validator runs
on every memory write. It merges several signals using **upgrade-only**
semantics — a detected category overrides the caller's claim only when it is more
restrictive:

1. The caller's declared `consent_category` is the primary signal.
2. A PII regex pass classifies content into `memory:health`,
   `memory:identity`, `memory:location`.
3. When an embedding Provider is configured, an embedding-similarity pass
   classifies content against per-category exemplar centroids
   (`memory:preferences`, `memory:context`, `memory:history`, plus
   reinforcement of the regex categories).

The classification drives per-category expiry overrides
(`spec.tiers.<tier>.perCategory`), Galaxy masking, and consent-revocation
handling. In Core deployments the validator is off and `consent_category`
stays `NULL` — a binary opt-out is the only gate.

:::note[Enterprise Feature]
Consent classification runs only under enterprise privacy enforcement. See
[Installing a License](/how-to/operations/install-license/).
:::

## Recommendations

- Configure an embedding Provider (`role: embedding`) for every memory service group. Memory requires one: without it the operator provisions no memory-api. See [Configure workspace memory](/how-to/memory/configure-workspace-memory/).
- Leave `ttl` unset on the institutional tier. Authoritative content then leaves only through an explicit delete, `memory__forget` or a DSAR delete.
- Set `recall.halfLife` per tier to match the shelf life of the data: short for volatile user context, long for durable policy. This is an Enterprise setting.
- Require `about` hints for identity-class kinds with `dedup.requireAboutForKinds`. The hint engages the structured-key dedup path, so identity memories do not pile up as duplicates.
- Before a demo, confirm that your consent policy classifies the categories you expect the Galaxy to mask (`memory:identity`, `memory:location`, `memory:health`).

## Related Resources

- [MemoryPolicy CRD](/reference/policies/memorypolicy/) — full field-by-field reference.
- [Change the memory embedding model](/how-to/memory/change-memory-embedding-model/) —
  move memory to a different embedding model.