# VeilCLI — API Requirements for Chat Studio Features

This document lists every API endpoint or field change needed to implement the features in `chat-features-roadmap.md`.
Each item includes which feature(s) need it, the current state, and the exact contract required.

---

## 1. PATCH /sessions/:id — Update session metadata

**Needed by:** Rename session · Auto-title generation · Session config page (change model / compaction settings)

**Current state:** No PATCH or PUT exists on sessions. The only write operations are `POST /sessions/:id/reset` (clear messages) and `DELETE /sessions/:id` (close/delete). Session metadata (title, model, compaction config) is set only at creation time and is immutable after that.

**Required contract:**

```
PATCH /sessions/:id
Content-Type: application/json
```

All fields optional — only supplied fields are updated:

```json
{
  "title": "My session title",
  "model": "anthropic/claude-opus-4-6",
  "model_thinking": { "type": "enabled", "budget_tokens": 8000 },
  "compact_enabled": 1,
  "compact_auto_threshold": 80,
  "compact_count": 70,
  "compact_model": "default",
  "compact_custom_instructions": "Focus on code changes only."
}
```

Response: full updated session object.

Error responses:

| Code | Condition |
|------|-----------|
| `404 SESSION_NOT_FOUND` | No session with that ID |
| `400 SESSION_CLOSED` | Cannot modify a closed session |
| `400 VALIDATION_ERROR` | Unknown field or invalid value |

**Notes:**
- `title` is the most urgently needed field (Rename + Auto-title).
- Compaction fields (`compact_*`) are needed for the Session Config page.
- `model` and `model_thinking` overrides are needed for the Session Config page.
- All fields listed in the `GET /sessions` session object that are runtime-configurable should be patchable here.

---

## 2. DELETE /sessions/:id/messages/after/:messageId — Trim messages after a point

**Needed by:** Rewind · Edit & Resend (user message)

**Current state:** No message-level delete exists. There is only `POST /sessions/:id/reset` (wipes *all* messages). There is no way to delete a subset of messages.

**Required contract:**

```
DELETE /sessions/:id/messages/after/:messageId
```

Permanently deletes all messages with `id > :messageId`. Does not delete the message at `:messageId` itself.

Response:

```json
{
  "sessionId": "sess_4f3a1b9c2d8e7f01",
  "deletedCount": 7,
  "lastRemainingMessageId": 12
}
```

Error responses:

| Code | Condition |
|------|-----------|
| `404 SESSION_NOT_FOUND` | No session with that ID |
| `404 MESSAGE_NOT_FOUND` | No message with that ID in this session |
| `400 SESSION_CLOSED` | Cannot modify a closed session |

**Notes:**
- This is the core primitive for Rewind. After trimming, the Studio re-sends the original user message via the normal `POST /agents/:name/chat` endpoint.
- Also used by "Edit & Resend": edit the user message (see §3), trim everything after it, re-send.
- Alternatively, a query-param form `DELETE /sessions/:id/messages?after=:messageId` is also acceptable.

---

## 3. PATCH /sessions/:id/messages/:messageId — Edit message content

**Needed by:** Edit user message · Edit assistant message

**Current state:** No message edit endpoint exists. Messages are append-only.

**Required contract:**

```
PATCH /sessions/:id/messages/:messageId
Content-Type: application/json
```

```json
{
  "content": "Updated message text here."
}
```

Response: full updated message object.

Error responses:

| Code | Condition |
|------|-----------|
| `404 SESSION_NOT_FOUND` | No session with that ID |
| `404 MESSAGE_NOT_FOUND` | No message with that ID in this session |
| `400 SESSION_CLOSED` | Cannot modify a closed session |
| `400 VALIDATION_ERROR` | `content` missing or not a string |

**Notes:**
- Used in two flows:
  1. **Edit user message then resend** — PATCH the message, then trim after it (§2), then re-send via chat endpoint.
  2. **Edit assistant message in-place** — PATCH only, no re-send. Allows the user to manually correct a bad response and continue the conversation from that corrected point.
- Only `content` needs to be editable for now. `role` and other fields should remain immutable.

---

## 4. DELETE /sessions/:id/messages/:messageId — Delete a single message

**Needed by:** Delete message action (× in context menu)

**Current state:** No message-level delete endpoint exists.

**Required contract:**

```
DELETE /sessions/:id/messages/:messageId
```

Permanently removes the single message with the given ID.

Response:

```json
{
  "sessionId": "sess_4f3a1b9c2d8e7f01",
  "deletedMessageId": 14
}
```

Error responses:

| Code | Condition |
|------|-----------|
| `404 SESSION_NOT_FOUND` | No session with that ID |
| `404 MESSAGE_NOT_FOUND` | No message with that ID in this session |
| `400 SESSION_CLOSED` | Cannot modify a closed session |

**Notes:**
- This is distinct from §2 (trim after a point). This deletes exactly one message.
- The Studio will warn the user if they try to delete a message in the middle of a tool call chain (e.g., deleting an assistant message that has a paired `tool` result message still present).
- Deleting system messages should be disallowed by the server (return `400 CANNOT_DELETE_SYSTEM_MESSAGE`).

---

## 5. POST /sessions/:id/fork — Fork session from a message

**Needed by:** Fork conversation from here (in `···` context menu)

**Current state:** No fork endpoint. `POST /sessions` creates a fresh empty session. There is no way to start a session pre-loaded with an existing conversation's history up to a given point.

**Required contract:**

```
POST /sessions/:id/fork
Content-Type: application/json
```

```json
{
  "upToMessageId": 18
}
```

Creates a new session as a copy of this session, including all messages up to and including `upToMessageId`. The new session is created in `active` state, ready to accept new messages.

Response `201 Created`:

```json
{
  "sessionId": "sess_newid",
  "session": {
    "id": "sess_newid",
    "agent_name": "assistant",
    "mode": "chat",
    "status": "active",
    "model": "...",
    "message_count": 8,
    "created_at": "..."
  },
  "forkedFrom": {
    "sessionId": "sess_4f3a1b9c2d8e7f01",
    "upToMessageId": 18
  }
}
```

Error responses:

| Code | Condition |
|------|-----------|
| `404 SESSION_NOT_FOUND` | No session with that ID |
| `404 MESSAGE_NOT_FOUND` | No message with that ID in this session |
| `400 VALIDATION_ERROR` | `upToMessageId` missing or invalid |

**Notes:**
- This feature is 🟢 Polish / later in the roadmap, so it is lower priority than items §1–§4.
- The fork should inherit the source session's `agent_name`, `model`, `mode`, and `model_thinking` config.
- `title` on the forked session can start as null (the Studio will auto-title it).

---

## 6. Continue flag in POST /agents/:name/chat

**Needed by:** Continue button in the ⚙ cog menu

**Current state:** `message` field is required (`✓`) and must be a non-empty string. Sending an empty string or omitting the field returns `400 VALIDATION_ERROR`.

**Required change:**

Add an optional `continue` boolean to the request body:

```json
{
  "sessionId": "sess_4f3a1b9c2d8e7f01",
  "continue": true,
  "sse": true
}
```

When `continue: true` is set, `message` becomes optional. The agent resumes the session without adding a new user message — useful when the agent stopped mid-task and the user wants it to keep going.

Alternatively (simpler): allow `message: ""` (empty string) as a valid input that the agent interprets as a continue signal.

**Preferred:** Explicit `continue: true` flag — cleaner semantics and avoids the agent treating an empty string as a meaningful message.

**Notes:**
- The Studio's ⚙ cog menu will surface this as a "Continue" option.
- The roadmap specifies this works regardless of whether the last message is from the user or the assistant.

---

## 7. Per-message turn stats (iterations + duration_ms)

**Needed by:** Turn Stats Row (expandable stats below each assistant message)

**Current state:** The SSE `done` event and WS `session.stream / done` data carries `iterations` and `durationMs` for the *current* turn in real time. However, these fields are **not stored** in the messages table and are **not returned** by `GET /sessions/:id/messages`. For sessions loaded from history (not currently streaming), this data is lost.

The messages API already stores `input_tokens`, `output_tokens`, `cache_tokens`, `cost` per assistant message — adding turn-level stats follows the same pattern.

**Required change:**

Add two fields to the assistant message schema returned by `GET /sessions/:id/messages`:

| Field | Type | Description |
|-------|------|-------------|
| `iterations` | integer\|null | Number of LLM loop iterations in this turn (the final assistant message of each turn only; null for intermediate assistant messages with `tool_calls`) |
| `duration_ms` | integer\|null | Wall-clock duration of the full turn in ms (same scoping as `iterations`) |

These should be populated from the `done` event data when a turn completes, and stored on the **last** assistant message of that turn (i.e., the message with `finishReason: "stop"` or the final non-tool-call assistant message).

**Notes:**
- `iterations` can be inferred client-side by counting tool-call rounds in the message list, but `duration_ms` cannot be reconstructed after the fact — it must be stored server-side.
- The Studio will show: `▶ 3 iterations · 1.2s · $0.004` expandable row below each assistant turn.

---

## 8. Thinking content in assistant messages

**Needed by:** Thinking / Reasoning Block display

**Current state:** `model_thinking` on the session object is the *configuration* (e.g., `{"type":"enabled","budget_tokens":5000}`). The actual *thinking text* generated by the model during a turn is currently not stored in the messages table and not returned by `GET /sessions/:id/messages`.

**Required change:**

Add a `thinking_content` field to the assistant message schema:

| Field | Type | Description |
|-------|------|-------------|
| `thinking_content` | string\|null | The raw reasoning/thinking text from the model for this message. Null if thinking was not enabled or produced no output. |

Return it in `GET /sessions/:id/messages` and in the SSE/WS `message` events.

**Notes:**
- The Studio will show this as a collapsible "Thinking (N tokens)" block at the top of the assistant message.
- The token count for the thinking block, if available, should ideally be its own field: `thinking_tokens: integer|null`.
- This only applies when `session.model_thinking` is non-null (extended thinking models).

---

## 9. File / Image attachment support in POST /agents/:name/chat

**Needed by:** File / Image Attachment (📎 paperclip button)

**Current state:** The chat endpoint accepts only `message: string`. No multimodal or attachment support is documented.

**Status:** The roadmap marks this as 🔵 Pending API confirmation. The Studio needs to know:

1. Is multimodal input (images, files) supported by the underlying LLM routing layer?
2. If yes, what is the expected request format?

**Proposed contract (to confirm or correct):**

```json
{
  "message": "What is in this image?",
  "sessionId": "sess_abc123",
  "sse": true,
  "attachments": [
    {
      "type": "image",
      "filename": "screenshot.png",
      "mediaType": "image/png",
      "data": "<base64-encoded>"
    },
    {
      "type": "file",
      "filename": "notes.txt",
      "mediaType": "text/plain",
      "data": "File contents as plain text..."
    }
  ]
}
```

**Notes:**
- The Studio will not implement this until the CLI developer confirms support and provides the exact format.
- This is listed as 🟢 Polish in the roadmap — lowest priority.

---

## Summary Table

| # | API Change | Priority | Needed by |
|---|------------|----------|-----------|
| 1 | `PATCH /sessions/:id` — update title + config | 🔴 Critical | Rename session, Auto-title, Session config page |
| 2 | `DELETE /sessions/:id/messages/after/:messageId` — trim messages | 🔴 Critical | Rewind, Edit & Resend |
| 3 | `PATCH /sessions/:id/messages/:messageId` — edit message content | 🔴 Critical | Edit user message, Edit assistant message |
| 4 | `DELETE /sessions/:id/messages/:messageId` — delete one message | 🔴 High | Delete message action |
| 5 | `POST /sessions/:id/fork` — fork from message point | 🟢 Low | Fork conversation |
| 6 | `continue: true` flag in `POST /agents/:name/chat` | 🔴 High | Continue button |
| 7 | `iterations` + `duration_ms` stored per assistant message | 🟡 Medium | Turn stats row |
| 8 | `thinking_content` field on assistant messages | 🟡 Medium | Thinking block display |
| 9 | Attachment support in chat endpoint | 🟢 Low | File/image attachments (pending confirmation) |

---

## What Already Exists (No API Change Needed)

For reference — these roadmap features are fully implementable with the current API:

| Feature | How |
|---------|-----|
| Reset session | `POST /sessions/:id/reset` ✅ |
| Delete session | `DELETE /sessions/:id?hard=true` ✅ |
| Export conversation | Client-side from `GET /sessions/:id/messages` ✅ |
| Compaction divider | `compact_size` + `compact_summary` already on session object ✅ |
| Context ring | `context_size` + `context_size_limit` already on session object ✅ |
| Timestamps | `created_at` already on every message ✅ |
| Tab icon animation | Client-side WS/SSE streaming state ✅ |
| Streaming indicator | Client-side SSE `inference.chunk` events ✅ |
| Scroll-to-bottom button | Client-side UI only ✅ |
| Message search | Client-side filter on loaded messages ✅ |
| Code block copy button | Client-side markdown renderer change ✅ |
| Tool call group collapse/expand | Client-side UI only ✅ |
| Input history (↑/↓) | In-memory per session, no persistence needed ✅ |
| Session metadata panel | All fields already on session object ✅ |
| Token cost display | `cost`, `input_tokens`, `output_tokens` on messages ✅ |
| Model display | `model_key` on assistant messages, `model` on session ✅ |
