# Follow a turn

Every message you send to a chat session starts a turn, or waits Up next behind the one running. Send the message, read the `delivery` field, then follow the turn's job until it ends. When it ends, read the agent's reply from the session's messages.

## Send a message

```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": "Add a loading state to the search results."}'
```

With the CLI, `kosuke sessions send <projectId> <sessionId> --content "..."` does the same.

| Field | Type | What it does |
| --- | --- | --- |
| `content` | string | The message. Required. It may be empty when you send `diffCommentsContext`. |
| `model` | string | A model id for this turn, which the session then keeps. See [Choose the model](https://kosuke.ai/docs/api/turns.md#choose-the-model). |
| `previewContext` | object | `{ "frontendName", "path" }`, the preview page the message is about. |
| `diffCommentsContext` | array | Up to 50 diff comments to send as review feedback. See [Send review comments to the agent](https://kosuke.ai/docs/recipes/send-review-comments-to-the-agent.md). |
| `retryFailedJobId` | uuid | The id of the session's last job, when it failed with `errorCode` `context_window_exceeded`. The new turn starts with the conversation handed over as text. |
| `runningJobId` | uuid | The job you saw running when you sent. If a person stopped it meanwhile, the send answers 409 `JOB_STOPPED` instead of starting a turn. |

To attach files, send `multipart/form-data` with the fields above, `attachmentCount`, and the files as `attachment_0` to `attachment_19`. In multipart, `previewContext` and `diffCommentsContext` are JSON strings. The CLI takes files with `--attach`. [Responses and errors](https://kosuke.ai/docs/api/errors.md) lists the attachment limits.

A message that starts with a skill's name runs the skill. Claude, GLM, Kimi and DeepSeek sessions take `/name`, and Codex sessions take `$name`. `/clear` and `/compact` are Runtime Commands, run with `POST /api/projects/{projectId}/chat-sessions/{sessionId}/runtime-commands`. Sent as a message, they reach the agent as plain text.

## Read the delivery

A send answers 201 with a `delivery` field, and a steer answers 200:

```json
{ "data": { "delivery": "job", "chatJobId": "c47a1d3b-…", "jobNumber": 3 } }
```

| `delivery` | What happened | Id you get | What to do |
| --- | --- | --- | --- |
| `job` | The message started a turn. | `chatJobId` | Follow the job. |
| `queued` | A turn was running, so the message waits Up next. It starts its own turn when the running one ends. | `queuedMessageId` | Wait for the running turn, or [steer the message](https://kosuke.ai/docs/api/turns.md#steer-a-running-turn) into it. |
| `steer` | You steered a queued message into your running turn. | `steerId` | Keep following the running turn's job. No new job starts. |

When a queued message starts its turn, `GET /api/projects/{projectId}/chat-sessions/{sessionId}` names the new job in `latestChatJob`.

## Follow the job

Poll the job every few seconds:

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

```json
{
  "data": {
    "job": {
      "id": "c47a1d3b-…",
      "status": "running",
      "jobNumber": 3,
      "error": null,
      "errorCode": null,
      "createdAt": "2026-09-30T09:12:04.118Z",
      "startedAt": "2026-09-30T09:12:09.502Z",
      "completedAt": null
    }
  }
}
```

| `status` | Meaning | What to do next |
| --- | --- | --- |
| `pending` | The turn is waiting to start, for example while the sandbox starts. | Keep polling. |
| `running` | The agent is working. | Keep polling. You can [steer](https://kosuke.ai/docs/api/turns.md#steer-a-running-turn) or [stop](https://kosuke.ai/docs/api/turns.md#stop-a-turn) it. |
| `awaiting_response` | The turn ended with a Clarification for you. Up next pauses. | [Answer it](https://kosuke.ai/docs/api/turns.md#answer-a-clarification). |
| `completed` | The turn finished. | [Read the reply](https://kosuke.ai/docs/api/turns.md#read-the-reply). |
| `failed` | The turn failed. `error` and `errorCode` say why. Up next pauses. | Fix the cause and send again. For `context_window_exceeded`, resend with `retryFailedJobId`. For `out_of_memory`, the sandbox ran out of memory: send again, and ask the agent to run fewer things at once. |
| `cancelled` | A person stopped the turn. Up next pauses. | Start nothing new until a person asks you to. |

Polling faster does not make a turn finish sooner. To follow a turn live instead, stream it with the CLI:

```bash
kosuke sessions jobs watch "$PROJECT_ID" "$SESSION_ID" "$CHAT_JOB_ID" \
  | jq -r 'select(.type == "message") | .data.text'
```

The MCP server's `watch` tool polls a job until it settles and returns the final status, the last assistant message and the preview URLs. Call it again when it returns `done: false`. The CLI command's exit code says how the job ended:

| Exit code | Meaning |
| --- | --- |
| 0 | The job ended `completed` or `awaiting_response`. |
| 1 | The job failed or was stopped. |
| 3 | The token was refused. |
| 4 | The project, session or job was not found. |
| 124 | `--timeout` passed first. |

## Read the reply

`GET /api/projects/{projectId}/chat-sessions/{sessionId}/messages` returns the whole conversation, oldest first, and `limit=20` only its newest 20 messages. Your messages carry `content`, and the agent's carry `blocks`. The reply is in the `text` blocks of the last message with `role` `assistant`:

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/messages" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  | jq -r '[.data.messages[] | select(.role == "assistant")] | last | .blocks[] | select(.type == "text") | .content'
```

Other blocks record the agent's `plan`, each `tool` it ran, each `artifact` it wrote, any `clarification` it asked, and the Backlog sessions it filed (`filed_sessions`). Tool calls and subagents come summarized: a `tool` block keeps its short input fields and the first 500 characters of a long result, and a subagent its last output entry. Read one message with every block as recorded with `GET .../messages/{messageId}`. [Chat sessions](https://kosuke.ai/docs/endpoint-reference/chat-sessions.md#read-one-session) lists what each read keeps, and how to page back through a long conversation.

## Answer a Clarification

A turn that ends `awaiting_response` holds a `clarification` block. Its `clarificationData.questions` lists each question with a `topic`, the `question` text and two to four `options`. The recommended option comes first, with `isRecommended: true`.

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/messages" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  | jq '[.data.messages[] | select(.role == "assistant")] | last | .blocks[]
        | select(.type == "clarification") | .clarificationData.questions[]
        | {topic, question, options: [.options[].title]}'
```

Send the answers as an ordinary message. It starts the next turn and resumes Up next. The web app words its answers like this, and the agent reads any clear wording:

```text
My answers:
- **Display order:** Newest first
- **Empty state:** Show a message
```

## Steer a running turn

A message sent during a turn waits Up next. To fold it into the running turn instead, steer it with the `queuedMessageId` the send returned:

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

```json
{ "data": { "delivery": "steer", "steerId": "e1f09a7c-…" } }
```

Only the message's author can steer it, and only into a turn of their own. Claude, Codex, GLM, Kimi and DeepSeek sessions all take steers. When no turn can take the message, the steer answers 409 `STEER_UNAVAILABLE` and the message stays Up next. A steer the turn has not taken when it ends goes back to the top of Up next.

The CLI manages Up next with `kosuke sessions queue list`, `update`, `move`, `send`, `steer`, `delete` and `resume`. [Messages, Up next and steering](https://kosuke.ai/docs/sessions/messages.md) explains Up next in the web app.

## Stop a turn

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

The job ends `cancelled` and Up next pauses. Steers the turn had not taken go back to the top of Up next. Stopping a job that ended `completed`, `failed` or `cancelled` answers 400. To carry on, send a message, which goes at once, or resume Up next with `POST /api/projects/{projectId}/chat-sessions/{sessionId}/queue/resume`.

## Choose the model

Send `model` with a message to switch the session's model from that turn on. Model ids name a provider, a model and a reasoning effort, such as `anthropic:opus__high`. `kosuke models list` prints every id.

- The override applies only to a message that starts a turn. A message that waits Up next ignores it.
- Sensei ignores an id missing from the catalog, and a model the session's Assignee cannot run. The turn then runs on the session's current model.
- Switching to another provider starts the agent fresh, with the conversation handed over as text.
- To confirm the switch, read `data.session.model` from `GET /api/projects/{projectId}/chat-sessions/{sessionId}`.

Creating or updating a session, saving a cron and setting the deploy model check the id strictly. An unknown id there answers 400.

## When a send is refused

| Code | Meaning | What to do |
| --- | --- | --- |
| `JOB_IN_PROGRESS` | A turn is running while you send an Up next message now. A fresh message never gets this code, because it waits Up next. | Leave it Up next. It goes when the turn ends. |
| `QUEUE_FULL` | Up next already holds 20 messages. | Wait for one to go, or delete one. |
| `JOB_STOPPED` | A person stopped the turn named by `runningJobId`. | Do not resend on your own. |
| `SESSION_UPDATING` | Another change to the session is in progress. | Send the same request again. |
| `SESSION_STATUS_CHANGED` | The session's status, branch, model or Assignee changed, or someone ran `/clear`. | Read the session again, then decide whether to send. |
| `SESSION_CHANGED` | The session changed while your message was being queued. | Read the session again, then send. |
| `SANDBOX_LIMIT_REACHED` | Waking the sandbox would pass the workspace's concurrent sandbox limit. | [Free a slot](https://kosuke.ai/docs/api/sandboxes.md), then send again. |
| `SANDBOX_STARTING` | The session's sandbox is still starting. | Send the same message again shortly. |
| `DEPLOY_ACTIVE` | The sandbox is gone or paused and cannot be created while the project deploys. | Send again once the deploy ends. |

All of these answer 409. A send can also answer:

- 400 when the session is in the Backlog, Completed or Archived. Start a Backlog session first.
- 400 when a Cron or another session sends into an Under Review session. A member's message, Sensei's relay of one and a Merge agent run of the project are sent, and the session stays Under Review; resume it first for any other sender.
- 402 `CREDIT_LIMIT_REACHED` when the workspace is blocked on billing.

## Next steps

- [Sandboxes and previews](https://kosuke.ai/docs/api/sandboxes.md)
- [Responses and errors](https://kosuke.ai/docs/api/errors.md)
- [Messages and turns reference](https://kosuke.ai/docs/endpoint-reference/messages-and-turns.md)
