# Conversations and memory

> Open, continue, read and delete conversations with an agent, what each turn costs, and how facts and relationships carry from one conversation to the next.

A **conversation** (a session) is one thread between one end user and one agent. Your user's device opens it with their [ephemeral key](https://docs.lumavox.ai/agents/end-users.md#ephemeral-keys), then talks on [the WebSocket](https://docs.lumavox.ai/agents/websocket.md). Everything here is called with that token, never with an API key, and answers for the token's person alone.

```bash title="curl"
curl https://api.lumavox.ai/v1/agent/sessions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07" }'
```

```json title="Response — 201"
{
  "id": "0193b1f4-8e02-7c3a-a1d5-6b7e90c4f218",
  "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07",
  "end_user_id": "eu_3kQ9x7MzT2pL8vNc",
  "title": null,
  "turn_seq": 0,
  "resumed": false,
  "messages": { "data": [], "next_cursor": null },
  "relation": null,
  "created_at": "2026-10-24T09:31:40.000Z",
  "updated_at": "2026-10-24T09:31:40.000Z"
}
```

Take the `id` to the socket: `wss://api.lumavox.ai/v1/agent/stream?session_id=<id>`.

## Open or continue a conversation

`POST /v1/agent/sessions` takes:

| Field | What it does |
|---|---|
| `agent_id` | Required. The agent to talk to. An unknown one is 404; one the token is not allowed to use is 403. |
| `resume` | Leave it unset to **continue the person's most recent conversation with this agent**, if it is recent enough, and start a new one otherwise. `false` always starts a new one: send it from a "new chat" button. |
| `end_user_id` | Optional. If you send it, it must name the token's own person, or the call is 403. |
| `title` | An optional label, up to 255 characters. |

"Recent enough" is `resume_max_age_days` in your [organisation's defaults](https://docs.lumavox.ai/agents.md#organisation-wide-defaults), else the platform's `agents.sessions.resume_max_age_days` ([Limits](https://docs.lumavox.ai/limits.md)). The response's `resumed` says which happened. Branch on it: a continued conversation arrives with its latest messages already in `messages` and its `turn_seq` past zero.

Starting fresh loses nothing about the person. Facts and the relationship belong to the person, not the conversation, and carry into every new one.

**Opening a session makes no model call and no speech call.** A new conversation comes back with no messages, even for an agent with an opening line. The opening line is the agent's first turn on the socket, written when the device connects.

`relation` is how the agent stands with this person as the conversation opens. It is `null` when the agent does not keep relationships. See [Relationships](#relationships).

## Read a conversation

`GET /v1/agent/sessions` lists the person's conversations, most recently used first, without messages — only conversations with agents the token may use. It is a page: `limit` (1 to 200, default 50) and `cursor`, answered as `data` and `next_cursor` ([Conventions](https://docs.lumavox.ai/conventions.md#lists-and-pages)).

`GET /v1/agent/sessions/{session_id}` returns the conversation and **a page of its most recent messages** in `messages`, oldest first:

```json title="A message"
{
  "id": "0193b1f5-0c31-7f0e-b8a2-1d4c6e9a7b30",
  "role": "assistant",
  "content": "Then let's start in Alfama. It's quiet before ten.",
  "mood": "cheerful",
  "mood_intensity": 0.6,
  "knowledge": null,
  "created_at": "2026-10-24T09:32:05.000Z"
}
```

- `role` is `user` or `assistant`.
- `mood` and `mood_intensity` are what the agent reported for that reply, when it reports moods; `null` otherwise.
- `knowledge` lists what the agent looked up in its [knowledge](https://docs.lumavox.ai/agents/knowledge.md) for that reply: each lookup's query, outcome and the entries it found, with scores. It is `null` when nothing was looked up.

`limit` sets how many messages (default 50, up to 200), counting back from the newest. To read further back, pass the previous response's `messages.next_cursor` as `cursor`. It is `null` once you have reached the start.

```js title="JavaScript"
let page = await get(`/v1/agent/sessions/${id}?limit=200`);
const messages = [...page.messages.data];
while (page.messages.next_cursor !== null) {
  const cursor = encodeURIComponent(page.messages.next_cursor);
  page = await get(`/v1/agent/sessions/${id}?limit=200&cursor=${cursor}`);
  messages.unshift(...page.messages.data);
}
```

The transcript is kept exactly as it happened. [Compaction](#compaction) shortens what the model is sent, never what you can read back.

## Delete a conversation

`DELETE /v1/agent/sessions/{session_id}` deletes the conversation and every message in it, and answers **204** with no body. It cannot be undone. What the agents remember about the person, and the relationship, stay.

## What a turn costs

The conversation is held for you, so each turn sends only the new message. That saves bandwidth, **not tokens**: on every turn the model reads the conversation so far (or, once compacted, its summary and what came after) and the turn is billed for it.

- The socket's `usage` event and `turn.done` report the model and its token counts for each reply.
- A spoken turn is also charged for its transcription, and a speaking agent for its speech, on separate usage rows.
- Everything a conversation causes is charged to the token that caused it, including the memory work below.

To keep long conversations affordable, compact them.

## Compaction

Compaction summarises the conversation so far, all but its last few exchanges. Later turns start from the summary and those exchanges, word for word, instead of the full history, so the agent still has what was just said exactly: a list's items, a number, the question it asked. How many messages stay is the platform's `agents.memory.compaction.keep_messages` ([Limits](https://docs.lumavox.ai/limits.md)), never more than a quarter of the threshold, and starting at something the person said. A compaction you ask for on a conversation no longer than that summarises it whole. The transcript itself does not change.

**Automatically**: after a reply whose first model call read the agent's `compact_threshold_tokens`, the conversation is compacted, after the reply and never inside it. What counts is what the next turn sends again: the conversation, its summary and the instruction. The inline knowledge, the turn's looked-up passages and its tool results are left out, since no summary shrinks them. An agent that sets no threshold compacts at the platform's `agents.memory.compaction.default_threshold_tokens` ([Limits](https://docs.lumavox.ai/limits.md)). An agent may set at most 200,000.

**By hand**:

```bash title="curl"
curl https://api.lumavox.ai/v1/agent/sessions/$SESSION_ID/compact \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json title="Response"
{
  "session_id": "0193b1f4-8e02-7c3a-a1d5-6b7e90c4f218",
  "summary": "The traveller arrives Friday, likes quiet mornings and wants to see Alfama first.",
  "after_seq": 18,
  "trigger": "manual",
  "model": "gemini-3.1-flash-lite",
  "input_tokens": 2310,
  "output_tokens": 96,
  "facts_stored": 0
}
```

`after_seq` is the last message the summary covers. Everything after it, the kept exchanges and what comes next, is still sent to the model word for word. Every socket on the conversation is sent a `compacted` event, for this one and the agent's own ([WebSocket](https://docs.lumavox.ai/agents/websocket.md#what-arrives)).

What it may answer instead, none of it charged:

- **402**: the token has spent its credit.
- **409**: another compaction of the same conversation finished first.
- **422**: nothing new since the last compaction; or the model's summary was empty, or no shorter than what it would replace.
- **502**: the model provider failed or timed out.

Compaction is a model call charged to the token. The agent's `compact_instruction`, `compact_model`, `compact_thinking_level` and `compact_max_output_tokens` shape it.

- **The summary is capped**: at the agent's `compact_max_output_tokens`, or the platform's `agents.memory.compaction.max_output_tokens`, 4,000 tokens unless the platform changes it ([Limits](https://docs.lumavox.ai/limits.md) has today's), the model's thinking included, and twice that when the compaction extracts facts too. A summary no shorter than what it would replace is not kept.
- **A long stretch is compacted in passes**, oldest first, each from the last one's summary. A compaction you ask for answers with its first pass; the rest follow on their own, each charged to the same token.
- **When the model fails**, the next automatic compaction waits, longer after each failure in a row.

## Facts

**Facts** are durable things the agents have learned about a person: "has a daughter called Ana", "prefers mornings". They are shared across every agent you run, because the same person talking to a different character is still the same person.

Two of an agent's fields change that:

- `facts_enabled` — on unless you turn it off. Off, the agent's conversations are never read for facts and its turns recall none, so nothing is charged for either. A compaction of its conversations extracts nothing, whatever the request asks. Facts already stored stay.
- `facts_scope` — `person` (the default) or `agent`. With `agent`, what the agent learns is kept to it: your other agents do not recall it, and it does not recall theirs. A fact keeps the scope it was learnt under, and says so in its `scope`.

Facts are extracted **after** a reply, never during one:

- the platform's `agents.memory.facts.idle_minutes` after a conversation's last turn, and once more `agents.memory.facts.retry_minutes` later if that reading fails ([Limits](https://docs.lumavox.ai/limits.md));
- when the person opens a session, for their most recent other conversation still unread — one whose token was revoked or spent before those minutes were up, or whose readings failed — however old it is, charged to the token that opens;
- with a compaction, when the agent's `extract_facts_on_compact` is on or the request sends `{"extract_facts": true}`;
- on demand, with `POST /v1/agent/sessions/{session_id}/extract`: a compaction and an extraction in one model call. `facts_stored` in its response says how many were kept.

Each reading reads what was said since the last one, with the platform's `agents.memory.reading.context_messages` before it for context — never the whole conversation again. A compaction that extracts reads every turn since the last compaction; when that covers everything since the last reading, it counts as the reading, and the one after the quiet minutes finds nothing new and costs nothing.

The agent's `fact_instruction` says what is worth remembering. Every turn, the person's facts are put in front of the model: as many as fit in the platform's `agents.memory.recall.budget_chars` ([Limits](https://docs.lumavox.ai/limits.md); a character is about a quarter of a token), and never more than the agent's `max_facts` (1 to 100) when it sets one. They are prompt tokens on every turn, mostly served from the model's cache.

When they do not all fit, the ones mentioned most recently go in — and, from the person's next turn on, the ones nearest what the conversation is about: after each turn the last exchange and any facts not yet seen are embedded, and the following turn recalls a quarter of the budget by recency and the rest by relevance. That costs an embedding at cost (`facts_embed` usage, a fraction of a credit), only for people whose facts no longer all fit. A reading that hears a known fact again — "I'm Ada, remember?" — confirms it rather than storing it twice, and a confirmed fact moves to the front. So what a person keeps saying stays in front of the agent, and what they said once long ago is the first to give way.

Facts reach the model as what the person said about themselves, never as instructions. Whatever `fact_instruction` says, a reading never keeps:

- instructions addressed to the agent — how to behave, which rules to ignore;
- claims about the person's standing with your service — that they are your staff or an administrator, or are owed a refund, a discount or access;
- secrets — passwords, PINs and codes, card, bank and account numbers, identity numbers, keys and tokens.

The built-in instruction also leaves out health, beliefs, sex life, political views, ethnicity and crime, unless the person asks to be remembered by them. If your agents need those, write your own `fact_instruction`.

`GET /v1/agent/facts` lists what is remembered about the token's person, oldest first: 100 a page by default, up to 200 with `limit`. Pass `next_cursor` back as `cursor` for the next page; it is `null` on the last. A fact learnt while you page lands on the last page:

```json title="Response"
{
  "data": [
    {
      "id": "0193b201-77aa-7d40-9c11-3e5b8f2a6d09",
      "text": "Arrives in Lisbon on Friday.",
      "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07",
      "confirmed_at": "2026-10-26T18:02:40.000Z",
      "created_at": "2026-10-24T09:48:12.000Z",
      "replaced_at": null,
      "replaced_by": null,
      "scope": "person",
      "source": "conversation",
      "updated_at": "2026-10-24T09:48:12.000Z"
    }
  ],
  "next_cursor": null
}
```

`agent_id` is the agent that was being talked to when the fact was learned. `confirmed_at` is when a conversation last said it again, `null` if none has.

### When a fact changes

A person who moves, changes job or ends a relationship should not be remembered both ways. A reading sees the known facts and may **replace** one with what is true now — "Lives in London" by "Lives in Paris" — or **remove** one that is no longer true, with nothing in its place. It never touches more than ten in one reading.

A replaced or removed fact is hidden from the agents and from the list at once, and deleted the platform's `agents.memory.facts.replaced_kept_days` later ([Limits](https://docs.lumavox.ai/limits.md)). Until then `include_replaced=true` lists it too, with `replaced_at` set and `replaced_by` naming the fact that took its place (`null` when it was removed). If a reading got it wrong, correct the fact that replaced it; a replaced fact said again stands again.

### Adding and correcting facts

Your app can tell the agents what it already knows about the person, and correct what they got wrong:

```bash title="curl"
curl -X POST https://api.lumavox.ai/v1/agent/facts \
  -H "Authorization: Bearer $EPHEMERAL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Prefers to be called Sam."}'
```

- `POST /v1/agent/facts` adds one: `201` with the fact, or `200` with the one that already said the same thing. With `agent_id`, only that agent recalls it. It works before the person has ever talked to an agent.
- `PATCH /v1/agent/facts/{fact_id}` with `{"text": …}` rewrites one. The old words are kept as forgotten, so no reading of what was said before brings them back. A fact a later conversation replaced, or words another fact already says, are a `409`.

**The agent forgets when the person asks.** "Forget where I live" makes it call its own `forget_fact` tool with the line it remembers, and that fact is forgotten as `DELETE /v1/agent/facts/{fact_id}` would — gone from every later conversation, and not learnt again from the old words. It is offered only when the agent has facts in front of it, and shows on the socket as a `platform` tool call.

**What you write is yours**: its `source` is `customer`, and a later conversation confirms it but never replaces or removes it. A fact learnt in a conversation has `source` `conversation`. Both are free, and both refuse a secret (`422`). From your server, the same is `/v1/end-users/{end_user_id}/facts` with your API key — see [End users](https://docs.lumavox.ai/agents/end-users.md#what-is-remembered-about-them).

## Relationships

An agent with `relation_enabled` keeps a standing relationship with each person: how warm it feels, how much it trusts them and how well they know each other. It changes only when something in a conversation changes the agent's view, which is rare, and it is kept until the person is deleted.

`GET /v1/agent/relations` returns one row per agent the person has talked to that keeps a relationship, most recently active first. Send `agent_id` for one agent's row. It is a page: `limit` (1 to 200, default 50) and `cursor`, answered as `data` and `next_cursor`.

```json title="A relation"
{
  "agent_id": "0193b1e2-6c1d-7a4e-9f20-5d8a3c2b1e07",
  "agent_name": "Mira",
  "end_user_id": "eu_3kQ9x7MzT2pL8vNc",
  "tier": "Acquaintances",
  "warmth": 0.3,
  "trust": 0.2,
  "disclosure": 0.1,
  "turns": 14,
  "conversations": 2,
  "recorded": 1,
  "note": "Curious and easy to talk to.",
  "summary": "You have spoken a couple of times and you like them.",
  "first_met_at": "2026-10-20T18:02:44.000Z",
  "last_seen_at": "2026-10-24T09:47:51.000Z"
}
```

| Field | Meaning |
|---|---|
| `tier` | What the agent would call the relationship, and what it is told: Despised, Disliked, Acquaintances, Friends or Good Friends. Familiarity caps how far it can rise: a first conversation tops out at Acquaintances. Dislike is not capped. |
| `warmth`, `trust` | From -1 to 1. Zero is indifference, where every relationship starts. |
| `disclosure` | How much the agent has told *them* about itself, -1 to 1. |
| `turns`, `conversations` | Counted, not judged. |
| `recorded` | How many times the model recorded a change. Zero on a long history means the agent is not recording anything. |
| `note` | One line the model wrote about them, replaced each time; `null` until it writes one. |
| `summary` | The relationship as the model is told it. An empty string means nothing is worth saying yet. |

The same object appears as `relation` when a session opens and in the socket's `ready` frame, and a `relation` event arrives on the socket on the rare turn when it changes.

## Forgetting

| Call | Erases | Answers |
|---|---|---|
| `DELETE /v1/agent/facts/{fact_id}` | One fact. | `204`, no body |
| `DELETE /v1/agent/facts` | Every fact about the token's person. Their conversations, their relationships and the end user remain. | how many: `{"facts_deleted": 11}` |
| `DELETE /v1/end-users/{end_user_id}` | The person, every conversation and every fact. Called from your server with an API key. See [End users](https://docs.lumavox.ai/agents/end-users.md#delete-one). | what went, counted |

None of them can be undone.

**A forgotten fact stays forgotten.** The conversation it came from still holds the words, but no reading of what was said before the fact was forgotten stores it again. If the person says it again afterwards, it is remembered again. To remove the words too, delete the conversation. A conversation's own summaries keep what they summarised until it is deleted.

## Next

- [The WebSocket](https://docs.lumavox.ai/agents/websocket.md): take turns in a conversation
- [Agent reference](https://docs.lumavox.ai/reference/agent.md): every field of these routes
