Conversation management
The memory module gives you first-class, org-tenant-scoped conversations — named, durable threads of messages (and threaded traces) that you create, list, fetch, update, delete, and search through a small REST surface under _plugins/_memory/conversations. Each org's conversations and messages live in their own physical per-org indices (.plugins-memory-conversations-<org> and .plugins-memory-messages-<org>), the same physical-isolation model the rest of the memory layer uses.
A conversation is a different kind of object from the working/long-term memory a memory verb writes. A remember/recall memory is a curated fact that is ranked by relevance, decayed with age, and aged out or rolled up over time. A conversation is a record of record: it is durable, explicitly listable, addressable by id, and searchable with a raw query DSL — it is not ISM-aged and never silently disappears. Use conversations when you need an auditable, retrievable transcript store (a chat history, an agent run log, a case thread); use the memory verbs when you need semantic recall.
The two compose: a conversation id is the scope's session (it is the conversation document's id). So a conversation named thread-42 shares the same session dimension as a recall or history call scoped to session: "thread-42" — the durable transcript and the semantic memory for a thread line up under one key.
Conversation management replaces the legacy /_plugins/_ml/memory/* conversation CRUD. Those routes still work for backward compatibility but are deprecated as of 0.12.0 — see Migrating from the legacy memory CRUD API. New integrations should target /_plugins/_memory/conversations.
Table of contents
Endpoints
Eleven routes cover the conversation lifecycle, messages, traces, and search. All are REST endpoints authenticated with a personal access token (or JWT bearer) — they are not MCP tools.
| Method | Path | Purpose |
|---|---|---|
POST | /_plugins/_memory/conversations | Create a named conversation. |
GET | /_plugins/_memory/conversations | List the caller org's conversations. |
POST | /_plugins/_memory/conversations/_search | Search conversations with a raw query DSL. |
GET | /_plugins/_memory/conversations/{conversation_id} | Fetch one conversation's metadata. |
PUT | /_plugins/_memory/conversations/{conversation_id} | Update a conversation's name/metadata. |
DELETE | /_plugins/_memory/conversations/{conversation_id} | Delete a conversation and cascade-delete its messages. |
POST | /_plugins/_memory/conversations/{conversation_id}/messages | Append a message to a conversation. |
GET | /_plugins/_memory/conversations/{conversation_id}/messages | List a conversation's messages. |
POST | /_plugins/_memory/conversations/{conversation_id}/messages/_search | Search one conversation's messages. |
GET | /_plugins/_memory/messages/{message_id} | Fetch one message. |
GET | /_plugins/_memory/messages/{message_id}/traces | List a message's threaded trace children. |
Tenancy and user scoping
Conversation management is doubly scoped. First, the organization is pinned from the caller's verified identity — a conversation and its messages physically live in that org's own indices, and a request that asserts a different org is rejected with 403 (a cross-org fetch by id returns { "found": false }, never another org's data). Second, within an org, every conversation and message is additionally user-scoped: a same-org user cannot get, rename, delete, or search another user's conversation or messages by id — the tenancy layer injects the caller's user as a mandatory filter. See Memory tenancy and isolation.
Because org and user are pinned from identity, you never send them to widen access. On the CRUD and message endpoints the scope you send only refines within your own org/user (for example, choosing the session/conversation id or a namespace); on the _search endpoints the scope comes from query parameters and is injected server-side as filter clauses (see Searching conversations and messages).
Create and manage a conversation
Create
POST /_plugins/_memory/conversations. The scope's session dimension becomes the conversation id; omit it and a fresh id is assigned and returned. name, application_type, and additional_info are optional; name and additional_info are compliance-redacted on write when a redaction profile is configured (see Compliance and redaction).
POST /_plugins/_memory/conversations
Authorization: ApiKey azLThjJhLi4uLi4uOnNlY3JldA==
Content-Type: application/json
{
"scope": { "session": "case-7841", "namespace": "support", "agent": "triage-bot" },
"name": "Refund dispute — order 5591",
"application_type": "support-agent",
"additional_info": { "priority": "high" }
}
The response is the conversation id (the session, or the auto-assigned UUID when you omitted it):
{ "conversation_id": "case-7841" }
List
GET /_plugins/_memory/conversations returns the caller org's conversations, most-recently-updated first. Paginate with from/size as query parameters or in the request body. The default page size is 50, capped at 1000.
GET /_plugins/_memory/conversations?from=0&size=20
Authorization: ApiKey azLThjJhLi4uLi4uOnNlY3JldA==
{
"count": 1,
"conversations": [
{
"conversation_id": "case-7841",
"name": "Refund dispute — order 5591",
"application_type": "support-agent",
"additional_info": { "priority": "high" },
"created_time": 1751500000000,
"updated_time": 1751500000000
}
]
}
Get
GET /_plugins/_memory/conversations/{conversation_id} returns one conversation's metadata, or { "found": false } when it does not exist in the caller's org and user scope. Timestamps are epoch milliseconds.
{
"conversation_id": "case-7841",
"name": "Refund dispute — order 5591",
"application_type": "support-agent",
"additional_info": { "priority": "high" },
"created_time": 1751500000000,
"updated_time": 1751500000000
}
Update
PUT /_plugins/_memory/conversations/{conversation_id} updates the name and/or additional_info (both redacted on write when a profile is configured). It returns an acknowledgement.
PUT /_plugins/_memory/conversations/case-7841
{ "name": "Refund dispute — order 5591 (resolved)", "additional_info": { "priority": "low" } }
{ "acknowledged": true }
Delete
DELETE /_plugins/_memory/conversations/{conversation_id} deletes the conversation and cascade-deletes all of its messages.
{ "acknowledged": true }
Messages and traces
Append a message
POST /_plugins/_memory/conversations/{conversation_id}/messages appends a message to the conversation named in the path. A message is a first-class turn with dedicated input / prompt_template / response fields (unlike an aged-out episodic turn). Supply parent_interaction_id and trace_number to thread a sub-step (a tool call, a reasoning branch) under a parent message and order it within that thread. input, response, and additional_info are compliance-redacted on write.
POST /_plugins/_memory/conversations/case-7841/messages
Authorization: ApiKey azLThjJhLi4uLi4uOnNlY3JldA==
Content-Type: application/json
{
"input": "The customer wants a refund on order 5591.",
"prompt_template": "support-triage-v3",
"response": "Opened refund case; ETA 3-5 business days.",
"origin": "triage-bot",
"additional_info": { "channel": "email" }
}
The response is the created message (interaction) id:
{ "interaction_id": "a1b2c3d4-..." }
List messages
GET /_plugins/_memory/conversations/{conversation_id}/messages returns the conversation's messages, oldest first. from/size work as query parameters or body fields.
{
"count": 1,
"messages": [
{
"id": "a1b2c3d4-...",
"conversation_id": "case-7841",
"input": "The customer wants a refund on order 5591.",
"prompt_template": "support-triage-v3",
"response": "Opened refund case; ETA 3-5 business days.",
"origin": "triage-bot",
"additional_info": { "channel": "email" },
"created_time": 1751500000000
}
]
}
Get one message
GET /_plugins/_memory/messages/{message_id} returns a single message object, or { "found": false } when it is not in the caller's scope.
Get message traces
GET /_plugins/_memory/messages/{message_id}/traces returns the message's threaded sub-steps — the child messages that carry it as their parent_interaction_id — ordered by trace_number.
{
"count": 2,
"messages": [
{ "id": "...", "parent_interaction_id": "a1b2c3d4-...", "trace_number": 1, "origin": "lookup-tool", "response": "order 5591 found", "created_time": 1751500000100 },
{ "id": "...", "parent_interaction_id": "a1b2c3d4-...", "trace_number": 2, "origin": "refund-tool", "response": "refund initiated", "created_time": 1751500000200 }
]
}
Searching conversations and messages
POST /_plugins/_memory/conversations/_search and POST /_plugins/_memory/conversations/{conversation_id}/messages/_search take a raw search DSL as the request body — a standard search source, for example { "query": {...}, "from": N, "size": M }.
The tenancy scope for these endpoints does not come from the body (the body is the query). It comes from query parameters (tenant, namespace, agent, user), and it is injected server-side as filter clauses wrapped around your query. This is a hard boundary: your DSL is added as a must inside a bool query whose filters pin the caller's org (and user), so a crafted query can never widen results past your own org/user. The message search is additionally fenced to the one conversation in the path. Matching hits are compliance-redacted on read unless the caller is exempt. The search default page size is 10, capped at 1000.
POST /_plugins/_memory/conversations/_search?namespace=support
Authorization: ApiKey azLThjJhLi4uLi4uOnNlY3JldA==
Content-Type: application/json
{
"query": { "match": { "name": "refund" } },
"from": 0,
"size": 10
}
The response uses the same list shape as GET /conversations (a count and a conversations array); the message search returns the count + messages shape.
Compliance and redaction
Conversation content is redacted natively by the store — the same PII/PHI compliance engine the ingest-time and search-time processors use, brought to conversation management so a regulated customer using memory as a conversation store gets its policies enforced on conversation data. Redaction runs at two points: on write (content is redacted before it is indexed, so sensitive data never lands in the index in the clear) and on read (returned content is redacted at response assembly, unless the caller holds a configured exempt role).
| Setting | Type | Default | Effect |
|---|---|---|---|
plugins.memory.conversation.redact_profile | Compliance profile wire name | none | The redaction profile applied to input / response / additional_info and a conversation's name / metadata — on write (never stored in the clear) and again on read. none disables redaction. |
plugins.memory.conversation.redact_exempt_roles | list | (empty) | Roles whose members see stored content in the clear at read time. |
plugins.memory.conversation.redact_exempt_backend_roles | list | (empty) | Backend roles exempt from read-time redaction. |
plugins.memory.conversation.redact_exempt_when_no_identity | boolean | false | Fail-closed control: a caller with no verified identity is redacted unless this is true. |
All four are node-scoped and dynamic (settable with PUT _cluster/settings).
Write-time redaction is never bypassed by an exemption — what is not persisted cannot be revealed. Exemptions only reveal already-stored content at read time to a privileged role. So with a profile set, sensitive data is redacted the moment it is written regardless of who wrote it; only reads can be exempted.
Valid redact_profile values are the compliance profiles: none, pii_basic, gdpr, ccpa, hipaa, pci_dss, soc2, fedramp, iso_27001, and nist_800_53. See the compliance processor for what each profile detects, and Compliance & content governance for how a governance blueprint sets this per regime.
Access control (PAT scopes)
The conversation endpoints map to cluster:admin/lucenia/memory/* cluster actions granted by the built-in memory roles. Mint a personal access token carrying the least-privilege role your integration needs (see API keys for memory).
| Role | Conversation management grants | Underlying actions |
|---|---|---|
memory_read | Get, list, and search conversations; get, list, trace, and search messages (reads only). | conversation_{get,list,search}, interaction_{get,list,traces,search} |
memory_read_write | All of the above, plus create/update/delete a conversation and append a message. | adds conversation_{create,update,delete}, interaction_create |
memory_full_access | Everything (the cluster:admin/lucenia/memory/* wildcard), plus org/model administration. | cluster:admin/lucenia/memory/* |
All underlying actions are named cluster:admin/lucenia/memory/conversation_{create,get,list,update,delete,search} and cluster:admin/lucenia/memory/interaction_{create,get,list,traces,search}.
Migrating from the legacy memory CRUD API
The older /_plugins/_ml/memory/* conversation CRUD API is deprecated as of 0.12.0. Its routes still function (they emit a deprecation warning only) so existing clients keep working during a rolling upgrade, but new integrations should use /_plugins/_memory/conversations. The differences that matter:
- Org-tenant scoping. The legacy API is user-owner-scoped only. Conversation management is scoped to the caller's organization (physical per-org indices,
403on cross-tenant assertion) and user. - Compliance and ABAC. Conversation management runs every write and read through the compliance engine (write- and read-time redaction) and the tenancy ABAC gate; the legacy CRUD does neither.
Related pages
- Memory tenancy and isolation — the organization/user boundary and per-organization physical indices.
- Monitoring memory usage and data residency — per-org storage footprint and data-residency reporting.
- Inference memory — the
remember/recall/drift/rollupmemory verbs that compose with a conversation on the samesession. - Compliance & content governance — governance blueprints that set the conversation redaction profile per regime.
- Memory APIs (legacy) — the deprecated
/_plugins/_ml/memory/*conversation CRUD this replaces.