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.

EntryMeaning
Supervisorissuing Supervisor’s name
Reciperecipe name (root name=, else the Supervisor name)
RecipeIDcorrelation key<supervisor>-<recipe>-N, unique per Supervisor
Nodetarget node name
NodeIDtarget node id; the Recipe branch reads this to bake-locally vs forward

Message types

TypeDirectionPurpose
Psyclone.Builder.RecipeSupervisor → Builderrecipe intake. XML rides on the RecipeSpec entry (not Recipe — entry keys are case-insensitive). Baked exactly once; duplicate RecipeID rejected.
Psyclone.Builder.Bake.StatusBuilder → Supervisorprogress readout only. kind = start / step / interim / ack; pct (0–100, −1 = n/a); summary.
Psyclone.Builder.Bake.Success / .Failed / .Paused / .Cancelled / .RestartedBuilder → Supervisorterminal / lifecycle status. The type itself is the outcome; final=1 on the terminal.
Psyclone.Builder.Bake.DataBuilder → Supervisorthe actual step output. Lossless, strictly ordered by seq.
Psyclone.Builder.BakeControl.*Supervisor → Builderone control verb per message (verb is the type suffix).
Psyclone.Builder.BakeDataSupervisor → Buildera 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).