--- 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=` 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=&limit=&exclude_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.
Registered actions and emitted notifications ## 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": "", "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=` 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.
## 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).