# Messages and turns

Send a message to a chat session with `POST .../jobs/chat`. If the agent is idle, the message starts a turn and you get its `chatJobId`. If the agent is working, the message waits Up next and you get a `queuedMessageId`. Follow the turn until it ends, then read the reply from the session's messages. [Follow a turn](https://kosuke.ai/docs/api/turns.md) explains how deliveries, job statuses and waiting fit together.

| Method | Path                                                                            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GET    | `/api/projects/{projectId}/chat-sessions/{sessionId}/jobs/chat?page=&pageSize=` | List a chat session's jobs, one per Turn, newest first: each one's status, error and timestamps, with the total in meta.pagination.                                                                                                                                                                                                                                                                                                                                                                                 |
| POST   | `/api/projects/{projectId}/chat-sessions/{sessionId}/jobs/chat`                 | Send a message: starts a turn, or waits Up next while one is running. To steer the running turn with it, steer the Queued Message it becomes. Attachments ride along as multipart. Use it over steer only for attachments, a message that runs a skill, or text over 10,000 characters. From Sensei, /kosuke:sync-branch is refused with 409 SYNC_OWNED_BY_MERGE_AGENT when the project is on the merge train and the pull request has the go-ahead, unless a member started the Turn: the merge agent syncs those. |
| GET    | `/api/projects/{projectId}/chat-sessions/{sessionId}/jobs/chat/{jobId}`         | Read one turn's job: its status, error and timestamps.                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| POST   | `/api/projects/{projectId}/chat-sessions/{sessionId}/jobs/chat/{jobId}/cancel`  | Stop a running turn. Steers it had not taken go back to the top of Up next, which pauses.                                                                                                                                                                                                                                                                                                                                                                                                                           |

## Send a message

`POST /api/projects/{projectId}/chat-sessions/{sessionId}/jobs/chat` sends a message into an Active session.

| Field | Type | What it does |
| --- | --- | --- |
| `content` | string | Required. The message. It can be empty when you send `diffCommentsContext` or attachments. |
| `model` | string | Optional. A value from [`GET /api/models`](https://kosuke.ai/docs/endpoint-reference/models.md). The session keeps it from this turn on. Ignored when the message waits Up next, or when the value is unknown. |
| `previewContext` | object | Optional. The preview page the message is about, as `{ "frontendName": "web", "path": "/settings" }`. `path` starts with `/`. |
| `diffCommentsContext` | array | Optional. Up to 50 review comments from the diff, handed to the agent as feedback. |
| `retryFailedJobId` | uuid | Optional. Retries a turn that overflowed the context window. See [below](https://kosuke.ai/docs/endpoint-reference/messages-and-turns.md#retry-after-the-context-window-overflows). |
| `runningJobId` | uuid | Optional. The turn you saw running when you sent. If a member stopped it meanwhile, you get `409` with `JOB_STOPPED` instead of a new turn. |

A `model` from another provider starts the agent fresh. The turn opens with the conversation handed over as text.

Each `diffCommentsContext` item holds a comment's `id`, `filePath`, `side`, `startLineNumber`, `endLineNumber`, `body` and `lineSnapshot`. Sensei reads each comment again by `id` and leaves out resolved ones. [Send review comments to the agent](https://kosuke.ai/docs/recipes/send-review-comments-to-the-agent.md) walks through it.

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Make the toggle remember the choice after a reload"}'
```

The answer is `201`. Its `delivery` says what happened to the message:

| `delivery` | Also returned | What happened |
| --- | --- | --- |
| `job` | `chatJobId`, `jobNumber` | The message started a turn. |
| `queued` | `queuedMessageId` | A turn or a Runtime Command was running, or one started while the message was on its way. The message waits Up next and starts its own turn after the ones ahead of it. |

```json
{ "data": { "delivery": "job", "chatJobId": "b7e41d09-2c5a-4f3e-8a61-0d9c7b2e5f14", "jobNumber": 3 } }
```

```json
{ "data": { "delivery": "queued", "queuedMessageId": "0a6f93c2-41d8-4e7b-b5a0-7c2d18e9f365" } }
```

A queued message has no job until its turn starts. Change, move or delete it with the Up next endpoints on [Chat sessions](https://kosuke.ai/docs/endpoint-reference/chat-sessions.md).

To attach files, send `multipart/form-data` with `content`, `attachmentCount` and one field per file, `attachment_0` to `attachment_19`. A message takes up to 20 files of 10 MiB (10,485,760 bytes) each. For batches larger than 20 MiB in total, use the [per-file staging transport](https://kosuke.ai/docs/endpoint-reference/chat-sessions.md) and send the returned opaque references in those same slots, in file order. In a multipart body, `previewContext` and `diffCommentsContext` are JSON strings.

```bash
kosuke sessions send $PROJECT_ID $SESSION_ID --content "Match this design" --attach mockup.png
```

Sending into an unassigned session makes you its Assignee. A session Sensei is the Assignee of stays Sensei's until a member reassigns it.

### What the session needs

| Session state | What happens |
| --- | --- |
| Active, sandbox running | The message is delivered. |
| Active, sandbox paused | The send wakes the sandbox first. |
| Active, sandbox still starting | `409` with `SANDBOX_STARTING`. Send again in a few seconds. |
| Active, no sandbox | The send starts a sandbox, and the turn runs once it is ready. `400` when the session has no branch, `409` with `DEPLOY_ACTIVE` during a deploy, `409` with `SANDBOX_LIMIT_REACHED` at the limit. |
| Backlog | `400`. Start it with `POST .../activate` first. |
| Under Review, sent as a member or by a Merge agent run of the project | The message is delivered by the rows above and the session stays Under Review. The agent turns the pull request back into a draft when it changes it. |
| Under Review, sent by a Cron or another session | `400`. Resume it by setting `status` to `active` on [Chat sessions](https://kosuke.ai/docs/endpoint-reference/chat-sessions.md), then send. |
| Completed or Archived | `400`. The session takes no new messages. |

## Follow a turn

`GET .../jobs/chat/{jobId}` reads one turn. Read it until `status` is no longer `pending` or `running`.

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat/$JOB_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY"
```

```json
{
  "data": {
    "job": {
      "id": "b7e41d09-2c5a-4f3e-8a61-0d9c7b2e5f14",
      "status": "completed",
      "jobNumber": 3,
      "error": null,
      "errorCode": null,
      "createdAt": "2026-09-30T09:12:04.118Z",
      "startedAt": "2026-09-30T09:12:09.502Z",
      "completedAt": "2026-09-30T09:14:41.077Z"
    }
  }
}
```

| `status` | What it means |
| --- | --- |
| `pending` | The turn waits to start. |
| `running` | The agent is working. |
| `awaiting_response` | The agent asked a question. The turn has ended. |
| `completed` | The turn finished. |
| `failed` | The turn stopped on an error. `error` and `errorCode` say why. |
| `cancelled` | A member stopped the turn. |

Instead of polling, `kosuke sessions jobs watch <projectId> <sessionId> <jobId>` streams the turn and exits when it ends. `GET .../jobs/chat` lists a session's turns, newest first, with `page` and `pageSize` (default 25, up to 100). The CLI commands are `kosuke sessions jobs get` and `kosuke sessions jobs list`.

When the turn ends, `GET .../chat-sessions/{sessionId}/messages` returns the conversation, and `?limit=` only its newest messages. The reply is the last message with `role` set to `assistant`, in its `blocks`. Its `content` is null for an ordinary reply. Its text blocks come whole, and its tool calls and subagents summarized; `GET .../chat-sessions/{sessionId}/messages/{messageId}` returns the message with every block as recorded. [Chat sessions](https://kosuke.ai/docs/endpoint-reference/chat-sessions.md#read-one-session) lists what each read keeps.

## Stop a turn

`POST .../jobs/chat/{jobId}/cancel` stops a turn that is `pending` or `running`. It takes no body.

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat/$JOB_ID/cancel" \
  -H "Authorization: Bearer $KOSUKE_API_KEY"
```

The answer is `{ "data": { "cancelled": 1, "message": "Chat job cancelled" } }`. Steers the turn had not taken go back to the top of Up next, and Up next pauses. Send its top message with `POST .../chat-sessions/{sessionId}/queue/resume`. A turn that ended `completed`, `failed` or `cancelled` answers `400`. The CLI command is `kosuke sessions jobs cancel <projectId> <sessionId> <jobId>`.

## Answer the agent's question

A turn that ends `awaiting_response` has asked you something. The last assistant message in `GET .../chat-sessions/{sessionId}/messages` carries a block with `type` set to `clarification`. Its `clarificationData.questions` each hold a `question` and two to four `options`, each with a `title`, a `description` and `isRecommended`.

Each question can include `multiSelect`, an optional boolean. Only `true` permits several choices together. Missing or `false` keeps single choice.

The web form lets you add notes to choices and enter an Other answer. It keeps unsent choices and text through navigation and reload.

Multiple selections appear in option order, separated by semicolons. Each selected note follows its choice after an em dash. Other text comes last, without an `Other:` prefix. Unselected notes stay in the draft and are omitted from the answer.

Send your answer as an ordinary message to `POST .../jobs/chat`. It starts the next turn.

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Keep the version from this branch."}'
```

## Run a skill or a command from a message

A message that starts with the session's invocation character runs a skill. The character is `$` when the session's `model` starts with `openai:`, and `/` for Claude, GLM, Kimi and DeepSeek sessions.

| Message starts with | What happens |
| --- | --- |
| `/name` or `$name` | Runs the skill `name` when the session's sandbox has it. Otherwise the agent reads the text as an ordinary message. |
| `/kosuke:open-pr` or `$kosuke:open-pr` | Opens the pull request, or updates the open one. See [Pull requests and sync](https://kosuke.ai/docs/endpoint-reference/pull-requests-and-sync.md). |
| `/kosuke:sync-branch` or `$kosuke:sync-branch` | Brings the Target Branch into the session's branch. Same page. |
| `/clear` or `/compact` | Starts an ordinary turn. To run them as runtime commands, use `POST .../chat-sessions/{sessionId}/runtime-commands` on [Chat sessions](https://kosuke.ai/docs/endpoint-reference/chat-sessions.md). |

`GET /api/projects/{projectId}/skills` with `provider` and `sessionId` lists the skills the session can run. `provider` is `anthropic` for Claude, GLM, Kimi and DeepSeek sessions `openai` for Codex sessions, `meta` for Muse Code sessions, `google` for Gemini sessions, and `xai` for Grok sessions.

## Retry after the context window overflows

A turn that outgrows the model's context window fails with `errorCode` set to `context_window_exceeded`. Send the message again with `retryFailedJobId` set to that job's id.

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/jobs/chat" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"content\": \"Continue where you stopped\", \"retryFailedJobId\": \"$JOB_ID\"}"
```

The new turn starts the agent fresh and hands it the conversation as text. The retry works only when the failed job is the session's latest. Any other id is ignored, and the message goes out as a plain message.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | A field has the wrong type or shape. |
| 400 | `BAD_REQUEST` | The session is Backlog, Completed or Archived, a multipart message is empty, an attachment is refused, or the Assignee cannot run the model. The message says which. An empty JSON message answers `VALIDATION_ERROR`. |
| 400 | `SESSION_UNDER_REVIEW` | The session is Under Review and the message cannot resume it. The sender is a Cron or another session, or the session went Under Review while the message was on its way. A member sends again to resume. |
| 402 | `CREDIT_LIMIT_REACHED` | The workspace is blocked on billing. |
| 403 | `FORBIDDEN` | The job belongs to another session. |
| 404 | `NOT_FOUND` | The project, session or job does not exist. |
| 409 | `SANDBOX_STARTING` | The sandbox is still starting. Send again in a few seconds. |
| 409 | `SANDBOX_LIMIT_REACHED` | Waking the paused sandbox, or starting a missing one, would pass the workspace's concurrent sandbox limit. Stop another session first. |
| 409 | `DEPLOY_ACTIVE` | The session's sandbox is gone and cannot be recreated while the project deploys. Send again once the deploy ends. |
| 409 | `JOB_IN_PROGRESS` | A turn is running while the Queue sends a message already in Up next (`queuedMessageId`). The message stays Up next. A fresh message never gets this code. When a turn starts while it is on its way, it waits Up next and answers `201` with `delivery: "queued"`. |
| 409 | `JOB_STOPPED` | The turn named in `runningJobId` was stopped. |
| 409 | `QUEUE_FULL` | Up next already holds 20 messages. |
| 409 | `COMMAND_IN_FLIGHT` | A Runtime Command is running while the Queue sends a message already in Up next (`queuedMessageId`), or during a steer-only send. A fresh message instead waits Up next and answers `201` with `delivery: "queued"`. |
| 409 | `SESSION_UPDATING`, `SESSION_STATUS_CHANGED`, `SESSION_CHANGED` | The session changed while the message was on its way. Read it again and retry. |

[Responses and errors](https://kosuke.ai/docs/api/errors.md) has the full table.

## Next steps

- [Follow a turn](https://kosuke.ai/docs/api/turns.md)
- [Pull requests and sync](https://kosuke.ai/docs/endpoint-reference/pull-requests-and-sync.md)
- [Send review comments to the agent](https://kosuke.ai/docs/recipes/send-review-comments-to-the-agent.md)
