Files
kandev/docs/public/websocket-api.md
T

899 lines
51 KiB
Markdown
Raw 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.
---
title: "WebSocket API"
description: "Reference for Kandev WebSocket requests, notifications, delivery, and connection behavior."
---
# Kandev WebSocket API
Kandev's web application uses one persistent WebSocket connection for most interactive requests and live notifications. This is an internal application protocol, not a versioned public SDK: action payloads can change with the backend and web client. For supported use, prefer the Kandev UI, CLI, MCP tools, and documented HTTP routes. Use this page when integrating a local client, reverse proxy, or diagnostic tool with the current implementation.
The normal local endpoint is:
```text
ws://localhost:38429/ws
```
Use `wss://` when the page is served over HTTPS. Workflow import/export, workflow sync, downloads, and several administrative operations remain HTTP APIs; a WebSocket action does not exist for every backend route.
The source of truth is the combination of:
- [`message.go`](../../apps/backend/pkg/websocket/message.go) for the envelope.
- [`actions.go`](../../apps/backend/pkg/websocket/actions.go) for names and standard error codes.
- Handler registration in [`gateway.go`](../../apps/backend/internal/backendapp/gateway.go) and each domain's `handlers` package for actions clients can actually request.
- The concrete request structs and validation in those handlers for payload fields.
- [`client.ts`](../../apps/web/lib/ws/client.ts) for the first-party web client's timeout, reconnect, and resubscription behavior.
An action constant alone is not evidence that an action is registered or emitted. The request catalog below was checked against non-test `RegisterFunc` calls and the gateway's special subscription dispatch, not just the constant list.
## Quick path
1. Prefer the UI, CLI, MCP tools, or documented HTTP routes for supported integrations.
2. If you need the WebSocket, connect to `/ws` and send one JSON request per frame.
3. Correlate responses by `id`, refetch after reconnects, and treat notifications as invalidation hints.
4. Protect the endpoint. Authentication is experimental and disabled by default, so an ordinary install has no client authentication boundary.
## Security and network boundary
The experimental [Authentication & Users](authentication.md) feature is disabled by default. In that mode, `/ws` gives every client the synthetic single-user administrator identity. When authentication is enabled, the upgrade requires a valid browser session, bearer token, or `?token=<PAT>` fallback for clients that cannot send headers. The default backend host is `0.0.0.0`, so a default process can listen on every interface even though examples use `localhost`.
The raw gateway rejects every action whose name starts with `mcp.` using a `FORBIDDEN` error before the shared dispatcher runs. This prevents raw clients from forging task or session identity that trusted MCP adapters inject; it is not user authentication. Internal MCP actions remain available through their mode-scoped MCP adapters and trusted in-process dispatch paths.
With authentication disabled, treat every client that can reach the backend as fully trusted. Actions can create and delete data, start agents and shells, read and change files, run Git operations, reveal stored secrets, and invoke configured integrations. Authentication does not replace TLS or isolate shared executors, agent credentials, and the host filesystem. Do not expose port `38429` directly to an untrusted LAN or the internet. Bind to loopback, firewall the port, or put Kandev behind an authenticated reverse proxy that terminates TLS and restricts access. See [Configuration](configuration.md) and [Run as a Service](run-as-a-service.md).
The upgrade does enforce an origin policy for browser clients:
- A missing `Origin` header is accepted for non-browser clients. This is why origin checks are not authentication.
- A browser origin is accepted when its hostname exactly matches the request hostname; ports are intentionally ignored.
- Different loopback names or addresses are accepted when both origin and request hosts are loopback, such as `localhost` and `127.0.0.1` on different ports.
- Origins must be well-formed `http` or `https` origins with no path, query, fragment, or user information. Other cross-site origins are rejected.
When proxying, preserve a request host that matches the public page's origin and forward WebSocket upgrades. The origin policy ignores `?token=` when deciding whether to accept an origin, but the authentication layer still accepts it as a PAT fallback when authentication is enabled. Prefer the bearer header where the client supports it, and do not treat a query PAT as a substitute for TLS or the origin policy.
## Wire envelope
Send one JSON object in each WebSocket text frame. The server writes each response or notification as one text frame. Its reader currently ignores whether an inbound frame was text or binary, but text JSON is the protocol contract.
### Request
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "request",
"action": "workflow.list",
"payload": {
"workspace_id": "workspace-uuid"
}
}
```
### Response
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "response",
"action": "workflow.list",
"payload": {
"workflows": [],
"total": 0
},
"timestamp": "2026-07-16T09:00:00Z"
}
```
### Notification
```json
{
"type": "notification",
"action": "task.updated",
"payload": {
"id": "task-uuid",
"title": "Update authentication"
},
"timestamp": "2026-07-16T09:00:01Z"
}
```
### Error
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "error",
"action": "task.get",
"payload": {
"code": "NOT_FOUND",
"message": "Task not found"
},
"timestamp": "2026-07-16T09:00:01Z"
}
```
| Field | Type | Current contract |
| ----------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Required for a useful request. Generate a unique value and correlate the response or error with it. Notifications omit it. |
| `type` | string | Send `request`. Server output uses `response`, `notification`, or `error`. The current dispatcher does not reject an inbound frame solely because `type` is missing or wrong, so do not treat it as an authorization control. |
| `action` | string | Exact registered action name; names are case-sensitive. |
| `payload` | JSON | Action-specific data. Most handlers expect an object and validate required fields themselves. |
| `timestamp` | RFC 3339 string | Included by the server. Optional and ignored on client requests. |
| `metadata` | object | Optional string-to-string metadata used by selected internal flows. It is not a general request-header mechanism. |
Malformed JSON produces a `BAD_REQUEST` error with empty `id` and `action`, because correlation fields could not be parsed. An unregistered action produces `UNKNOWN_ACTION`. Standard codes are `BAD_REQUEST`, `NOT_FOUND`, `INTERNAL_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `VALIDATION_ERROR`, `CONFLICT`, and `UNKNOWN_ACTION`; individual handlers may add codes or return an ordinary response with `success:false` instead. Always inspect both envelope type and response payload.
## Transport, concurrency, and loss behavior
| Behavior | Current value | Client consequence |
| ------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Maximum inbound message | 32 MiB | A larger request closes/fails the connection. Current file-backed attachments use HTTP staging and send only descriptors over this socket; legacy inline base64 data still consumes the limit. |
| Write deadline | 10 seconds | A peer that cannot accept a frame in time is disconnected. |
| Pong deadline | 60 seconds | The connection closes if the peer stops answering pings. |
| Server ping interval | 54 seconds | WebSocket libraries must process ping/pong control frames. Browsers do this automatically. |
| Per-client outbound queue | 256 frames | When full, the new frame is dropped and the connection stays open. Responses and notifications can therefore be lost under backpressure. |
| Hub-wide broadcast queue | 256 messages | Global publishers block when this internal queue is saturated; delivery to each client can still drop at that client's queue. |
| Request dispatch | One goroutine per inbound message | Handlers execute concurrently and responses can arrive out of request order. Correlate only by `id`. |
Ordinary notifications have no sequence number, durable replay,
acknowledgement, or exactly-once guarantee. They are invalidation hints: after
a reconnect, gap, or dropped frame, refetch authoritative state through the
appropriate list/get request or HTTP route. The Host-owned prompt-history
conversation stream is a separate ordered internal exception documented in
the plugin contract below.
### Prompt attachments
The web client uploads prompt files with authenticated multipart HTTP at
`POST /api/v1/attachments` before sending `task.create`, `session.launch`,
`message.add`, or `message.queue.*`. Those WebSocket payloads carry an opaque
`attachment_id` plus `name`, `mime_type`, `type`, `delivery_mode`, and raw
`size_bytes`; they do not carry file bytes or host paths. A submission may
contain up to ten files and up to 100 MiB in raw bytes per file and in total.
Older clients may still send inline `data` descriptors during the compatibility
window, but they must stay within the existing lower inline-data validation and
the 32 MiB frame limit. Inline data is not a way to bypass the 100 MiB staged
upload contract.
Request handlers use the server hub's lifetime context, not the socket's lifetime. If a client disconnects after sending a mutation, Git command, or `session.launch`, that work normally continues until completion even though its response cannot reach the old socket. Do not blindly retry an uncertain mutation. First reconcile state with a get/list/status request; use application-level idempotency only where the specific handler provides it.
## Subscriptions and routing
Subscription actions are handled by the gateway before the normal dispatcher:
| Action | Payload | Result |
| --------------------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `task.subscribe` / `task.unsubscribe` | `{"task_id":"..."}` | Records or removes task interest. Current ordinary task/workflow lifecycle broadcasters are global, so this subscription is not a filter for `task.updated`. |
| `session.subscribe` / `session.unsubscribe` | `{"session_id":"..."}` | Routes session-scoped traffic. Subscribe also pushes currently available initial session data, such as Git status. |
| `session.focus` / `session.unfocus` | `{"session_id":"..."}` | Marks an actively viewed session, changes its backend polling tier, and on focus pushes fresh session data. Focused clients receive session-scoped traffic even during a transient subscribe handoff. |
| `user.subscribe` / `user.unsubscribe` | `{}` or `{"user_id":"default-user"}` | Empty uses Kandev's default user. Any other user is rejected with `FORBIDDEN`. |
| `run.subscribe` / `run.unsubscribe` | `{"run_id":"..."}` | Routes future `run.event.appended` messages for one Office run. No snapshot is replayed. |
| `system.metrics.subscribe` / `system.metrics.unsubscribe` | `{}` | Starts/stops delivery of live `system.metrics.updated` snapshots and contributes to metrics collection interest. |
All ordinary subscriptions are connection-local and are removed on
disconnect. Ordinary `session.subscribe` and `session.focus` can send an
initial live snapshot, but other ordinary subscriptions do not replay missed
notifications. For an Office run, fetch the REST snapshot before subscribing
and reconcile again after a gap.
The first-party web client reconnects by default: at most 10 attempts, starting at 1 second, multiplying delay by 1.5, capped at 30 seconds. On open it flushes frames queued while disconnected, then resubscribes to tasks, sessions, focus state, runs, the default user, and metrics. Its ordinary request timeout defaults to 5 seconds; selected Git and PR requests use longer timeouts. A custom client must implement its own backoff, resubscription, refetch, and timeout policy.
### Host-only prompt-history conversation stream
The browser `host.conversation` facade uses an internal ordered session stream
that is not a caller-visible WebSocket API. It has Host-minted per-consumer
identities, monotonic sequence numbers, durable cursors, subscribe readiness,
ACKs, reconnect replay, and terminal session-removal barriers. Its migrated
event names are `message.added`, `message.updated`, `message.deleted`,
`session.turn.started`, `session.turn.completed`, and `session.removed`.
The authoritative request, ACK, result, event, and compatibility rules are in
the Host-only conversation wire contract in
`docs/plans/plugins/PLUGIN-API.md`; ordinary subscription behavior above does
not override that internal contract.
## Core task and session requests
The typical product flow is workspace → workflow → task → session → messages. IDs are opaque strings; do not derive one resource ID from another.
### Create a task
`task.create` requires `workspace_id`, `workflow_id`, and `title`; the handler rejects an empty string, and first-party clients trim it. Normal clients also provide `workflow_step_id`. Optional fields include `description`, `priority`, `state`, `position`, `metadata`, `parent_id`, `plan_mode`, and `repositories`. Each repository entry must provide at least one of `repository_id`, `local_path`, or `github_url`; supported repository fields also include `base_branch`, `checkout_branch`, `pr_number`, `name`, and `default_branch`.
Set `start_agent:true` to start immediately; that requires `agent_profile_id`. `executor_id`, `executor_profile_id`, and `attachments` affect that initial launch. The response embeds the task and, when launch succeeds, adds `session_id` and `agent_execution_id`.
Creation and immediate launch are not one rollback boundary. If the task is created but agent launch fails, the request returns an error while the task can remain in the workspace. List tasks before retrying creation.
```json
{
"id": "create-task-1",
"type": "request",
"action": "task.create",
"payload": {
"workspace_id": "workspace-uuid",
"workflow_id": "workflow-uuid",
"workflow_step_id": "step-uuid",
"title": "Implement feature X",
"description": "Implement it and add focused tests",
"priority": "high",
"repositories": [
{
"repository_id": "repository-uuid",
"base_branch": "main"
}
]
}
}
```
Task states on the wire are `TODO`, `CREATED`, `SCHEDULING`, `IN_PROGRESS`, `REVIEW`, `BLOCKED`, `WAITING_FOR_INPUT`, `COMPLETED`, `FAILED`, and `CANCELLED`. Use `task.move` to change the workflow step and `task.state` to change runtime state; these are separate operations.
`task.move` accepts an optional one-shot `entry_options` object. Its normalized fields are `reset_context`, `instructions`, and `skip_step_prompt`; empty optional strings are omitted. Reset is additive, and instructions are appended after the destination step's normal prompt. When `skip_step_prompt` is set, the destination step's prompt and its task-description fallback are suppressed for this entry: with instructions the agent starts a turn carrying only those instructions, and without instructions no turn starts and the task lands idle. The values do not mutate workflow defaults. A successful response includes, when supplied, the normalized `entry_options`, plus a `move_id` correlating options retained for a deferred move. The same options are accepted by `move_task_kandev`; its legacy top-level `prompt` is an alias for `entry_options.instructions`, and conflicting non-empty values are rejected. Actual workflow-step changes requested through `move_task_kandev` from a RUNNING or STARTING session use the deferred MCP path and persist their complete options through turn completion, WIP promotion, and backend restart. A valid same-step MCP request without entry options returns `disposition: "applied"` with the stored task, so the caller does not need to retry.
```json
{
"id": "move-1",
"type": "request",
"action": "task.move",
"payload": {
"id": "task-uuid",
"workflow_id": "workflow-uuid",
"workflow_step_id": "qa-step-uuid",
"position": 0,
"entry_options": {
"reset_context": true,
"instructions": "Run the checkout regression first.",
"skip_step_prompt": true
}
}
}
```
### Launch a session
`session.launch` always requires `task_id`. Explicit `intent` values are `prepare`, `start`, `start_created`, `resume`, `workflow_step`, and `restore_workspace`. Other fields are `session_id`, `agent_profile_id`, `executor_id`, `executor_profile_id`, `prompt`, `plan_mode`, `workflow_step_id`, `priority`, `launch_workspace`, `skip_message_record`, `auto_start`, and `attachments`.
If `intent` is omitted, the backend infers it from those fields. That inference exists for first-party compatibility; an external client should send an explicit intent so a later field change does not select a different path.
```json
{
"id": "launch-session-1",
"type": "request",
"action": "session.launch",
"payload": {
"task_id": "task-uuid",
"intent": "start",
"agent_profile_id": "profile-uuid",
"prompt": "Implement the task and run focused tests"
}
}
```
A successful response contains `success`, `task_id`, `state`, and usually `session_id`; it can also contain `agent_execution_id`, `worktree_path`, and `worktree_branch`. Session states use uppercase values such as `CREATED`, `STARTING`, `RUNNING`, `WAITING_FOR_INPUT`, `COMPLETED`, `FAILED`, and `CANCELLED`.
Every task-session response includes immutable `queue_incarnation_id`. Kandev generates a new value when a session is created, including when a deleted textual session ID is reused. Queue clients must retain this value with the task and session IDs rather than looking up a replacement after starting an operation.
### Search work-item references over HTTP
The structured chat composer's `#` search calls `GET /api/v1/workspaces/:workspaceId/mentions/search?q=<plain-text>&limit=<per-source-limit>&exclude_task_id=<optional-task-id>`. `q` is required after trimming and accepts 1–200 Unicode characters. `limit` defaults to 5 and is clamped to 1–10. When supplied, `exclude_task_id` must belong to the requested workspace.
A successful response returns the normalized query and an ordered `groups` array. Each group includes `source`, `provider`, `kind`, `display_name`, `kind_label`, `status`, and `results`. One source can report `not_configured`, `unauthorized`, `rate_limited`, `timeout`, `upstream_error`, or `unsupported_scope` while the request remains HTTP 200 and other groups remain usable. Each result is a versioned `EntityReference` with the fields described below; raw provider errors and credentials are not returned.
### Send a user turn
`message.add` requires `task_id`, `session_id`, and either non-whitespace `content` or at least one attachment. Optional fields are `author_id`, `model`, `plan_mode`, `has_review_comments`, `attachments`, `context_files`, and `entity_references`.
```json
{
"id": "message-1",
"type": "request",
"action": "message.add",
"payload": {
"task_id": "task-uuid",
"session_id": "session-uuid",
"content": "Also add regression coverage"
}
}
```
`entity_references` is the structured companion to work-item Markdown links inserted by the `#` composer. Each version-1 entry carries `ref`, `provider`, `kind`, immutable `id`, optional display `key`, title snapshot, canonical `url`, and provider-connection `scope`. The backend derives the workspace from the persisted session, validates canonical identity and destination through the registered provider, deduplicates by `ref`, and rejects the request when a supplied reference is invalid or unauthorized. CLI-passthrough sessions do not accept structured references.
`message.queue.add` accepts the same optional array. `message.queue.update` also accepts it and treats the array as a replacement: send `[]` after removing every generated reference link so stale metadata is cleared. Queue status and message responses return validated entries under `metadata.entity_references`.
Every browser queue read or mutation requires the exact `task_id`, `session_id`, and `session_incarnation_id` triplet. This applies to `message.queue.get`, `message.queue.add`, `message.queue.append`, `message.queue.cancel`, `message.queue.drain`, `message.queue.update`, `message.queue.remove`, `message.queue.merge`, `message.queue.reorder`, `message.queue.send_now`, `message.queue.auto_run.set`, and `message.queue.auto_merge.set`. The gateway authorizes the task/session pair and the queue layer compares the supplied incarnation before acting. A deleted or replaced session returns the same non-enumerating not-found response as an inaccessible session; the request is not redirected to a new session with the same textual ID and clients must not retry it against a freshly looked-up identity.
Ordinary `message.queue.add` admissions use one effective automatic-merge policy snapshot. An untouched session inherits the current install-wide value and its global revision. `message.queue.auto_merge.set` accepts required `enabled` and creates a persistent explicit session override; later global changes no longer affect that session. The newly admitted row folds only into its immediate pending predecessor when both rows have the same strict source and compatible task, model, plan mode, metadata, attachments, context files, and entity references; the merged context-file union is capped at 200 descriptors. An eligible direct fold can succeed at or above capacity because it adds no row. A staged-attachment admission at or above capacity is rejected before a fold or attachment claim. Otherwise, capacity or incompatibility leaves the admission rejected or separate as applicable. A successful fold returns the surviving earlier row's `entry_id`, while a separate admission returns the new row's ID. Automatic merging applies only to later ordinary admissions, not existing rows, explicit append/coalesce/restore/retry/send-now operations, or the independent manual merge action.
`message.queue.get`, the Auto-merge mutation response, and `message.queue.status_changed` expose `task_id`, `session_id`, `session_incarnation_id`, `status_epoch`, `status_generation`, `auto_merge_available`, `auto_merge_enabled`, `auto_merge_source`, and `auto_merge_revision` with the queue entries and existing Auto-run/manual-merge fields. `status_epoch` is the session incarnation. Within an epoch, clients apply only increasing status generations. `auto_merge_available:false` means the backend could not establish authoritative policy and the control must remain disabled; it is not an OFF value. A `session` source is an explicit override; a `global` source remains inherited. Session and global revisions are monotonic within their own source. Internal task-only recount events have explicit task scope and are not session status.
`message.queue.merge` folds a queued entry into the entry directly above it. In addition to the required identity triplet, its payload requires `entry_id` (the source entry being merged away); `user_id` is optional and defaults to `user`. On success the response carries the surviving merged entry's `entry_id` (the target's ID) and the server broadcasts an updated `message.queue.status_changed`. The request is rejected with `entry_not_found` when the entry was already drained or is not owned by the caller, with a validation error when no mergeable entry exists above it, and with `merge_reference_overflow` when the combined entity-reference lists would exceed the per-message cap. The overflow rejection is atomic; neither row changes.
If the agent is busy, use the `message.queue.*` operations rather than retrying `message.add`. Permission prompts are represented in persisted/session message data; answer one with `permission.respond`. Its payload requires `session_id` and `pending_id`, plus `option_id` unless `cancelled:true`; optional `rejected:true` distinguishes an explicit denial from dismissing the prompt.
For a raw diagnostic session, install a WebSocket client such as `websocat`, connect, then paste one request object per line:
```bash
websocat ws://127.0.0.1:38429/ws
```
```json
{ "id": "health-1", "type": "request", "action": "health.check", "payload": {} }
```
`websocat` is a third-party diagnostic dependency, not bundled with Kandev. Remember that an originless tool is accepted only because the current endpoint trusts its network boundary.
<details>
<summary>Registered actions and emitted notifications</summary>
## Registered request action catalog
The following 279 unique action names have concrete dispatcher registrations in the current backend. The 12 subscription/focus actions in the previous table are additional gateway-handled requests. Availability can still depend on a configured integration, handler mode, or service; registration does not supply credentials, provider installation, a running executor, or permission to external systems.
Payloads are not uniform. Read the corresponding handler request struct before building a non-first-party client. Names below are exact, including `vscode.openFile` and underscore-separated `user_shell.*` actions.
### Workspaces, workflows, and tasks
```text
health.check
workspace.create
workspace.delete
workspace.get
workspace.list
workspace.update
workflow.create
workflow.delete
workflow.get
workflow.history.list
workflow.list
workflow.reorder
workflow.step.create
workflow.step.get
workflow.step.list
workflow.template.get
workflow.template.list
workflow.update
task.archive
task.create
task.delete
task.get
task.list
task.move
task.plan.create
task.plan.delete
task.plan.get
task.plan.implementation_started
task.plan.revert
task.plan.revision.get
task.plan.revisions.list
task.plan.update
task.repository.update
task.session.list
task.session.status
task.state
task.update
task.walkthrough.delete
task.walkthrough.get
```
There are no ordinary dispatcher registrations for direct workflow-step update, delete, or reorder requests. Those operations are available through the workflow HTTP/configuration surfaces and relevant MCP tools.
### Sessions, messages, agents, and orchestration
```text
session.commit_diff
session.cumulative_diff
session.delete
session.ensure
session.file_review.get
session.file_review.reset
session.file_review.update
session.git.commits
session.git.snapshots
session.launch
session.recover
session.rename
session.reset_context
session.set_plan_mode
session.set_primary
session.shell.status
session.stop
message.add
message.list
message.queue.add
message.queue.append
message.queue.auto_merge.set
message.queue.auto_run.set
message.queue.cancel
message.queue.drain
message.queue.get
message.queue.merge
message.queue.remove
message.queue.reorder
message.queue.send_now
message.queue.update
message.search
agent.cancel
agent.launch
agent.list
agent.logs
agent.resize
agent.status
agent.stdin
agent.stop
agent.types
orchestrator.queue
orchestrator.status
orchestrator.stop
permission.respond
```
`agent.launch`, `agent.stdin`, and related calls are lower-level agent controls. Task clients should normally use `session.launch`, `message.add`, and `session.stop`. Constants such as `agent.prompt`, `message.get`, and `session.set_mode` currently have no dispatcher registration and are deliberately absent.
### Repositories, executors, files, Git, and terminals
```text
repository.create
repository.delete
repository.get
repository.list
repository.script.create
repository.script.delete
repository.script.get
repository.script.list
repository.script.update
repository.update
repository_set.create
repository_set.delete
repository_set.get
repository_set.list
repository_set.update
executor.create
executor.delete
executor.get
executor.list
executor.profile.create
executor.profile.delete
executor.profile.get
executor.profile.list
executor.profile.list_all
executor.profile.update
executor.update
environment.create
environment.delete
environment.get
environment.list
environment.update
ssh.sessions.list
ssh.test
workspace.file.create
workspace.file.delete
workspace.file.get
workspace.file.get_at_ref
workspace.file.rename
workspace.file.update
workspace.files.search
workspace.tree.get
worktree.abort
worktree.commit
worktree.create_pr
worktree.discard
worktree.merge
worktree.pull
worktree.push
worktree.rebase
worktree.rename_branch
worktree.reset
worktree.revert_commit
worktree.stage
worktree.unstage
shell.input
shell.subscribe
user_shell.create
user_shell.destroy
user_shell.list
user_shell.park
user_shell.rename
user_shell.resume
user_shell.stop
port.list
port.tunnel.list
port.tunnel.start
port.tunnel.stop
vscode.openFile
vscode.start
vscode.status
vscode.stop
```
`worktree.create_pr` is a provider-neutral compatibility action despite its name. For a successful GitLab remote it returns `success:true`, `provider:"gitlab"`, and the merge-request URL in `pr_url`; successful GitHub and Azure responses use the same URL field. See [Git Operations](git-operations.md#create-a-pull-request-or-merge-request) for provider detection, authentication, and partial-failure behavior.
`ssh.test` checks an SSH profile and `ssh.sessions.list` reads the backend's active SSH-session view; both are registered with literal action names rather than `actions.go` constants. `user_shell.stop` is a compatibility alias for destroy. Port-tunnel handlers require the tunnel service to be wired. Git semantics and destructive behavior are documented in [Git Operations](git-operations.md); executor-specific filesystem and credential boundaries are documented in [Executors](executors.md).
### Settings, secrets, and automations
```text
user.get
user.settings.update
secrets.create
secrets.delete
secrets.list
secrets.reveal
secrets.update
automation.create
automation.delete
automation.disable
automation.enable
automation.get
automation.list
automation.run.delete
automation.run.stop
automation.runs.delete_all
automation.runs.list
automation.runs.list_workspace
automation.summaries
automation.summary
automation.trigger
automation.trigger.add
automation.trigger.delete
automation.trigger.update
automation.trigger_types
automation.update
automation.webhook.reveal_secret
```
These are trusted local-administration operations. In particular, `secrets.reveal` and `automation.webhook.reveal_secret` make the lack of WebSocket authentication security-critical.
`secrets.delete` accepts `{ "id": "<secret-id>", "workspace_id": "<optional-workspace-id>", "force": false }`.
A Workspace secret requires its `workspace_id`. A referenced secret returns `CONFLICT` with `details.code: "secret_in_use"` and `details.references`.
References contain `kind` (`agent_profile`, `executor_profile`, or `repository`), `id`, `name`, and `key`.
An inaccessible workspace-scoped profile or repository exposes only its `kind`. Secret values and secret IDs never appear in conflict details.
The HTTP equivalent, `DELETE /api/v1/secrets/:id`, returns `409` with `code` and `references` at the top level.
`GET /api/v1/secrets/:id/references` performs the same authorized reference lookup without changing the secret and returns `{ "references": [...] }`. Add `?workspace_id=<workspace-id>` for a Workspace secret. Settings uses this endpoint before it enables deletion; the later `DELETE` still repeats the check.
With `force: true`, deletion preserves the broken bindings. Future launches fail until users repair those bindings.
The HTTP override is `?force=true`, combined with `workspace_id` for Workspace secrets.
Force does not bypass authorization. Reference lookup failures return `INTERNAL_ERROR` (HTTP `500`) and leave the secret intact.
`automation.run.stop` requires `automation_id` and `run_id`. It cancels the
selected open run's exact task/session/turn binding and returns `{run_id,
status}`. A stale or terminal binding returns not found; it never stops another
turn that happens to share the same task or session.
Automation payloads expose `task_mode` (`automation_run` or `normal_task`) and
`repository_mode` (`workspace_default`, `selected`, or `none`). Hidden runs may
omit workflow and repository data and use a Local scratch workspace. Normal
tasks require a workflow and remain visible in Kanban and the sidebar. A
repository-required provider trigger rejects `repository_mode=none` before it
creates an `AutomationRun`.
### GitHub and GitLab
```text
github.action_presets.list
github.action_presets.reset
github.action_presets.update
github.check_session_pr
github.cleanup.issue_tasks
github.cleanup.review_tasks
github.issue_watches.create
github.issue_watches.delete
github.issue_watches.list
github.issue_watches.trigger
github.issue_watches.trigger_all
github.issue_watches.update
github.pr_commits.get
github.pr_feedback.get
github.pr_files.get
github.pr_watches.delete
github.pr_watches.list
github.review_watches.create
github.review_watches.delete
github.review_watches.list
github.review_watches.trigger
github.review_watches.trigger_all
github.review_watches.update
github.stats
github.status
github.task_pr.get
github.task_pr.sync
github.task_prs.list
gitlab.check_session_mr
```
Provider actions make outbound calls with the backend's configured GitHub or GitLab identity. Status and registration do not imply a provider is authenticated, reachable, or authorized for a repository.
GitLab UI queries and mutations use the authenticated `/api/v1/gitlab/...` HTTP routes. The only remaining GitLab WebSocket request action is `gitlab.check_session_mr`, which refreshes a session's linked merge request.
### Jira, Linear, and Sprites
```text
jira.config.delete
jira.config.get
jira.config.set
jira.config.test
jira.projects.list
jira.ticket.get
jira.ticket.transition
linear.config.delete
linear.config.get
linear.config.set
linear.config.test
linear.issue.get
linear.issue.transition
linear.teams.list
sprites.instances.destroy
sprites.instances.list
sprites.network_policy.get
sprites.network_policy.update
sprites.status
sprites.test
```
Configuration actions can persist or test credentials and can cause outbound network requests. Sprites actions can inspect or destroy remote instances and change their network policy.
### MCP transport actions
```text
mcp.add_branch_to_task
mcp.archive_task
mcp.ask_user_question
mcp.clarification_timeout
mcp.create_agent_profile
mcp.create_executor_profile
mcp.create_task
mcp.create_task_plan
mcp.create_workflow
mcp.create_workflow_step
mcp.delete_agent_profile
mcp.delete_executor_profile
mcp.delete_task
mcp.delete_task_plan
mcp.delete_walkthrough
mcp.delete_workflow
mcp.delete_workflow_step
mcp.get_mcp_config
mcp.get_task_conversation
mcp.get_task_document
mcp.get_task_plan
mcp.get_walkthrough
mcp.import_workflow
mcp.list_agent_profiles
mcp.list_agents
mcp.list_executor_profiles
mcp.list_executors
mcp.list_related_tasks
mcp.list_repositories
mcp.list_task_documents
mcp.list_tasks
mcp.list_workflow_steps
mcp.list_workflows
mcp.list_workspaces
mcp.message_task
mcp.move_task
mcp.reorder_workflow_steps
mcp.show_walkthrough
mcp.spawn_session
mcp.step_complete
mcp.stop_task
mcp.update_agent
mcp.update_agent_profile
mcp.update_executor_profile
mcp.update_mcp_config
mcp.update_repository_base_branch
mcp.update_task
mcp.update_task_plan
mcp.update_task_state
mcp.update_workflow
mcp.update_workflow_step
mcp.write_task_document
```
These registrations back Kandev's agent/MCP bridge. The subset registered in a process depends on its MCP handler mode and enabled capabilities. They are internal transport shims: raw `/ws` rejects every one of them before handler dispatch. Use the MCP tools exposed to the agent so tool schemas, task/session scoping, and compatibility handling remain intact. In particular, `mcp.stop_task` is the internal action behind task-mode `stop_task_kandev`; External MCP does not register that tool.
## Emitted notifications and recipients
The following catalog lists actions with current non-test emission paths. It intentionally excludes constants for which no active emitter was found, including the old `acp.*` compatibility constants, `permission.requested`, `input.requested`, `agent.updated`, and `office.activity.created`. Permission and clarification state currently arrives through session message records instead.
### Shared broadcaster notifications
Their normal live event path uses a shared broadcaster. With authentication disabled, or when an event has no workspace context, it sends the event to every connected client. With authentication enabled and workspace reach resolves successfully, workspace-carrying events are narrowed to the permitted readers. The regular workspace broadcaster currently falls back to global delivery if both reach and owner resolution fail; only selected sensitive event paths use the fail-closed variant. Do not rely on notification fan-out as a tenant-isolation boundary while this experimental limitation remains. Session subscribe/focus hydration can also send selected state actions directly to the requesting client.
```text
workspace.created
workspace.updated
workspace.deleted
workflow.created
workflow.updated
workflow.deleted
workflow.step.created
workflow.step.updated
workflow.step.deleted
agent.profile.created
agent.profile.updated
agent.profile.mcp_config.updated
agent.profile.deleted
task.created
task.updated
task.deleted
task.state_changed
task.plan.created
task.plan.updated
task.plan.deleted
task.plan.revision.created
task.plan.reverted
task.walkthrough.created
task.walkthrough.updated
task.walkthrough.deleted
repository.created
repository.updated
repository.deleted
repository_set.created
repository_set.updated
repository_set.deleted
repository.script.created
repository.script.updated
repository.script.deleted
executor.created
executor.updated
executor.deleted
executor.profile.created
executor.profile.updated
executor.profile.deleted
executor.prepare.progress
executor.prepare.completed
environment.created
environment.updated
environment.deleted
session.state_changed
session.activity_changed
session.turn.started
session.turn.completed
session.poll_mode_changed
agent.available.updated
agent.install.started
agent.install.output
agent.install.finished
github.task_pr.updated
github.task_ci_options.updated
github.rate_limit.updated
gitlab.task_mr.updated
gitlab.task_mr_options.updated
system.job.update
```
Current lifecycle code broadcasts `session.turn.started` and `session.turn.completed` globally even though their payloads identify a session. `task.subscribe` does not change this global routing.
`session.activity_changed` carries `task_id`, `session_id`,
`foreground_activity`, and `active_subagent_count`. The activity is
`generating` while the session is `RUNNING` and otherwise clears with the
coarse lifecycle by default; background-work accounting does not expose a
separate activity tier. When
`KANDEV_FEATURES_CLAUDE_BACKGROUND_PROMPT_HANDOFF=true`, a persisted Claude
Code session may instead expose `background` while its foreground has yielded
and adapter-attested async subagent, background shell, or Monitor work remains
active. Other providers remain coarse. The count is always an integer and is
zero when no adapter-attested subagent is live for that session. Task lifecycle
notifications expose the same count aggregated across the task's sessions as
`active_subagent_count`. Count-only changes may emit
`session.activity_changed` and `task.updated` even when the coarse lifecycle
state does not change.
Office workspace notifications are also global; clients filter on `workspace_id`:
```text
office.task.updated
office.task.created
office.task.moved
office.task.status_changed
office.task.decision_recorded
office.task.review_requested
office.comment.created
office.agent.completed
office.agent.failed
office.agent.updated
office.approval.created
office.approval.resolved
office.cost.recorded
office.run.queued
office.run.processed
office.routine.triggered
office.provider.health_changed
office.route_attempt.appended
office.routing.settings_updated
```
### Session-scoped broadcasts
Clients subscribed to or focused on the matching `session_id` receive:
```text
session.message.added
session.message.updated
session.message.deleted
session.agentctl_starting
session.agentctl_ready
session.agentctl_error
message.queue.status_changed
session.git.event
session.workspace.file.changes
session.shell.output
session.process.output
session.process.status
session.available_commands
session.mode_changed
session.agent_capabilities
session.models_updated
session.info_updated
session.todos_updated
session.prompt_usage
```
Semantic `session.message.added`, `session.message.updated`, and
`session.message.deleted` notifications can include `pending_action` with
`"clarification"`, `"permission"`, or explicit `null`. When present, it is the
authoritative per-session input projection after that message mutation and is
paired with `pending_action_revision` (`epoch` plus `sequence`). REST task-session
snapshots carry the same logical clock. Epoch is a decimal, database-backed backend
generation that increases across restarts; sequence increases within one generation.
Clients compare epoch numerically, then sequence. This keeps delayed snapshots from
an earlier generation from replacing current state when HTTP and WebSocket delivery
overlap, even after client state is rebuilt.
When both fields are absent, clients preserve their current projection and
reconcile through normal session refetch after a gap.
File changes are batched for up to 100 ms and flushed immediately at 50 entries. `session.shell.output` also represents shell exit events through its payload. Treat all stream messages as lossy and refresh session/Git status after a gap.
### User-, run-, and metrics-scoped broadcasts
| Routing key | Action | Notes |
| ------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| subscribed user | `user.settings.updated` | Requires `user.subscribe`. |
| subscribed user | `session.turn_finished` | Local user notification for a completed agent turn, with task/session/occurrence/title/body fields; the turn ID is sent as both the envelope `id` and payload `occurrence_id`. It is not delivered by `session.subscribe`. |
| subscribed user | `session.clarification_requested` | Local user notification for a structured agent question that needs an answer, with task/session/occurrence/title/body fields; the clarification pending ID is sent as both the envelope `id` and payload `occurrence_id`. It is not delivered by `session.subscribe`. |
| subscribed user | `system.update_available` | Local user notification for a newer Kandev release. Its payload is `{ "version", "url"?, "title", "body", "occurrence_id" }`; `occurrence_id` is the normalized release version/tag and is also the envelope `id`. Subscribe with `user.subscribe`, not `session.subscribe`. |
| subscribed user | `office.inbox_item` | Local notification for a selected Office inbox event, with title/body fields. |
| subscribed run | `run.event.appended` | Future events only; there is no replay cursor. |
| metrics subscribers | `system.metrics.updated` | Live resource snapshot; collection interest follows subscribers. |
Routing is an efficiency mechanism, not the access-control boundary. With authentication enabled, the server resolves the caller's identity, checks workspace subscriptions, and normally filters workspace fan-out by reach; domain authorization still decides which actions the caller may perform. Because ordinary fan-out can fall back to global delivery on resolution errors as described above, use authorized read requests as the source of truth. With authentication disabled, the synthetic administrator retains the original single-user reach.
</details>
## Reconnect and troubleshooting
- **Upgrade returns 403:** inspect the browser `Origin` and proxy `Host`. The hostnames must match exactly or both be loopback; ports may differ. A scheme other than `http`/`https` or an origin containing a path is rejected.
- **Connection works locally but is unsafe remotely:** this is expected with authentication disabled and the `0.0.0.0` default. Enable authentication deliberately and add TLS, or bind/firewall the backend behind a protected proxy before allowing network access.
- **Request times out but the mutation happened:** the response may have been dropped or the socket may have closed while the server-side handler continued. Query current state before deciding whether to retry.
- **Notifications stop or state looks stale:** reconnect, resubscribe, and refetch. Check whether the client is consuming frames quickly enough to avoid the 256-frame drop-new queue.
- **Session stream is missing:** send `session.subscribe`; for an actively displayed session also send `session.focus`. Verify the payload's `session_id` matches exactly.
- **Unknown action:** confirm that the name appears in the registered catalog, not merely as a constant in `actions.go`, and check whether the required optional service was configured.
- **Large attachment disconnects:** keep the complete JSON frame under 32 MiB and obey the action's lower attachment-count and data-size validation too.
- **Responses arrive in the wrong order:** this is normal concurrent dispatch. Match by unique `id`, never arrival order.
Dedicated `/terminal/*target` and `/lsp/:sessionId` WebSockets, plus `/vscode/:sessionId/*path` and `/port-proxy/:sessionId/:port/*path` proxies, are separate protocols. They do not use this JSON envelope and should not be sent `/ws` actions.
The browser language-server socket at `/lsp/:sessionId?language=...` carries raw LSP JSON-RPC plus private Kandev control frames. With the restart-required `features.lspBrowserContinuity` flag enabled, a browser close or temporary network loss detaches the window while its task-host lease keeps running for up to one hour. Reopening the task within that hour can reattach to that lease without another `initialize`; each window and duplicated tab has its own lease. **Stop**, two minutes with no open editor, one hour detached, task-runtime shutdown, backend shutdown, or capacity eviction releases a lease. Reattachment cancels the detached deadline; an attached lease does not expire. `KANDEV_LSP_MAX_CONNECTIONS` counts attached and detached leases; at capacity, Kandev evicts the least-recently detached lease, or rejects a new request when all leases are attached. When continuity is disabled, closing the browser socket stops its process as before.
The browser shows **Reconnecting** for an uncertain transport loss and retries attachment. A browser-reported `1005` or `1006`, graceful backend restart close `1001`, or backend transport close `4009` does not confirm that the language-server process exited. Close `4006` means the task-host process exited; the editor clears that generation's providers and diagnostics and offers **Retry**. Close `4010` means the task runtime stopped, so the editor ends the lease without reconnecting it. A backend restart releases leases, so the next connection starts a fresh process and repeats project analysis.
Related guides: [Configuration](configuration.md), [Executors](executors.md), [Git Operations](git-operations.md), [Operations](operations.md), [Workflow Import / Export](workflow-import-export.md), and [Workflow Sync](workflow-sync.md).