Skip to main content
Status:✏️ DRAFT
Date:📅 2026-07-18
Domain:📦 Daemon Protocol (Control Plane + Attach Stream)

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 start is issued for a harness already running
  • 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 ERROR with a machine code and a human message the TUI/CLI can surface verbatim

Scenario: Replayable log tail

  • WHEN a client requests logs for 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 logs with events set 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 logs without events
  • THEN the reply is the durable log tail exactly as before, which the TUI peek pane and harness logs --raw depend on

Scenario: Run-scoped logs

  • WHEN a client requests logs with run set to a run id of a scheduled harness
  • THEN the raw reply is that run's own log, and the events reply describes exactly that run record's window
  • AND a run id the harness's history does not hold is an unknown_run ERROR

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_exited and harness_state_changed without 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 PING is 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).

Direct relationships declared in YAML frontmatter (per ADR-0023 / SPEC-0018). Run /sdd:graph chain SPEC-0002 for the transitive view.