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. |
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
ctrlseqis 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)
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).