Inference memory
Lucenia's inference memory layer gives an application (an agent, a chat loop, or an ingest pipeline) a durable place to remember what happened and later recall it by meaning. It is a small REST surface under _plugins/_memory built on Lucenia's own vector and lexical search — so recall is hybrid (lexical and vector) and recency-aware, not a plain nearest-neighbor lookup.
The layer is designed for the gaps that break long-running agents: a context window that compacts and forgets, a conversation that drifts off its goal, and a memory store that grows without bound. The verbs below address each of those directly — remember/recall bridge the compaction gap, drift/check keeps generation on the rails, and rollup consolidates aged memory so the store stays small.
The memory verbs are not the whole memory surface. For durable, named, listable conversation transcripts — a first-class, org-tenant-scoped conversation store with messages and traces — see Conversation management. A conversation composes with these verbs on the same session (a conversation id is the scope's session).
Recall quality depends on the embedding model memory is configured with — and memory is disabled until you configure one. Provider configuration (the hashing dev embedder, self-hosted HTTP, Bedrock, Vertex, Azure, and multimodal image memory) is documented separately in Inference memory embeddings. This page covers the memory verbs; it does not repeat the provider setup.
Table of contents
Concepts
What a memory is
A memory is a single durable fact — a short piece of text that is the model-agnostic source of truth. When you remember content, the layer:
- writes the raw turn into a rolling working (episodic) index, and
- embeds the text into a vector and writes a curated long-term memory that is immediately recallable.
The long-term id is content-addressed: re-remembering the same text in the same scope overwrites the same document instead of creating a duplicate. The embedding vector is treated as a disposable cache — it is rebuildable from the text, so you can change embedding models and re-embed without losing anything.
Each long-term memory carries a fact type that describes what kind of thing it is:
fact_type | Meaning |
|---|---|
semantic | A self-contained factual statement (the default). |
preference | A durable user/agent preference ("prefers metric units"). |
summary | A rolled-up memory standing in for many older, consolidated ones (see rollup). |
entity | An entity/relationship fact. |
anchor | A drift anchor: a goal or pinned constraint (see drift). |
Memories can be pinned ("pinned": true), which exempts them from the default forget, and carry arbitrary key/value tags.
Scope: tenancy and partitioning
Every write and every recall is scoped. The scope object is the partition-and-retrieval key that lets one memory layer serve wildly different callers without special-casing any of them. All five dimensions are optional:
| Scope field | Purpose |
|---|---|
tenant | Tenant/customer partition. |
namespace | Collection/document-set namespace (for example, an ingest collection). |
agent | Agent identifier (for example, coding-agent). |
session | Session/thread identifier. |
user | User identifier. |
Scope also namespaces the drift anchors, so two users never share a "sense of topic."
With the security plugin enabled, the tenant (organization) and user dimensions are not client-supplied — they are pinned from the caller's verified identity, and a request that asserts a different organization or user is rejected with 403. Each organization's memory — long-term, working, and drift anchors — lives in its own physical set of indices. See Memory tenancy and isolation and Enterprise authentication for memory.
You may pass scope as a nested object or inline the fields at the top level of the request body — both are accepted:
{ "scope": { "tenant": "acme", "user": "analyst-7" }, "content": "..." }
{ "tenant": "acme", "user": "analyst-7", "content": "..." }
forget and rollup reject a global (fully empty) scope. This is a guard rail: a scopeless forget could wipe every tenant's memory, and a scopeless rollup could consolidate every tenant's memory at once, so both require at least one scope dimension.
How recall ranks (hybrid + decay)
Recall does not just return the nearest vectors. For each query it runs two channels over long-term memory in scope — a vector (kNN) channel and a lexical (BM25) channel — over-fetching each (3×k) so there is depth to re-rank. It then unions and de-duplicates the two channels by memory id and fuses them:
- Normalize each channel's scores to
[0,1]across the candidate set (min-max). - Combine into a base relevance using the channel weights (vector-dominant by default).
- Rescore by an exponential recency-decay multiplier — a memory unused for one half-life is worth half as much — and by a light salience blend.
The fixed default fusion is a vector-dominant hybrid (lexical 0.35, vector 0.65), a 30-day recency half-life, and a 0.25 salience blend. Every hit reports its per-signal components (lexical, vector, base, decay, salience, fused) so you can see why a memory ranked where it did.
An image-only recall query has no text to match lexically, so it retrieves on the vector channel alone. A plain text query recalls both text and image memories through the shared embedding space when a multimodal provider is configured — see Inference memory embeddings.
Durability and the lifecycle
Working memory is a rolling episodic index aged out by an ISM policy; long-term memory is the curated, tiered store. Recall-time decay quietly demotes aging memory in ranking, while ISM and rollup reclaim it from storage. The memory indices are protected system indices — you never read or write them directly; the verbs below do, on your behalf.
The verbs
All bodies are JSON. Requests and responses below are taken from the action and model classes. The five core verbs (remember, recall, forget, drift/check, rollup) are complemented by the read verbs history and audit and the stats report. These cover the semantic-memory surface; durable conversation transcripts are a separate first-class API — see Conversation management.
| Method | Path | Purpose |
|---|---|---|
POST | /_plugins/_memory/remember | Record a turn/observation into memory. |
POST | /_plugins/_memory/recall | Hybrid + decay retrieval of memories in scope. |
POST | /_plugins/_memory/forget | Delete memories in a scope (pinned-exempt by default). |
POST | /_plugins/_memory/drift/check | Establish anchors and/or score a response for topic drift. |
POST | /_plugins/_memory/rollup | Consolidate aged long-term memories in a scope into a summary. |
POST | /_plugins/_memory/history | Return the raw episodic transcript (chronological turns) in scope. |
POST | /_plugins/_memory/audit | Read the operation-history / audit trail (who did what, with what outcome) in scope. |
GET | /_plugins/_memory/stats | Report the active embedder (mode, dimension, semantic, multimodal). |
Remember
Records a turn or observation. content is required unless an image is supplied. The response returns the content-addressed long-term memory id.
POST /_plugins/_memory/remember
{
"scope": { "tenant": "acme", "user": "analyst-7" },
"content": "The area of interest is the Port of Long Beach.",
"role": "user",
"fact_type": "semantic",
"pinned": false,
"tags": { "topic": "aoi" }
}
| Field | Type | Notes |
|---|---|---|
scope | object | Tenancy/task scope (may be inlined). |
content | string | The text to remember. Required unless image is present. |
role | string | Originating role (for example user, assistant). Optional. |
fact_type | string | One of semantic (default), preference, summary, entity, anchor. |
pinned | boolean | When true, the memory is exempt from the default forget. |
tags | object | Arbitrary key/value tags. |
image | string | Base64-encoded image to embed instead of text (multimodal). Optional. |
image_mime | string | Image MIME type (for example image/png). Optional. |
infer | boolean | When true, distill the raw turn into clean facts with the configured chat LLM before storing. Default false; a no-op when plugins.memory.llm.provider is unset. |
strategies | array | Extraction strategies for infer, as fact_type names (semantic, preference, summary, entity, anchor). An unknown value is rejected with 400. Optional. |
parent_id | string | Id of a parent memory this turn extends (threads related turns). Optional. |
trace_number | integer | A caller-supplied ordinal for tracing a turn within a session. Optional. |
Response:
{ "memory_id": "m-1f3a9c2e" }
To have the turn distilled into clean, self-contained facts by the chat LLM before it is stored (see the LLM dial), set infer (and optionally strategies):
POST /_plugins/_memory/remember
{
"scope": { "tenant": "acme", "user": "analyst-7" },
"content": "we agreed to standardize on metric units and to ship the AOI report by Friday",
"infer": true,
"strategies": ["semantic", "preference"]
}
When plugins.memory.llm.provider is unset, infer is a clean no-op — the raw turn is stored unchanged.
Recall
Hybrid + decay retrieval of the most relevant memories in scope. query is required unless an image is supplied. k defaults to 10.
POST /_plugins/_memory/recall
{
"scope": { "tenant": "acme", "user": "analyst-7" },
"query": "which port are we analyzing?",
"k": 5
}
| Field | Type | Notes |
|---|---|---|
scope | object | Restricts recall to this scope. |
query | string | Natural-language query. Required unless image is present. |
k | integer | Number of memories to return. Defaults to 10 when unset or non-positive. |
image | string | Base64-encoded image query (image→image recall). Optional. |
image_mime | string | Image MIME type. Optional. |
Response — hits are highest-relevance first, each with its fused score, fact_type, any tags, and the components that produced the score:
{
"took_millis": 7,
"count": 1,
"hits": [
{
"id": "m-1f3a9c2e",
"memory": "The area of interest is the Port of Long Beach.",
"fact_type": "semantic",
"score": 0.83,
"tags": { "topic": "aoi" },
"components": {
"lexical": 1.0,
"vector": 0.91,
"base": 0.94,
"decay": 0.98,
"salience": 1.0,
"fused": 0.83
}
}
]
}
Forget
Deletes memories in a scope and returns how many were deleted. A non-global scope is required. By default pinned memories are preserved; set pinned_exempt: false to delete pinned memories too.
POST /_plugins/_memory/forget
{
"scope": { "tenant": "acme", "user": "analyst-7" },
"pinned_exempt": true
}
| Field | Type | Notes |
|---|---|---|
scope | object | The non-global scope whose memories to delete. |
pinned_exempt | boolean | When true (the default), pinned memories survive. |
Response:
{ "deleted": 12 }
Drift
The drift rails keep generation on-topic. Drift works against per-scope anchors — a stated goal, pinned constraints, and a rolling topic centroid that the layer maintains as accepted turns come in. A single drift/check call can establish anchors, score a response, or both. Any of goal, constraints, or response may be supplied; at least one is required.
Scoring is pure vector math (a handful of dot products, not an LLM call), so it is cheap enough to run inline on every response. The layer never mutates your generation itself — it returns a verdict plus corrective context and lets you decide how to apply it.
POST /_plugins/_memory/drift/check
{
"scope": { "tenant": "acme", "session": "thread-42" },
"goal": "Assess vessel traffic in the Port of Long Beach.",
"constraints": ["report times in Zulu"],
"response": "Here is a recipe for sourdough bread."
}
| Field | Type | Notes |
|---|---|---|
scope | object | The scope whose anchors to establish and/or check. |
goal | string | Objective to (re)establish as the goal anchor. Optional. |
constraints | array of strings | Constraints to pin as anchors. Optional. |
response | string | Response text to score for drift. Optional — omit to only establish anchors. |
The response fuses three signals — distance from the goal, distance from the running centroid, and novelty (being unmoored from all anchors) — into one score in [0,1], then maps it onto an escalation ladder. Only accepted (on-track or nudge) turns are folded into the centroid, so drift can never redefine the topic.
{
"score": 0.88,
"verdict": "BLOCK",
"needs_intervention": true,
"components": {
"goal_distance": 0.92,
"centroid_distance": 0.85,
"novelty": 0.80
},
"nearest_anchor": "c-3b1f",
"corrective": ["Assess vessel traffic in the Port of Long Beach."]
}
The verdict is one of, in increasing severity:
| Verdict | Meaning |
|---|---|
ON_TRACK | On topic. Proceed unchanged. |
NUDGE | Mild drift. Re-inject the goal/anchors as a soft reminder. |
CORRECT | Significant drift. Inject the corrective context and ask the model to realign. |
BLOCK | Severe drift. Block/regenerate rather than surface the response. |
The default policy is goal-dominant (goal 0.55, centroid 0.30, novelty 0.15) with thresholds at 0.25 (nudge), 0.45 (correct), and 0.70 (block).
Rollup
Rollup is the consolidation half of the lifecycle. It folds many aged, low-touch long-term memories in a scope into one summary memory, cutting record count (and kNN graph size) without losing the gist. It runs off the write path — a scheduled job or an operator invokes it — so remember never pays for it. A non-global scope is required.
Selection is deliberately conservative: pinned memories and existing summaries are never touched, and a scope with fewer than the minimum number of aged memories (default 3) is left alone. The sources are deleted only after the summary is safely written, so a failure never drops data.
POST /_plugins/_memory/rollup
{
"scope": { "tenant": "acme", "user": "analyst-7" },
"older_than_days": 30,
"max_sources": 100
}
| Field | Type | Notes |
|---|---|---|
scope | object | The non-global scope to consolidate within. |
older_than_days | integer | Only memories older than this many days are eligible. Defaults to 30. |
max_sources | integer | Cap on sources folded in one pass. Defaults to 100. |
Response — rolled_up is false (and consolidated is 0) when too few memories aged in:
{
"rolled_up": true,
"consolidated": 7,
"summary_id": "s-9ab21c",
"summary": "Consolidated summary of 7 memories:\n- ...",
"source_ids": ["m-1f3a9c2e", "m-77c0d1a2", "..."]
}
By default, consolidation is a deterministic, near-lossless bulleted merge that preserves every source's text (so deleting the originals is safe). A deployment that registers a chat model behind an LLM provider gets abstractive summaries instead; if that model fails, rollup degrades to the safe merge rather than failing the pass.
History
Returns the raw episodic transcript — the chronological turns in a scope, oldest first — as opposed to recall, which returns the curated, relevance-ranked long-term memories. Use it to reconstruct exactly what happened in a session.
POST /_plugins/_memory/history
{
"scope": { "tenant": "acme", "session": "s-42" },
"from": 0,
"size": 50
}
| Field | Type | Notes |
|---|---|---|
scope | object | Tenancy/task scope (may be inlined). |
from | integer | Zero-based offset for pagination. Default 0. |
size | integer | Maximum turns to return. Default 50. |
Requires the memory_read or memory_read_write role.
Audit
Reads the operation-history / audit trail for a scope — who did what, with what outcome — most recent first. Available when auditing is enabled (plugins.memory.audit.enabled=true; see Security, privacy, and data sovereignty).
POST /_plugins/_memory/audit
{
"scope": { "tenant": "acme" },
"from": 0,
"size": 50
}
| Field | Type | Notes |
|---|---|---|
scope | object | Tenancy/task scope (may be inlined). |
from | integer | Zero-based offset for pagination. Default 0. |
size | integer | Maximum records to return. Default 50. |
Each record is bound to the caller's verified identity (not a client-supplied name). Requires the memory_read or memory_read_write role.
Stats
Reports the active embedding configuration. Use it to confirm which embedder recall is actually using.
GET /_plugins/_memory/stats
{
"embedding": {
"mode": "hashing-v1",
"dimension": 384,
"semantic": false,
"multimodal": false
}
}
| Field | Meaning |
|---|---|
mode | The active embedder's model id (for example a real model, hashing-v1 for the dev embedder, or disabled when no model is configured). |
dimension | The embedding width recall vectors are produced at (locked into the index). |
semantic | true when a real model is configured; false for the non-semantic hashing embedder and when disabled. |
multimodal | true when the embedder accepts images, so a text query can recall image memories. |
See Inference memory embeddings for a full breakdown of the stats fields and how to switch providers.
stats reports only which embedder is active. To see how much an organization has stored and embedded — storage footprint, provider tokens billed, and how much content left the trust boundary — use the usage endpoint (GET /_plugins/_memory/usage), documented in Monitoring memory usage and data residency.
Settings
The memory module's cluster settings are the embedding settings under plugins.memory.embedding.* — they govern which model recall vectors are produced from, and therefore recall quality. They are documented in full on Inference memory embeddings; the most important is:
| Setting | Description |
|---|---|
plugins.memory.embedding.provider | The embedding provider: http, bedrock, vertex, azure, azure_vision, or hashing (non-semantic, dev/CI). Unset by default — memory is disabled until it is set. |
plugins.memory.embedding.dimension | The embedding width. Locked into the memory index at creation — set it before the first remember. Default 384. |
The recall fusion (channel weights, recency half-life, salience), the drift policy (signal weights, thresholds), and the rollup minimum-sources threshold are not currently exposed as cluster settings — they use the fixed defaults described above (vector-dominant hybrid, 30-day half-life; goal-dominant drift with 0.25/0.45/0.70 thresholds; minimum 3 aged sources). The older_than_days, max_sources, and k values are set per request.
Embeddings
Recall is only as good as the vectors behind it, and memory is disabled until an embedding provider is configured. The provider — the local hashing dev embedder, a self-hosted or OpenAI-compatible HTTP endpoint, AWS Bedrock, GCP Vertex AI, Azure OpenAI, or a multimodal image model — is configured separately:
- Inference memory embeddings — provider setup, credentials, dimension locking, and multimodal (image) memory.
Related pages
- Memory tenancy and isolation — how each write and recall is bound to the caller's organization and user, and the per-organization physical index.
- Enable and configure memory — the admin quickstart: embedder, organization provisioning, and the MCP server.
- Enterprise authentication for memory — source the organization from JWT/OIDC, LDAP/AD, SAML, internal users, or client certificates.
- API keys for memory — personal access tokens for people and agents.
- Using memory in your organization — the end-user and MCP-client view.
- Conversation management — durable, org-tenant-scoped conversation transcripts, messages, and traces.
- Vector and semantic search — the k-NN and semantic search capabilities memory recall is built on.