# Sandboxes and previews

A session's sandbox pauses when nobody uses it. Before you read its files, diff or git state, request the session's preview. That request wakes the sandbox, or creates it. Both happen in the background, so the answer comes back before the sandbox is up, with `provisioning: true` and no URLs.

## When you need a running sandbox

| You want to | What to do |
| --- | --- |
| List files, read the diff or read git state | The read answers without the data. The file list, the changed files and the commit list come back empty, and the patch answers 503. Git state reports `available: false`. Wake the sandbox first. |
| Read or write one file | Wake the sandbox first. |
| Restart a stuck sandbox | Nothing to do. The restart kills the sandbox, paused or not, and Sensei creates a new one. |
| Commit or push | Nothing to do. These wake a paused sandbox themselves. |
| Send a message | Nothing to do. Sending wakes a paused sandbox itself, and creates one for a session that has none. The turn starts once the sandbox is up. |

## Wake the sandbox

Request the session's preview. Leave out `sessionId` to ask for the project's main preview instead.

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

With the CLI, run `kosuke preview get <projectId> --session-id <sessionId>`.

```json
{
  "data": {
    "frontendUrls": { "web": "https://…" },
    "projectId": "3f1c2a9e-…",
    "sandboxKey": "9b2e4f60-…"
  }
}
```

`frontendUrls` maps each endpoint the project shows in the preview to its URL. The key is the service name, or `<service>-<endpoint>` for a service's second and later endpoints.

## Read the response

Read the flags before the URLs. A refusal about capacity or billing still answers 200.

| Response | Meaning | What to do |
| --- | --- | --- |
| `frontendUrls` has URLs | The sandbox is up. | Wait for its services, then read. |
| `provisioning: true`, empty `frontendUrls` | The sandbox is starting. | Poll the health request until `state` is `healthy`, or until it reports `startFailure`. |
| `creditLimitReached: true`, empty `frontendUrls` | The workspace is blocked on billing, so nothing started. | Stop. Retrying fails until an admin sorts out billing under [Plans and billing](https://kosuke.ai/docs/workspace/plans-and-billing.md). |
| Empty `frontendUrls` and no flag | The session is in the Backlog, Completed or Archived. Or the main preview is being deployed. Or no service is exposed as a preview. | Read the session again. For a live session, poll the health request. |
| 400 with an `error` message | The project has never been deployed, a deploy is under way, the session has no branch yet, or the project's GitHub access needs reconnecting. | Act on the message. Ask again once the deploy ends. |

## Wait for the services

The preview request returns before the sandbox is up, and its services start after it. Poll the health request until `state` is `healthy`:

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

```json
{
  "data": {
    "state": "healthy",
    "frontendUrls": { "web": "https://…" },
    "services": [{ "name": "agent", "httpResult": "healthy" }, { "name": "web", "httpResult": "healthy" }],
    "sandboxAvailable": true
  }
}
```

`state` is `starting` or `healthy`. `startInFlight: true` means a start is bringing the sandbox up, so keep polling. `needsStart: true` means nothing is bringing the sandbox up, so request the preview once more.

Stop polling when health reports `startFailure` or `creditLimitReached`:

| Health reports | What to do |
| --- | --- |
| `startFailure` with `reason: "sandbox_limit"` | The workspace already runs as many session sandboxes as its plan allows. [Free a slot](https://kosuke.ai/docs/api/sandboxes.md#free-a-slot), then request the preview again. |
| `startFailure` with `reason: "failed"` | The start failed. Its `message` says why. Request the preview again later. |
| `creditLimitReached: true` | The workspace is blocked on billing. Stop until an admin sorts it out. |

A request for the preview clears `startFailure` and starts again. `indeterminate: true` means this read learned nothing, because a pause or resume held the sandbox or it did not answer. Ignore the rest of the answer and ask again in a few seconds, without requesting the preview. With the CLI, run `kosuke preview health get <projectId> --session-id <sessionId>`.

A sandbox you woke pauses again when it sits idle. Request the preview again before a later read.

## Free a slot

Set `WORKSPACE_ID` to the project's `orgId`, from `GET /api/projects/{projectId}`. Then read how many sandboxes may run at once, and which sessions hold them:

```bash
curl -s "https://app.kosuke.ai/api/organizations/$WORKSPACE_ID/sandbox-limit" \
  -H "Authorization: Bearer $KOSUKE_API_KEY"
```

```json
{
  "data": {
    "maxConcurrentSandboxes": 5,
    "runningSessions": [
      { "sessionId": "9b2e4f60-…", "title": "Add a healthz endpoint", "projectName": "storefront", "branch": "…" }
    ]
  }
}
```

`maxConcurrentSandboxes` is `null` on a plan with no limit. Pause the sandboxes of up to 50 sessions at once:

```bash
curl -s -X POST "https://app.kosuke.ai/api/organizations/$WORKSPACE_ID/stop-sandboxes" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sessionIds": ["9b2e4f60-…"]}'
```

The response lists each session with `stopped: true` or `false`. A paused sandbox keeps its work and wakes on the next preview request or message. [Plans and billing](https://kosuke.ai/docs/workspace/plans-and-billing.md) lists each plan's limit.

## Restart a stuck sandbox

When a sandbox is stuck and does not recover by itself, kill it and let Sensei create a new one. It works on a running, paused or unreachable sandbox. Uncommitted work in the sandbox is lost, and so is the agent's memory of the chat. The chat history stays, and the next message's agent reads it.

```bash
curl -s -X POST "https://app.kosuke.ai/api/projects/$PROJECT_ID/preview/sandbox/reset" \
  -H "Authorization: Bearer $KOSUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"sessionId\": \"$SESSION_ID\"}"
```

With the CLI, run `kosuke preview sandbox reset <projectId> --session-id <sessionId>`. Leave out the session for the project's main sandbox, which comes back through a Recovery Deploy.

The restart answers with `status: "queued"` inside `data`, next to `projectId`, `sandboxKey` and `alreadyInFlight`, and kills the sandbox in the background. A second restart while one runs kills nothing more. A session's new sandbox starts on the next preview request or message. A restart of a Backlog, Completed or Archived session answers 400. It answers 409 while the agent is working on a turn (stop it first), while a sandbox is still being started, or while the main is deploying.

## Next steps

- [Follow a turn](https://kosuke.ai/docs/api/turns.md)
- [Previews reference](https://kosuke.ai/docs/endpoint-reference/previews.md)
- [Preview and sandboxes in the web app](https://kosuke.ai/docs/sessions/preview.md)
