---
title: "Threads"
---

> Documentation Index
> Fetch the complete documentation index at: https://truememory-docs.mvstg.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Threads

Threads persist conversation state across requests. You never create one explicitly — pass a `thread_id` on a [chat request](/ai-gateway/chat-completions/#threads) and Truememory creates or resumes it. This page covers managing them afterwards: list, rename, delete, and history. All endpoints require a Bearer token from [Truememory Auth](/authentication/overview/).

## Creating Threads

Pass a `thread_id` in your `/v1/chat/completions` request to maintain conversation state. Truememory uses it to persist message history so the LLM has context from previous turns.

- If **omitted**, Truememory auto-generates a UUID for the thread. Each request starts a new conversation.
- If **provided**, Truememory resumes the conversation from where it left off. Use any string (e.g. a UUID you generate client-side).

:::note
The `thread_id` is not returned in the response. Track it client-side if you want to continue conversations.
:::

## Retrying a Failed Turn

A chat request that fails partway through can still have stored its messages on the thread. Sending the same request again would store them twice. To avoid that, anchor each request on the last message you received.

- **`message_id`** — the ID of the last message the turn stored. A non-streaming response returns it as a top-level field. A streaming response adds it as a top-level key on the chunk that carries the `finish_reason` (`stop` or `tool_calls`). It is not sent when `finish_reason` is `error`, and it is only returned for requests with a `thread_id`.
- **`parent_message_id`** — send the last `message_id` you received. `messages` holds the messages after it. It may also start with replayed history that ends at the parent; that history is dropped exactly as it is without `parent_message_id`.

```json
{
  "model": "anthropic/claude-sonnet-4-5",
  "thread_id": "my-thread",
  "parent_message_id": "<message_id from the previous response>",
  "messages": [{ "role": "user", "content": "What about tomorrow?" }]
}
```

If the request fails, send it again unchanged, with the same `parent_message_id` and `messages`. You never need to trim it. Truememory drops the replayed history the same way the first attempt did, compares the rest with what is already stored after the parent, and only appends what is missing. When the whole request is already stored and the turn did not finish, the model runs again on the stored thread. A server tool that already returned a result is not run again.

A turn that finished is never run again. That includes a turn that ended in tool calls for your client tools: you already received those calls. You get a `409 THREAD_PARENT_STALE` instead. Read the thread [history](#get-thread-history) and use the last message's `run_id` as the new `parent_message_id`.

In a client tool loop, use the `message_id` from the `tool_calls` finish as the parent of the request that sends the tool result.

`parent_message_id` is ignored when `thread_id` is not set. Without it, requests behave as before.

| Code | Status | When |
|------|--------|------|
| `THREAD_PARENT_NOT_FOUND` | 409 | `parent_message_id` is not a message on this thread |
| `THREAD_PARENT_STALE` | 409 | The turn after the parent already completed, including one that ended in client tool calls (read it from [history](#get-thread-history)), or the thread holds different or later messages after the parent |
| `SERVICE_UNAVAILABLE` | 503 | The thread could not be read to check the parent. Send the same request again later |

## Per-App Thread Isolation

Threads can be scoped to a specific application using the `X-App-Id` header. This allows multiple apps to share the same Truememory backend while keeping their conversation threads separate.

- **`X-App-Id` provided** — the thread is scoped to that app. Only requests with the same `X-App-Id` can see, rename, or delete it.
- **`X-App-Id` omitted** — the thread is unscoped (shared). It is visible when listing threads without `X-App-Id`, but hidden from app-scoped lists.

```bash
# Create a thread scoped to "vibrantly"
curl -X POST https://your-tie-host/v1/chat/completions \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-App-Id: vibrantly" \
  -H "X-Agent-Id: chatbot" \
  -d '{"model": "anthropic/claude-sonnet-4-5", "messages": [{"role": "user", "content": "Hello"}], "thread_id": "my-thread"}'
```

:::caution
To resume a thread created with `X-App-Id`, you must send the same `X-App-Id` header. Omitting it will create a new, separate conversation.
:::

:::note
Memory (facts, entities, relationships) is **not** scoped by app. A user's memories are shared across all apps — only conversation threads are isolated.
:::

## Thread Metadata

Each thread can carry an arbitrary JSON metadata object. Use it to store application-specific context that doesn't belong in the conversation itself — for example, where the thread originated, which page it's associated with, or whether it's pinned in a UI.

```json
{
  "source": "whatsapp",
  "pageType": "whatsapp-dm",
  "pageId": "550e8400-e29b-41d4-a716-446655440000",
  "isPinned": false
}
```

Metadata is set and updated via the [Update Thread](#update-thread) endpoint. It is returned in every thread summary (list and update responses). Truememory stores metadata as-is and does not interpret it — your application defines the schema.

:::note
Metadata is `null` by default. Setting it to `null` or `{}` clears any previously stored metadata.
:::

## List Threads

Returns threads for the authenticated user. Supports cursor-based pagination, sorting, and date filtering.

```bash
GET https://your-tie-host/threads
Authorization: Bearer $TOKEN
```

**Headers:**

| Header | Type | Default | Description |
|--------|------|---------|-------------|
| `X-App-Id` | string | null | Filter threads by app. When set, only threads matching that exact app ID are returned (unscoped threads are excluded). When omitted, all threads are returned (both scoped and unscoped). |

**Query parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `agent_id` | string | null | Filter threads by agent |
| `cursor` | string | null | Pagination token from a previous response (treat as opaque; do not parse or construct it) |
| `limit` | integer | 20 | Results per page (1-100) |
| `order_by` | string | `created_at` | Sort field: `created_at` or `updated_at` |
| `asc` | boolean | false | Sort direction: `true` for ascending (oldest first), `false` for descending (newest first) |
| `created_after` | ISO 8601 datetime | null | Only return threads created on or after this timestamp |
| `created_before` | ISO 8601 datetime | null | Only return threads created on or before this timestamp |

**Response:**

```json
{
  "threads": [
    {
      "thread_id": "847c6285-8fc9-4560-a83f-4e6285809254",
      "agent_id": "chatbot",
      "app_id": "vibrantly",
      "title": "What is the weather in Tokyo?",
      "metadata": {
        "source": "whatsapp",
        "isPinned": false
      },
      "created_at": "2026-03-25T10:00:00+00:00",
      "updated_at": "2026-03-25T10:05:00+00:00"
    }
  ],
  "next_cursor": "MjAyNi0wMy0yNVQxMDowMDowMCswMDowMHw4NDdjNjI4NS04ZmM5LTQ1NjAtYTgzZi00ZTYyODU4MDkyNTQ="
}
```

Pass `next_cursor` as the `cursor` query parameter to fetch the next page. When `next_cursor` is `null`, there are no more results.

The `app_id` field is `null` for unscoped threads. The `metadata` field is `null` when no metadata has been set.

## Get Thread History

Retrieve the full message history for a thread:

```bash
POST https://your-tie-host/history
Authorization: Bearer $TOKEN
Content-Type: application/json

{ "thread_id": "<your-thread-id>" }
```

:::note
History requires ownership: the thread must belong to the authenticated user, otherwise the endpoint returns `404 THREAD_NOT_FOUND` (a 404 rather than 403, so thread IDs cannot be probed). Note the lookup is not scoped by `X-App-Id` — pass the right `X-App-Id` for the thread's app.
:::

**Response:**

```json
{
  "messages": [
    {
      "type": "human",
      "content": "What is the weather in Tokyo?",
      "metadata": {
        "session_id": "8b94776f-31ae-49ac-9f06-3be137d69186",
        "platform": "ios",
        "app_version": "2.4.1"
      }
    },
    {
      "type": "ai",
      "content": "The current weather in Tokyo is...",
      "metadata": null
    }
  ]
}
```

The `metadata` field on user messages contains the key-value pairs sent with the original request. AI messages have `metadata: null` since metadata is attached at request time, not per-response.

## Update Thread

Update a thread's title and/or metadata. The title is automatically set from the first user message, but you can override it. Metadata can be set at any time.

At least one of `title` or `metadata` must be provided in the request body.

```bash
PATCH https://your-tie-host/threads/<thread-id>
Authorization: Bearer $TOKEN
Content-Type: application/json
# Required only if the thread was created with X-App-Id
X-App-Id: vibrantly

{ "title": "My weather conversation", "metadata": { "isPinned": true } }
```

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | No | New title for the thread (1-500 characters, cannot be blank) |
| `metadata` | object \| null | No | Arbitrary metadata dict. Replaces existing metadata entirely. Pass `null` to clear. |

You can send either field alone or both together:

```bash
# Rename only
{ "title": "New title" }

# Set metadata only
{ "metadata": { "source": "telegram", "isPinned": false } }

# Clear metadata
{ "metadata": null }
```

**Response:**

```json
{
  "thread_id": "847c6285-8fc9-4560-a83f-4e6285809254",
  "agent_id": "chatbot",
  "app_id": "vibrantly",
  "title": "My weather conversation",
  "metadata": { "isPinned": true },
  "created_at": "2026-03-25T10:00:00+00:00",
  "updated_at": "2026-03-25T10:10:00+00:00"
}
```

:::caution
Metadata uses **replace** semantics — the entire metadata object is overwritten, not merged. To update a single key, read the current metadata first, modify it, and send the full object back.
:::

:::note
If the thread was created with `X-App-Id`, you must send the same header to update it. Mismatched or missing `X-App-Id` will return 404.
:::

## Delete Thread

Permanently delete a thread and all its data (title, metadata, timestamps, agent/app association):

```bash
DELETE https://your-tie-host/threads/<thread-id>
Authorization: Bearer $TOKEN
# Required only if the thread was created with X-App-Id
X-App-Id: vibrantly
```

**Response:**

```json
{ "status": "deleted", "thread_id": "847c6285-8fc9-4560-a83f-4e6285809254" }
```

:::caution
This action is irreversible. The thread and all its data are permanently removed and the thread will no longer appear in list results.
:::

:::note
If the thread was created with `X-App-Id`, you must send the same header to delete it. Mismatched or missing `X-App-Id` will return 404.
:::

## Error Codes

| Code | Status | When |
|------|--------|------|
| `AUTH_TOKEN_MISSING` | 401 | No Bearer token provided |
| `THREAD_NOT_FOUND` | 404 | Thread does not exist or is not owned by the user |
| `VALIDATION_ERROR` | 422 | Invalid cursor, empty title, title exceeds 500 characters, or empty update body |
| `THREADS_FETCH_FAILED` | 500 | Unexpected server error while listing threads |
| `THREAD_UPDATE_FAILED` | 500 | Unexpected server error while updating a thread |
| `THREAD_DELETE_FAILED` | 500 | Unexpected server error while deleting a thread |
| `SERVICE_UNAVAILABLE` | 501 | Thread management requires the PostgreSQL memory backend |

Source: https://truememory-docs.mvstg.com/ai-gateway/threads/index.mdx
