diff options
Diffstat (limited to '.dispatch/transport-contract.reference.md')
| -rw-r--r-- | .dispatch/transport-contract.reference.md | 584 |
1 files changed, 581 insertions, 3 deletions
diff --git a/.dispatch/transport-contract.reference.md b/.dispatch/transport-contract.reference.md index 02b48a0..70d64d9 100644 --- a/.dispatch/transport-contract.reference.md +++ b/.dispatch/transport-contract.reference.md @@ -1,3 +1,100 @@ +# `@dispatch/transport-contract` — in-repo reference (read THIS, not node_modules) + +> MIRRORS the backend's `@dispatch/transport-contract` package source so headless FE agents can read +> the transport types WITHOUT following the `file:` dep symlink out of this repo (which hangs on a +> permission prompt). Your CODE still imports `@dispatch/transport-contract` normally — this file is for +> READING only. +> +> **Orchestrator:** SNAPSHOT of `[email protected]` (MCP status + computers + provider concurrency + cancel queued message). Regenerate whenever +> it changes. +> +> **2026-06-29 update (cancel-queued-message — ADDITIVE, 0.23.0 → 0.24.0):** a per-message +> **cancel** for the steering message queue ships. While a turn is GENERATING and a user +> message is queued (awaiting steering delivery), the client can cancel a single queued +> message by id so it never runs. New `WsClientMessage` member `ChatQueueCancelMessage` +> (`{ type: "chat.queue.cancel"; conversationId; messageId }` — fire-and-forget, idempotent; +> success confirmed by the `message-queue` surface updating, failure as `chat.error`). New +> HTTP path `DELETE /conversations/:id/queue/:messageId` → `QueueCancelResponse` +> (`{ conversationId; cancelled: boolean; queue }`). The cancel is scoped per conversation; +> cancelling a drained/already-cancelled/unknown message is a silent no-op. `@dispatch/wire` +> is unchanged (`QueuedMessage.id` is the cancel target). +> +> **2026-06-27 update (concurrency-fixes — ADDITIVE, NO version bump):** the provider concurrency surface gains +> (a) a configurable + persisted per-provider release COOLDOWN, and (b) adaptive headroom. `ConcurrencyStatusEntry` +> gains FOUR new fields: `cooldownMs: number` (REQUIRED — per-slot release cooldown in ms, default 350; a recycled slot is +> held this long before the next waiter is admitted), `autoReduced: boolean` (REQUIRED — true when the limit was auto-reduced +> by 1 after a 429, one-way + persisted; the FE renders a visible banner), `autoReducedFrom?: number` (present only when +> `autoReduced===true` — the original limit before reduction), and `notice?: string` (present only when `autoReduced===true` — +> a human-readable banner message). The banner is DISMISSIBLE / persists while `autoReduced===true`; it clears when the user +> restores the limit via `PUT /concurrency/limits/:providerId` (a manual PUT clears `autoReduced` server-side). NEW cooldown +> endpoints: `GET /concurrency/cooldown/:providerId` → `ConcurrencyCooldownResponse` (`{ providerId, cooldownMs }`) — 404 when +> the provider has no concurrency config at all (no limit, no cooldown), 503 when the extension isn't loaded; +> `PUT /concurrency/cooldown/:providerId` ← `SetConcurrencyCooldownRequest` (`{ cooldownMs }` — must be a non-negative integer, +> 0 = no cooldown / instant re-admission) → `ConcurrencyCooldownResponse` — 400 on an invalid body, 503 when not loaded. +> Persists + applies immediately to subsequently recycled slots. Also (backend-only, no FE surface): a usage gate polls upstream +> `concurrent_sessions` before admitting a queued agent. See `backend-handoff.md` §2j-update-3. +> +> **2026-06-26 delta (provider concurrency — `[email protected]` bump):** adds the +> per-provider concurrency-limits API types: `ConcurrencyLimitsResponse` (`GET /concurrency/limits`), +> `SetConcurrencyLimitRequest` + `ConcurrencyLimitResponse` (`GET`/`PUT /concurrency/limits/:providerId`), +> and `ConcurrencyStatusEntry` + `ConcurrencyStatusResponse` (`GET /concurrency/status`). The +> `concurrency` extension tracks/limits in-flight token-generating requests per provider with +> oldest-agent-first queueing; when it isn't loaded the list + status endpoints return empty arrays and +> the single/PUT/DELETE return `503`. See `backend-handoff.md` §2j. +> **2026-06-26 delta (vision handoff — ADDITIVE, NO version bump):** adds the vision/image surface. +> `ChatRequest` (+ `ChatSendMessage`/`QueueRequest`) gains an optional `images?: readonly ImageInput[]` +> (each entry: `{ url, mimeType? }` — a base64 data URL or `http(s)://` URL; validated non-array/no-url/ +> empty-url → 400, empty array treated as absent). `ModelMetadata` gains `vision?: boolean` (true when the +> model natively accepts images; absent → the server's vision handoff transcribes images to text before the +> model sees them). `ImageChunk`/`ImageInput` are `@dispatch/wire` types (re-exported here). +> +> **2026-06-26 update (consult_vision + vision settings — ADDITIVE, NO version bump):** the `read_image` +> tool is REPLACED by `consult_vision` (`{ question: string, imageIds?: number[], path?: string }`) — it +> opens a NEW conversation tab with a vision-capable model, attaches the image + question, and returns the +> vision model's answer (rendered like any tool call/result). Non-vision models now get NUMBERED +> PLACEHOLDERS (`[Image N attached — call consult_vision with imageIds=[N] and a specific question to +> analyze it]`) instead of auto-transcriptions — these are regular `text` chunks (render as-is). Image +> compaction transcribes the oldest images past `imageLimit` to `[Compacted image]: <description>` text +> chunks (also regular `text` — render as-is; the persisted `image` chunk stays for rendering). NEW global +> vision settings API: `GET /settings/vision` → `VisionSettingsResponse` (`{ imageLimit, compactionModel }`), +> `PUT /settings/vision` ← `SetVisionSettingsRequest` (partial: `imageLimit?` non-negative int, 0 = disable +> compaction; `compactionModel?` `<key>/<model>` or null = auto). See `backend-handoff.md` §2j. +> +> **2026-06-26 update (image storage — NO type change, behavior only):** persisted `ImageChunk.url`s are now +> compact relative HTTP paths (`/images/<conversationId>/<uuid>.png`) served by the new +> `GET /images/:conversationId/:imageId` endpoint (raw image bytes + correct Content-Type) — NOT base64 data +> URLs (images are stored on disk under tmp, not in the SQLite store). `ChatRequest.images` (`ImageInput.url`) +> is UNCHANGED — clients still send data URLs; the backend saves them to tmp and returns compact paths in +> the persisted chunks. A client resolves a relative `url` against its API base (`resolveImageUrl`); the +> optimistic echo's data URL and any absolute URL pass through. See `backend-handoff.md` §2j. +> +> **2026-06-25 delta (SSH handoff #2 — ADDITIVE to `[email protected]`, NO version bump):** adds the +> computer HTTP API types: `ComputerListResponse` (`GET /computers`), `ComputerResponse` (`GET /computers/:alias`), +> `ComputerStatusResponse` (`GET /computers/:alias/status`), `TestComputerResponse` (`POST /computers/:alias/test`), +> `SetConversationComputerRequest` + `ConversationComputerResponse` +> (`GET`/`PUT`/`DELETE /conversations/:id/computer`), `SetWorkspaceDefaultComputerRequest` +> (`PUT /workspaces/:id/default-computer`). Also `computerId?: string` on `ChatRequest`/`ChatSendMessage`/ +> `QueueRequest` (per-turn override; resolved server-side from the persisted per-conversation value in the MVP, so +> `chat.send` need not send it). `Computer`/`ComputerEntry` themselves are `@dispatch/wire` types. See +> `backend-handoff.md` §2e. (The `ssh` extension that provides the ComputerService is the last backend wave — +> until it lands, `GET /computers` returns `[]` and statuses return `disconnected`.) +> +> **2026-06-24 delta (MCP status handoff — package bumped `0.18.0` → `0.22.0`, ADDITIVE):** adds +> `McpServerState`, `McpServerInfo`, and `McpStatusResponse`; endpoint +> `GET /conversations/:id/mcp`. Mirrors the existing `GET /conversations/:id/lsp` shape (returns +> `{cwd, servers}`, empty `servers` when no cwd is set). Each `McpServerInfo` reports an `id`, +> `state` (`connecting` | `connected` | `error` | `disconnected`), optional `error`, `toolCount`, +> and optional `configSource`. Also adds the previously-missing `configSource` field to +> `LspServerInfo`. See `frontend-mcp-status-handoff.md`. +> +> **2026-06-24 delta (system prompt handoff — package bumped `0.17.0` → `0.18.0`, ADDITIVE):** adds +> `SystemPromptTemplateResponse`, `SetSystemPromptTemplateRequest`, `SystemPromptVariable`, and +> `SystemPromptVariablesResponse`; endpoints `GET /system-prompt`, `PUT /system-prompt`, and +> `GET /system-prompt/variables`. The system prompt template is global (resolved once per conversation +> at construction time, persisted for cache safety). Variables include `system:*`, `prompt:*`, `git:*`, +> and dynamic `file:<path>`; conditional blocks use `[if]`, `[else]`, `[endif]`. See +> `frontend-system-prompt-handoff.md`. + /** * Transport contract — the typed description of Dispatch's client–server API * (HTTP + WebSocket). @@ -22,24 +119,35 @@ import type { SurfaceClientMessage, SurfaceServerMessage } from "@dispatch/ui-contract"; import type { AgentEvent, + Computer, + ComputerEntry, ConversationMeta, ConversationStatus, + ImageInput, QueuedMessage, ReasoningEffort, StoredChunk, TurnMetrics, + Workspace, + WorkspaceEntry, } from "@dispatch/wire"; export type { AgentEvent, CompactionResult, + Computer, + ComputerEntry, ConversationMeta, ConversationStatus, + ImageChunk, + ImageInput, QueuedMessage, ReasoningEffort, StepMetrics, StoredChunk, TurnMetrics, + Workspace, + WorkspaceEntry, } from "@dispatch/wire"; /** @@ -60,6 +168,21 @@ export interface ChatRequest { readonly message: string; /** + * Images attached to this turn (e.g. a user-pasted screenshot). Each entry's + * `url` is a base64 data URL (`data:image/…;base64,…`) or an `http(s)://` + * URL. The server converts these to `image` chunks on the persisted user + * message. For a VISION-capable model (e.g. kimi), the images are passed + * through to the provider natively. For a NON-vision model (e.g. glm-5.2), + * the server's vision handoff transcribes each image to a text description + * (via a vision-capable model) and feeds that text instead — so a text-only + * model can still reason about the image's contents. Optional — omit for a + * text-only turn (backward compatible). Validation: non-array `images` → + * 400; an image without `url` → 400; empty `url` → 400. An empty array is + * accepted and treated as absent. + */ + readonly images?: readonly ImageInput[]; + + /** * The model to use, as a model name in `<credentialName>/<model>` form — one * of the exact strings returned by `GET /models`. Omit to use the server's * default credential + model. @@ -80,6 +203,23 @@ export interface ChatRequest { * unrecognized value → HTTP 400 `{ error }`. */ readonly reasoningEffort?: ReasoningEffort; + + /** + * The workspace to assign this conversation to. Omit for `"default"`. + * If the workspace doesn't exist yet, it is auto-created (title = id, + * defaultCwd = null). + */ + readonly workspaceId?: string; + + /** + * The computer to run this turn's tools on — an SSH config `Host` alias + * (one of the `alias` values returned by `GET /computers`). Omit to inherit + * the resolved chain: per-conversation `computerId` → the workspace's + * `defaultComputerId` → `null`/local (today's behavior). Like `cwd`, this is + * a per-turn tool-execution target forwarded to tools and never part of the + * model prompt (so it does not affect prompt caching). Mirrors `cwd`. + */ + readonly computerId?: string; } /** @@ -99,6 +239,14 @@ export interface ModelsResponse { /** Per-model metadata returned alongside the model catalog. */ export interface ModelMetadata { readonly contextWindow?: number; + /** + * Whether this model can natively accept image input (vision/multimodal). + * When `true`, image chunks in a user message are passed through to the + * provider. When `false`/absent, the server's vision handoff transcribes + * images to text before the model sees them. A client may use this to show + * a vision badge in the model picker. Optional — absent when unknown. + */ + readonly vision?: boolean; } /** @@ -170,6 +318,14 @@ export interface ConversationMetricsResponse { readonly turns: readonly TurnMetrics[]; } +export interface ConversationStatusResponse { + readonly conversationId: string; + /** True if the orchestrator has an in-memory active turn for this conversation. */ + readonly isActive: boolean; + /** The persisted lifecycle status from the conversation store. */ + readonly status: ConversationStatus; +} + /** The aggregation window for `GET /metrics/throughput`. */ export type ThroughputPeriod = "day" | "week" | "month"; @@ -221,9 +377,19 @@ export interface CwdResponse { readonly cwd: string | null; } -/** Body of `PUT /conversations/:id/cwd`. */ +/** + * Body of `PUT /conversations/:id/cwd`. + * + * When `workspaceId` is provided, the conversation is assigned to that + * workspace BEFORE the cwd is persisted — so a subsequent + * `GET /conversations/:id/lsp` resolves a relative cwd against the + * workspace's `defaultCwd` (not the server default). Omit for unchanged + * workspace assignment (the conversation keeps its current workspace, or + * `"default"` if none). + */ export interface SetCwdRequest { readonly cwd: string; + readonly workspaceId?: string; } // ─── Per-conversation reasoning effort ──────────────────────────────────────── @@ -248,6 +414,29 @@ export interface SetReasoningEffortRequest { readonly reasoningEffort: ReasoningEffort; } +// ─── Per-conversation model persistence ─────────────────────────────────────── + +/** + * Response of `GET /conversations/:id/model`. `model` is the persisted model + * name in `<credentialName>/<model>` form, or null when never set (the server + * then resolves turns using the default provider + model). + */ +export interface ModelResponse { + readonly conversationId: string; + readonly model: string | null; +} + +/** + * Body of `PUT /conversations/:id/model` — persists the conversation's sticky + * model selection (used for every later turn that does not carry a per-turn + * `ChatRequest.model` override). Pass `null` to clear the persisted selection. + * An unrecognized model name is not validated here (the provider resolves it + * at turn time; an unknown model → turn error, not a 400). + */ +export interface SetModelRequest { + readonly model: string | null; +} + // ─── Conversation close (explicit tab close) ────────────────────────────────── /** @@ -270,6 +459,78 @@ export interface CloseConversationResponse { readonly abortedTurn: boolean; } +// ─── System prompt template ─────────────────────────────────────────────────── + +/** + * Response of `GET /system-prompt` — the current global system prompt template. + * + * The template is a text string with variable placeholders (`[type:name]`) and + * conditional blocks (`[if]`/`[else]`/`[endif]`). At construction time (first + * turn or compaction), variables are resolved against the conversation's cwd + * and system state. The resolved system prompt is persisted per conversation + * and reused on all subsequent turns (cache-safe — no per-turn reconstruction). + */ +export interface SystemPromptTemplateResponse { + /** The template text (may be empty — then no system prompt is sent). */ + readonly template: string; +} + +/** + * Body of `PUT /system-prompt` — set the global system prompt template. + * + * Changing the template does NOT affect existing conversations until they are + * compacted (the persisted resolved system prompt is stable). New + * conversations use the new template on their first turn. + */ +export interface SetSystemPromptTemplateRequest { + readonly template: string; +} + +/** + * One available variable for the system prompt template, as reported by + * `GET /system-prompt/variables` so the frontend can render the variable + * selector buttons. + */ +export interface SystemPromptVariable { + /** The variable type/source: `"system"`, `"file"`, `"prompt"`, `"git"`. */ + readonly type: string; + /** The variable name (e.g. `"time"`, `"date"`, `"os"`). For dynamic types, a description. */ + readonly name: string; + /** Human-readable description of what the variable resolves to. */ + readonly description: string; + /** + * When `true`, any name is valid for this type (e.g. `file:<path>` accepts + * any file path). The frontend should allow free-text input for the name. + */ + readonly dynamic?: boolean; +} + +/** Response of `GET /system-prompt/variables`. */ +export interface SystemPromptVariablesResponse { + readonly variables: readonly SystemPromptVariable[]; +} + +// ─── Vision settings (global) ─────────────────────────────────────────────── + +/** + * Response of `GET /settings/vision` — the global vision configuration shared + * across all conversations and vision models. + */ +export interface VisionSettingsResponse { + /** Max native images per turn (default 10); 0 disables image compaction. */ + readonly imageLimit: number; + /** Which model transcribes old images (null = auto-select a vision model). */ + readonly compactionModel: string | null; +} + +/** Body of `PUT /settings/vision` — a partial update. */ +export interface SetVisionSettingsRequest { + /** Non-negative integer (0 = disable compaction). */ + readonly imageLimit?: number; + /** A model name (`<key>/<model>`) or null (auto). */ + readonly compactionModel?: string | null; +} + // ─── Message queue (steering) ───────────────────────────────────────────────── /** @@ -289,6 +550,11 @@ export interface CloseConversationResponse { */ export interface QueueRequest { readonly text: string; + /** + * The workspace to assign the conversation to (if a new conversation is + * started). Omit for `"default"`. Auto-creates if missing. + */ + readonly workspaceId?: string; } /** @@ -305,6 +571,27 @@ export interface QueueResponse { readonly queue: readonly QueuedMessage[]; } +/** + * Response body for + * `DELETE /conversations/:id/queue/:messageId` — cancel (remove) a single + * queued steering message by id so it never runs. + * + * `cancelled` is `true` when a message with the given id was found in the + * conversation's queue and removed (it will never be delivered as steering nor + * carried into a new turn). `cancelled` is `false` when the message was not in + * the queue (already drained/delivered, never existed, unknown conversation) + * OR when the message-queue extension isn't loaded (degraded — feature off). + * `queue` is the post-cancel snapshot (empty when no queue extension is + * loaded). Idempotent — cancelling a message that is no longer queued returns + * `cancelled: false` with HTTP 200 (not an error), so a client may optimistically + * fire-and-forget a cancel and reconcile from the surface. + */ +export interface QueueCancelResponse { + readonly conversationId: string; + readonly cancelled: boolean; + readonly queue: readonly QueuedMessage[]; +} + // ─── Per-conversation LSP status ────────────────────────────────────────────── /** The connection state of a single language server for a workspace. */ @@ -324,17 +611,60 @@ export interface LspServerInfo { readonly state: LspServerState; /** Present only when `state === "error"`: a short human-readable reason. */ readonly error?: string; + /** + * Which config source this server was resolved from: `".dispatch/lsp.json"`, + * `"opencode.json"`, or `"built-in"` (the built-in TypeScript default). Omitted + * when not yet resolved. Surfaces config-shadow debugging to the status caller + * (a broken `.dispatch/lsp.json` silently shadowing `opencode.json`). + */ + readonly configSource?: string; } /** Response of `GET /conversations/:id/lsp`. */ export interface LspStatusResponse { readonly conversationId: string; - /** The conversation's persisted cwd, or null if unset (then `servers` is empty). */ + /** + * The resolved working directory the LSP connects on, or `null` when no + * cwd has been set for the conversation (then `servers` is empty). When + * non-null, this is the effective cwd — a relative persisted cwd resolved + * against the conversation's workspace `defaultCwd`. + */ readonly cwd: string | null; /** The language servers configured for `cwd` and their live state. */ readonly servers: readonly LspServerInfo[]; } +// ─── MCP status ────────────────────────────────────────────────────── + +export type McpServerState = "connecting" | "connected" | "error" | "disconnected"; + +/** One MCP server's status as reported to the frontend. */ +export interface McpServerInfo { + /** Stable server id (the config key from `.dispatch/mcp.json`), e.g. "freecad". */ + readonly id: string; + /** Current connection state. */ + readonly state: McpServerState; + /** Present only when `state === "error"`: a short human-readable reason. */ + readonly error?: string; + /** Number of tools discovered from this server. */ + readonly toolCount: number; + /** Which config source this server was resolved from. */ + readonly configSource?: string; +} + +/** Response of `GET /conversations/:id/mcp`. */ +export interface McpStatusResponse { + readonly conversationId: string; + /** + * The resolved working directory the MCP servers are configured for, or + * `null` when no cwd has been set for the conversation (then `servers` is + * empty). Mirrors the LSP status endpoint behavior. + */ + readonly cwd: string | null; + /** The MCP servers configured for `cwd` and their live state. */ + readonly servers: readonly McpServerInfo[]; +} + /** * Request body for `POST /chat/warm` — manually trigger a prompt-cache WARMING * request for a conversation (e.g. a frontend "warm now" button, or fast tests @@ -474,6 +804,29 @@ export interface ChatQueueMessage { readonly type: "chat.queue"; readonly conversationId: string; readonly text: string; + /** + * The workspace to assign the conversation to (if a new conversation is + * started). Omit for `"default"`. Auto-creates if missing. + */ + readonly workspaceId?: string; +} + +/** + * Client → server: cancel (remove) a SINGLE queued steering message by id so + * it never runs. The WebSocket counterpart of the HTTP + * `DELETE /conversations/:id/queue/:messageId` (`QueueCancelResponse`). + * Fire-and-forget: success is confirmed by the message-queue SURFACE updating + * (the cancelled message leaves the snapshot); a failure (missing/empty + * `conversationId` or `messageId`) arrives as a `chat.error`. Idempotent — + * cancelling a message that is no longer queued (already drained/delivered) is + * a silent no-op (no surface update, no error). `messageId` is the stable + * client-visible `QueuedMessage.id` (obtained from the queue surface snapshot + * or the enqueue response). + */ +export interface ChatQueueCancelMessage { + readonly type: "chat.queue.cancel"; + readonly conversationId: string; + readonly messageId: string; } /** @@ -485,7 +838,8 @@ export type WsClientMessage = | ChatSendMessage | ChatSubscribeMessage | ChatUnsubscribeMessage - | ChatQueueMessage; + | ChatQueueMessage + | ChatQueueCancelMessage; /** * Every server → client WS message: surface ops (`@dispatch/ui-contract`) + chat @@ -509,6 +863,12 @@ export type WsServerMessage = export interface ConversationOpenMessage { readonly type: "conversation.open"; readonly conversationId: string; + /** + * The conversation's actual workspace id, so a frontend can open/focus it + * in the correct workspace instead of stamping it with the viewer's current + * workspace. + */ + readonly workspaceId: string; } /** @@ -520,6 +880,12 @@ export interface ConversationStatusChangedMessage { readonly type: "conversation.statusChanged"; readonly conversationId: string; readonly status: ConversationStatus; + /** + * The conversation's actual workspace id, so a frontend can open/focus it + * in the correct workspace instead of stamping it with the viewer's current + * workspace. + */ + readonly workspaceId: string; } /** @@ -606,3 +972,215 @@ export interface CompactPercentResponse { export interface SetCompactPercentRequest { readonly threshold: number; } + +// ─── Workspaces ─────────────────────────────────────────────────────────────── + +/** + * Body of `PUT /workspaces/:id` — the idempotent create-on-miss call. All + * fields are optional and only applied when the workspace is first created; + * an existing workspace is returned as-is. + */ +export interface EnsureWorkspaceRequest { + /** Display title. Default: the workspace id. Only used on create. */ + readonly title?: string; + /** Default cwd. Default: null (inherit server default). Only used on create. */ + readonly defaultCwd?: string | null; +} + +/** Response of `GET`/`PUT /workspaces/:id` — the workspace itself. */ +export interface WorkspaceResponse extends Workspace {} + +/** Response of `GET /workspaces` — all workspaces sorted by `lastActivityAt` desc. */ +export interface WorkspaceListResponse { + readonly workspaces: readonly WorkspaceEntry[]; +} + +/** Body of `PUT /workspaces/:id/title` — rename (display only; id unchanged). */ +export interface SetWorkspaceTitleRequest { + readonly title: string; +} + +/** Body of `PUT /workspaces/:id/default-cwd` — set or clear the default cwd. */ +export interface SetWorkspaceDefaultCwdRequest { + readonly defaultCwd: string | null; +} + +/** + * Response of `DELETE /workspaces/:id`. All conversations in the workspace + * are closed (status → "closed") and reassigned to "default", then the + * workspace entity is deleted. `"default"` is non-deletable (HTTP 409). + */ +export interface DeleteWorkspaceResponse { + readonly workspaceId: string; + /** Conversations that were closed (status → "closed") by this delete. */ + readonly closedCount: number; +} + +// ─── Computers (SSH handoff #2) ───────────────────────────────────────────── + +/** + * Response of `GET /computers` — every remote computer discovered from the + * system's `~/.ssh/config`, sorted by `alias`. Parallel to + * `WorkspaceListResponse`: each entry is a `ComputerEntry` (a `Computer` plus a + * usage count). There is no Computer CRUD — to add one, the user adds a `Host` + * block to `~/.ssh/config` and Dispatch discovers it on the next read. + */ +export interface ComputerListResponse { + readonly computers: readonly ComputerEntry[]; +} + +/** + * Response of `GET /computers/:alias` — a single computer. Parallel to + * `WorkspaceResponse` (the entity itself). `alias` is the `computerId` users + * select; the remaining fields are resolved from the SSH config. + */ +export interface ComputerResponse extends Computer {} + +/** + * Response of `GET /computers/:alias/status` — the live connection state of a + * computer (whether Dispatch currently holds an open SSH session to it). Drives + * the frontend connection indicator. `error` is present only when + * `state === "error"`; `knownHost` mirrors the read-only `Computer` field. + */ +export interface ComputerStatusResponse { + readonly alias: string; + readonly state: "disconnected" | "connecting" | "connected" | "error"; + readonly error?: string; + readonly knownHost: boolean; +} + +/** + * Body of `PUT /conversations/:id/computer` — set or clear the conversation's + * persisted computer selection (the computer analog of `SetCwdRequest`). Pass + * `null` to clear → the conversation inherits the workspace's + * `defaultComputerId`, then `null`/local. An unknown alias is not validated here + * (the connection resolves at turn time; an unreachable host → turn error, not + * a 400). Mirrors the cwd/model PUT clear semantics. + */ +export interface SetConversationComputerRequest { + readonly computerId: string | null; +} + +/** + * Response of `GET /conversations/:id/computer`. `computerId` is the persisted + * SSH `Host` alias, or `null` when never set (the conversation then inherits + * the workspace default → local). Parallel to `CwdResponse`. + */ +export interface ConversationComputerResponse { + readonly conversationId: string; + readonly computerId: string | null; +} + +/** + * Body of `PUT /workspaces/:id/default-computer` — set or clear the workspace's + * default computer (the computer analog of `SetWorkspaceDefaultCwdRequest`). + * `null` means local (no SSH). Conversations in the workspace with no + * `computerId` of their own inherit this. + */ +export interface SetWorkspaceDefaultComputerRequest { + readonly computerId: string | null; +} + +/** + * Response of `POST /computers/:alias/test` — the result of a one-shot + * connectivity probe (Dispatch opens an SSH connection to the alias, runs a + * trivial command, then closes). `ok` is true on success; `error` carries the + * failure reason (e.g. auth refused, host unreachable) when `ok` is false. + */ +export interface TestComputerResponse { + readonly alias: string; + readonly ok: boolean; + readonly error?: string; +} + +// ── Provider concurrency limits ([email protected]) ─────────────────── + +/** + * Response of `GET /concurrency/limits` — all providers with configured + * concurrency limits. Each entry pairs a provider id (e.g. "umans", + * "openai-compat") with its maximum concurrent in-flight requests. Providers + * not listed here have no limit (unlimited). + */ +export interface ConcurrencyLimitsResponse { + readonly limits: readonly { + readonly providerId: string; + readonly limit: number; + }[]; +} +/** + * Body of `PUT /concurrency/limits/:providerId` — set or update the concurrency + * limit for a provider. `limit` must be a positive integer. When a limit is + * set, requests beyond the limit queue (oldest-agent-first) rather than being + * sent immediately. + */ +export interface SetConcurrencyLimitRequest { + readonly limit: number; +} +/** Response of `GET/PUT /concurrency/limits/:providerId` — the configured limit. */ +export interface ConcurrencyLimitResponse { + readonly providerId: string; + readonly limit: number; +} +/** + * One provider's live concurrency status. + * + * - `inFlight`: how many slots are currently held (tokens being generated). + * - `queued`: how many agents are waiting for a slot. + * - `paused`: whether the queue is paused due to a 429 backoff. + * - `pausedUntil`: when the pause expires (epoch-ms), present only when paused. + * - `cooldownMs`: the per-slot release cooldown (ms). A recycled slot is held + * this long before the next waiter is admitted — covers the upstream + * provider's accounting lag. Configurable + persisted per provider. + * - `autoReduced`: whether the limit was auto-reduced by 1 after a 429 + * (adaptive headroom, one-way, persisted). The user restores the limit + * manually via `PUT /concurrency/limits/:providerId`, which clears the flag. + * When `true`, the frontend renders a visible notice/banner. + * - `autoReducedFrom`: the original limit before auto-reduction (present only + * when `autoReduced` is true). + * - `notice`: a human-readable notice string for the frontend to render as a + * banner when the limit was auto-reduced (present only when `autoReduced`). + */ +export interface ConcurrencyStatusEntry { + readonly providerId: string; + readonly limit: number; + readonly inFlight: number; + readonly queued: number; + readonly paused: boolean; + readonly pausedUntil?: number; + readonly cooldownMs: number; + readonly autoReduced: boolean; + readonly autoReducedFrom?: number; + readonly notice?: string; +} +/** + * Response of `GET /concurrency/status` — live status for every provider with a + * configured limit. Providers without a limit are absent (they are unlimited). + */ +export interface ConcurrencyStatusResponse { + readonly providers: readonly ConcurrencyStatusEntry[]; +} + +// ─── Provider concurrency cooldown ──────────────────────────────────────────── + +/** + * Response of `GET /concurrency/cooldown/:providerId` — the per-slot release + * cooldown (ms) for a provider. A recycled slot is held this long before the + * next waiter is admitted, covering the upstream provider's accounting lag. + * When no cooldown was explicitly set, the server default (350ms) is returned. + */ +export interface ConcurrencyCooldownResponse { + readonly providerId: string; + readonly cooldownMs: number; +} + +/** + * Body of `PUT /concurrency/cooldown/:providerId` — set the release cooldown + * (ms) for a provider. `cooldownMs` must be a non-negative integer (0 = no + * cooldown, instant re-admission). The value is persisted and applied to + * subsequently recycled slots. + */ +export interface SetConcurrencyCooldownRequest { + readonly cooldownMs: number; +} +``` + |
