Skip to content

Server protocol

The zidane-server wire protocol, with the WebSocket upgrade, the ipc.hello handshake, every JSON-RPC method and ipc.signal kind, errors, resume and paging, and captured examples.

Protocol version 16 is JSON-RPC 2.0 over one WebSocket, with one message per text frame. The client sends ipc.hello first, then requests. The server answers them and pushes ipc.signal notifications.

The tables come from src/ipc/protocol.ts and src/chat/host/signals.ts. The JSON examples come from a real server run with a scripted provider. The local daemon speaks the same protocol over a unix socket. zidane/ipc and zidane/ipc/remote type it for TypeScript clients.

Connect#

StepClientServer
1. UpgradeGET with Upgrade: websocket, the token, Sec-WebSocket-Protocol: zidaneChecks the address rate, the origin, the proxy headers, the token and the connection caps. Selects zidane
2. Handshakeipc.hello with its protocol versionAccepts or answers VersionMismatch
3. WorkRequests, and notifications (no id) for fire-and-forget callsResponses, and ipc.signal notifications
4. Liveness$/ping requestsWebSocket pings every 20 s

The token travels in a header or a subprotocol:

CarrierHeaderUsed by
Header (preferred)Authorization: Bearer zds_…Clients that can set handshake headers (Bun's WebSocket: the TUI, the CLI)
SubprotocolSec-WebSocket-Protocol: zidane, zidane.bearer.zds_…Node's and the browser's WebSocket

A proxy can log Sec-WebSocket-Protocol. Use the header where the runtime can set it.

TypeScript
const ws = new WebSocket('wss://zidane.example.com', ['zidane', `zidane.bearer.${token}`])

HTTP on the same port#

The port also answers plain HTTP. GET /healthz needs no token. A refused upgrade gets its status before any JSON-RPC. Captured answers:

JSON
[
  {
    "title": "health",
    "request": {
      "method": "GET",
      "path": "/healthz",
      "headers": []
    },
    "status": 200,
    "body": "ok"
  },
  {
    "title": "no token",
    "request": {
      "method": "GET",
      "path": "/",
      "headers": [
        "Upgrade",
        "Connection",
        "Sec-WebSocket-Key",
        "Sec-WebSocket-Version"
      ]
    },
    "status": 401,
    "body": "Unauthorized"
  },
  {
    "title": "unknown token",
    "request": {
      "method": "GET",
      "path": "/",
      "headers": [
        "Upgrade",
        "Connection",
        "Sec-WebSocket-Key",
        "Sec-WebSocket-Version",
        "Sec-WebSocket-Protocol"
      ]
    },
    "status": 401,
    "body": "Unauthorized"
  },
  {
    "title": "browser origin",
    "request": {
      "method": "GET",
      "path": "/",
      "headers": [
        "Upgrade",
        "Connection",
        "Sec-WebSocket-Key",
        "Sec-WebSocket-Version",
        "Origin",
        "Sec-WebSocket-Protocol"
      ]
    },
    "status": 403,
    "refusal": "origin",
    "body": "Forbidden: origin not allowed"
  }
]

Every refusal.

POST /zidane/pair trades a pairing code for a token (pairing). GET /zidane/media serves a workspace's images, videos and audio to a page that plays them by address, under the ticket its connection registered (files.mediaTicket, below). GET /zidane/oauth/callback takes a browser client's MCP sign-in back (auth.oauth.begin, below). With the web app on, every other plain GET serves the app; upgrades on any path still reach the WebSocket, so the app connects to wss://<host>/.

Handshake#

request
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "ipc.hello",
  "params": {
    "protocolVersion": 13,
    "appVersion": "docs",
    "pid": 4242
  }
}
response
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "protocolVersion": 13,
    "appVersion": "docs",
    "pid": 28973,
    "startedAt": 1791968973803,
    "buildId": "1791968503753",
    "codeVersion": "8.0.4",
    "hostId": "08b260f9-090c-445b-957a-1b6f4822f245",
    "hostKind": "server",
    "hostName": "docs-box",
    "signalEpoch": "ed173759-2d4f-436a-b39a-36c1c969e088",
    "remoteInstalls": true,
    "sync": true
  }
}

ipc.hello params#

ipc.hello params
FieldTypeDescription
protocolVersionnumberProtocol version the client speaks.
minProtocolVersion?numberThe oldest version the client can speak down to (IPC_MIN_PROTOCOL_VERSION). A zidane-server accepts when the ranges overlap. Absent: protocolVersion only. Additive.
appVersionstringClient's zidane version; surfaced in daemon diagnostics.
pidnumberProcess id of the client.
buildId?stringBuild fingerprint (module mtime; 'unknown' in compiled binaries). When BOTH sides report a real id and they differ, the daemon rejects the handshake like a protocol-version mismatch; the client drains it and respawns. Closes same-version code drift during development.
codeVersion?stringVersion of the zidane code the client runs (ZIDANE_VERSION, inlined at build). Unlike buildId this survives compiled binaries, so it closes the gap released builds had: same protocol version, older code. A client STRICTLY NEWER than the daemon is rejected like a build mismatch and the stale daemon is drained; an older client is accepted and degrades via optional().

ipc.hello result#

ipc.hello result
FieldTypeDescription
protocolVersionnumberProtocol version the host speaks.
minProtocolVersion?numberThe oldest version the host still speaks (IPC_MIN_PROTOCOL_VERSION). Additive.
negotiatedProtocolVersion?numberThe version this connection speaks: the lower of the client's and the host's. Additive: absent, the connection speaks protocolVersion.
scope?ClientScopeThe client scope this connection's token grants (./scopes): calls above it are refused. Absent: every method (a local window). Additive.
methods?IpcMethodName[]The methods this connection may call on this host: the ones it handles, within scope. Clients show what is offered instead of failing on what isn't. Additive.
appVersionstringzidane version of the host.
pidnumberProcess id of the host.
startedAtnumberStart time of the host process, epoch ms.
buildId?stringBuild fingerprint of the host code. 'unknown' in compiled binaries.
codeVersion?stringVersion of the zidane code the host runs.
hostId?stringHost identity (additive). hostId is a UUID persisted once in the host's data dir, so it survives restarts; the in-process host and the local daemon share the data dir, and so the id: they are the same computer.
hostKind?HostKindWhat kind of host this is.
hostName?stringDisplay name of the host.
retiring?booleanThe daemon is older than a client that attached while it had active work: it accepted the handshake instead of draining (which would abort those runs) and exits once idle. Clients attach normally; their reconnect path lands on a fresh daemon after the exit.
signalEpoch?stringNames this host process's signal numbering (SIGNAL_SEQ_FIELD): a position from another epoch cannot be resumed. Additive.
machineServe?booleanThis server lends itself as a machine to the hosts of its clients (machine.attach, allowAsMachine on). Absent: it doesn't. Additive.
adminCommands?{ token: string, pair: string }How to get an admin token on this zidane-server: the commands to run on its machine, with its own binary, user and data dir (<device> is the name to fill in). token prints a token to paste into an app; pair prints a code for a browser. Absent: not a zidane-server. Additive.
remoteInstalls?booleanThis server takes installs and removals from its clients (catalog.install / remove / update, mcp.upsert / remove; allowRemoteInstalls on). Absent: it doesn't (or predates them). Additive.
clone?booleanClients may clone repositories into its workspace roots (git.clone). A zidane-server's allowClone; a local host always does. Absent: an older host, where git.clone needs an admin token. Additive.
createFolders?booleanClients may create folders in its workspace roots (fs.mkdir). A zidane-server's allowCreateFolders; a local host always does. Absent: an older host, without fs.mkdir. Additive.
homeDir?stringThe host process's home folder (os.homedir()). Clients show it as ~ in the paths they display; nothing else reads it. Absent: an older host. Additive.
sync?booleanThis server takes settings sync from a client's computer (sync.*). Absent: it doesn't. Additive.
syncCategories?SyncCategory[]The sync categories it takes. Absent with sync: the ones before environments. Additive.
projectTerminals?trueThis host runs terminals without a session (terminal.openInProject and the other project / draft terminal methods). Absent: sessions only. Additive.
activeWork?booleandaemon.info only: a run, compaction, / command or background task is in flight on this host (open terminals do not count). zidane server wait-idle waits for false before a restart. Additive.

remoteInstalls, sync, machineServe and projectTerminals say what this server offers. An absent field means no. clone and createFolders say whether clients may clone into the workspace roots and create folders there (allowClone, allowCreateFolders); an older server leaves them out. methods lists what this connection may call: the server's methods within the token's scope (Scopes). A client shows what it lists instead of failing on the rest. hostId stays the same across restarts. A client that saved the server checks it on every connect and refuses another server at the same URL. signalEpoch names this server process for resume. adminCommands ({ token, pair }) are the commands that mint an admin token or an admin pairing code, run on the server's machine with its binary, its user and its data dir. The apps show them to a chat token.

Compatibility#

Each side speaks a range of versions, from its minProtocolVersion to its protocolVersion. A server accepts a client whose range overlaps its own, and the connection speaks the higher version both share (negotiatedProtocolVersion in the result). A client that sends no minProtocolVersion speaks its protocolVersion only. A release keeps the previous version in its range when it raises the current one, so apps built against one release keep working against the next. Ranges that don't overlap get VersionMismatch, with the server's hello fields in data.daemon and the side to upgrade in the message:

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32010,
    "message": "Protocol version mismatch: the server speaks v13, the client v12. Upgrade the client.",
    "data": {
      "daemon": {
        "protocolVersion": 13,
        "appVersion": "docs",
        "pid": 28973,
        "startedAt": 1791968973803,
        "buildId": "1791968503753",
        "codeVersion": "8.0.4",
        "hostId": "08b260f9-090c-445b-957a-1b6f4822f245",
        "hostKind": "server",
        "hostName": "docs-box"
      }
    }
  }
}

Within a shared version, the server ignores buildId and codeVersion, so older and newer clients attach. Only breaking changes to the method map or the signal union bump the version.

New methods and fields arrive without a bump. A client treats MethodNotFound (-32601) as "not offered". daemon.shutdown does nothing on a server. No client can drain or restart one.

Before the handshake, the server answers only ipc.hello, $/ping, daemon.info and daemon.shutdown:

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32011,
    "message": "Handshake required: send ipc.hello first."
  }
}

Requests and notifications#

Every method takes an object as params and answers with a result or an error. A call without an id runs as a notification. The server sends no answer and drops errors. Drafts use notifications for every keystroke.

request
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "sessions.create",
  "params": {
    "cwd": ""
  }
}
response
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "id": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
    "title": "New session",
    "projectRoot": "/data/workspaces",
    "cwd": "/data/workspaces",
    "agentId": "build",
    "status": "idle",
    "createdAt": 1791968973862,
    "updatedAt": 1791968973862,
    "archived": false,
    "hostId": "08b260f9-090c-445b-957a-1b6f4822f245"
  }
}

cwd: '' asks for the server's first workspace root. A folder outside the workspace roots is refused.

request
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "chat.send",
  "params": {
    "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
    "prompt": "Say hello."
  }
}
response
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "runId": "6f577015-ffbf-4204-ba81-320f71e76f46"
  }
}

chat.send answers once the run starts. The run itself arrives as signals.

Signals#

The server pushes host events as ipc.signal notifications whose params are the signal, kind first. A session signal also carries $seq, its number in that session (from 1, without gaps, within one server process). Signals without a session, such as presence:changed, have none.

The run above, in order: run:start, plans:snapshot, prompt-turns, one event per streamed piece, stream:progress, turn:end, run:end.

JSON
{
  "jsonrpc": "2.0",
  "method": "ipc.signal",
  "params": {
    "kind": "run:start",
    "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
    "runId": "6f577015-ffbf-4204-ba81-320f71e76f46",
    "$seq": 4
  }
}
JSON
{
  "jsonrpc": "2.0",
  "method": "ipc.signal",
  "params": {
    "kind": "event",
    "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
    "event": {
      "kind": "markdown",
      "text": "H",
      "streaming": true,
      "turnId": "999068c5-c52b-4ccd-a936-682bea162657"
    },
    "$seq": 7
  }
}
JSON
{
  "jsonrpc": "2.0",
  "method": "ipc.signal",
  "params": {
    "kind": "turn:end",
    "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
    "turnId": "999068c5-c52b-4ccd-a936-682bea162657",
    "usage": {
      "input": 10,
      "output": 33,
      "modelId": "mock",
      "timeToFirstTokenMs": 2,
      "streamDurationMs": 2
    },
    "$seq": 42
  }
}
JSON
{
  "jsonrpc": "2.0",
  "method": "ipc.signal",
  "params": {
    "kind": "run:end",
    "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
    "runId": "6f577015-ffbf-4204-ba81-320f71e76f46",
    "ok": true,
    "$seq": 45
  }
}
JSON
{
  "jsonrpc": "2.0",
  "method": "ipc.signal",
  "params": {
    "kind": "presence:changed",
    "sessions": [
      {
        "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
        "windows": 1,
        "running": true,
        "attention": false
      }
    ]
  }
}

Most signals reach every connection. Streamed command output (workspace:exec:data, workspace:exec:exit) goes only to the connection that started the command. A client ignores unknown kinds, because new kinds arrive without a version bump.

Signal kinds
KindDescription
run:startA run started in the session.Params { kind: 'run:start', sessionId: string, runId: string, startedAt?: number }
run:endtakenOver: another process claimed the run, which goes on there (error says so). Optional: older clients show error.Params { kind: 'run:end', sessionId: string, runId: string, ok: boolean, error?: string, detail?: RunErrorDetail, background?: true, takenOver?: true }
turn:endA turn finished. usage carries the loop-reported token/cost figures that drive footer gauges (context size = prompt-side tokens of the LAST parent turn; cost accrues across parent + child turns). A parent turn's runOutputTokens is its run's output so far, this turn included: a run readout takes it as is instead of adding turns up, so a window that read the count mid-run never counts a turn twice.Params { kind: 'turn:end', sessionId: string, turnId: string, childId?: string, depth?: number, usage?: TurnUsage, runOutputTokens?: number }
eventecho: true marks host-generated SEND/STEER echoes of the user's own message. Clients that already render the prompt locally (the GUI's enriched append) drop echo-tagged signals; command-module user-prompts are never echoes and always render.Params { kind: 'event', sessionId: string, event: StreamEvent, echo?: boolean }
compacting:startCompaction of the session's history started.Params { kind: 'compacting:start', sessionId: string }
compacting:progressLive token estimate for an in-flight compaction summary. Throttled by the loop, so this is a low-rate progress tick rather than a stream. Without it a daemon-mode window shows a static "Compacting…" with no indication of progress; a compaction of a large transcript can run for minutes, which is indistinguishable from a hang.Params { kind: 'compacting:progress', sessionId: string, estOutputTokens: number }
stream:progressLive counters for the parent turn currently streaming; the loop's stream:progress (throttled ~4/s, one final tick before stream:end). outputTokens is provider-counted unless estimated; estCostUsd is the running catalog-rate estimate for THIS turn (absent for unpriced models and flat-rate subscription traffic). Clients add it to the settled session figure while streaming and drop it when the matching turn:end brings the real usage.cost.Params ({ kind: 'stream:progress', sessionId: string } & StreamProgressInfo)
prompt-turnsPrompt turns that just persisted, reduced to the fields needed to bind a live user-prompt event to its turn id (see PromptTurnRef). Deliberately NOT the full SessionTurn: those carry every content block, including base64 image/document payloads, which would be megabytes per prompt to ship for the sake of an id. Without this a prompt sent from a daemon-attached window is unselectable; no hover, edit, or delete; until the run-end transcript replace.Params { kind: 'prompt-turns', sessionId: string, refs: readonly PromptTurnRef[] }
compacting:endeffectiveTokens (success only) is the loop's post-compact context estimate; windows drop their footer ctx gauge to it immediately instead of waiting for the next turn's usage.Params { kind: 'compacting:end', sessionId: string, ok: boolean, effectiveTokens?: number }
model:fallbackThe run moved from one model to the next in the session's fallback chain.Params { kind: 'model:fallback', sessionId: string, from: { provider: string, model: string }, to: { provider: string, model: string }, discardTurnId?: string }
work:snapshotThe session's running work, as chat.listWork returns it, in full.Params { kind: 'work:snapshot', sessionId: string, items: RunningWorkItem[] }
plans:snapshotThe session's plans, in full, and the one worth a glance.Params { kind: 'plans:snapshot', sessionId: string, snapshot: PlansSnapshot }
artifact:recordedThe session recorded an artifact (PR, branch, commit, file, extension kind; see session/artifacts.ts). Carries the stored row so a window can badge / list without a round-trip; sessions.artifacts is the authoritative query. Additive; older clients ignore unknown kinds.Params { kind: 'artifact:recorded', sessionId: string, artifact: SessionArtifact }
artifact:changedAn artifact was removed or refreshed through the host (artifacts.delete / artifacts.upsert); windows re-query. Carries the row for upsert.Params { kind: 'artifact:changed', sessionId: string, op: 'delete' | 'upsert', artifactId: string, artifact?: SessionArtifact }
interaction:requestThe agent asked a question or proposed a plan and waits for an answer.Params { kind: 'interaction:request', sessionId: string, request: InteractionRequest, live: boolean }
interaction:resolvedA pending question or plan was answered or withdrawn.Params { kind: 'interaction:resolved', sessionId: string, requestId: string, answeredBy?: InteractionAnswerer }
secrets:changedA project's secret store changed (a secret saved, revoked or expired in this process). Carries no names or values: a client lists again.Params { kind: 'secrets:changed', projectKey: string }Not numbered: no session.
browser:requestDev browser (browser.ts hub): a tool call for the page, addressed to the registered browser host (hostId); other clients ignore it. The host answers through browser.respond. browser:cancel withdraws it.Params { kind: 'browser:request', sessionId: string, hostId: string, request: BrowserRequest }
browser:cancelWithdraws a browser:request.Params { kind: 'browser:cancel', sessionId: string, hostId: string, requestId: string }
terminal:changedTerminals (terminal/ manager). changed carries the session's full list; output is a batch starting at offset (clients drop what they already have and terminal.ack what they wrote); reset replaces a view's content with history after a client fell too far behind.Params { kind: 'terminal:changed', sessionId: string, terminals: TerminalInfo[] }
terminal:outputOutput of a session terminal, from offset.Params { kind: 'terminal:output', sessionId: string, terminalId: string, offset: number, data: string }
terminal:resetA terminal's whole view with its history, for a client that fell too far behind.Params { kind: 'terminal:reset', sessionId: string, terminalId: string, offset: number, data: string }
terminal:project-changedThe full list of a project's terminals (draftId absent) or of one draft's. No sessionId: these belong to no session (session signals are numbered and replayed per session). Output of these terminals still arrives as terminal:output / terminal:reset with sessionId: '', sent only to the views attached to them.Params { kind: 'terminal:project-changed', root: string, draftId?: string, terminals: TerminalInfo[] }Not numbered: no session.
workspace:exec:dataA streamed workspace command (sessions.workspaceExecStream): its output in arrival order, then exactly one exit. Sent to the connection that started it only, never fanned out or replayed. code is null and signal set when the host killed it (cancelled: the client asked; truncated: the output cap was reached).Params { kind: 'workspace:exec:data', execId: string, stream: 'stdout' | 'stderr', chunk: string }Not numbered: no session.
workspace:exec:exitA streamed workspace command ended: its exit code or signal; truncated when the output cap was reached.Params { kind: 'workspace:exec:exit', execId: string, code: number | null, signal: string | null, truncated?: true, cancelled?: true }Not numbered: no session.
block-run:dataA chunk of a Run here's output (sessions.runBlock), to the connection that started it only.Params { kind: 'block-run:data', runId: string, stream: 'stdout' | 'stderr', chunk: string }Not numbered: no session.
block-run:endA Run here ended: the saved run, or why it could not run. To the connection that started it only.Params { kind: 'block-run:end', runId: string, run?: BlockRun, error?: string }Not numbered: no session.
git:clone:progressGit's progress on the connection's git.clone, to that connection only.Params { kind: 'git:clone:progress', phase: string, percent: number }Not numbered: no session.
git:refsA checkout's refs moved (git.watch), to the connections watching cwd only.Params { kind: 'git:refs', cwd: string }Not numbered: no session.
panel:changedAn extension panel's content changed (panels.watch), to the connections watching it only.Params { kind: 'panel:changed', cwd: string, panelId: string }Not numbered: no session.
palette:changedcwd's extension palette items may have changed (palette.watch), to the connections watching only.Params { kind: 'palette:changed', cwd: string }Not numbered: no session.
logs:recordA new record in a zidane-server's log (logs.watch), to the connections following it only.Params { kind: 'logs:record', record: LogRecord }Not numbered: no session.
perf:sampleThe host's performance sample (perf.watch), to the connections watching it only.Params { kind: 'perf:sample', sample: PerfSample }Not numbered: no session.
catalog:changedA client installed, updated or removed something on the host (catalog.install / remove / update, mcp.upsert / remove): every window re-reads that catalog. models: what models.list answers changed (an extension's provider or account model list).Params { kind: 'catalog:changed', catalog: 'skills' | 'mcps' | 'extensions' | 'plugins' | 'models' }Not numbered: no session.
sync:changedA server's settings-sync record changed (a claim, a push, an override, a reset): settings UIs re-read sync.status.Params { kind: 'sync:changed' }Not numbered: no session.
tokens:changedA zidane-server's tokens changed (one created, revoked, paired, or the token file edited): token lists re-read tokens.list.Params { kind: 'tokens:changed' }Not numbered: no session.
approval:requestA tool call waits for approval (safe mode).Params { kind: 'approval:request', request: WireApprovalRequest }Not numbered: no session.
approval:resolvedAn approval left the pending queue (answered or denied in bulk); every OTHER window clears its surface. The window that resolved it already knows; the signal makes the queue converge everywhere else.Params { kind: 'approval:resolved', sessionId: string, requestId: string, decision: ApprovalDecision, answeredBy?: InteractionAnswerer }
sessions:changedA session was created, updated or deleted.Params { kind: 'sessions:changed', op: 'create' | 'update' | 'delete', sessionId: string }
block-runs:changedA session's saved block runs changed (a Run here ended, a run was removed): windows read them again.Params { kind: 'block-runs:changed', sessionId: string }
sessions:facetsA session's facet values changed (ctx.sessions.facets): every facet it holds now, keyed <extensionId>.<key>, the same as SessionRow.facets. Not a sessions:changed: the row's other fields stand.Params { kind: 'sessions:facets', sessionId: string, facets: Record<string, JsonValue> }
settings:changedSettings were written through the host (settings.set); every window applies the new state so settings UIs converge without a restart. Windows that persist settings themselves must treat this as authoritative for the delivered snapshot.Params { kind: 'settings:changed', settings: HostSettings }Not numbered: no session.
draft:changedAnother window edited the session's shared prompt draft. The daemon EXCLUDES the originating peer from this fan-out, so applying it can never fight the local cursor; clients apply unconditionally (last-writer-wins whole-text replacement). Empty text clears the draft (broadcast when any window submits the prompt).Params { kind: 'draft:changed', sessionId: string, text: string, shell?: boolean }
draft:attachmentsThe shared draft's composing ATTACHMENTS changed (paste/drop/remove in any window). Payloads ride along so a remote window can submit the shared draft attachments-and-all. Same origin-exclusion as text.Params { kind: 'draft:attachments', sessionId: string, attachments: PromptAttachment[] }
search:changedThe session's shared transcript-search state changed (mode entered / exited, query edited) in another window. Same origin-exclusion and last-writer-wins contract as the draft; every window on the session shows the same filtered transcript.Params { kind: 'search:changed', sessionId: string, active: boolean, query: string }
suggest:changedThe daemon resolved another window's inline-autocomplete query (suggest.resolve). Excludes the requesting peer (it already has the result from its RPC reply). Receivers mirror the ghost ONLY when their live buffer still equals basis; the standard staleness guard; otherwise discard.Params { kind: 'suggest:changed', sessionId: string, seq: number, basis: string, caret: number, suggestion: string | null, candidates?: string[] }
suggestion:readyThe text model's guess at the user's next prompt after run runId (Settings.promptSuggestions). Windows offer it in the empty composer and drop it on the next run:start, a send, an edit or a compaction.Params { kind: 'suggestion:ready', sessionId: string, runId: string, text: string }
run:estimateA run of the session was estimated, or its wait on the user started or ended (Settings.runEstimates): the session's own run (depth 0) or a subagent's (label, callId). Saved on the run record too, so a window that missed it reads it from the session row's runEstimates.Params { kind: 'run:estimate', sessionId: string, estimate: RunEstimateView }
skills:snapshotThe daemon-side agent's active-skill set changed (skills_use / run-end deactivation). Windows mirror it into their skill chips.Params { kind: 'skills:snapshot', sessionId: string, active: string[] }
mcp:authA daemon-side MCP server needs (re-)authentication. Windows surface the badge; the interactive login flow itself runs in whichever window the user triggers it from (tokens land in the shared credential store; the daemon reconnects on the next agent rebuild).Params { kind: 'mcp:auth', sessionId: string, name: string, reason: string }
mcp:settledThe session agent's MCP bootstrap finished: every server has its outcome (mcp:status / mcp:auth came first). A window's deadline for the one-shot auto-login it arms when the user adds a server.Params { kind: 'mcp:settled', sessionId: string }
mcp:statusA host-side MCP server connected, or failed to. Windows feed it to the same status reducer the in-process path uses (badges: connected, signed in, needs sign-in, key refused, couldn't connect). error is the failure's message and code its HTTP-ish code when it had one, enough for the shared classifier (classifyMcpTest) to read it the same way.Params { kind: 'mcp:status', sessionId: string, name: string, status: 'connected' } | { kind: 'mcp:status', sessionId: string, name: string, status: 'failed', transport: string, error: string, code?: number }
stream:retryProvider retry in progress (rate-limit / overload / silent stall / dropped connection); drives the footer's "retrying n/m" indicator. source is 'parent' or the retrying child's id; the indicator clears when that source streams again or the run ends. discardTurnId: the failed attempt had streamed reasoning, or text before its connection dropped, for that turn; windows drop it (discardThinking), since the retry streams its own and the saved turn never holds it.Params { kind: 'stream:retry', sessionId: string, attempt: number, max: number, source: string, discardTurnId?: string }
stream:retry:clearedA retried stream reached a terminal state for source ('parent' or a child id); clear that source's retry indicator. Distinct kind rather than a nullable attempt so the two states stay unambiguous on the wire. Consumers also clear at run end as a backstop; this exists for the mid-run case, where a subagent's stream fails while the parent run continues and the indicator would otherwise stick.Params { kind: 'stream:retry:cleared', sessionId: string, source: string }
system-mdWhich SYSTEM.md override (if any) the host folded into this session's system prompt, emitted at agent-build time. The prompt is composed where the agent is built; in the daemon, not in the window; so a window cannot derive this by probing its own disk: it may have a different cwd, and userInstructionsScope is the daemon's. Windows need it to tell "no override on disk" apart from "override in force", which is the difference between doctrine the user wrote and doctrine they think they replaced. shadowed carries precedence losers so the Instructions surface can mark them inactive.Params { kind: 'system-md', sessionId: string, file: SystemMdFile | null, shadowed: readonly SystemMdFile[] }
cache:breakA cache-prefix break was detected for this session's last request; the footer chip names the culprit dimension and the re-billed prefix size. Detection is per-agent closure state inside the loop, so in daemon mode only the daemon can observe it. Cost pricing stays window-side (it needs only the provider key and the token count).Params { kind: 'cache:break', sessionId: string, info: CacheBreakInfo }
provider:diagnosticA provider reported a diagnostic for the session.Params { kind: 'provider:diagnostic', sessionId: string, diagnostic: ProviderDiagnostic }
presence:changedPresence snapshot; recomputed and fanned out whenever a window attaches/detaches or any tracked session's run/HITL state changes. Full snapshot (the set is small: only live sessions appear).Params { kind: 'presence:changed', sessions: SessionPresence[] }Not numbered: no session.
integrations:changedThe host's integrations changed (integrations.register / update, or the client that registered one disconnected). Full list, like presence.Params { kind: 'integrations:changed', integrations: IntegrationInfo[] }Not numbered: no session.
schedules:changedA schedule was created, changed, fired or removed (scheduleId), or several were. Clients read schedules.list again.Params { kind: 'schedules:changed', scheduleId?: string }Not numbered: no session.
machine:statusA machine of this host came online or went offline (docs/MACHINES.md).Params ({ kind: 'machine:status' } & MachineStatusChange)Not numbered: no session.
machine:linksThis local host's machine links changed (machines.links.status): shares of this computer, servers used as machines.Params { kind: 'machine:links', links: MachineLinksStatus }Not numbered: no session.
machine:notificationA server lending itself as a machine to the local host on this connection (machine.attach): one of its machine notifications (terminal.data, task.exit, exec.stream.*). Sent to that connection only, never broadcast.Params { kind: 'machine:notification', method: string, params: JsonValue }Not numbered: no session.
queue:changedThe session's type-ahead queue changed (message queued while a run is in flight, drained by the host on run end, pushed early, or dropped). Broadcast to EVERY peer including the origin; the queue box is shared state and all windows render the same list.Params { kind: 'queue:changed', sessionId: string, items: QueuedItem[] }
delivery:changedA message sent with a clientMessageId changed state (DeliveryState). runId: the run that started with it, took it, or ended with it. For integrations; windows ignore it.Params { kind: 'delivery:changed', sessionId: string, clientMessageId: string, state: DeliveryState, runId?: string }
rpc.eventAn RPC event (RPC_EVENT_SIGNAL).Params { kind: 'rpc.event', envelope: RpcEventEnvelope }Not numbered: no session.
extensions:uiA UI item changed in cwd's runtime. reset: the runtime was rebuilt, list again.Params { kind: 'extensions:ui', cwd: string, change: UiChange | { type: 'reset' } }Not numbered: no session.
extensions:commandsThe commands of cwd's runtime changed (registered, retitled, rebound): list again.Params { kind: 'extensions:commands', cwd: string }Not numbered: no session.
extensions:keymapA keymap mode was pushed or popped. mode: the global mode on top now.Params { kind: 'extensions:keymap', cwd: string, mode?: string }Not numbered: no session.
extensions:integrationsAn integration of cwd's runtime registered, connected or disconnected.Params { kind: 'extensions:integrations', cwd: string }Not numbered: no session.
extensions:integration-flowA step of a sign-in flow, to the client running it.Params { kind: 'extensions:integration-flow', flowId: string, integrationId: string, step: IntegrationFlowStep }Not numbered: no session.
extensions:storageA storage entry changed. projectRoot set for project and session scope. value absent: deleted. version counts the entry's writes on this host (see WireStorageSetResult).Params { kind: 'extensions:storage', extensionId: string, scope: StorageScope, sessionId?: string, projectRoot?: string, area: StorageArea, key: string, value?: Json, version?: number }Not numbered: no session.
extensions:entriesThe extensions of cwd changed (loaded, reloaded, failed, unloaded): list extensions.entries again.Params { kind: 'extensions:entries', cwd: string }Not numbered: no session.
extensions:panelA panel of cwd's runtime would load something new: its subscribe emitted, or its handle was updated. panelId is <extension>/<id>; sessionId set: only that session's content changed. Reload the panel where it is visible. Also sent as an extensions:ui panel-changed change, for clients that follow that one.Params { kind: 'extensions:panel', cwd: string, extensionId: string, panelId: string, sessionId?: string }Not numbered: no session.
ui:dialogShow a dialog. Sent to the routed client only; answer with ui.dialog.answer.Params { kind: 'ui:dialog', dialogId: string, extensionId: string, sessionId?: string, request: DialogRequest }Not numbered: no session.
ui:dialog:closeClose a dialog the host gave up on (timeout, abort, unload).Params { kind: 'ui:dialog:close', dialogId: string }Not numbered: no session.
ui:clientA UI request for the routed client. requestId absent for notify.Params { kind: 'ui:client', requestId?: string, request: UiClientRequest }Not numbered: no session.

Reconnect and resume#

The server keeps the latest signals of each session (2000 signals or 4 MiB per session, 32 MiB in total). Past the total, it drops the least recently written session first. A client that kept its last handshake's signalEpoch and the last $seq it applied asks for what it missed:

request
{
  "jsonrpc": "2.0",
  "id": 9,
  "method": "sessions.resume",
  "params": {
    "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
    "epoch": "ed173759-2d4f-436a-b39a-36c1c969e088",
    "afterSeq": 43
  }
}
response
{
  "jsonrpc": "2.0",
  "id": 9,
  "result": {
    "ok": true,
    "signals": [
      {
        "seq": 44,
        "signal": {
          "kind": "plans:snapshot",
          "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
          "snapshot": {
            "plans": [],
            "active": null
          }
        }
      },
      {
        "seq": 45,
        "signal": {
          "kind": "run:end",
          "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
          "runId": "6f577015-ffbf-4204-ba81-320f71e76f46",
          "ok": true
        }
      }
    ]
  }
}

The answer is { ok: false, reason } when the log cannot fill the gap. The reason is epoch (another server process, after a restart) or gap (the log dropped those signals). The client then reloads the transcript with sessions.loadEvents.

JSON
{
  "jsonrpc": "2.0",
  "id": 10,
  "result": {
    "ok": false,
    "reason": "epoch"
  }
}

To resume without applying a signal twice:

  1. Hold the live signals of the session while sessions.resume runs.
  2. Apply the replay.
  3. Apply the held signals that the replay did not carry.

The apps reconnect with a backoff from 1 to 30 seconds. createServerHost uses 0.5 to 15 seconds. A refused token or another server at the URL stops the retries.

Paging#

sessions.loadEvents without before or limit returns the whole transcript as an array. With either, it returns the last limit turns (default 200, at most 1000) before turn position before. Pass the cursor of the page as the next before. A page without cursor holds the first turn. A page starts on an exchange boundary, so a tool call always comes with its result. start counts the events of the transcript before the page.

request
{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "sessions.loadEvents",
  "params": {
    "sessionId": "7f6b2bc1-27b2-4052-a2b7-de05d2538178",
    "limit": 2
  }
}
response
{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "events": [
      {
        "kind": "user-prompt",
        "text": "Say hello.",
        "turnId": "57942b84-c9cb-42c4-be2b-79aa9fc2171e"
      },
      {
        "kind": "markdown",
        "text": "Hello from the scripted provider.",
        "streaming": false,
        "turnId": "999068c5-c52b-4ccd-a936-682bea162657"
      },
      {
        "kind": "run-summary",
        "text": "Mock · <1s · ↻1 · ↑10 ↓33 · 33000 tk/s",
        "runSummary": {
          "modelId": "mock",
          "durationMs": 10,
          "turns": 2,
          "tokensIn": 10,
          "tokensOut": 33,
          "tokensPerSecond": 33000
        },
        "turnId": "999068c5-c52b-4ccd-a936-682bea162657"
      }
    ],
    "exact": true,
    "start": 1
  }
}

A page over the message size comes back as ResponseTooLarge. If that happens, ask again with a smaller limit. sessions.getTurns pages the raw turns the same way (from, limit).

Errors#

Errors are JSON-RPC error objects. A handler that throws becomes HandlerFailed with its message:

JSON
{
  "jsonrpc": "2.0",
  "id": 13,
  "error": {
    "code": -32014,
    "message": "Session \"no-such-session\" not found. Create the session before sending a prompt."
  }
}
JSON
{
  "jsonrpc": "2.0",
  "id": 11,
  "error": {
    "code": -32601,
    "message": "Unknown method \"sessions.nope\"."
  }
}
JSON
{
  "jsonrpc": "2.0",
  "id": 12,
  "error": {
    "code": -32602,
    "message": "params must be an object."
  }
}
JSON
{
  "jsonrpc": "2.0",
  "id": 15,
  "error": {
    "code": -32040,
    "message": "\"machine.register\" needs a machine connection (a machine-scoped token); this is a client connection."
  }
}
Error codes
CodeNameKindDescription
-32700ParseErrorJSON-RPCThe received text is not valid JSON.
-32600InvalidRequestJSON-RPCThe message is not a valid JSON-RPC request.
-32601MethodNotFoundJSON-RPCThe method does not exist.
-32602InvalidParamsJSON-RPCThe method params are invalid.
-32603InternalErrorJSON-RPCThe handler failed with an unexpected error.
-32010VersionMismatchIPCThe ipc.hello protocol versions differ. The message names both and the side to upgrade; data.daemon carries the host's hello fields.
-32011NotHandshakenIPCA method other than $/ping, daemon.info or daemon.shutdown before ipc.hello.
-32012RateLimitedIPCA network transport's per-connection request budget is spent; retry later.
-32013ResponseTooLargeIPCThe reply would exceed the transport's message size: nothing was sent but this error (the connection stays). Ask for less (a transcript: a smaller page, see loadAllEvents).
-32014HandlerFailedIPCThe handler threw: the host refused or failed the call (message says why). Hosts before it answered -32000, which ACP uses for AuthenticationRequired; clients treat both as a plain remote error.
-32015ForbiddenIPCThe connection's token doesn't reach this method (./scopes: a chat token calling an admin method). data.required names the scope it needs. Additive: hosts before scopes never refuse this way.
-32040MethodNotAllowedMachineA machine connection called a client method, or a client a machine one.
-32041VersionSkewMachinemachine.register with a protocolVersion other than this host's MACHINE_PROTOCOL_VERSION.
-32042NameTakenMachineThe name is held by another machine on this host.
-32043OutsideRootsMachineA path or cwd outside the machine's roots, or in its data dir.
-32044IdBoundMachineThe machine id is bound to another (live) token, or this token was used by the machine before the host forgot it.
-32045BusyMachineNo room now (a capacity limit): try again later, unlike a refusal.

Constants#

Protocol constants
ConstantValueDescription
IPC_PROTOCOL_VERSION16Protocol version a client sends in ipc.hello; a server accepts only its own.
WS_SUBPROTOCOLzidaneThe subprotocol clients offer and the server selects.
WS_BEARER_PREFIXzidane.bearer.Prefix of the subprotocol entry that carries the token, for clients that cannot set headers.
WS_REFUSAL_HEADERX-Zidane-RefusalResponse header naming why an upgrade got a 403.
PING_METHOD$/pingClient heartbeat request, answered before the handshake too.
SIGNAL_SEQ_FIELD$seqField on a session signal's params: its number in the session, from 1, gap-free within one server process.
WS_HEARTBEAT_MS20000Ping interval of both ends by default, in ms.
HEARTBEAT_MISSES2Unanswered pings in a row before a connection is dropped.
WS_MAX_MESSAGE_BYTES33554432Largest message by default (maxMessageBytes).
PEER_WRITE_HIGH_WATERMARK8388608Bytes of pushed signals that may wait for a client that stopped reading before it is dropped.
LOAD_EVENTS_PAGE_TURNS200Turns per sessions.loadEvents page when only before is given.
LOAD_EVENTS_MAX_PAGE_TURNS1000Largest limit a host honors.
SERVER_HEALTH_PATH/healthzUnauthenticated liveness endpoint on the server's port.
SERVER_PAIR_PATH/zidane/pairWhere a browser posts a pairing code (POST, JSON) and gets a device token back.
MAX_MINTED_MACHINE_TOKENS16Live machine tokens one client token may mint with tokens.createMachineToken.

Methods#

Grouped by the part before the dot. Params and result are the method map's TypeScript types, exported from zidane/ipc.

chat#

chat.* methods
MethodScopeDescription
chat.sendchatwake is host-internal (see SendOptions.wake); not accepted over the wire. requestId: see SendRequestId. The prompt goes through the extensions' prompt transforms and prompt interceptors of the session's folder first, once per submission, on the host (clients never run them). consumed says one took it instead of a run (runId is then ''): handled (an extension consumed it), steer / followUp (delivered to the live run). Additive.Params Omit<SendOptions, 'wake'> & SendRequestIdResult { runId: string, consumed?: 'handled' | 'steer' | 'followUp' }
chat.interruptchatAborts the session's live run. Does nothing when no run is live.Params { sessionId: string }Result void
chat.pushQueuedchatDelivers a queued message. Steers a live run, otherwise starts a run or runs it as a command. delivered says which path it took. keepDraft keeps the shared composer draft instead of clearing it on the other clients. The message goes through the prompt interceptors first, as chat.send; consumed says one took it (delivered is then send for handled, steer otherwise). Additive.Params { sessionId: string, message: string, activatedSkills?: string[], keepDraft?: boolean, origin?: SendOptions['origin'], literal?: boolean, clientMessageId?: string, metadata?: SendOptions['metadata'] } & SendRequestIdResult { delivered: 'steer' | 'send' | 'command' | 'queued', consumed?: 'handled' | 'steer' | 'followUp' }
chat.isRunningchatResolves true while the session has a live run.Params { sessionId: string }Result boolean
chat.rebuildAgentchatRetire the session's live agent so the next run rebuilds against fresh config/credentials (MCP login, extension reload). Run-end deferred when a run is in flight; HITL/queue state survives.Params { sessionId: string }Result void
chat.listWorkchatLists the tool calls and background tasks the session's live run tracks and can cancel. Empty when no run is live. children: true adds the subagents' running calls (childId set), for their starts: a transcript read from disk times them too. Older hosts ignore it.Params { sessionId: string, children?: boolean }Result RunningWorkItem[]
chat.cancelWorkchatCancels one tracked work item by id, or every visible item with "all". Resolves true when something was cancelled. reason is passed to a cancelled tool call.Params { sessionId: string, id: string | 'all', reason?: string }Result boolean
chat.deliveryStatechatWhat became of a send, by its requestId, its clientMessageId, or both: the send request record, then the last announced delivery:changed state, then the queue and the transcript (origin.messageId). Resolves a send answered "already accepted" (SendRequestPendingError). state: the last known DeliveryState, or unknown when the host holds no trace. runId: the run that took it, when known. settled: nothing more will happen to it: a final state (answered, failed, interrupted, dropped), or, with unknown, it is in neither the queue nor the transcript and the send request record says no send of it runs (it never landed: send it again with a new requestId); without a requestId an unknown is never settled. Additive.Params { sessionId: string, requestId?: string, clientMessageId?: string }Result { state: DeliveryState | 'unknown', runId?: string, settled: boolean }

sessions#

sessions.* methods
MethodScopeDescription
sessions.setLaunchOverridesadminzidane run --system / --mcp for a host-backed window: launch overrides for the session's agent (ChatHost.setLaunchOverrides), owned by the calling client and dropped when it disconnects. A call with neither field clears them.Params { sessionId: string } & SessionLaunchOverridesResult void
sessions.listchatLists session rows. projectRoot filters by project, archive picks active, archived or all sessions.Params { projectRoot?: string | null, archive?: 'active' | 'archived' | 'all' }Result SessionRow[]
sessions.listMetachatThe session list as a window renders it (SessionMeta: title, turn / message / run counts, project, cwd, environment, artifact count, the orchestration main flag), newest first. projectRoot: omitted = every project, null = untagged sessions.Params { projectRoot?: string | null }Result SessionMeta[]
sessions.getMetachatOne SessionMeta by id (rows a filtered list can't return, e.g. the main); null when unknown.Params { sessionId: string }Result SessionMeta | null
sessions.getchatThe whole session document as the host holds it: the live copy when an agent is live (turns persisted mid-run, todos kept in memory), else the stored one, a stale running marker demoted. null when unknown.Params { sessionId: string, turns?: boolean }Result SessionDocumentHead | null
sessions.getTurnschatA slice of a session's turns, [from, from + limit) (default from 0, SESSION_TURNS_PAGE turns, at most SESSION_TURNS_MAX_PAGE), from the same copy sessions.get reads, and how many turns it has: a client reads a long session's document a page at a time (loadSessionDocument) instead of in one reply. With sessions.get turns: false (the document without its turns), what an export needs. Pages and that head come from one snapshot of the document per content version (read once, not per page), and name it (version): a client that sees it change mid-load starts over rather than mix two versions (loadSessionDocument). null when unknown. Additive: an older host answers "method not found", ignores turns: false, and names no version.Params { sessionId: string, from?: number, limit?: number }Result { turns: SessionTurn[], total: number, version?: string } | null
sessions.getToolResultchatThe full text of one tool result (callId; turnId, when known, the turn that holds it): what a clipped tool row shows on expand, without the session crossing the wire. null when no such result. Additive.Params { sessionId: string, callId: string, turnId?: string }Result string | null
sessions.getHeaderchatOne session's header: its list row plus metadata, profile and status, without turns or runs (the live copy when an agent holds it). What a window re-reads on sessions:changed. Additive. usage: true adds the footer's numbers, computed host-side from the turns and runs (contextTokens, costUsd, and while the host runs the session runStartedAt / runOutputTokens): a client needs no document for its context gauge, cost pill or run readout. Older hosts ignore it.Params { sessionId: string, usage?: boolean }Result SessionHeader | null
sessions.setExtensionStatechatOne key of an extension's per-session state (metadata.extensions, the bag ctx.session and renderer useSessionState share), written by the host into its copy of the session. value omitted deletes the key.Params { sessionId: string, extension: string, key: string, value?: JsonValue }Result void
sessions.createchatCreates a session and resolves with its row.Params CreateSessionParamsResult SessionRow
sessions.deletechatResolves with what happened to the session's environment (a provider may keep it).Params { sessionId: string }Result { environment?: EnvironmentTeardownResult }
sessions.sideSessionchatA session's /side session (sessions.create with sideOf; hidden from sessions.list), the newest when there are several, with the session's own row (null: unknown). Additive.Params { sessionId: string }Result SideSessionLookup
sessions.retryEnvironmentchatProvision a failed (or restart-stranded) environment again.Params { sessionId: string }Result void
sessions.moveEnvironmentchatMove an idle session to another environment (local, worktree, local@M, worktree@M, any direction), in cwd on the target; provisioning continues in the background.Params { sessionId: string, environment: EnvironmentRequest, cwd?: string, removeOldWorktree?: boolean }Result { oldWorktree?: EnvironmentTeardownResult }
sessions.environmentActionschatThe per-session commands the session's environment offers right now (push, open PR, …).Params { sessionId: string }Result EnvironmentActionSummary[]
sessions.runEnvironmentActionadminRun one of them in the session's environment (resumed when suspended).Params { sessionId: string, actionId: string }Result EnvironmentActionResult
sessions.workspaceExecadminThe session's workspace (ChatHost.workspace*): its execution context, rooted inside the environment when the files live there, else at the session's cwd. Paths and exec cwd resolve against the root as the execution context resolves them; containment is the context's own. cwd / env on exec and the list / size methods are additive: an older daemon ignores the former and answers "method not found" to the latter (active clients are build-fenced, so this is a transient case).Params { sessionId: string, command: string, cwd?: string, env?: Record<string, string>, timeoutSeconds?: number }Result ExecResult
sessions.workspaceExecStreamadminThe same command, streamed (! on a remote host): the reply names the exec, its output follows as workspace:exec:data signals and ends with one workspace:exec:exit, all to this connection only (signals may precede the reply). Bounded per connection (concurrent commands, chunk size, total output: past it the process is stopped and the exit says truncated). A dropped connection kills what it started. Additive: an older host answers "method not found" and clients use workspaceExec.Params { sessionId: string, command: string, cwd?: string }Result { execId: string }
sessions.workspaceExecCanceladminKill a streamed command's process group (this connection's only; cancelled: false otherwise or when it already ended).Params { execId: string }Result { cancelled: boolean }
sessions.runBlockadminRuns a shell block of a reply (Run here) in the session's folder, with no input. The reply names the run; its output follows as block-run:data signals and ends with one block-run:end carrying the saved run, to this connection only. Nothing enters the conversation; the run becomes the block's last (sessions.blockRuns), announced to every window by block-runs:changed. A dropped connection stops what it started.Params { sessionId: string, turnId: string, code: string, language: string }Result { runId: string }
sessions.runBlockCanceladminStops a Run here this connection started (cancelled: false when it is not running).Params { runId: string }Result { cancelled: boolean }
sessions.blockRunschatThe session's saved block runs, by key (blockRunKey).Params { sessionId: string }Result { runs: Record<string, BlockRun> }
sessions.removeBlockRunchatForgets a block's saved run.Params { sessionId: string, key: string }Result void
sessions.workspaceReadFilechatReads a text file from the session's workspace.Params { sessionId: string, path: string }Result string
sessions.workspaceWriteFileadminWrites a text file in the session's workspace.Params { sessionId: string, path: string, content: string }Result void
sessions.workspaceListFileschatLists the entries under path in the session's workspace.Params { sessionId: string, path: string, options?: WorkspaceListFilesOptions }Result ListFilesEntry[]
sessions.workspaceFileSizechatSize in bytes of a file in the session's workspace, or null when it does not exist.Params { sessionId: string, path: string }Result number | null
sessions.workspaceCreateadminCreates a file (content: base64 bytes, default empty) or a folder; freeName takes the next free name instead of refusing a taken one.Params { sessionId: string, path: string, kind: 'file' | 'dir', content?: string, freeName?: boolean }Result EntryResult
sessions.workspaceMoveadminMoves (renames) from to to.Params { sessionId: string, from: string, to: string }Result EntryResult
sessions.workspaceCopyadminCopies from into folder toDir ('': the root) under a free name.Params { sessionId: string, from: string, toDir: string }Result EntryResult
sessions.workspaceRemoveadminRemoves path (a folder whole), for good.Params { sessionId: string, path: string }Result EntryResult
sessions.workspaceEmptyDirschatThe workspace's empty folders, relative to its root (a file listing leaves them out), capped.Params { sessionId: string }Result string[]
sessions.setTitlechatRenames a session.Params { sessionId: string, title: string }Result void
sessions.generateTitlechatTitle the session ON the host: its providers and credentials (a server session's transcript never goes to the window's provider), the same generator (generateSessionTitle), written as sessions.setTitle does. providerKey / model: the window's pick, used when the host has that provider; otherwise the session's own provider and model. The Main session is refused (it keeps its name). Additive: an older host answers "method not found".Params { sessionId: string, providerKey?: string, model?: string }Result { title: string }
sessions.titleStatuschatHow the session's title came about, as the session details dialog words it (describeTitleStatus): where it came from (auto, typed by you, untitled, …), then the last automatic titling attempt when there was one (generated 2h ago, check failed 5m ago (…)). Additive.Params { sessionId: string }Result string[]
sessions.setArchivedchatArchives or restores a session.Params { sessionId: string, archived: boolean }Result void
sessions.setFavoritechatKeep the session at the top of lists (metadata.favorite). Not activity: no recency bump. Additive.Params { sessionId: string, favorite: boolean }Result void
sessions.setPolicychatReplaces the session's rules (metadata.policy, SessionPolicy); {} clears them. Loosening the current rules (a looser approvalMode, 'ask' > 'decide' > 'off', removing a rule, raising a ceiling) needs an approve token. Additive.Params { sessionId: string, policy: SessionPolicy }Result void
sessions.setSpacechatMove the session to a space (metadata.spaceId); the Default space id clears it. See chat/spaces.Params { sessionId: string, spaceId: string }Result void
sessions.setAgentIdchatSwitches the agent the session runs as.Params { sessionId: string, agentId: string }Result void
sessions.setEffortchatSets the thinking effort of the session.Params { sessionId: string, effort: ThinkingLevel }Result void
sessions.setModelchatThe one model-swap contract (ChatHost.setSessionModel). A pick is a whole: fallbacks (ordered, primary excluded) REPLACES the persisted metadata.modelChain (omitted/empty clears it); effort and modelOptions are reset to the supplied values (omitted clears them; per-model knobs never leak onto the new model). The live agent is retired so the next run rebuilds on the new pick (run-end deferred while a run is in flight).Params { sessionId: string, providerKey: ProviderKey, modelId: string, account?: string, fallbacks?: ModelFallbackRef[], effort?: ThinkingLevel, modelOptions?: Record<string, boolean> }Result void
sessions.setModelOptionschatReplaces the model-specific boolean options of the session.Params { sessionId: string, options: Record<string, boolean> }Result void
sessions.loadEventschatThe session's transcript as canonical events. Without before / limit: every turn, as a plain array (the shape every client predating paging reads). With either: one page, { events, cursor? }, the last limit turns (default LOAD_EVENTS_PAGE_TURNS, at most LOAD_EVENTS_MAX_PAGE_TURNS) before turn position before (default: the end), its start moved back to an exchange boundary when one is near so a tool call is never cut from its result. cursor: the before of the previous page, absent at the first turn. Typed here as the unpaged call every client makes; a paged call goes through loadEventsPage. Paging is additive: an older host ignores the fields and answers the whole array.Params { sessionId: string }Result StreamEvent[]
sessions.resumechatReattach without a reload: the session's signals numbered after afterSeq (see SIGNAL_SEQ_FIELD), in order, as long as this host process (epoch, from HelloResult.signalEpoch) still holds them all. Otherwise refused, and the client reloads with sessions.loadEvents. Additive: an older host answers "method not found", same fallback.Params { sessionId: string, epoch: string, afterSeq: number }Result SessionResumeResult
sessions.getTurnchatOne turn of a session by id, or null when unknown.Params { sessionId: string, turnId: string }Result SessionTurn | null
sessions.contextBreakdownchatThe session's context usage broken down by category, or null when unknown.Params { sessionId: string }Result ContextBreakdown | null
sessions.artifactschatArtifacts one session recorded (PRs, branches, commits, files, extension kinds; see session/artifacts.ts), newest first. Empty on stores without artifact support. Additive; pre-artifact daemons reject the method and clients treat that as empty.Params { sessionId: string, kind?: string, limit?: number }Result SessionArtifact[]
sessions.touchchatA window appended transcript content OUT-OF-BAND (local / command output, ! shell turns; written straight to the shared store, not through a host run). The daemon fans out sessions:changed so every window (issuer included) re-converges on the canonical replay NOW instead of at the next run end. Sent as a notification.Params { sessionId: string }Result void
sessions.forkchatFork at turnId into a new session (ChatHost.forkSession); announced as a sessions:changed create.Params { sessionId: string, turnId: string, title?: string }Result { id: string }
sessions.deleteTurnchatHistory edits (ChatHost.deleteTurn / editTurnText): refused while the session runs.Params { sessionId: string, turnId: string }Result void
sessions.editTurnTextchatReplaces the text of one turn. Refused while the session runs.Params { sessionId: string, turnId: string, text: string }Result void
sessions.rewindchatCut the history at turnId in place (ChatHost.rewindSession); inclusive keeps the turn. Refused while the session runs.Params { sessionId: string, turnId: string, inclusive: boolean }Result void
sessions.setFolderchatBind to a folder, or back to a plain chat with cwd: '' (ChatHost.setSessionFolder).Params { sessionId: string, cwd: string }Result void
sessions.appendTurnschatTurns written outside a run, e.g. ! shell output (ChatHost.appendTurns). whenIdle: refused while a run is live or an interrupted run waits (a channel's context messages).Params { sessionId: string, turns: SessionTurn[], whenIdle?: boolean }Result void
sessions.appendNotechatctx.appendNote outside a run: a display-only note, rendered live and persisted (ChatHost.appendNote). False for an unknown session or no blocks.Params { sessionId: string, blocks: CommandOutputBlock[] }Result boolean
sessions.compactchatCompact now (ChatHost.compactSessionNow): what was done, null when nothing was. Older daemons resolve null.Params { sessionId: string }Result CompactionReport | null
sessions.systemMdchatWhich SYSTEM.md override composes this session's prompt, for windows attaching to an already-warm agent. The system-md signal fires once per agent BUILD and the daemon caches agents, so the normal multi-window case (daemon already running, open a second terminal or the GUI) would otherwise never learn that the built-in safety and git doctrine has been replaced. null = no live agent yet, in which case the next build broadcasts it.Params { sessionId: string }Result { file: SystemMdFile | null, shadowed: readonly SystemMdFile[] } | null
sessions.contextSegmentschatEvery chunk of the session's system prompt with provenance; the Instructions tab. Built from the live agent's assembly, so it reflects MCP instructions, extension fragments and the resolved SYSTEM.md.Params { sessionId: string }Result ContextSegmentsReport | null
sessions.cacheImpactchatWhat a switch would cost the session's prompt cache, as the host applies it (ChatHost.cacheImpact): what pickers show next to each option. null without a live agent or for an empty change. Additive: an older host answers "method not found", and clients show no estimate.Params { sessionId: string, change: CacheImpactChange }Result CacheImpact | null
sessions.cacheReportchatThe session's last requests and what the cache did for each (ChatHost.cacheReport): what /cache prints, for the GUI's session details. null for an unknown session. Additive: an older host answers "method not found".Params { sessionId: string, limit?: number }Result CacheReport | null
sessions.exportchatThe session's transcript rendered as a file to hand to the user: Markdown (title, metadata, every turn) or the session's JSON. Nothing is written on the host. Additive (no version bump).Params { sessionId: string, format: SessionExportFormat }Result SessionExportFile
sessions.createSystemOverrideadminSeed a user-scope SYSTEM.md from the session's doctrine; throws when an override could not apply.Params { sessionId: string }Result { path: string, created: boolean }

runtime#

runtime.* methods
MethodScopeDescription
runtime.reloadadminDrop the daemon's cached runtime configs (extension registries, per cwd) and retire every live agent so the next command or run rebuilds against what is on disk now. Sent by a window after it reloaded its own extensions (install, update, remove, /reload): extension / commands execute daemon-side, so without this the daemon keeps running the code it loaded at boot. Busy sessions rebuild at run end.Params Record<string, never>Result void

environments#

environments.* methods
MethodScopeDescription
environments.listchatEvery environment kind the host can create, described for a new session in cwd (availability, default, branch suggestion, options; secrets only as "configured"). What pickers and the Environments settings render. kind: a machine kind (local@laptop) says cwd is that machine's folder, checked against its roots instead of this host's (docs/MACHINES.md § Folders); the same on commands.*, catalog.* and models.list. Additive.Params { cwd?: string, kind?: string }Result EnvironmentDescriptor[]
environments.updateadminApply one Environments-settings edit (enable, default, options, secrets), validated against the kind's schema. Settings changes fan out as settings:changed; secrets are stored host-side and never returned.Params EnvironmentSettingsChangeResult void
environments.verifychat"Test connection" for a kind with its current options.Params { kind: string }Result EnvironmentVerifyResult

artifacts#

artifacts.* methods
MethodScopeDescription
artifacts.listchatArtifacts across sessions, filtered by project root (null = untagged sessions, omitted = every project) and/or kind/uri (reverse lookup).Params { projectRoot?: string | null, kind?: string, uri?: string, since?: number, limit?: number }Result SessionArtifact[]
artifacts.deletechatRemove one artifact. false when the store has no artifact support. Fans out artifact:changed.Params { sessionId: string, artifactId: string }Result boolean
artifacts.upsertchatUpsert an artifact; the lazy resolve() write-back (refreshed label / meta). Same merge semantics as SessionStore.upsertArtifact; null when the session is unknown or the store lacks support.Params { artifact: SessionArtifact }Result SessionArtifact | null
artifacts.refreshchatRe-resolves one stored artifact (is the pull request merged?): its kind's resolve() runs on the host, with the extensions of the folder the artifact landed in. A change is stored and fans out artifact:changed. force asks past the kind's own freshness window. null when nothing changed (no resolver, nothing new, an unknown artifact). Additive.Params { sessionId: string, artifactId: string, force?: boolean }Result SessionArtifact | null
artifacts.badgeschatThe badges under sessions' titles (commits, pull requests, extension kinds with a badge), for up to ARTIFACT_BADGES_MAX sessions at once, keyed by session id. Each session's badges come from the extensions of the folder its newest artifact landed in. They change with the session's artifact:recorded / artifact:changed and with catalog:changed (extensions): read them again then. Additive.Params { sessionIds: string[] }Result Record<string, SessionBadgeEntry[]>

history#

history.* methods
MethodScopeDescription
history.searchSessionchatTranscript search over ONE session, daemon-side. The daemon owns the authoritative turns (live wrapper when an agent is active; fresh mid-run, which a window's load-time snapshot is not) and an LRU index cache shared by every window, so repeat queries pay match cost only. Results come back in TURN ORDER (search is a transcript filter).Params { sessionId: string, query: string, limit?: number }Result SearchTurnResult[]
history.searchAllchatCross-session content search, daemon-side. One RPC replaces a window's per-session index/build/search round-trips; indexes are built once in the daemon and shared by every window.Params { query: string, sessionIds: string[], excerptLimit?: number }Result Record<string, SessionHistoryMatch>

plans#

plans.* methods
MethodScopeDescription
plans.getchatThe session's plans (from its live agent, else the store), or null for an unknown session.Params { sessionId: string }Result PlansSnapshot | null
plans.editchatA user edit to a plan's steps: rename, add or cancel (a step is never deleted). Through the live agent's session, else the store; refused while another process runs the session. The host fans plans:snapshot out to every window; the result is that snapshot. A refused edit is an error carrying the gate's reason.Params { sessionId: string, planId: string, op: PlanEditOp }Result PlansSnapshot
plans.controlchatStop or drop a plan. Stopping or dropping the plan a live run applies interrupts that run. Resume is a message (buildPlanResumeMessage), not a call.Params { sessionId: string, planId: string, action: 'stop' | 'drop' }Result PlansSnapshot

orchestration#

orchestration.* methods
MethodScopeDescription
orchestration.mainchatThe main session id, creating it (directory-less) when the setting is on and none exists.Params Record<string, never>Result string | null
orchestration.createWorkerchatCreates a session (optionally holding orchestration tools and a subscription) and resolves with its id.Params OrchestrationCreateParamsResult string
orchestration.listchatLists sessions with their live status, newest first, up to limit. The list can be partial.Params { limit?: number }Result OrchestrationWorkerRow[]
orchestration.readchatSummary of one session (with its link); rejects when unknown. lastMessage is cut at 600 characters; full: true returns it whole.Params { sessionId: string, full?: boolean }Result OrchestrationWorkerSummary
orchestration.sendchatQueues a prompt as the session's next turn; false when declined (the person stopped the session, or their queued prompts go first).Params { sessionId: string, prompt: string }Result boolean
orchestration.steerchatSends a steering message, with optional attachments, to a session's live run.Params { sessionId: string, message: string, attachments?: PromptAttachment[] }Result void
orchestration.cancelchatCancels a session's run.Params { sessionId: string }Result void
orchestration.transcriptchatThe last limit turns of a session as plain text, each cut at 1200 characters; full: true returns them whole.Params { sessionId: string, limit?: number, full?: boolean }Result string
orchestration.waitchatResolves once the session is idle or blocked, or after timeoutSec (default 30, at most 50).Params { sessionId: string, timeoutSec?: number }Result OrchestrationWaitResult
orchestration.eventschatSearches the recent session events (runs ended, sessions blocked), newest first.Params OrchestrationEventQueryResult SessionEvent[]
orchestration.pendingchatThe prompts waiting for an answer (questions, plans, tool approvals), of one session or all.Params { sessionId?: string }Result OrchestrationPendingRequest[]
orchestration.answerchatAnswers a question or a plan, recording the connection as answeredBy. false when it was answered first.Params { requestId: string, response: InteractionResponse }Result boolean
orchestration.approvechatDecides a tool approval (an approve token), recording the connection as answeredBy. false when it was decided first.Params { requestId: string, decision: ApprovalDecision }Result boolean

commands#

commands.* methods
MethodScopeDescription
commands.listchatExtension-contributed / commands discovered for cwd.Params { cwd: string, kind?: string }Result CommandSummary[]
commands.completeArgschatArgument suggestions for /name <prefix>; best-effort, empty on failure.Params { cwd: string, name: string, argsPrefix: string, kind?: string }Result CommandArgSuggestionSummary[]

agents#

agents.* methods
MethodScopeDescription
agents.listchatThe agent profiles cwd offers ('': no folder), as the agent picker lists them: a profile a setting turns off (Codemode) is left out. Additive.Params { cwd: string }Result HostAgentProfile[]

completions#

The extensions' composer completions, run on the server: the triggers a folder offers, a provider's items, and the submit hooks on a prompt about to be sent.

completions.* methods
MethodScopeDescription
completions.providerschatThe extension completion providers (ctx.registerCompletionProvider) of cwd a composer can render: a one-character trigger each, @ / # left to the built-in completers, first wins on id and trigger. Additive.Params { cwd: string }Result ExtensionCompletionProviderSummary[]
completions.suggestchatOne provider's items for query (the text after its trigger), at most 8. A newer call for the same provider on the connection aborts this one; [] past 5 s or when the provider throws. Additive.Params { cwd: string, providerId: string, query: string, text: string, cursor: number }Result ExtensionCompletionItemSummary[]
completions.submitchatRuns the providers' submit hooks (resolveReferences, onSubmit) on a prompt about to be sent; resolves after them, or after 5 s. Additive.Params { cwd: string, text: string, cursor: number }Result void

interactions#

interactions.* methods
MethodScopeDescription
interactions.listchatLists pending interactions (questions and prompts the agent asked the user), optionally for one session.Params { sessionId?: string }Result PendingInteraction[]
interactions.respondchatAnswers a pending interaction.Params { requestId: string, response: InteractionResponse }Result { ok: boolean }
interactions.cancelchatCancels a pending interaction. reason is passed back to the agent.Params { requestId: string, reason?: string }Result { ok: boolean }
interactions.rehydratechatRe-sends the session's pending interactions, for a client that connected after they were raised.Params { sessionId: string }Result PendingInteraction[]
interactions.rehydrateSessionschatinteractions.rehydrate for each of sessionIds not running on this host, loading only those whose run waits to be resumed or whose last turns hold an unanswered question or plan (a channel start, over every conversation it had).Params { sessionIds: string[] }Result PendingInteraction[]

browser#

browser.* methods
MethodScopeDescription
browser.registerHostchatDev browser: a window offers to host the pages the agent's browser_* tools drive (the GUI's main process). Released when the connection drops. It then answers each browser:request signal addressed to it.Params { hostId: string }Result { ok: boolean }
browser.unregisterHostchatWithdraws a window as dev browser host.Params { hostId: string }Result { ok: boolean }
browser.respondchatAnswers a browser:request signal with the host's result.Params { requestId: string, response: BrowserResponse }Result { ok: boolean }

terminal#

terminal.* methods
MethodScopeDescription
terminal.stateadminTerminals, run by the host in the session's environment. clientId names one view (a window's drawer or panel); views attached through a connection are detached when it drops. Output arrives as terminal:output signals; ack what was written to keep it flowing.Params { sessionId: string }Result TerminalSessionState
terminal.openadminOpens a terminal in a session's environment.Params TerminalOpenInputResult TerminalInfo
terminal.attachadminmaxLines caps the history snapshot to the last lines a view can show (0 or absent: no cap).Params { terminalId: string, clientId: string, maxLines?: number }Result TerminalAttachResult
terminal.detachadminDetaches a view from a terminal.Params { terminalId: string, clientId: string }Result void
terminal.writeadminclientId + response: bytes the view's emulator generated (DSR/DA replies), dropped unless that view owns the size.Params { terminalId: string, data: string, clientId?: string, response?: boolean }Result void
terminal.resizeadminResizes the terminal to a view's size. claim makes this view own the size.Params { terminalId: string, clientId: string, cols: number, rows: number, claim?: boolean }Result void
terminal.ackadminAcknowledges terminal output up to offset, so the host keeps it flowing.Params { terminalId: string, clientId: string, offset: number }Result void
terminal.restartadminRestarts a terminal's process and resolves with its info.Params { terminalId: string }Result TerminalInfo
terminal.closeadminCloses a terminal.Params { terminalId: string }Result void
terminal.setHiddenadminHides a terminal from the agent, or shows it again.Params { terminalId: string, hidden: boolean }Result void
terminal.registerVieweradminThis connection can show terminals (the desktop app). While at least one viewer is attached, agents get the terminal tools; the registration is dropped with the connection.Params { viewerId: string }Result { ok: boolean }
terminal.unregisterVieweradminWithdraws a viewer registered with terminal.registerViewer.Params { viewerId: string }Result { ok: boolean }
terminal.projectStateadminTerminals without a session: a shell on the host's machine in a project's folder, or in a new-chat draft's (draftId). Agents never see them. terminal:project-changed carries a scope's full list.Params TerminalProjectStateInputResult TerminalProjectState
terminal.openInProjectadminOpens a terminal bound to a project rather than a session.Params TerminalProjectOpenInputResult TerminalInfo
terminal.adoptadminThe draft became sessionId: its terminals move there in one pass (over the cap: back to the project).Params { draftId: string, sessionId: string }Result TerminalAdoptResult
terminal.releaseDraftadminThe draft was discarded (or became a server session): its terminals go back to the project.Params { draftId: string }Result void

approvals#

approvals.* methods
MethodScopeDescription
approvals.resolvechatAnswers a pending approval request with a decision.Params { requestId: string, decision: ApprovalDecision }Result void
approvals.denyAllchatDenies every pending approval, or only those of sessionId when given.Params { sessionId?: string }Result void
approvals.listPendingchatLists the approval requests waiting for an answer.Params Record<string, never>Result WireApprovalRequest[]

safelist#

The "Always allow" entries approvals saved, per project. A chat token can revoke one.

safelist.* methods
MethodScopeDescription
safelist.listchatThe host's "Always allow" entries, per project (its projects.json): what approving with accept-safelist added. Additive.Params Record<string, never>Result { projects: SafelistProject[] }
safelist.removechatRevoke one "Always allow" entry of a project; removed: false when it was not there. Additive.Params { project: string, entry: string }Result { removed: boolean }

queue#

queue.* methods
MethodScopeDescription
queue.listchatType-ahead queue, host-owned so every window sees and can manipulate the same list. queue.add queues when a run is in flight and sends immediately otherwise; the host auto-drains the queue FIFO at run end.Params { sessionId: string }Result QueuedItem[]
queue.addchatSends a message: right away when the session is idle; while a run works, into the run or into the queue (delivery). delivered says which: sent, steer (the run took it, as a steer or a follow-up) or queued. The message goes through the prompt interceptors first, as chat.send; consumed says one took it (delivered is then sent for handled, steer otherwise).Params { sessionId: string, text: string, activatedSkills?: string[], attachments?: PromptAttachment[], asCommand?: string, keepDraft?: boolean, origin?: SendOptions['origin'], literal?: boolean, clientMessageId?: string, metadata?: SendOptions['metadata'], delivery?: QueueDelivery } & SendRequestIdResult { delivered: 'queued' | 'sent' | 'steer', consumed?: 'handled' | 'steer' | 'followUp' }
queue.removechatRemoves a queued entry by id. Resolves true when it was removed.Params { sessionId: string, id: string }Result boolean
queue.updatechatEdit a queued entry in place; order is kept. attachments replaces the entry's list when given. false when already sent / dropped.Params { sessionId: string, id: string, text: string, attachments?: PromptAttachment[] }Result boolean
queue.attachmentschatA queued entry's attachments with their bytes; null when already sent / dropped.Params { sessionId: string, id: string }Result PromptAttachment[] | null
queue.pushNowchatSteer a queued entry, attachments included, into the live run now (falls back to send when idle).Params { sessionId: string, id: string }Result { delivered: 'steer' | 'send' | 'queued' | 'missing' }

drafts#

drafts.* methods
MethodScopeDescription
drafts.getchatCurrent shared draft for a session; backfill on window attach.Params { sessionId: string }Result { text: string, shell?: boolean, attachments?: PromptAttachment[], search?: { active: boolean, query: string } }
drafts.setchatReplace the session's shared draft. Fans out draft:changed to every OTHER peer. Ephemeral daemon state; drafts don't survive a daemon restart and are cleared when any window submits the prompt. shell carries the prompt's shell mode OUT OF BAND; never encoded into the text, so a literal leading ! in a normal draft can't flip other windows into shell-execution semantics.Params { sessionId: string, text: string, shell?: boolean }Result void
drafts.setAttachmentschatReplace the shared draft's composing attachments (notification).Params { sessionId: string, attachments: PromptAttachment[] }Result void
search.* methods
MethodScopeDescription
search.setchatReplace the shared transcript-search state (notification, per keystroke).Params { sessionId: string, active: boolean, query: string }Result void

extensions#

What a folder's extensions declare in Settings: their options and action rows (extensions.actions), the actions' status probes (extensions.actionStatuses) and a run (extensions.runAction: the token needs the action's own scope, admin unless the extension lowers it; the config patch it returns is the client's to save with settings.set), and a new extension's scaffold (admin, behind allowRemoteInstalls).

extensions.* methods
MethodScopeDescription
extensions.configSchemachatThe options the enabled extensions declare for cwd (registerConfigItem), without the action items or the callbacks; the values live on the settings' extensionConfig. Additive.Params { cwd: string }Result WireExtensionConfigSchema
extensions.actionschatThe action rows (registerConfigItem type 'action') the enabled extensions declare for cwd. Additive.Params { cwd: string }Result ExtensionActionSummary[]
extensions.actionStatuseschatThe actions' passive status() probes, keyed extension:key; a probe that throws or takes over 3 s is left out. Additive.Params { cwd: string }Result Record<string, ExtensionConfigActionStatus>
extensions.runActionchatRuns an action on the host. The config patch it returns comes back sanitized, not written: the client saves it (settings.set). One run of an action at a time. A token needs the action's scope (the action row's scope; default, and for config-declared actions, admin). Additive.Params { cwd: string, extension: string, key: string }Result ExtensionActionRunResult
extensions.createadminScaffolds a new extension and reloads the registries: in the host user's extensions folder (scope: 'user', the default), or in a project (scope: 'project': cwd's checkout, which must lie in the workspace roots; it loads once the project is trusted). Sessions stay in the roots: one may open in a project extension's folder, never in a user extension's. It writes code the host will run: a zidane-server's allowRemoteInstalls gates it, every attempt is audited. Every window gets catalog:changed. Additive.Params CreateExtensionInputResult CreateExtensionResult
extensions.ui.listchatEvery UI item of cwd's extensions, per token, in wire form, and each active extension's compiled ui bundle (bundles, by extension id). extensions:ui pushes each change after it. Additive.Params { cwd: string }Result WireUiList
extensions.entrieschatThe extensions of cwd: status, folder, tui entry and compiled ui bundle. What a window draws extensions from in daemon mode, without running their setup. extensions:entries says when to list again. Additive.Params { cwd: string }Result WireExtensionEntry[]
extensions.ui.invokechatRuns one callback of a UI item by its id (a panel's load / onAction, a reference resolve, a badge menu or action, a settings page action, status or check). Callbacks run on the host under their extension's guard. Additive.Params { cwd: string } & UiInvokeRequestResult unknown
extensions.links.openchatOffers a link the user opened to cwd's extensions' link handlers (LinkHandler), higher priority first; a handler that returns false, throws or times out passes it on. handled: false: open it the app's own way. Apps call it for every link they open, except with Alt held. Additive.Params LinkOpenParamsResult { handled: boolean }
extensions.storage.loadchatEverything stored at a location of cwd's extension storage, and the project root its project/session files live under. Additive.Params { cwd: string } & WireStorageLocationResult StorageSnapshot & { projectRoot?: string }
extensions.storage.setchatWrites (or, value absent, deletes) one storage entry. The extensions holding it see the change at once; every other connection gets extensions:storage. A stores write must be a { v, value } record of the store's version whose value passes its schema (checked against every runtime of the folder holding the store); otherwise it is refused and nothing changes. The result says which, with the entry as the host holds it now, for the client to reconcile. Additive (the result was void).Params { cwd: string, area: StorageArea, key: string, value?: Json } & WireStorageLocationResult WireStorageSetResult
extensions.settingschatThe extensions' settings of cwd with values, locks and the layer each value comes from. sessionId adds that session's layer. Additive.Params { cwd: string, sessionId?: string }Result WireExtensionSettings
extensions.settings.setadminWrites one extension setting at a scope: user in the settings file, project in the project's extension-settings.json, session in memory; a secret goes to the credentials file, never a settings file. Refused for a locked, unknown or action key, a scope the setting doesn't allow, or a value that fails its checks. Additive.Params SettingsSetParamsResult void
extensions.commandschatThe extension commands of cwd with their effective bindings, scopes and modes, and the keymap mode on top for sessionId. Additive.Params { cwd: string, sessionId?: string }Result WireExtensionCommands
extensions.command.runchatRuns a command by its published id (an extension command or a built-in). false when nothing ran. Additive.Params CommandRunParamsResult boolean
extensions.keymap.pushchatActivates a keymap mode on the host for cwd (as ctx.keymap.mode.push) until extensions.keymap.pop or this connection closes. Returns its handle. Additive.Params { cwd: string, mode: string, priority?: number, sessionId?: string }Result string
extensions.keymap.popchatPops a mode this connection pushed. Additive.Params { handle: string }Result boolean
extensions.keymap.listchatEvery command of cwd with its effective bindings, scope and mode (ctx.keymap.list), and the active mode stack for sessionId (ctx.keymap.active). Additive.Params { cwd: string, sessionId?: string }Result WireKeymap
extensions.integrations.listchatThe extension integrations of cwd (ctx.integrations.register) and their connections. Additive.Params { cwd: string }Result WireIntegration[]
extensions.integrations.connectKeyadminConnects with an API key: verified by the extension, then kept in the credentials file. Additive.Params { cwd: string, integrationId: string, key: string, methodId?: string }Result WireIntegration
extensions.integrations.startadminRuns an OAuth or command method. Its steps (open this URL, enter this code, paste the redirect) come to this connection as extensions:integration-flow with the returned flowId; the call resolves when the flow ends. Additive.Params { cwd: string, integrationId: string, methodId: string, flowId?: string }Result WireIntegration
extensions.integrations.flowInputadminAnswers a flow step that asked for input (inputId). value absent cancels the flow. Additive.Params { flowId: string, inputId: string, value?: string }Result boolean
extensions.integrations.disconnectadminRemoves the connection of account, or every connection of the integration when it is absent. Additive.Params { cwd: string, integrationId: string, account?: string }Result void
extensions.integrations.setDefaultadminMakes the connection of account the integration's default. Additive.Params { cwd: string, integrationId: string, account: string }Result void
extensions.catalogs.provenancechatWhich catalog items of cwd extensions added, changed or removed, with the extension that did it (Settings shows it). Additive.Params { cwd: string, kind: CatalogKind }Result CatalogItemProvenance[]
extensions.browseadminThe extensions of a source (owner/repo[#ref][:dir], a GitHub URL, an npm package, or a folder on the host), read from their manifests by the host with its GitHub login; nothing runs. Never fails for an unreadable source: the listing carries error (the default source Tahul/zidane then lists the native catalog, builtin: true). Each row's install is what catalog.install takes (spec, extension). Additive.Params { source: string }Result ExtensionSourceListing
extensions.changeOriginadminInstall an installed extension again from another source (GitHub or npm): the source must hold an extension of the same name (else the name-mismatch install error, its data an ExtensionInstallErrorData with the names found); the folder is replaced only once the new copy validated, and .install.json records the new source, which catalog.update then follows. A zidane-server takes scope: 'user' only, gated and audited as catalog.install. Additive.Params { name: string, scope: 'user' | 'project', source: string }Result { name: string, origin: ExtensionInstallOrigin, message: string, progress: string[] }

rpc#

Methods of the RPC contracts that extensions register, called by protocol clients. A client's token must reach the method's scope (default chat). Events arrive on the rpc.event signal. RPC and contracts explains the contracts.

rpc.* methods
MethodScopeDescription
rpc.callchatCalls an extension's RPC method as this connection. Never throws for a failed call: a declared error, invalid input, a missing provider or a method whose scope this connection's token does not reach (rpc.forbidden; a local connection is not limited) comes back as ok: false with its type. directory (or cwd) picks the runtime; without it, the newest runtime that has the contract. callId lets rpc.cancel abort it. Also served as POST /rpc/<contract>/<method> on a zidane-server, with the same token. Additive.Params RpcCallParams & { cwd?: string }Result RpcResponseEnvelope
rpc.cancelchatAborts this connection's rpc.call of callId; it answers rpc.aborted. false when none is in flight. Additive.Params { callId: string }Result boolean

ui#

Dialogs and UI requests from extensions, routed to the client that views the session. A client answers a dialog with ui.dialog.answer and reports what it views through presence. Placements explains the routing.

ui.* methods
MethodScopeDescription
ui.client.announcechatWhat this connection shows and whether it shows dialogs: dialogs (ui:dialog) and UI requests (ui:client) go to the client viewing the session, else the last active one. Only announced clients get them. Send again on focus or session change. Additive.Params UiClientAnnounceResult void
ui.client.replychatAnswers a ui:client request sent to this connection. false when it is no longer pending. Additive.Params { requestId: string, result: unknown }Result boolean
ui.dialog.answerchatAnswers a ui:dialog shown on this connection (answer absent: cancelled). false when it is no longer pending. Additive.Params { dialogId: string, answer?: unknown }Result boolean
ui.dialog.validatechatChecks one value of a form dialog shown on this connection: the field's error message, or null when it is valid (or the dialog is no longer pending here). Clients call it as the user edits a field and before they answer, and show the message under the field. The host checks the answer again: an answer with an invalid value makes the extension's ctx.ui.form reject, so a client must not answer until every field validates. Additive.Params { dialogId: string, key: string, value: unknown }Result string | null

references#

The extensions' text references in prose: the patterns, then the views of the keys a client found.

references.* methods
MethodScopeDescription
references.providerschatThe extension text reference providers (ctx.registerReferences) of cwd: the patterns a client matches prose with. peek: only from a registry already built for cwd (null when none is), so a list of titles across projects never builds one per project. Additive.Params { cwd: string, peek?: boolean }Result ReferenceProviderWire[] | null
references.resolvechatThe views of the keys a client found, in order: null for one that names nothing. At most 50 keys per call. Additive.Params { cwd: string, items: { providerId: string, key: string }[] }Result (ExtensionReferenceView | null)[]

badges#

badges.* methods
MethodScopeDescription
badges.actionschatThe extensions' items (ctx.registerBadgeActions) of a badge's menu in cwd; the built-in ones are the client's. Additive.Params { cwd: string, target: BadgeTarget }Result BadgeMenuItem[]
badges.runchatRuns an extension's badge item; the effect (copy, open, a notice) is the client's to apply. Additive.Params { cwd: string, source: string, id: string, target: BadgeTarget }Result BadgeActionEffect

panels#

A folder's extension side panels, run on the server. A failing panel answers its error as content, never as a JSON-RPC error. panels.watch is counted per connection: a client calls it when it shows a panel and panels.unwatch when it hides it, and a dropped connection releases its own; panel:changed goes to the watching connections only.

panels.* methods
MethodScopeDescription
panels.listchatThe side panels cwd's extensions register. A panel's load and onAction run on the host, bound to cwd and the session ctx names, with a hard timeout: a failing panel answers an empty content naming why (load) or ok: false (action), never an error. Additive.Params { cwd: string }Result PanelSummary[]
panels.loadchatOne panel's content for ctx (the session it shows beside, what opened it). Additive.Params { cwd: string, panelId: string, ctx?: PanelLoadContextInput }Result PanelContent
panels.actionchatRun one of a panel's actions (a row's or the footer's actionId, its payload). Additive.Params { cwd: string, panelId: string, actionId: string, payload?: unknown, ctx?: PanelLoadContextInput }Result { ok: true } | { ok: false, error: string }
panels.watchchatKeep a panel's live updates on: this connection gets panel:changed when its content changes. Calls count; a dropped connection releases its own. Additive.Params { cwd: string, panelId: string }Result void
panels.unwatchchatRelease one panels.watch of this connection.Params { cwd: string, panelId: string }Result void

palette#

A folder's extension command palette items, run on the server, watched like panels: palette.watch while the palette is open, palette.unwatch once it closes, palette:changed in between.

palette.* methods
MethodScopeDescription
palette.listchatThe command palette items cwd's extensions contribute (each item's source is its provider), and running one. A provider that fails lists one disabled row naming why; a failed run answers ok: false. Additive.Params { cwd: string, ctx: PaletteContext }Result PaletteItem[]
palette.runchatRun itemId on its provider (source); a nested list, a prompt or a draft comes back as result. Additive.Params { cwd: string, source: string, itemId: string, ctx: PaletteContext }Result { ok: true, result: PaletteRunResult } | { ok: false, error: string }
palette.watchchatAs panels.watch, for every provider of cwd: palette:changed. Additive.Params { cwd: string }Result void
palette.unwatchchatRelease one palette.watch of this connection.Params { cwd: string }Result void

suggest#

suggest.* methods
MethodScopeDescription
suggest.resolvechatResolve inline ghost-text autocomplete daemon-side (the typing window's request IS the single computation): canonical resolver against the session cwd's extension registry. Latest-wins per session; a seq older than the newest seen returns { suggestion: null } without computing, and a newer request aborts an in-flight compute. The result additionally fans out as suggest:changed to every OTHER peer.Params { sessionId: string, text: string, caret: number, seq: number }Result { suggestion: string | null, seq: number, candidates?: string[] }

settings#

settings.* methods
MethodScopeDescription
settings.getchatReads the host settings.Params Record<string, never>Result HostSettings
settings.setadminApplies a partial settings patch and resolves with the full settings after it.Params { patch: Partial<HostSettings> }Result HostSettings

prefs#

The picks a client remembers across apps, in the server's state.json.

prefs.* methods
MethodScopeDescription
prefs.getchatThe picks remembered across apps (state.json): effort and options per model, the next chat's model chain. null when none is. Additive.Params Record<string, never>Result HostComposerPrefs | null
prefs.setchatMerges picks into state.json (each top-level key replaces). Additive.Params { patch: HostComposerPrefs }Result void

The sidebar's views per space and the space on show, in the server's state.json: a web app finds them again in any browser. A client reads them (sidebar.getViews, sidebar.getSpace) and saves one when it changes (sidebar.setView, sidebar.setSpace); every client of the server shares them. The kit's view record also carries session, the id of the conversation last open in that space, which a client reopens when it switches back to the space; the server stores the record as given.

sidebar.* methods
MethodScopeDescription
sidebar.getViewschatThe sidebar views a client saved on this host, by space id (state.json's sidebarViews): each client checks what it reads. Additive.Params Record<string, never>Result Record<string, unknown>
sidebar.setViewchatSaves one space's sidebar view (at most 4 KiB of JSON), stored as given: the kit's record is the filter axes plus session, the conversation last open in that space. Additive.Params { spaceId: string, view: Record<string, unknown> }Result void
sidebar.getSpacechatThe space a client's sidebar showed last, null when none was saved. Additive.Params Record<string, never>Result string | null
sidebar.setSpacechatSaves the space the sidebar shows. Additive.Params { spaceId: string }Result void

presence#

presence.* methods
MethodScopeDescription
presence.listchatCurrent presence snapshot; backfill for a freshly opened session list.Params Record<string, never>Result SessionPresence[]
presence.watchchatTag the calling peer as watching a session (one per connection; the TUI opens one connection per activated session). Sent as a notification right after the handshake. Detach is implicit; the daemon recomputes presence when the socket drops.Params { sessionId: string }Result void

integrations#

Clients that run beside the host, such as the channels runner, register their state here. Every window gets integrations:changed when the list changes.

integrations.* methods
MethodScopeDescription
integrations.registerchatReport an integration this client runs (a channel). Registering a name again from the same connection replaces it; a name another live connection registered is refused. It goes away when this connection closes. Every window gets integrations:changed. Additive.Params { name: string, kind: string, state: IntegrationState, detail?: string }Result void
integrations.updatechatChange the state of an integration this connection registered; detail replaces the previous one (absent clears it). Additive.Params { name: string, state: IntegrationState, detail?: string }Result void
integrations.listchatThe integrations registered on this host, in registration order. Additive.Params Record<string, never>Result IntegrationInfo[]

host#

What the server's machine can do: its OS, the tools on its PATH, the sandbox, the environment kinds and what it allows. The answer is cached for a minute; refresh: true probes again.

host.* methods
MethodScopeDescription
host.capabilitieschatWhat this host can do: OS, tools on PATH, sandbox, environment kinds, what it allows. Cached a minute; refresh probes again.Params { refresh?: boolean }Result HostCapabilities
host.installTooladminInstall a missing tool on this host, as the host's user, when it can without root (toolInstall says run). Then probes again: capabilities is the new answer.Params { name: string }Result { ok: boolean, output: string, capabilities: HostCapabilities }

server#

The server's self-update: its state, the setting and the newest release seen. Any token reads it (server.update.status); turning it on or off, checking now and installing now need admin. With it off, server.update.check downloads and verifies a newer release and reports state: 'ready'; server.update.install installs it.

server.* methods
MethodScopeDescription
server.update.statuschatA zidane-server's self-update: its state, the setting, the newest release seen.Params Record<string, never>Result ServerUpdateStatus
server.update.configureadminTurn the zidane-server's self-update on or off.Params { enabled: boolean }Result ServerUpdateStatus
server.update.checkadminCheck for a newer zidane-server release now. With auto-update on, a newer one installs; off, it is downloaded and verified, and the state turns ready.Params Record<string, never>Result ServerUpdateStatus
server.update.installadminInstall the release that is ready or waiting now, without waiting for background work to finish.Params Record<string, never>Result ServerUpdateStatus

fs#

The folders of the server's workspace roots, for a folder picker: list them, search them by name (bounded), and create one (allowCreateFolders). Each takes a chat token. A path outside the roots is refused.

fs.* methods
MethodScopeDescription
fs.rootschatWhere this host lets a session's folder be: a server's workspace roots (confined: sessions.create / setFolder refuse anything outside), or the user's home on a local host (unconfined). Additive.Params Record<string, never>Result FsRoots
fs.listDirschatThe folders in path (default: the first root), for a folder picker on the host's machine. Only inside the roots (real paths: .. and symlinks out are refused), no dotfiles. Additive.Params { path?: string }Result FsDirListing
fs.mkdirchatCreates folder name in parent (inside the roots, real paths: .. and symlinks out are refused). name is one path segment and does not start with a dot. Refused when the host turns it off (a zidane-server's allowCreateFolders, the hello's createFolders). Additive.Params { parent: string, name: string }Result FsDirEntry
fs.searchDirschatThe folders whose name holds query (case-insensitive), in path and below it (default: every root). Bounded: four levels down, 200 results, no dot folders, no node_modules, symlinks not followed. truncated: the result or read bound cut the search short. Additive.Params { query: string, path?: string }Result FsDirSearch

git#

A folder's git checkout, inside the server's workspace roots. The writes git.checkout, git.createBranch and git.sync need an admin token. git.clone needs a chat token, and the server refuses it while allowClone is off. A clone's progress comes as git:clone:progress signals to the connection that started it. A connection that calls git.watch gets git:refs for that folder once its refs move (a commit, a fetch, a branch switch, from any process on the server), until it calls git.unwatch or drops. git.commit reads one commit's facts, for a transcript's SHA chip.

git.* methods
MethodScopeDescription
git.statuschatWhere the checkout stands (branch, upstream, ahead / behind, uncommitted changes); null outside a checkout.Params { cwd: string }Result GitInfo | null
git.brancheschatThe local branches, most recently committed first, each against the default branch.Params { cwd: string }Result GitBranchRow[]
git.pullRequestschatOpen pull requests (gh): waiting on you, yours, then recent others; search searches every open one. null without gh (or signed out); rateLimited while GitHub refuses and nothing is cached.Params { cwd: string, search?: string, fresh?: boolean }Result GitPrListResult
git.branchPullRequestchatbranch's open (or draft) pull request, or null. fresh: past the cache.Params { cwd: string, branch: string, fresh?: boolean }Result GitPr | null
git.commitchatOne commit's facts, for a sha chip; null when the repository has no such commit.Params { cwd: string, sha: string }Result GitCommitInfo | null
git.fileStatuschatEach changed path's status against HEAD, untracked files included.Params { cwd: string }Result GitStatusEntry[]
git.diffchatThe working tree against HEAD as a unified patch (untracked files as new files), the whole checkout or one path, with context lines (absent: 3; null: whole files).Params { cwd: string, path?: string, context?: number | null }Result string
git.checkoutadminSwitches the checkout to a branch or a pull request ({ pr }, through gh). Never forced; refused while an agent runs in the checkout (sessionId: the asking session, counted too). A branch another worktree holds is git's refusal plus that worktree and its sessions (sessionId left out).Params { cwd: string, target: GitSwitchTarget, sessionId?: string }Result GitCheckoutResult
git.createBranchadminStarts branch name from the current checkout and switches to it; refused while an agent runs in it.Params { cwd: string, name: string, sessionId?: string }Result GitCheckoutResult
git.syncadminPulls (merging) then pushes the current branch, or publishes it without an upstream; refused while an agent runs in it.Params { cwd: string, sessionId?: string }Result GitSyncResult
git.clonechatClones input (a git or forge URL, or owner/repo) into a new folder of parent (inside the roots). A folder already holding that repository is answered as existed. One clone per connection; git's progress comes as git:clone:progress signals to the caller only. A dropped connection cancels it. Refused when the host turns cloning off (a zidane-server's allowClone, the hello's clone).Params { input: string, parent: string }Result RepoCloneResult
git.cloneCancelchatCancels this connection's clone: the half-cloned folder is removed and git.clone answers cancelled.Params Record<string, never>Result void
git.watchchatFollows cwd's checkout: once its refs move (a commit, a fetch, a push, a branch switch, from this host or any other process), this connection gets git:refs for cwd. false: not a repository, nothing watched. Calls count: each git.watch takes one git.unwatch; a dropped connection releases its own. Additive.Params { cwd: string }Result boolean
git.unwatchchatReleases one git.watch of cwd by this connection.Params { cwd: string }Result void

files#

A folder's files beyond a session's workspace: search in files, what names in prose point at (files.resolvePaths, for a transcript's path chips), the project's own diagnostics (files.diagnostics runs project tools: admin), and its media. Diagnostics are cached until the files move; files.diagnosticsInvalidate drops a folder's cache so the next run checks again, and files.diagnosticsTools lists the tools and whether each runs there, for a settings list.

An <img> or <video> can't send a token, so media go over HTTP under a ticket: the client picks a random one (at least 32 URL-safe characters) and registers it with files.mediaTicket on each connection; it is good while that connection is open. GET /zidane/media?ticket=…&cwd=…&path=… serves a file of a folder in the workspace roots, &side=old or &side=new one side of its git change (the repository must lie in the roots too). Only image, video and audio types are served, with byte ranges, at most 64 MiB per response (a longer range is cut short, a larger whole file refused with 413), never sniffed into another type (X-Content-Type-Options: nosniff) and sandboxed if opened as a document. A missing or dropped ticket is 401, anything outside the roots 404.

files.* methods
MethodScopeDescription
files.searchchatLines containing query (fixed string, at most 500 characters) under cwd: ripgrep, else git grep, else a walk. A later search with the same searchId on the connection cancels this one.Params { cwd: string, query: string, caseSensitive?: boolean, searchId?: string }Result ContentSearchResult
files.resolvePathschatWhat names in prose point at (files, folders, else branches), in order: what a transcript's path chips open.Params { cwd: string, texts: string[] }Result ResolvedPath[]
files.listchatThe files under cwd and the folders holding them, as @ completes them: git's list (ignore rules kept), else a walk; capped.Params { cwd: string }Result FileEntry[]
files.locationchatWhere cwd sits: its git checkout (the folder itself outside git) and its project (the main checkout for a worktree).Params { cwd: string }Result FolderLocation
files.diagnosticsadminRuns the project's own linters and typecheckers on paths (relative to cwd), cached until the files move. disabled: tool ids switched off; timeoutMs: per tool.Params { cwd: string, paths: string[], disabled?: string[], timeoutMs?: number }Result DiagnosticsResult
files.diagnosticsInvalidatechatDrops the cached diagnostics of cwd: the next run checks again.Params { cwd: string }Result void
files.diagnosticsToolschatThe diagnostics tools and whether each runs for cwd (configured, missing its binary, not used).Params { cwd: string }Result DiagnosticsToolInfo[]
files.mediaTicketchatLets this connection's media play from GET /zidane/media (SERVER_MEDIA_PATH): ?ticket&cwd&path serves a file of a folder in the workspace roots, &side=old|new one side of its git change, with byte ranges, for an image, a video or audio only. The client picks the ticket (at least 32 random URL-safe characters) so an address is known before the call lands; it is good while this connection is open and carries its token's scope. One per connection: a new one replaces it. Only a zidane-server serves media. Additive.Params { ticket: string }Result void

usage#

The welcome screen's numbers over the server's session store, as the desktop app sums its own. Days are the server's local days.

usage.* methods
MethodScopeDescription
usage.summarychatToken spend, models, streaks and activity over range, from the host's session store, as the desktop app's welcome screen shows its own (summarizeUsage). Days are the host's local days; userName is null (the host's user is not the reader). Refused by a store that keeps no usage history (not a SQLite file). Additive.Params { range: UsageRange }Result UsageSummary
usage.providerschatThe plan and subscription windows of each provider the host holds a credential for (every account), probed with the host's credentials and cached a few minutes; refresh probes past the cache. A provider that can't be read reports status: 'unavailable'. Additive.Params { refresh?: boolean }Result readonly ProviderUsageSnapshot[]
usage.githubchatGitHub's API limits as the host's GitHub client last saw them, per GitHub host; probe asks GitHub (/rate_limit, which costs no call). Additive.Params { probe?: boolean }Result ProviderUsageSnapshot[]

auth#

auth.remove is also a sign-out: it drops one stored credential, or all of them.

A browser can't listen on a loopback port for an OAuth redirect, so a browser client signs in to an MCP server through the server: it passes redirectUri as <origin>/zidane/oauth/callback, the origin it reaches the server by. The server registers that redirect, takes the code at that address itself, and answers begin with mode: 'browser'. The client opens url and calls auth.oauth.complete with no code; it resolves once the token exchange is done. A server whose pre-registered client pins its redirect stays 'loopback': the user pastes the address of the page that failed to load (complete with input). The code is matched to the flow by its state, which is random and single-use. An MCP server's stored client registered for another redirect registers again.

auth.* methods
MethodScopeDescription
auth.statuschatThe host's provider credentials, as the host's agents use them (its data dir's credentials.json and environment): per provider, whether something is configured, its stored API key / OAuth accounts (labels, which is active) and an env var it reads. Never a secret. Additive.Params Record<string, never>Result { providers: HostProviderAuth[] }
auth.setApiKeyadminStore an API key for one of the host's providers as its active credential, apply it to the host's environment, and rebuild agents (idle now, busy ones at run end). Returns the provider's new status. OAuth sign-in goes through auth.oauth.*. Additive.Params { provider: string, key: string }Result HostProviderAuth
auth.removeadminRemove one stored credential (account: its key from auth.status) or, without one, every stored credential of the provider; then as auth.setApiKey. A key the host only has from its own environment is left alone. Additive.Params { provider: string, account?: string }Result { removed: boolean, provider: HostProviderAuth }
auth.configureadminStores a provider's API key and its custom fields (a local server's URL, a cloud's region; onboarding.providers lists them), merged into the stored credential: a field left out keeps its value, an empty one is removed (a blank secret keeps the stored one), a required one may not be emptied, no value keeps the stored key. Additive.Params { provider: string, value?: string, customFields?: Record<string, string> }Result HostProviderAuth
auth.setPreferredadminMake one of a provider's stored credentials the active one (target: an account key from auth.status, or the first stored one of a source), then as auth.setApiKey. A credential no longer stored is refused. Additive.Params { provider: string, target: 'apikey' | 'oauth' | { key: string } }Result HostProviderAuth
auth.renameadminRename a stored OAuth account (account: its key from auth.status). Presentation only: nothing is rebuilt. Additive.Params { provider: string, account: string, label: string }Result HostProviderAuth
auth.oauth.beginadminOAuth sign-in with the browser on the CLIENT and the tokens on the host: the host starts a provider's login (provider) or an MCP server's (mcpServer, found in cwd's catalog, as catalog.mcps), keeping the PKCE verifier and state, and answers with the authorization URL. mode: 'loopback': the client listens at redirectUri (a provider's fixed port; for an MCP server the loopback URI the client passed) and forwards the code; 'browser': the client opens url (pasting a code if the page shows one); 'device': shows userCode. The flow belongs to this connection, is single-use and expires (expiresAt). Additive. A client that can't listen (a browser) signs in to an MCP server through a zidane-server's own redirect: redirectUri is the server's /zidane/oauth/callback (SERVER_OAUTH_CALLBACK_PATH) at the origin the client reaches it by. The server takes the code there and the flow is 'browser' (the client opens url, then completes without a code). A server whose pre-registered client pins its redirect stays 'loopback': its page fails to load and the user pastes that page's address (complete with input). account (a provider's only): the stable id of a stored OAuth account (AuthFailure.accountId) this sign-in re-authenticates. Its tokens replace that account's in place (same slot, id and name), instead of becoming a new active account. An id the host does not store is refused. Additive.Params { provider?: string, account?: string, mcpServer?: string, cwd?: string, kind?: string, redirectUri?: string }Result HostOAuthBegin
auth.oauth.completeadminDeliver the code (code + state from the loopback, or input: what the user pasted) once, then wait for the host's token exchange; without either, just wait (device and polling flows). A wrong state or another connection's flow is refused. Tokens stay on the host: the result says what changed, never a secret.Params { flowId: string, code?: string, state?: string, input?: string }Result HostOAuthComplete
auth.oauth.respondadminAnswer the step a flow is waiting on (HostOAuthBegin.step before its URL, HostOAuthComplete.step after): a select's option id (none: cancel), a prompt's text. Answers with where the flow is now: its URL, or the next step. Then complete as before. Additive.Params { flowId: string, stepId: string, value?: string }Result HostOAuthBegin
auth.oauth.canceladminAbandon a flow of this connection.Params { flowId: string }Result { cancelled: boolean }

catalog#

catalog.* methods
MethodScopeDescription
catalog.skillschatWhat the host discovered for the settings tabs, each entry with whether the host's settings enable it (an unset allowlist enables everything). cwd adds the project scope of that folder (inside the workspace roots on a server); without it, the user scope (the host user's home). The enabled lists are written with settings.set. Installing, removing and MCP sign-in stay on the host's machine. Additive.Params { cwd?: string, kind?: string }Result { skills: HostCatalogSkill[] }
catalog.mcpschatMCP servers: env and headers values, URL credentials and query values are redacted.Params { cwd?: string, kind?: string }Result { servers: HostCatalogMcp[], errors: HostCatalogError[] }
catalog.extensionschatExtensions installed for cwd.Params { cwd?: string, kind?: string }Result { extensions: HostCatalogExtension[] }
catalog.pluginschatPlugins installed for cwd, with what each contributes.Params { cwd?: string, kind?: string }Result { plugins: HostCatalogPlugin[] }
catalog.installadminInstall a skill or an extension on the host, in its user's scope only (scope may only be 'user'): a GitHub (owner/repo[#ref][:dir], a GitHub URL, a skills.sh URL) or npm spec; paths are refused. skills / all pick from a multi-skill source (the multiple-candidates error lists them), extension from a multi-extension one (an extension's multiple-candidates or no-extension error carries the names found as ExtensionInstallErrorData). Runs the host's package managers and fetches code there: a zidane-server's allowRemoteInstalls gates it, every attempt is audited. Bounded in time; the progress lines come back with the result. Every window gets catalog:changed. Additive (remoteInstalls on ipc.hello).Params { kind: HostInstallKind, spec: string, scope?: 'user', skills?: string[], all?: boolean, extension?: string, overwrite?: boolean }Result { installed: string[], message: string, progress: string[] }
catalog.removeadminRemove a user-scope skill or extension of the host by name (its directory). As catalog.install.Params { kind: HostInstallKind, name: string }Result { removed: string, message: string }
catalog.updateadminUpdate a user-scope extension installed from a source. As catalog.install.Params { kind: HostInstallKind, name: string }Result { updated: string, upToDate: boolean, message: string }

skills#

skills.* methods
MethodScopeDescription
skills.browseadminThe skills of a source (owner/repo[#ref][:dir], a GitHub or skills.sh URL), fetched by the host with its GitHub login; catalog.install with skills installs picked ones. Additive.Params { source: string }Result SkillSourceListing

onboarding#

What the setup wizard finds on the server's machine and the providers it lists, with their custom fields (stored with auth.configure).

onboarding.* methods
MethodScopeDescription
onboarding.detectchatWhat the setup wizard finds on the host's machine for cwd (default: the host user's home): the providers and their credentials, other agents to import from, local model servers and the install signals. Additive.Params { cwd?: string }Result OnboardingDetection
onboarding.providerschatThe providers as the wizard's Connect step lists them, with their custom fields and the model each runs when none is picked. Additive.Params { cwd?: string }Result HostOnboardingProvider[]
onboarding.completeadminThe setup wizard's write: settings.set sets (settings.reset back to their defaults, unless also set), as settings.set applies them, and records (the run finished, its steps) merged into the host's stored ones. Returns the settings in effect. Additive.Params { settings: OnboardingSettingsChange, records: OnboardingState }Result HostSettings

claudeImport#

A Claude Code setup in the server user's ~/.claude: claudeImport.detect says whether there is one, claudeImport.preview what each piece would copy and skip, and claudeImport.run imports it. The import copies commands the server will run: admin, behind allowRemoteInstalls.

claudeImport.* methods
MethodScopeDescription
claudeImport.detectadminWhat ~/.claude on the host holds: whether there is a setup, and its transcripts for cwd and in every project. Additive.Params { cwd?: string }Result ClaudeImportDetection
claudeImport.previewadminWhat each piece of the Claude Code import would copy and skip. Additive.Params { cwd?: string }Result OnboardingImportPreview
claudeImport.runadminImports a Claude Code setup into the host's own places: conversations into its session store, MCP servers, skills, commands and subagents into the host user's files. Idempotent (what is there is skipped, never overwritten). It copies commands the host will run: a zidane-server's allowRemoteInstalls gates it, every attempt is audited. Every window gets catalog:changed. Additive.Params { cwd?: string } & ClaudeImportSelectionResult ClaudeImportSummary

mcp#

catalog.mcps rows carry each server's status: its sign-in state, its tools as last listed and its last Test. A stdio row always says where it runs (scope: host or context), and shared comes from its config. An absent shared means shared, and the mcpSharing setting wins over it. mcp.upsert writes the server user's mcps.json, or with scope: 'project' the project of cwd. An Advanced OAuth client secret goes in clientSecret. The server keeps it in its MCP credential store, never in mcps.json.

mcp.* methods
MethodScopeDescription
mcp.upsertadminAdd or replace an MCP server in the host user's mcps.json, or with scope: 'project' in cwd's project (a folder inside the workspace roots on a server). Its env / headers values are stored there and never come back (catalog.mcps redacts them, the reply names the server only). clientSecret, the secret of its oauth client, goes to the host's MCP credential store, never to mcps.json; an entry without an oauth client drops a stored one. As catalog.install.Params { config: McpServerConfig, scope?: 'user' | 'project', cwd?: string, clientSecret?: string }Result { name: string, created: boolean }
mcp.removeadminRemove an MCP server from the host user's mcps.json; removed: false when it has none. As catalog.install.Params { name: string }Result { removed: boolean }
mcp.testchatConnect to one of cwd's MCP servers (as catalog.mcps), list its tools into the host's tools cache and record how it went: the server's new status. Every window gets catalog:changed. Additive.Params { name: string, cwd?: string, kind?: string }Result McpServerStatus
mcp.signOutadminForget an MCP server's sign-in on the host (its tokens). Every window gets catalog:changed. Additive.Params { name: string }Result void
mcp.instanceschatThe host's shared MCP server processes (src/mcp/server-pool.ts), cwd's only when given: name, folder, shared or per session, how many sessions hold it, its pid, whether it is retired. Remote servers and scope: 'context' ones are never listed (they are per session). Additive.Params { cwd?: string }Result McpServerInstance[]
mcp.restartadminEnd name's process now (in cwd only, when given) for every session holding it. Calls in flight fail with "<name> restarted" and are not retried; the next call starts a fresh process. Answers how many processes were restarted (0: none was running). Additive.Params { name: string, cwd?: string }Result { restarted: number }

models#

models.* methods
MethodScopeDescription
models.listchatThe host's credentialed providers (what its agents can run) and their models, for a model picker of a session on that host. cwd adds the providers its extensions contribute there. Additive.Params { cwd?: string, kind?: string }Result { providers: HostModelProvider[] }
models.testchatSend one tiny prompt with this host's credentials, to check a chat model answers. model is "providerKey[@account]/modelId", tested exactly. Never fails: a broken model comes back as ok: false with a reason. Additive.Params { cwd?: string, kind?: string, model: string }Result HostModelTestResult

decisions#

decisions.* methods
MethodScopeDescription
decisions.testchatSend one tiny decision with this host's credentials, to check a decision model works. model is "providerKey/modelId", tested exactly; omitted = the host's Settings.decisionModel (or the first available). Never fails: a broken model comes back as ok: false with a reason. Additive.Params { cwd?: string, kind?: string, model?: string }Result HostDecisionTestResult

machines#

machines.* methods
MethodScopeDescription
machines.listchatThe machines this host knows (docs/MACHINES.md), online or not, and forgetting one (it disconnects; its local@ / worktree@ kinds stop resolving). Additive.Params Record<string, never>Result MachineInfo[]
machines.forgetadminForgets a machine. It disconnects and its kinds stop resolving.Params { machineId: string }Result { forgotten: boolean }
machines.listDirschatFolders of a machine, for a client's folder picker: the host proxies a listing to the machine (fs.list, confined to its roots there): the folders in path (absent: its first root), and every root. Additive.Params { machineId: string, path?: string }Result { path: string, dirs: string[], roots: string[] }
machines.share.startadminA local host's machine links (docs/MACHINES.md § Client UI), behind remote hosts: share this computer with a saved server (the machine agent runs in this host's process, with the machine token token or the one already stored for that server), stop sharing, use a saved server as a machine (reverse direction), and read their state. Refused while remote hosts are off. Additive.Params { hostId: string, name: string, roots: string[], token?: string }Result MachineLinksStatus
machines.share.stopadminStops sharing this computer with a server.Params { hostId: string }Result MachineLinksStatus
machines.useServeradminTurns on or off using a server as this computer's linked server.Params { hostId: string, enabled: boolean }Result MachineLinksStatus
machines.links.statuschatState of this host's machine links: the shares it offers and the servers it uses.Params Record<string, never>Result MachineLinksStatus

machine#

machine.* methods
MethodScopeDescription
machine.attachadminReverse direction (docs/MACHINES.md § Reverse direction): a client asks this server to lend itself as a machine over this very connection (allowAsMachine on, else refused). hostId names the asking host, so a reconnect finds its tasks and terminals again. Then machine.call carries each host→machine request (MachineMethods), answered by the server's confined executor; machine notifications come back as machine:notification signals on this connection only.Params { hostId: string }Result MachineRegisterParams
machine.calladminRuns one machine-protocol method on this server's executor and resolves with its JSON result. Refused when the server does not lend itself as a machine.Params { method: string, params?: JsonValue }Result JsonValue
machine.detachadminEnds this connection's machine attachment. detached is false when it had none.Params Record<string, never>Result { detached: boolean }

tokens#

tokens.* methods
MethodScopeDescription
tokens.createMachineTokenadminMint a machine-scoped token on this server (a zidane-server only), so a window can share its computer in one step. An admin token may (./scopes). Audited.Params { name: string }Result { id: string, name: string, token: string }
tokens.listadminThis server's tokens, revoked ones included, never their secrets (a zidane-server only). Additive.Params Record<string, never>Result ServerTokenInfo[]
tokens.createadminCreate a token with a scope (./scopes; machine lends a computer). The secret is in the result only: show it once. Bound to the calling token (createdBy), so revoking that one revokes this one too. Audited. Additive.Params { name: string, scope: ServerTokenScope }Result ServerTokenInfo & { token: string }
tokens.revokeadminRevoke a live token by id, with the live tokens it minted; their connections drop and the runs and terminals they started stop. Audited. Additive.Params { id: string }Result { revoked: ServerTokenInfo[] }
tokens.revokeSelfchatRevoke the token this connection authenticated with, and the live tokens it minted (a browser's sign out). Any client scope may: it only gives up its own access. Audited. Additive.Params Record<string, never>Result { revoked: ServerTokenInfo[] }
tokens.pairadminIssue a one-time pairing code (XXXX-XXXX, a zidane-server only): a browser posts it to POST /zidane/pair and gets a token of scope, named browser: <name> and bound to the calling token (createdBy). ttlMs: 30000 to 3600000, default 600000. Single use; five wrong secrets burn it. Audited. Additive.Params { scope: ClientScope, ttlMs?: number, name?: string }Result { code: string, expiresAt: number }
tokens.machineInviteadminIssue a one-time machine join code (a zidane-server only): a machine posts it to POST /zidane/pair and gets a machine token named name. A public server caps ttlMs at 10 minutes. Audited as pair.create with scope machine. Additive.Params { name?: string, ttlMs?: number }Result { code: string, expiresAt: number }

logs#

A zidane-server's recent log records: its own lines and its extensions' (setup, runtime warnings), from a bounded in-memory buffer. Admin only. logs.watch follows the log (logs:record for each new record) until the client calls logs.unwatch or drops; calls count per connection.

logs.* methods
MethodScopeDescription
logs.listadminA zidane-server's recent log records, oldest first: its own lines and its extensions' (setup, runtime warnings), from a bounded in-memory buffer. Other hosts refuse it. Additive.Params Record<string, never>Result { records: LogRecord[] }
logs.watchadminFollow the log: this connection gets logs:record for each new record. Calls count; a dropped connection releases its own. Additive.Params Record<string, never>Result void
logs.unwatchadminRelease one logs.watch of this connection.Params Record<string, never>Result void

perf#

The host's own performance, for a client's Settings › Performance on it: the host process as the root of its tree, what its sessions launched, its event loop and, on macOS, the GPU. Admin only. perf.watch sends perf:sample every 2 s until the client calls perf.unwatch or drops; calls count per connection, and the host samples only while a connection watches. perf.threads answers only for a process the host lists.

perf.* methods
MethodScopeDescription
perf.watchadminFollow the host's own performance (Settings › Performance on a server): this connection gets perf:sample every 2 s. The host samples only while a connection watches. Calls count; a dropped connection releases its own. Additive.Params Record<string, never>Result void
perf.unwatchadminRelease one perf.watch of this connection. Additive.Params Record<string, never>Result void
perf.historyadminThe samples the host holds (the last 5 minutes of watching), oldest first. Additive.Params Record<string, never>Result { samples: PerfSample[] }
perf.threadsadminThe OS threads of one of the processes the host lists (its own tree), busiest first. Refuses any other pid. Additive.Params { pid: number }Result { threads: PerfOsThread[] }

sync#

sync.* methods
MethodScopeDescription
sync.statusadminSettings sync, a client's computer → this server (docs/SERVER.md § Settings sync). A zidane-server only; other hosts refuse. One source device per server: sync.claim makes the caller it (or says who is, unless takeover), sync.push applies one category's snapshot through the host's own setters, skipping fields edited here since (overrides), sync.reset drops an override, sync.release stops the source. Every call is audited with its token. Additive.Params Record<string, never>Result SyncStatus
sync.claimadminMakes the caller the settings source device. When another device is the source, the result says which, unless takeover is set. categories and keys narrow what it syncs.Params { deviceId: string, deviceName: string, takeover?: boolean, categories?: SyncCategory[], keys?: string[] }Result SyncClaimResult
sync.pushadminApplies one category's snapshot from the source device.Params { deviceId: string, category: SyncCategory, payload: SyncPayload }Result SyncPushResult
sync.resetadminDrops the local override of one field so sync applies to it again.Params { field: string }Result { reset: boolean }
sync.releaseadminStops the source device's sync.Params { deviceId: string }Result { released: boolean }

daemon#

daemon.* methods
MethodScopeDescription
daemon.infochatDaemon identity and version: protocolVersion, appVersion, pid, startedAt and the host identity fields of the hello result.Params Record<string, never>Result HelloResult
daemon.ownedProcesseschatThe processes the daemon started for its own work (MCP servers, bounded tool runs such as git and linters), which lead their own process group like a session's command does. The desktop app's performance monitor counts them as the app's. Additive: an older daemon answers "method not found", read as none.Params Record<string, never>Result OwnedProcess[]
daemon.sessionProcesseschatThe processes the daemon started for a session (its shell tool's commands and background tasks, its terminals, its own MCP servers), by session id. The desktop app's performance monitor shows what each session uses. Additive: an older daemon answers "method not found", read as none.Params Record<string, never>Result SessionProcess[]
daemon.shutdownadminGraceful drain: deny pending approvals, abort live runs, close the socket. Used by clients that detect a protocol-version mismatch and need the stale daemon gone before respawning. whenIdle (version-skew retirement): a daemon with active work does NOT abort it. It answers { deferred: true } and exits once idle. Explicit stops (zidane daemon stop) omit it and drain immediately. Daemons predating the flag ignore it and drain immediately.Params { whenIdle?: boolean }Result { deferred?: boolean } | void