CMLabs · Psyclone AIOS

# Builder & Supervisor Protocol

The wire-level contract between Supervisors and node-local Builders: message types, identity, the split Status/Data reverse streams, control verbs, lifecycle, and the multi-node caveats an administrator needs to know.

## Roles

A Builder is the per-node authority that executes recipes: it creates, registers, runs and heals local components. There is exactly one per node and it is always present. A Supervisor is a component (a continuous crank) that mints recipes, sends them to a Builder, consumes the reverse streams, and can steer a running bake. Supervisors are plain deterministic code today; the LLM tier is roadmap. The built-in Startup Supervisor now owns normal system bring-up.

## Identity: the five entries + RecipeID

Every message in a bake carries five identity entries, and the Builder echoes them onto every reverse message so any observer can group and attribute a bake. The RecipeID string is the sole correlation key end to end.

Entry | Meaning
--- | ---
`Supervisor` | issuing Supervisor’s name
`Recipe` | recipe name (root `name=`, else the Supervisor name)
`RecipeID` | correlation key — `<supervisor>-<recipe>-N`, unique per Supervisor
`Node` | target node name
`NodeID` | target node id; the Recipe branch reads this to bake-locally vs forward

## Message types

Type | Direction | Purpose
--- | --- | ---
`Psyclone.Builder.Recipe` | Supervisor → Builder | recipe intake. XML rides on the `RecipeSpec` entry (not `Recipe` — entry keys are case-insensitive). Baked exactly once; duplicate RecipeID rejected.
`Psyclone.Builder.Bake.Status` | Builder → Supervisor | progress readout only. `kind` = start / step / interim / ack; `pct` (0–100, −1 = n/a); `summary`.
`Psyclone.Builder.Bake.Success` / `.Failed` / `.Paused` / `.Cancelled` / `.Restarted` | Builder → Supervisor | terminal / lifecycle status. The type itself is the outcome; `final=1` on the terminal.
`Psyclone.Builder.Bake.Data` | Builder → Supervisor | the actual step output. Lossless, strictly ordered by `seq`.
`Psyclone.Builder.BakeControl.*` | Supervisor → Builder | one control verb per message (verb is the type suffix).
`Psyclone.Builder.BakeData` | Supervisor → Builder | a data push answering a `<requestdata>` step.

Why two reverse streams. `Bake.Status` is progress only and never carries payload; `Bake.Data` carries the payload and nothing else. A progress dashboard subscribes to Status and stays cheap; a log sink subscribes to Data and reassembles it. The property that Status never carries data is asserted in the tests.

## The Status stream (progress)

Fields: `kind` (start/step/interim/ack/…), `pct`, `summary`, `final` (1 on the terminal), `outcome` (on the terminal and on acks). A monotonic `serial` per bake makes delivery idempotent — because a message can arrive both by direct fan-out and by subscription, a repeat serial is counted once. The heartbeat (`kind="interim"`) is time-driven and progress-independent (default `status-interval-ms=5000`, `0` = none): it is a liveness signal, so “working, nothing new” stays distinguishable from “wedged”. A node-level sweep covers steps that are not `<cli>` (a slow `<attach>`/`<code>`/module create) so every running bake still beats.

## The Data stream (payload)

Fields: `seq` (monotonic from 1, gap-free), `stepindex`, `stream` (stdout/stderr), `data`, `finalchunk`, `partial`. Integrity is a hard contract: a wire `seq` that is not exactly last+1 is a latched hard error at both ends — never reordered, never dropped-and-continued. A repeat of the last accepted seq is treated as a duplicate delivery (idempotent), not an integrity error. Cadence floor is `data-interval-ms` (default 1000; `0` = no floor); a buffer over `max-data-bytes` (default 256 KB) forces an early flush with `partial=true`.

## Control verbs

Each control is one message whose type is the verb: `Psyclone.Builder.BakeControl.Pause` / `.Start` / `.Cancel` / `.SkipStep` / `.RetryStep`. It carries a monotonic `ctrlseq` and `issuedby`, plus the five identity entries. Semantics:

- Pause holds at the next step boundary; Start resumes. Cancel supersedes a pending pause; a stale `ctrlseq` is rejected.
- Cancel/Terminate is immediate — it kills an in-flight `<cli>` child mid-step rather than waiting for the boundary.
- SkipStep / RetryStep act on the live cursor; Retry requires `retryable="yes"` on the step (protects non-idempotent commands).

Every control is acknowledged on the Status stream as `kind="ack"` with `outcome` = paused / restarted / cancelled. Unknown RecipeID, malformed verb, terminal session and unauthorised issuer are all rejected cleanly (typed result + log) — never a crash, never silent.

## Lifecycle & the Startup Supervisor

Since the strangler defaults were switched on, a node’s own startup runs through the built-in Startup Supervisor crank (a continuous crank in the Root space). On the first `Psyclone.SystemStatus` (the deterministic “config is complete” kick) it parses the node’s PsySpec, groups the component elements by target node, and dispatches one bring-up recipe per node. The local group is baked immediately; a group pinned to a peer is dispatched once that peer’s id appears in a `SystemStatus`, else deferred. It publishes `Psyclone.Ready` exactly once — only after the local bring-up recipe reaches `Bake.Success`, so “ready” genuinely means “all components exist”. It then stays registered and running idle (a deliberate departure from the older design’s self-remove) so it can accept later recipes.

## Multi-node caveats (read before you rely on it)

Verified single-node, hardening multi-node. Sending recipes to a remote node’s Builder is built and the routing decision is tested. Getting the reverse Status/Data streams back across a node boundary rides on subscription sync; it is plausible and wired but not yet proven end to end with a two-process harness. Treat cross-node result collection as experimental until that test lands.
Not built yet. Cohort messaging (`Builder.<NodeID>.cohort.*`), Supervisor auto-subscription to performance/cohort messages, the whole-system status API, and the LLM Supervisor tier are roadmap. The bare `<supervisor>` PsySpec tag is still parsed-and-discarded — build a custom Supervisor as a crank component (see the User Guide).