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) orballet.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¶
Two API styles to document and test; stream replay requires a bounded event buffer (backed by the orchestration event log, ADR-0016: Durable Orchestration in the Database).
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.