SPEC-0002: Daemon Protocol (Control Plane + Attach Stream)
Overview
One framed, length-prefixed message protocol over the local Unix socket
($XDG_RUNTIME_DIR/harness.sock) carrying two kinds of traffic multiplexed on
a single connection: control (structured request/response plus a
state-change subscription) and attach (opaque terminal byte streams).
Remote clients never speak a separate wire protocol — the Wish/SSH session runs
the TUI inside the daemon, and that TUI is a local client of this same contract
(📝 ADR-0004). See design.md for framing details and rationale.
Requirements
Requirement: Message Framing
Every message SHALL be framed as uint32 length (big-endian) + uint8 type +
payload, with types HELLO, CONTROL_REQ, CONTROL_RESP, EVENT,
ATTACH_OPEN, ATTACH_DATA, ATTACH_RESIZE, ATTACH_CLOSE, PING/PONG,
and ERROR. Control payloads SHALL be JSON; attach payloads SHALL be raw bytes
tagged with a session_id so multiple attach sessions can share one
connection.
Scenario: Mixed traffic on one connection
- WHEN a client holds an attach session open and issues a control request
- THEN both flow over the same connection without corrupting either stream
Requirement: Handshake And Versioning
A client SHALL open with HELLO { proto_version, client_version, wants } and
the daemon SHALL reply HELLO { proto_version, daemon_version, capabilities }.
The same proto_version major is REQUIRED; on mismatch the daemon SHALL return
a clear ERROR ("client too old/new; daemon proto vN") rather than garbling.
Scenario: Old client, upgraded daemon
- WHEN a client with an older proto major connects
- THEN the daemon responds with a structured version-mismatch ERROR and closes cleanly
Requirement: Control Operations
The control plane SHALL mirror the CLI verbs and the TUI 1:1 (📝 ADR-0002):
list, describe, start, stop, restart, logs, profiles,
use_profile, reload, daemon_info, and the scheduled-run ops jobs,
trigger and runs (SPEC-0008 REQ "Protocol Operations"). Operations SHALL be idempotent
where that makes sense (double-start is a no-op). Errors SHALL come back as
structured ERROR frames with a code and a human message. A response that
carries raw terminal output SHALL also carry the viewport that output was drawn
at, so a client replaying it through an emulator reconstructs the screen instead
of reflowing it.
Scenario: Idempotent start
- WHEN
startis issued for a harness alreadyrunning - THEN the daemon replies success without disturbing the process
Scenario: Structured failure
- WHEN a control request references an unknown harness
- THEN the client receives an
ERRORwith a machine code and a human message the TUI/CLI can surface verbatim
Scenario: Replayable log tail
- WHEN a client requests
logsfor a harness whose PTY has an authoritative viewport (📝 ADR-0003 smallest-attached-wins) - THEN the reply carries that viewport (
cols/rows) alongside the text, so the tail reconstructs at the geometry it was drawn at rather than the client's own
Scenario: Structured run activity
- WHEN a client requests
logswitheventsset for a harness whose adapter records a native trajectory - THEN the reply describes one run — the latest, or the run bounded by the
request's
since/until— as the supervisor's lifecycle lines interleaved with the agent events SPEC-0006 REQ "Run Correlation" attributes to that run, lists the sessions correlation excluded, and works whether or not the harness is still running - AND when no agent activity is attributable the reply carries notices
saying why, and points at
--raw— it SHALL NOT carry the durable log, whose tail for a full-screen agent is a screenshot of its idle TUI - AND a harness whose adapter records no native trajectory answers with the
durable log tail and no
source, as a daemon predating the field would
Scenario: Raw log tail is unchanged
- WHEN a client requests
logswithoutevents - THEN the reply is the durable log tail exactly as before, which the TUI
peek pane and
harness logs --rawdepend on
Scenario: Run-scoped logs
- WHEN a client requests
logswithrunset to a run id of a scheduled harness - THEN the raw reply is that run's own log, and the
eventsreply describes exactly that run record's window - AND a run id the harness's history does not hold is an
unknown_runERROR
Requirement: Event Subscription
After a HELLO that includes wants: ["events"], the daemon SHALL push
EVENT frames on state changes (harness_state_changed, harness_exited,
harness_flapping, config_reloaded, profile_changed, and the scheduled-run
events job_run_started, job_run_finished and job_schedule_changed of
SPEC-0008 REQ "Lifecycle Events") so the TUI re-renders
reactively without polling. One-shot CLI invocations MAY skip the
subscription entirely.
Scenario: Reactive dashboard
- WHEN a harness crashes while a subscribed TUI is on the dashboard
- THEN the TUI receives
harness_exitedandharness_state_changedwithout issuing any request
Requirement: Attach Session
ATTACH_OPEN { name, cols, rows, mode } SHALL allocate a session_id and the
daemon SHALL then send, in order: a screen snapshot (repaint of the current
x/vt screen), a bounded tail of scrollback, and the live stream as
ATTACH_DATA frames. Client keystrokes flow back as ATTACH_DATA and SHALL be
ignored for ro sessions (📝 ADR-0008). ATTACH_RESIZE SHALL apply the
smallest-attached-client-wins policy (📝 ADR-0003). ATTACH_CLOSE from either
side SHALL tear down only that session — the harness and other attached
sessions are untouched.
Scenario: Instant repaint on attach
- WHEN a client attaches to a running harness
- THEN it receives a full screen snapshot first, so the terminal is correct before any live bytes arrive
Scenario: Read-only attach
- WHEN a session opened with
mode: "ro"sends keystrokes - THEN the daemon discards them and the PTY never sees the input
Requirement: Backpressure Isolation
The daemon's PTY reader MUST NOT block on any client: output always reaches
the emulator (screen + ring) and the on-disk log. Each attach session SHALL
have a bounded outbound queue; when a slow client can't drain it, the daemon
SHALL coalesce by dropping that session's queued incremental frames and
sending a fresh snapshot instead. PING/PONG heartbeats SHALL detect dead
clients so their sessions get reaped.
Scenario: Slow SSH client
- WHEN a remote client stalls mid-stream
- THEN the harness and all other clients continue at full speed, and the slow client eventually receives a snapshot repaint instead of the backlog
Scenario: Wedged client pinning the viewport
- WHEN an attached client stays connected but stops reading its socket, so
the daemon's writes keep succeeding into its receive buffer and its
PONGs stop - THEN the daemon reaps that connection once its silence passes the
liveness timeout, tears down its attach sessions, and recomputes
smallest-attached-wins over the survivors so the guest PTY is no longer
clamped by it — a client that has never answered a
PINGis left alone
Requirement: Transport Bindings
Locally, the daemon SHALL serve this protocol on a Unix socket with 0600
permissions (📝 ADR-0008). Remotely, the daemon MAY run a Wish SSH server whose
sessions host the TUI in-process as a local client of the same protocol — no
separate remote protocol exists (📝 ADR-0004). SSH provides auth, encryption, and
host-key verification.
Scenario: Remote parity
- WHEN a user attaches over SSH
- THEN snapshot-on-attach, backpressure, and resize policy behave identically to a local attach
Non-goals (v1)
- A stable public API for third-party clients (the CLI is the supported programmatic surface).
- Session recording/replay as first-class protocol messages (📝 ADR-0007 defers the full event store).
- Multi-daemon federation (one daemon per host).
Related Artifacts
Direct relationships declared in YAML frontmatter (per ADR-0023 / SPEC-0018). Run /sdd:graph chain SPEC-0002 for the transitive view.