ADR-0018: REST for Stateless, WebSocket + JSON-RPC + MessagePack for Stateful

Status:

Accepted

Date:

2026-10-01

Context

Ballet’s interfaces carry two kinds of traffic:

  • Stateless operations: reading and changing tickets, projects, pipelines, skills, knowledge entries; queries for lists, reports and usage. Request/response, cacheable, easy to script.

  • Stateful interactions: planner chat and sub-chats, live session output, inbox and board updates, Runner ↔ Core control (start/stop sessions, stream logs, report status). Long-lived, bidirectional, latency-sensitive, often high-volume.

Long-running unattended operation needs prompt detection of dead connections, dead Runners and dead sessions (Unattended Operation).

Decision

REST (JSON over HTTP) for stateless operations — UI ↔ Core, UI ↔ Knowledge, and external automation.

WebSocket carrying JSON-RPC 2.0 messages encoded with MessagePack for stateful interactions — UI ↔ Core and Runner ↔ Core:

  • JSON-RPC requests/responses for commands (chat.send, session.cancel); JSON-RPC notifications for server push (session.output, ticket.changed, question.raised).

  • Clients subscribe to streams (a session’s output, a project’s board). Every stream event carries a monotonically increasing sequence number.

  • Heartbeats in both directions as JSON-RPC notifications ($/heartbeat) at a fixed interval, carrying the sender’s time and last seen sequence. A peer that misses a configurable number of heartbeats is considered gone; the connection is closed and reconnection begins. Application-level heartbeats are used because browsers cannot send WebSocket ping frames.

  • On reconnect, the client resumes each subscription from its last sequence number; if the server can no longer replay that far, it answers resync required and the client reloads the snapshot via REST. REST provides snapshots, WebSocket provides deltas.

  • The WebSocket subprotocol selects the encoding: ballet.v1.msgpack (default) or ballet.v1.json (same messages as text, for debugging and tooling).

  • Authentication: the first message carries the access token (browsers cannot set WebSocket headers); tokens are refreshed in-band before expiry.

Runner heartbeats also carry the liveness of each session it hosts; Core uses them for stuck detection and to reconcile runs after a Runner loses contact.

Agent-facing interfaces stay on MCP (its own transports); LLM traffic stays on the providers’ HTTP APIs through the gateway.

Alternatives Considered

REST plus Server-Sent Events

Advantages:

  • Simple; plain HTTP; automatic browser reconnection.

Disadvantages:

  • One-directional; chat and Runner control need a second channel; text-only.

gRPC (streaming)

Advantages:

  • Typed contracts, efficient binary encoding, bidirectional streams.

Disadvantages:

  • Needs gRPC-Web and a proxy in the browser; heavier toolchain for the Svelte UI.

WebSocket with JSON only

Advantages:

  • Human-readable on the wire.

Disadvantages:

  • Larger and slower for high-volume log and output streams. Kept as the debugging subprotocol.

Decision Criteria

Bidirectionality, efficiency for streaming, browser support, robustness of long-lived connections, debuggability.

Rationale

REST keeps the bulk of the API simple, cacheable and scriptable. WebSocket covers all bidirectional traffic with one connection; JSON-RPC gives a minimal, well-known message envelope; MessagePack keeps streaming efficient while the JSON subprotocol preserves debuggability. Sequence numbers and heartbeats make long-lived connections recoverable and observable.

Consequences

Positive

  • One stateful protocol for UI and Runner; fast failure detection; lossless resume.

Negative

Risks

  • WebSocket connections through proxies and load balancers need idle timeouts above the heartbeat interval.

Follow-up

  • Specify REST contracts (OpenAPI) and the JSON-RPC method catalogue.

  • Choose heartbeat interval and miss threshold defaults.

References