# Send review comments to the agent

Leave review comments on specific lines of the agent's diff, then send them back as the next message. The agent reads each comment with the lines it points at and addresses them in one turn. [Review the changes](https://kosuke.ai/docs/review/review-the-changes.md) shows the same flow in the web app.

## Before you start

Set `PROJECT_ID` and `SESSION_ID`, as in [Ship a change end to end](https://kosuke.ai/docs/recipes/ship-a-change-end-to-end.md). Reading the diff needs a running sandbox, so request the session's preview first:

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/preview?sessionId=$SESSION_ID" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" | jq '.data'
```

[Sandboxes and previews](https://kosuke.ai/docs/api/sandboxes.md) explains the flags this can answer with.

## 1. Find the changed lines

Without a `scope`, both requests below show what the agent's last turn changed. To see everything the pushed branch changes, add `?scope=branch` to the file list and `--data-urlencode "scope=branch"` to the patch request. The patch answers 404 for a file outside its scope.

List the changed files:

```bash
curl -s "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/preview/diff/files" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" | jq '.data.files[] | {path, status, additions, deletions}'
```

An empty list with `unavailable` set to "Changes require a running sandbox" means the sandbox is paused. Wake it and ask again.

Read one file's patch. The hunk headers, such as `@@ -38,6 +38,9 @@`, give the line numbers on each side:

```bash
curl -s -G "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/preview/diff/patch" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  --data-urlencode "path=src/search/route.ts" | jq -r '.data[0].windows[].patch'
```

## 2. Comment on a line range

```bash
curl -s -X POST \
  "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/preview/diff/comments" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filePath": "src/search/route.ts",
    "side": "new",
    "startLineNumber": 42,
    "endLineNumber": 44,
    "body": "This shares one limiter across tenants. Key it by tenant id.",
    "lineSnapshot": "const limiter = new RateLimiter()"
  }' | jq '.data' >> comments.json
```

| Field | Required | What it holds |
| --- | --- | --- |
| `filePath` | Yes | The file's path, as the file list shows it. |
| `side` | Yes | `new` for lines in the changed file. `old` for lines the change removed. |
| `startLineNumber` | Yes | The first line of the range, counted on that side. |
| `endLineNumber` | Yes | The last line of the range. Equal to or after `startLineNumber`. |
| `body` | Yes | Your comment, up to 4,000 characters. |
| `lineSnapshot` | Yes | The text of the commented lines as they read now. The web app compares it with the current diff to show the comment as outdated. The API never does, and always returns `isOutdated: false`. It may be empty when the side you comment on has no lines. |

The response answers 201 with the comment, including its `id`:

```json
{
  "id": "5d0c8e21-…",
  "chatSessionId": "7a3f0b92-…",
  "filePath": "src/search/route.ts",
  "side": "new",
  "startLineNumber": 42,
  "endLineNumber": 44,
  "body": "This shares one limiter across tenants. Key it by tenant id.",
  "lineSnapshot": "const limiter = new RateLimiter()",
  "resolvedAt": null,
  "resolvedByUserId": null,
  "createdAt": "2026-10-08T09:12:04.000Z",
  "updatedAt": "2026-10-08T09:12:04.000Z",
  "author": {
    "userId": "u8Rk2mQ4xT7vB1nP9sL3cW6yD0fH5jZa",
    "name": "Sam Rivera",
    "email": "sam@acme.com",
    "imageUrl": null
  },
  "isOutdated": false
}
```

The command appends each comment to `comments.json`, which keeps the ids for the next step. Repeat it for every comment. With the CLI, run `kosuke sessions diff comments create`.

## 3. Send the comments to the agent

Build `diffCommentsContext` from the saved comments. Each item carries exactly these fields, and one message takes up to 50 items:

```bash
jq -s '{
  content: "Please address these review comments.",
  diffCommentsContext: [.[] | {id, filePath, side, startLineNumber, endLineNumber, body, lineSnapshot: .lineSnapshot[:4000], isOutdated}] | .[:50]
}' comments.json > message.json

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 @message.json | jq '.data'
```

`content` may be an empty string when you send only comments. Sensei reads each comment again by its `id`, so the agent sees the comment as it is stored, and a resolved comment is left out. Items with fields beyond those listed, other than an optional `createdAt`, fail validation.

The response carries `delivery`. Follow the turn as [Follow a turn](https://kosuke.ai/docs/api/turns.md) shows.

## 4. Resolve the comments

When the agent has dealt with a comment, resolve it:

```bash
for COMMENT_ID in $(jq -r '.id' comments.json); do
  curl -s -X PATCH \
    "https://app.kosuke.ai/api/projects/$PROJECT_ID/chat-sessions/$SESSION_ID/preview/diff/comments/$COMMENT_ID" \
    -H "Authorization: Bearer $KOSUKE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"resolved": true}' > /dev/null
done
```

Anyone who can reach the project can resolve a comment, and `{"resolved": false}` reopens it. Only its author can change its `body` or delete it. A request changes the body or the resolved state, one of the two. To see what is still open, list the comments with `GET /api/projects/{projectId}/chat-sessions/{sessionId}/preview/diff/comments`.

## Next steps

- [Follow a turn](https://kosuke.ai/docs/api/turns.md)
- [Diff reference](https://kosuke.ai/docs/endpoint-reference/diff.md)
- [Review the changes](https://kosuke.ai/docs/review/review-the-changes.md)
