kimi-code/docs/en/reference/server-api.md
liruifengv eaa3969dd3
feat(kap-server): add page mode, updated_before, and batch archive/restore to v2 sessions (#2983)
* feat(kap-server): add page-number mode and total to GET /api/v2/sessions

The v2 session list gains a stateless 1-based `page` parameter beside the
opaque page_token cursor for admin-style lists that jump arbitrarily:
each request stays a full independent snapshot, no token is minted, and
`page` + `page_token` together fail 40001. Every response now carries
`total` (the filtered/sorted set size) in both pagination modes.

* feat(kap-server): add meta.updated_before filter to GET /api/v2/sessions

Symmetric with meta.updated_after (inclusive boundary, Unix ms), applied
at the edge over the drained set and bound into the page_token query
fingerprint like every other condition.

* feat(kap-server): add POST /api/v2/sessions:archive and :restore batch endpoints

Batch archive/restore for session-management views: { ids } (non-empty,
≤5000 unique after dedup) answers per-item results in input order with
succeeded/failed counts — only a body validation failure fails the whole
request, and an unknown id folds into its own item as 40401.

The live/cold split keeps the batch cheap: a session with a live handle
goes through the full ISessionLifecycleService chain (agents drain,
scope teardown, mirror drain), while a cold session is never
materialized — the new setColdSessionArchived helper in agent-core-v2
patches the persisted state.json (archived/archivedAt, updatedAt
preserved, mirroring setArchived's touchUpdatedAt: false semantics),
mirrors the flipped summary into the read-model queue, and republishes
the same event.session.archived bus event the live lifecycle emits
(:restore publishes nothing, matching the live restore). Hot items run
with bounded concurrency and the batch ends with one shared
ISessionIndexMirror.drain().

* docs(server-api): document v2 sessions page mode, total, updated_before, and batch archive/restore

* fix(kap-server): deep-import workspace lifecycle symbols in the v2 sessions route

CI's tsgo/rolldown (Linux) fail to bind liveHandlerForSession and
IWorkspaceLifecycleService through the agent-core-v2 package-root
barrel even though it re-exports them; the same files use the
established deep-import pattern already used for the git domain.

* fix(kap-server): inline the live-handler lookup in the batch route

The previous deep imports still fail to resolve on CI's Linux toolchain
(tsgo TS2307, rolldown MISSING_EXPORT) while every other module path
from the same package binds fine. Keep the route self-contained: the
hot-path lookup is a five-line loop over IWorkspaceLifecycleService's
handlers (mirrors agent-core-v2's liveHandlerForSession), and the tests
assert non-materialization behaviorally via the live map instead of
importing the same two symbols for spies.

* fix(kap-server): drive the batch hot path through getLiveSessionById

The phantom only hits the workspaceLifecycle-group symbols in these two
files on CI's Linux toolchain; getLiveSessionById is observed to bind
fine there. It returns the session's live scope directly (no resume),
which is exactly what the batch hot path needs.

* refactor(kap-server): move the batch live/cold split into agent-core-v2

setSessionArchivedBatch owns the split next to the cold patch: live
sessions go through the full lifecycle chain via the workspace handler
accessor (the v1-proven resolution path), cold sessions through the
direct write. The route becomes a thin wire-code adapter, and the batch
tests assert the live chain behaviorally (disposal, events, index)
instead of spying through scope accessors.

* fix(agent-core-v2): import sessionLookup relatively from coldSessionArchive

The '#/app/workspaceLifecycle/*' specifier resolves from src/ and
src/app/* files on CI's Linux toolchain but not from
src/workspace/sessionLifecycle/ (tsgo TS2307, rolldown follows); a
relative import bypasses the package-imports mapping.

* fix(agent-core-v2): migrate the batch hot path to ISessionManager

Main's workspace/session DI refactor removed the workspaceLifecycle
lookup modules; the live branch now goes through the App-level
ISessionManager (the same entry the v1 action route uses post-refactor)
with getLiveSessionById from the new sessionManager lookup.

* feat(kap-server): add the id,archived item projection to GET /api/v2/sessions

fields=id,archived trims each item to { id, archived } for
select-all-matching flows (the session admin page's Gmail-style
select-all). Only that projection gets the relaxed page_size ceiling
(10000); unknown fields, non-pair subsets, and include=git combinations
are 40001, and the projection binds into the page_token fingerprint so
shapes never flip mid-pagination.

* fix(agent-core-v2): serialize the batch cold write against in-flight resumes

Codex review on #2983: while a resume is in flight the live registry
hides the handle, so the batch route could classify the session as cold
and its direct write would race the materializing metadata service (its
stale in-memory document wins the next write, silently un-archiving the
session after the endpoint reported success).

The batch now settles the resume first: SessionManager registers the
whole resume promise synchronously at the App level (controllerForSession
is async, so the controller's own resuming map learns about it a few
microtasks late) and whenResumeSettled awaits it before classification —
a settled resume lands the item on the live chain, a failed one falls
back to the cold path. Also folds the module header down to the
package's external-role comment convention.

* fix(agent-core-v2): publish SessionArchived as an Event2 class in cold archive

* fix(agent-core-v2): serialize batch archive/restore with session lifecycle transitions

* fix(agent-core-v2): serialize session delete with the lifecycle chain

* fix(agent-core-v2): mirror the persisted metadata on cold archive, not the index summary

* docs(agent-core-v2): bring sessionManager comments and new tests to package conventions

* fix(agent-core-v2): normalize legacy session metadata before the cold archive write

* fix(kap-server): serialize the v1 single-session archive with the lifecycle chain

* chore: drop changesets for internal-only protocol work

* fix(agent-core-v2): encode cold-archived metadata for v1 readers

* fix(agent-core-v2): serialize fork and createChild with the source session's chain

* refactor(agent-core-v2): chain every session lifecycle method and hand batch sections unguarded ops

* fix(agent-core-v2): propagate failed resumes to the next settle

* fix(agent-core-v2): roll back the unannounced handle when a resume fails mid-materialization

* fix(agent-core-v2): read and migrate the legacy session-meta location on cold archive

* fix(agent-core-v2): serialize explicit-id session creation with the lifecycle chain

create() with a caller-supplied sessionId bypassed the per-session chain,
so a concurrent batch archive could classify the half-created session as
cold and write archived state that the live metadata service later
overwrites. Creation now queues on the target id's chain whenever an
explicit id is present.

Also type the resume-failure maps as Error and normalize at the catch
site, satisfying only-throw-error.

* style(kap-server): strip comments from the session routes per the no-comments convention

* fix(agent-core-v2): serialize explicit fork and child target ids on the lifecycle chain

fork() and createChild() with a newSessionId locked only the source id, so
a batch archive of the target could slip into the creation window: the
index already knows the half-created session, the batch writes archived
state to its document, and the fork's in-memory metadata later overwrites
it. Both operations now acquire the deduped, sorted key set so multi-key
sections always take locks in one deterministic order.
2026-08-18 13:57:37 +08:00

371 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Server API
The local server started by `kimi web` exposes two programmatic surfaces: a REST API (`/api/v1`, plus `/api/v2/sessions`) and a WebSocket event stream (`/api/v1/ws`). This page is the protocol reference for both. For how to start the server and its command-line options, see the [kimi command](./kimi-command.md#kimi-web) reference; for an end-to-end walkthrough, see [Local server and API](../guides/server.md).
The complete request/response schema of every endpoint is owned by the server's live specification documents: `GET /openapi.json` (OpenAPI) and `GET /asyncapi.json` (AsyncAPI). Both require authentication.
::: warning
The REST and WebSocket APIs described on this page are experimental: interface stability is not guaranteed, and endpoints, fields, and event types may change in any release. When integrating, rely on the `/openapi.json` and `/asyncapi.json` documents served by your version.
:::
## Conventions
### Address
The default address is `http://127.0.0.1:58627`. When the port is taken, the server retries with the next port (up to 100 times); use `--port` / `--host` to change the bind. Multiple instances can coexist under the same home directory; running instances register under `~/.kimi-code/server/instances/`.
### Authentication
All `/api/*` paths (including `/openapi.json` and `/asyncapi.json`) require the bearer token, except:
- `OPTIONS` preflight requests
- `GET /api/v1/healthz` (liveness probe)
- Static web assets (non-`/api/` paths)
How to carry it: REST uses the `Authorization: Bearer <token>` header; the WebSocket upgrade accepts the same header or the subprotocol `kimi-code.bearer.<token>`. Token generation and rotation are covered in [Local server and API: Authentication](../guides/server.md#authentication).
Failed authentication returns HTTP 401 with envelope code `40101`. On non-loopback binds, a source that fails authentication 10 times within 60 seconds is banned for 60 seconds, during which every request gets HTTP 429 (code `42901`).
### Response envelope
Every JSON response is wrapped in a uniform envelope:
```json
{
"code": 0,
"msg": "success",
"data": {},
"request_id": "01JZX4A6E7M8V0R3Q0N2K2M5Q9"
}
```
- `code`: the business outcome; `0` means success. See the error-code bands below.
- `data`: the payload on success. Note that some "error" envelopes also carry a non-null `data` — for example, resolving an already-resolved approval returns `40902` with `data.resolved` set to `false` — so clients should check `code` first, then `data`.
- `request_id`: a ULID for this request. Clients may supply one via the `X-Request-Id` header; invalid values are regenerated by the server.
The HTTP status is almost always 200; the business outcome lives in `code`. Exceptions:
| Situation | HTTP status |
| --- | --- |
| Authentication failure / rate limit | 401 / 429 |
| Provider created, provider catalog imported | 201 |
| Provider deleted | 204 |
| Binary/streaming endpoints | 206 (Range) / 304 (ETag unchanged) where supported — capabilities differ per endpoint, see [Binary and streaming endpoints](#binary-and-streaming-endpoints) |
| `GET /api/v1/files/{file_id}` download errors | real 404 / 500 (still carrying an envelope body) |
The 201 responses still carry the standard envelope (`code` 0) — only the status line follows the REST convention for resource creation. A 204 response has no body by definition, so a successful delete is reported by the status code itself.
### Error codes
Error codes are grouped by band:
| Band | Meaning | Examples |
| --- | --- | --- |
| `0` | Success | |
| `400xx` | Bad request | `40001` validation failed (`details` lists each field), `40003` provider is OAuth-managed |
| `401xx` | Auth and readiness | `40101` unauthorized, `40110` no provider configured, `40113` model not resolved |
| `404xx` | Not found | `40401` session, `40408` MCP server, `40409` file path |
| `409xx` | State conflict | `40901` session busy, `40902` approval already resolved, `40922` page conditions mismatch `page_token` |
| `410xx` | Expired | `41001` approval timed out, `41002` question timed out, `41003` temporary file expired |
| `413xx` | Size or boundary exceeded | `41302` file read over 10 MB, `41304` path escapes the session directory |
| `429xx` | Rate limited | `42901` auth-failure ban, `42902` too many fs watches |
| `500xx` | Server internal error | `50001` uncaught exception, `50003` persistence failure |
| `6xxxx` / `7xxxx` / `8xxxx` | Tool runtime / LLM provider / MCP passthrough errors; `msg` carries the upstream text | |
### Pagination
List endpoints come in two styles:
- **Cursor style**: `before_id` / `after_id` (mutually exclusive) plus `page_size` (1100), responding with `{ items, has_more }`. Used by the session list, message list, transcript, and others.
- **`page_token`**: an opaque token (bound to a fingerprint of the query conditions), used by `POST /api/v1/search` and `GET /api/v2/sessions`. Changing any query condition mid-pagination invalidates the token: v2 returns `40922`, search returns `40001`. `GET /api/v2/sessions` also offers a stateless `page` page-number mode as an alternative.
## REST endpoints
Endpoints are grouped by resource below. A `:{action}` suffix in a path is the action convention — POST to `path:action` on a single resource for non-CRUD operations (such as `:fork` and `:archive` on a session).
### Server and metadata
| Method and path | Description |
| --- | --- |
| `GET /api/v1/healthz` | Liveness probe; auth-exempt |
| `GET /api/v1/meta` | Server version, capability map, `server_id`, experimental flags |
| `POST /api/v1/shutdown` | Graceful shutdown (replies 200 first); mounted only on loopback binds |
### Login and usage
| Method and path | Description |
| --- | --- |
| `GET /api/v1/auth` | Auth readiness snapshot |
| `POST /api/v1/oauth/login` | Start the OAuth device-code login flow |
| `GET /api/v1/oauth/login` | Poll the login flow state |
| `DELETE /api/v1/oauth/login` | Cancel a pending login flow |
| `POST /api/v1/oauth/logout` | Log out the managed provider |
| `GET /api/v1/oauth/usage` | Plan usage and limits |
| `GET /api/v1/oauth/userinfo` | Account profile |
### Config
| Method and path | Description |
| --- | --- |
| `GET /api/v1/config` | Read the global config (secret fields redacted) |
| `POST /api/v1/config` | Merge-patch the config; broadcasts `event.config.changed` |
### Models and providers
| Method and path | Description |
| --- | --- |
| `GET /api/v1/models` | List configured model aliases |
| `POST /api/v1/models/{model_id}:set_default` | Set the global default model |
| `GET /api/v1/providers` | List providers |
| `POST /api/v1/providers` | Create a provider (201) |
| `GET /api/v1/providers/{provider_id}` | Read a provider (reveals the stored key) |
| `PUT /api/v1/providers/{provider_id}` | Replace a provider |
| `DELETE /api/v1/providers/{provider_id}` | Delete a provider (204) |
| `POST /api/v1/providers/{provider_id}:refresh` | Refresh one provider's model metadata |
| `POST /api/v1/providers:{action}` | Collection actions: `refresh` / `refresh_oauth` / `import_catalog` / `import_registry` |
| `GET /api/v1/catalog/providers` | Browse the models.dev directory (server-proxied) |
| `GET /api/v1/catalog/providers/{catalog_id}` | Read one directory entry |
### Sessions
| Method and path | Description |
| --- | --- |
| `POST /api/v1/sessions` | Create a session (requires `workspace_id` or `metadata.cwd`) |
| `GET /api/v1/sessions` | List sessions; cursor pagination with filters such as `busy` and `archived_only` |
| `GET /api/v1/sessions/{session_id}` | Read one session |
| `GET /api/v1/sessions/{session_id}/profile` | Read the session profile |
| `POST /api/v1/sessions/{session_id}/profile` | Update title, metadata, agent config |
| `POST /api/v1/sessions/{session_id}:{action}` | Session actions: `fork` / `compact` / `undo` / `abort` / `btw` / `archive` / `restore` |
| `GET /api/v1/sessions/{session_id}/children` | List child sessions |
| `POST /api/v1/sessions/{session_id}/children` | Create a child session (fork with a tag) |
| `GET /api/v1/sessions/{session_id}/status` | Realtime status rollup |
| `GET /api/v1/sessions/{session_id}/goal` | Current goal snapshot (`null` when none) |
| `GET /api/v1/sessions/{session_id}/warnings` | Session-level warnings |
| `POST /api/v1/sessions/{session_id}/export` | Export the session with diagnostics (zip stream, not enveloped) |
| `GET /api/v1/sessions/{session_id}/snapshot` | Full snapshot for client rebuilds (with `as_of_seq` and `epoch`) |
### Messages and transcript
| Method and path | Description |
| --- | --- |
| `GET /api/v1/sessions/{session_id}/messages` | Page messages (`before_id` / `after_id` / `role`) |
| `GET /api/v1/sessions/{session_id}/messages/{message_id}` | Read one message |
| `GET /api/v1/sessions/{session_id}/transcript` | Turn-paged transcript (requires `agent_id`); global state rides along unpaginated |
| `GET /api/v1/sessions/{session_id}/transcript/ops` | Op-batch catch-up (`since_seq`); `complete: false` means a full refresh is needed |
| `GET /api/v1/sessions/{session_id}/transcript/user-messages` | Turn-opening user inputs, unpaginated |
| `GET /api/v1/sessions/{session_id}/transcript/plan` | ExitPlanMode plan content, path, and review outcome |
### Prompts
| Method and path | Description |
| --- | --- |
| `GET /api/v1/sessions/{session_id}/prompts` | Active and queued prompts |
| `POST /api/v1/sessions/{session_id}/prompts` | Submit a prompt (content-part array, optional model / permission-mode overrides) |
| `POST /api/v1/sessions/{session_id}/prompts:steer` | Steer queued prompts into the active turn |
| `POST /api/v1/sessions/{session_id}/prompts/{prompt_id}:abort` | Abort a running prompt |
| `POST /api/v1/sessions/{session_id}/prompts/{prompt_id}:steer` | Steer one queued prompt |
### Approvals and questions
| Method and path | Description |
| --- | --- |
| `GET /api/v1/sessions/{session_id}/approvals` | List approval requests (filter with `status=pending`) |
| `POST /api/v1/sessions/{session_id}/approvals/{approval_id}` | Resolve an approval |
| `GET /api/v1/sessions/{session_id}/questions` | List questions |
| `POST /api/v1/sessions/{session_id}/questions/{question_id}` | Answer a question |
| `POST /api/v1/sessions/{session_id}/questions/{question_id}:dismiss` | Dismiss a question |
### Background tasks
| Method and path | Description |
| --- | --- |
| `GET /api/v1/sessions/{session_id}/tasks` | List background tasks |
| `GET /api/v1/sessions/{session_id}/tasks/{task_id}` | Read a task (optional output preview) |
| `POST /api/v1/sessions/{session_id}/tasks/{task_id}:cancel` | Cancel a task |
### Skills, tools, and MCP
| Method and path | Description |
| --- | --- |
| `GET /api/v1/sessions/{session_id}/skills` | Per-session skill catalog |
| `GET /api/v1/workspaces/{workspace_id}/skills` | Session-less skill catalog for a workspace |
| `POST /api/v1/sessions/{session_id}/skills/{skill_name}:activate` | Activate a skill (starts a turn) |
| `GET /api/v1/tools` | List tools of the effective agent |
| `GET /api/v1/mcp/servers` | List MCP servers |
| `POST /api/v1/mcp/servers/{mcp_server_id}:restart` | Restart an MCP server |
### Terminals
PTY terminal endpoints; mounted only on loopback binds.
| Method and path | Description |
| --- | --- |
| `GET /api/v1/sessions/{session_id}/terminals` | List terminals |
| `POST /api/v1/sessions/{session_id}/terminals` | Create a terminal |
| `GET /api/v1/sessions/{session_id}/terminals/{terminal_id}` | Read a terminal (including scrollback) |
| `POST /api/v1/sessions/{session_id}/terminals/{terminal_id}:close` | Close a terminal |
### Workspaces
| Method and path | Description |
| --- | --- |
| `GET /api/v1/workspaces` | List registered workspaces |
| `POST /api/v1/workspaces` | Register a workspace (idempotent on the root path) |
| `PATCH /api/v1/workspaces/{workspace_id}` | Rename |
| `DELETE /api/v1/workspaces/{workspace_id}` | Unregister (keeps on-disk content) |
| `GET /api/v1/workspaces/{workspace_id}/trust` | Read the trust state |
| `POST /api/v1/workspaces/{workspace_id}/trust` | Grant trust |
| `POST /api/v1/workspaces/{workspace_id}/untrust` | Revoke trust |
### File system
In-session file operations go through `POST /api/v1/sessions/{session_id}/fs:{action}` with JSON bodies; actions are `list` / `read` / `list_many` / `stat` / `stat_many` / `mkdir` / `search` / `grep` / `git_status` / `diff` / `open` / `open-in` / `reveal`. In addition:
| Method and path | Description |
| --- | --- |
| `POST /api/v1/workspace/fs:search` | Session-less workspace search (the body carries the workspace reference) |
| `GET /api/v1/sessions/{session_id}/fs/{path}:download` | Download a session file (binary, see below) |
| `GET /api/v1/fs:browse` | List host directories (folder picker) |
| `GET /api/v1/fs:home` | The user's home directory and recent workspaces |
| `GET /api/v1/fs:content` | Raw bytes of any host file (gated only by the token — be careful when exposing the port) |
| `POST /api/v1/fs:mkdir` | Create a directory by absolute path |
### File uploads
| Method and path | Description |
| --- | --- |
| `POST /api/v1/files` | Multipart upload (`file` field, optional `name` and `expires_in_sec`); returns file metadata |
| `GET /api/v1/files/{file_id}` | Download (binary; errors use real HTTP statuses) |
| `DELETE /api/v1/files/{file_id}` | Delete |
### Global search and misc
| Method and path | Description |
| --- | --- |
| `POST /api/v1/search` | Cross-session full-text search; `mode` is `terms` (default) or `literal` (exact substring); `page_token` pagination |
| `GET /api/v1/connections` | List live WebSocket connections |
| `GET /api/v2/sessions` | Next-generation session list, see below |
| `POST /api/v2/sessions:archive` | Batch-archive sessions, see below |
| `POST /api/v2/sessions:restore` | Batch-restore archived sessions, see below |
| `/api/v1/debug/*` | Reflection debug RPC; mounted only with `--debug-endpoints` on loopback, not a stable protocol |
### `GET /api/v2/sessions`
A next-generation session query for list views — filtering, sorting, and field groups all travel in query parameters:
| Parameter | Description |
| --- | --- |
| `workspace.id` | Filter by workspace; repeatable |
| `activity.status` | Filter by activity status: `running` / `approval` / `question` / `failed` / `idle`; repeatable |
| `meta.updated_after` | Only sessions updated after this time (epoch milliseconds) |
| `meta.updated_before` | Only sessions updated before this time (epoch milliseconds) |
| `meta.archived` | `true` / `false` (default) / `all` |
| `sort` | `meta.updated_at_desc` (default) / `meta.updated_at_asc` / `meta.created_at_desc` |
| `include` | Comma-separated extra field groups; currently only `git` (branch and PR info, deduplicated per directory and cached for 60 seconds) |
| `fields` | Comma-separated item projection; currently only `id,archived`, trimming each item to `{ id, archived }` (select-all-matching flows). Not combinable with `include=git` (`40001`) |
| `page_size` | 1100, default 50; up to 10000 with the `id,archived` projection |
| `page_token` | Pagination token from the previous page |
| `page` | Stateless 1-based page number; mutually exclusive with `page_token` (`40001` when combined) |
Every response item carries the `workspace`, `meta`, and `activity` groups, plus `git` when `include=git` — or just `{ id, archived }` under `fields=id,archived`. Every page additionally carries `total`, the size of the filtered set. The page token binds the first page's query conditions (including the projection); changing them mid-pagination returns `40922`. `page` mode is a stateless alternative for jumping to arbitrary pages: every request is an independent snapshot, no token is minted, and `next_page_token` is always `null`.
### `POST /api/v2/sessions:archive` and `POST /api/v2/sessions:restore`
Batch archive/restore for session-management views. The body is `{ "ids": ["session_..."] }` — non-empty, at most 5000 unique ids (duplicates collapse). Live sessions go through the full lifecycle; cold sessions are patched on disk without being loaded.
Only a body validation failure fails the whole request (`40001`). Otherwise the response is per-item: `data.results` keeps the input order with `{ id, ok }` or `{ id, ok: false, error }` (an unknown id reports `40401` in its own item), plus `succeeded` / `failed` counts.
```json
{
"code": 0,
"msg": "success",
"data": {
"results": [
{ "id": "session_a", "ok": true },
{ "id": "session_b", "ok": false, "error": { "code": 40401, "message": "session session_b does not exist" } }
],
"succeeded": 1,
"failed": 1
},
"request_id": "req_..."
}
```
## WebSocket protocol
### Connect
The only endpoint is `ws://<host>:<port>/api/v1/ws`; authentication happens at the upgrade request (see [Authentication](#authentication) above). Once connected, the server immediately sends `server_hello`:
```json
{
"type": "server_hello",
"timestamp": "2026-01-01T00:00:00.000Z",
"payload": {
"ws_connection_id": "conn_01JZX4...",
"protocol_version": 2,
"max_event_buffer_size": 1000,
"capabilities": { "event_batching": false, "compression": false }
}
}
```
Note that the server never sends heartbeats and never disconnects an idle connection — keepalive and reconnection are the client's job.
### Control frames
Clients send JSON frames `{ "type", "id"?, "payload" }`; every request frame gets an acknowledgement `{ "type": "ack", "id", "code", "msg", "payload" }`, where `code` 0 means success.
| Frame | payload | Description |
| --- | --- | --- |
| `subscribe` | `{ session_ids, cursors?, agent_filter? }` | Subscribe to session events; with `cursors` (per-session `{seq, epoch}`) the server replays missed durable events |
| `unsubscribe` | `{ session_ids }` | Drop session subscriptions |
| `subscribe_v2` | `{ session_id, transcript, transcript_since? }` | Subscribe to transcript streams (the only transcript channel); `transcript` sets per-agent grades |
| `unsubscribe_v2` | `{ session_id, agent_ids? }` | Detach transcript streams; omitting `agent_ids` means the whole session |
| `watch_fs_add` / `watch_fs_remove` | `{ session_id, paths, recursive? }` | Subscribe to / unsubscribe from file-change notifications (`event.fs.changed`) |
| `client_hello` | `{ client_id }` | Handshake frame; the remaining fields are legacy compatibility |
### Events
Event frames look like `{ "type", "seq", "epoch"?, "volatile"?, "offset"?, "session_id"?, "timestamp", "payload" }`, where `type` is the event type itself. Two delivery scopes:
- **Global events**: sent to every established connection, no subscription needed — `session.meta.updated`, `event.session.created`, `event.session.work_changed`, `event.session.status_changed`, `event.workspace.*`, `event.config.*`.
- **Session events**: sent only to connections subscribed to that session, subject to `agent_filter`. Main families:
| Family | Main events |
| --- | --- |
| Turns | `turn.started`, `turn.ended`, `turn.step.started` / `completed` / `interrupted` / `retrying` |
| Streaming text | `assistant.delta`, `thinking.delta` (carry `offset` for alignment) |
| Tool calls | `tool.call.started`, `tool.call.delta`, `tool.progress`, `tool.result` |
| Interactions | `event.approval.requested` / `resolved`, `event.question.requested` / `answered` / `dismissed` |
| Subagents | `subagent.spawned` / `started` / `suspended` / `completed` / `failed` |
| Background | `task.started` / `terminated`, `shell.started` / `output` / `completed` |
| Misc | `compaction.*`, `skill.activated`, `goal.updated`, `prompt.*`, `error`, `warning` |
Events also split into durable and volatile: durable events carry a strictly increasing `seq`, are journaled, and can be replayed; volatile events (the `*.delta` family, `tool.progress`, `shell.*`, and similar) are marked `volatile: true` and never replayed. When consuming a volatile text stream, compare `offset` (the cumulative character offset within the turn) against your locally accumulated text: below the local length means a duplicate frame; above means a gap that needs snapshot recovery.
### Reconnect and recovery
After reconnecting, pass each session's last applied `{seq, epoch}` in `subscribe`'s `cursors`; the server replays the gap. If you fall more than the buffer (1000 events) behind, or the cursor is no longer valid, you get `resync_required` instead. In that case, call `GET /api/v1/sessions/{session_id}/snapshot` for a full snapshot (with `as_of_seq` and `epoch`), then subscribe again with the fresh cursor.
### Transcript protocol
`subscribe_v2`'s `transcript` field sets a per-agent grade: `off` / `turn` / `block` / `delta` (the `"*"` key sets the default grade), with higher grades pushing finer detail. An agent with a non-`off` grade receives two frame types: `transcript.reset` (a baseline snapshot; history pages in over REST) and `transcript.ops` (incremental op batches with a per-agent strictly increasing `seq`). The agent's legacy events are suppressed on that connection and carried by transcript frames instead. After a disconnect, resume with `transcript_since`; when the server's op journal cannot cover the gap (REST catch-up returns `complete: false`), do a full refresh. The REST counterparts are `GET .../transcript` (turn-paged) and `GET .../transcript/ops?since_seq=` (op-batch catch-up).
## Binary and streaming endpoints
The following endpoints stream binary bodies instead of a JSON payload. Their HTTP capabilities differ per endpoint:
| Method and path | Description | Range (206) | ETag / 304 |
| --- | --- | --- | --- |
| `GET /api/v1/files/{file_id}` | Download an uploaded file | Yes | No (sends an `etag` header but ignores `If-None-Match`) |
| `GET /api/v1/sessions/{session_id}/fs/{path}:download` | Download a session workspace file | Yes | Yes |
| `GET /api/v1/fs:content` | Raw bytes of any host file (gated only by the token — be careful when exposing the port) | Yes | Yes |
| `POST /api/v1/sessions/{session_id}/export` | Export the session with diagnostics (zip stream) | No | No |
Error semantics differ as well: `GET /api/v1/files/{file_id}` answers lookup and storage failures with real 404 / 500 statuses (parameter validation still uses the HTTP 200 envelope), while the other three report every failure through the standard [response envelope](#response-envelope) — clients must keep checking the envelope `code` on those endpoints.
## Next steps
- [Local server and API](../guides/server.md) — startup, authentication, and the end-to-end calling flow
- [kimi command](./kimi-command.md#kimi-web) — all `kimi web` command-line options