Skip to main content
Version: 0.12.0

Inference memory

Introduced 0.12.0

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).

tip

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:

  1. writes the raw turn into a rolling working (episodic) index, and
  2. 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_typeMeaning
semanticA self-contained factual statement (the default).
preferenceA durable user/agent preference ("prefers metric units").
summaryA rolled-up memory standing in for many older, consolidated ones (see rollup).
entityAn entity/relationship fact.
anchorA 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 fieldPurpose
tenantTenant/customer partition.
namespaceCollection/document-set namespace (for example, an ingest collection).
agentAgent identifier (for example, coding-agent).
sessionSession/thread identifier.
userUser identifier.

Scope also namespaces the drift anchors, so two users never share a "sense of topic."

note

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": "..." }
note

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:

  1. Normalize each channel's scores to [0,1] across the candidate set (min-max).
  2. Combine into a base relevance using the channel weights (vector-dominant by default).
  3. 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.

note

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.

MethodPathPurpose
POST/_plugins/_memory/rememberRecord a turn/observation into memory.
POST/_plugins/_memory/recallHybrid + decay retrieval of memories in scope.
POST/_plugins/_memory/forgetDelete memories in a scope (pinned-exempt by default).
POST/_plugins/_memory/drift/checkEstablish anchors and/or score a response for topic drift.
POST/_plugins/_memory/rollupConsolidate aged long-term memories in a scope into a summary.
POST/_plugins/_memory/historyReturn the raw episodic transcript (chronological turns) in scope.
POST/_plugins/_memory/auditRead the operation-history / audit trail (who did what, with what outcome) in scope.
GET/_plugins/_memory/statsReport 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" }
}
FieldTypeNotes
scopeobjectTenancy/task scope (may be inlined).
contentstringThe text to remember. Required unless image is present.
rolestringOriginating role (for example user, assistant). Optional.
fact_typestringOne of semantic (default), preference, summary, entity, anchor.
pinnedbooleanWhen true, the memory is exempt from the default forget.
tagsobjectArbitrary key/value tags.
imagestringBase64-encoded image to embed instead of text (multimodal). Optional.
image_mimestringImage MIME type (for example image/png). Optional.
inferbooleanWhen 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.
strategiesarrayExtraction strategies for infer, as fact_type names (semantic, preference, summary, entity, anchor). An unknown value is rejected with 400. Optional.
parent_idstringId of a parent memory this turn extends (threads related turns). Optional.
trace_numberintegerA 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
}
FieldTypeNotes
scopeobjectRestricts recall to this scope.
querystringNatural-language query. Required unless image is present.
kintegerNumber of memories to return. Defaults to 10 when unset or non-positive.
imagestringBase64-encoded image query (image→image recall). Optional.
image_mimestringImage 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
}
FieldTypeNotes
scopeobjectThe non-global scope whose memories to delete.
pinned_exemptbooleanWhen 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."
}
FieldTypeNotes
scopeobjectThe scope whose anchors to establish and/or check.
goalstringObjective to (re)establish as the goal anchor. Optional.
constraintsarray of stringsConstraints to pin as anchors. Optional.
responsestringResponse 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:

VerdictMeaning
ON_TRACKOn topic. Proceed unchanged.
NUDGEMild drift. Re-inject the goal/anchors as a soft reminder.
CORRECTSignificant drift. Inject the corrective context and ask the model to realign.
BLOCKSevere 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
}
FieldTypeNotes
scopeobjectThe non-global scope to consolidate within.
older_than_daysintegerOnly memories older than this many days are eligible. Defaults to 30.
max_sourcesintegerCap 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", "..."]
}
note

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
}
FieldTypeNotes
scopeobjectTenancy/task scope (may be inlined).
fromintegerZero-based offset for pagination. Default 0.
sizeintegerMaximum 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
}
FieldTypeNotes
scopeobjectTenancy/task scope (may be inlined).
fromintegerZero-based offset for pagination. Default 0.
sizeintegerMaximum 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
}
}
FieldMeaning
modeThe active embedder's model id (for example a real model, hashing-v1 for the dev embedder, or disabled when no model is configured).
dimensionThe embedding width recall vectors are produced at (locked into the index).
semantictrue when a real model is configured; false for the non-semantic hashing embedder and when disabled.
multimodaltrue 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.

note

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:

SettingDescription
plugins.memory.embedding.providerThe 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.dimensionThe embedding width. Locked into the memory index at creation — set it before the first remember. Default 384.
note

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: