Skip to main content
Version: 0.12.0

Conversation management

Introduced 0.12.0

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.

important

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.

MethodPathPurpose
POST/_plugins/_memory/conversationsCreate a named conversation.
GET/_plugins/_memory/conversationsList the caller org's conversations.
POST/_plugins/_memory/conversations/_searchSearch 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}/messagesAppend a message to a conversation.
GET/_plugins/_memory/conversations/{conversation_id}/messagesList a conversation's messages.
POST/_plugins/_memory/conversations/{conversation_id}/messages/_searchSearch one conversation's messages.
GET/_plugins/_memory/messages/{message_id}Fetch one message.
GET/_plugins/_memory/messages/{message_id}/tracesList a message's threaded trace children.

Tenancy and user scoping

note

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

SettingTypeDefaultEffect
plugins.memory.conversation.redact_profileCompliance profile wire namenoneThe 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_roleslist(empty)Roles whose members see stored content in the clear at read time.
plugins.memory.conversation.redact_exempt_backend_roleslist(empty)Backend roles exempt from read-time redaction.
plugins.memory.conversation.redact_exempt_when_no_identitybooleanfalseFail-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).

warning

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

RoleConversation management grantsUnderlying actions
memory_readGet, list, and search conversations; get, list, trace, and search messages (reads only).conversation_{get,list,search}, interaction_{get,list,traces,search}
memory_read_writeAll of the above, plus create/update/delete a conversation and append a message.adds conversation_{create,update,delete}, interaction_create
memory_full_accessEverything (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, 403 on 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.