# Conversations API

## Endpoints

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/api/conversations` | List conversations |
| `POST` | `/api/conversations` | Create a conversation |
| `GET` | `/api/conversations/:id` | Get conversation with messages, attachments, and tool calls |
| `PATCH` | `/api/conversations/:id` | Update title, folder, or model |
| `DELETE` | `/api/conversations/:id` | Delete conversation and all attachments |
| `GET` | `/api/conversations/:id/export` | Export as Markdown |
| `POST` | `/api/conversations/:id/chat` | Stream chat response (SSE) |
| `POST` | `/api/conversations/:id/stop` | Cancel active generation |
| `POST` | `/api/conversations/:id/fork` | Fork at a message position |
| `POST` | `/api/conversations/:id/rewind` | Rewind to a position, optionally revert file changes |
| `POST` | `/api/conversations/:id/copy` | Copy to another database |

## List Conversations

```
GET /api/conversations
```

Query parameters:

| Parameter | Type | Description |
|---|---|---|
| `search` | string | Full-text search across messages and titles |
| `space_id` | UUID | Filter by space |
| `db` | string | Database name (default: `personal`) |

## Stream Chat

```
POST /api/conversations/:id/chat
```

Sends a message and streams the AI response via Server-Sent Events (SSE).

If a stream is already active for the conversation, the message is queued (up to 10) and the endpoint returns:

```json
{"status": "queued", "position": 1, "queue_depth": 1}
```

| Field | Type | Description |
|---|---|---|
| `status` | string | Always `"queued"` |
| `position` | integer | Position of this message in the queue (1-based) |
| `queue_depth` | integer | Total messages in the queue after enqueue |

If the queue is full, returns HTTP 429 with `{"detail": "Message queue full (max 10)"}`.

When the agent loop processes a queued message, it emits a `queued_message` SSE event:

```json
{"role": "user", "content": "...", "position": 1, "queue_depth": 0}
```

| Field | Type | Description |
|---|---|---|
| `role` | string | Always `"user"` |
| `content` | string | The queued message text |
| `position` | integer | 1-based position in the current processing batch |
| `queue_depth` | integer | Messages remaining in the queue after this one |

### Tool-Result Density Fields (#1467)

When `cli.density.mode` is `compact` or `minimal`, `tool_call_start` and `tool_call_end` SSE events carry additive summary fields so the web UI can render a smart summary without re-parsing the raw JSON. These fields are **additive and backward-compatible**: existing clients continue to parse the legacy `input` / `output` / `status` fields and can safely ignore the new ones.

When `cli.density.mode` is `normal` (the default) or `detailed`, all density fields are emitted as `null` (or omitted). The web UI falls back to today's raw-JSON rendering — DOM is byte-identical to pre-#1467 behaviour.

`tool_call_start` additive fields:

| Field | Type | Description |
|---|---|---|
| `input_preview` | `string \| null` | Head/tail slice of the tool input, or `null` in `normal`/`detailed` mode. |

`tool_call_end` additive fields:

| Field | Type | Description |
|---|---|---|
| `summary` | `string \| null` | One-line human-readable summary; `null` in `normal`/`detailed` mode. |
| `output_preview` | `string \| null` | Head/tail slice of the tool output; `null` in `normal`/`detailed` mode. |
| `error_class` | `string \| null` | One of `"exit_nonzero"`, `"exception"`, `"timeout"`, `"denied"`; `null` when the call succeeded or when density is `normal`/`detailed`. |
| `output_shape_hash` | `string \| null` | SHA-256 truncated to 16 hex chars, derived from the output's **structure** (keys + types + exit_code), not bulk content. Used by the web UI to collapse ≥3 consecutive identical shapes; `null` in `normal`/`detailed` mode. |

The `GET /api/conversations/{id}` replay response applies the same fields to each entry in `messages[].tool_calls[]`, computed at serialisation time (not persisted to SQLite). See `docs/web-ui/tool-density.md` for the user-visible rendering behaviour.

## Hook outcome SSE event (#1491)

When a pre-tool or post-tool hook blocks a tool call, the stream emits a `hook_outcome` SSE event immediately **before** the `tool_call_end` event for that tool call. The event is additive and backward-compatible: clients that do not handle it simply ignore it.

```
event: hook_outcome
data: {
  "tool_name": "bash",
  "tool_call_id": "tc-abc123",
  "outcome": "deny",
  "approval_decision": "hook_denied" | "post_hook_denied" | "hook_escalated_denied",
  "message": "<hook-provided reason string, may be empty>"
}
```

| Field | Type | Description |
|---|---|---|
| `tool_name` | `string` | Name of the tool that was blocked. |
| `tool_call_id` | `string` | ID of the associated tool call. |
| `outcome` | `string` | Always `"deny"` in phase 1 (hook warn outcomes are informational and live-only). |
| `approval_decision` | `string` | One of `hook_denied` (pre-tool block), `post_hook_denied` (post-tool block), or `hook_escalated_denied` (user denied after hook ask escalation). |
| `message` | `string` | Human-readable reason from the hook, or empty string. |

The `hook_outcome` event is **live-only**: it is not stored in SQLite and does not appear in conversation replay. It is rendered in the web UI as a compact red notice below the tool call card, and in the CLI as an error-surface line before the tool call completion line.

## Fork

```
POST /api/conversations/:id/fork
```

Creates a new conversation branching from the specified message position.

## Rewind

```
POST /api/conversations/:id/rewind
```

Rolls back to the specified message position. Optionally reverts file changes via `git checkout`.

## Copy

```
POST /api/conversations/:id/copy?target_db=team-shared
```

Duplicates the conversation (with all messages and tool calls) to another database.
