v2026.5.18 - Actors v2 Stage 1: Payload Messages, Receive Arms, SBI Gate
v2026.5.18 - Actors v2 Stage 1: Payload Messages, Receive Arms, SBI Gate
Section titled “v2026.5.18 - Actors v2 Stage 1: Payload Messages, Receive Arms, SBI Gate”Release date: 2026-05-18
Profiles affected: :cluster
Status: Compiler feature closure
Stage 1 closes the payload-bearing actor message path that Phase B left as “future work”. Message types can now carry fields, receive arms destructure them, guards can read those destructured bindings, timeout arms are wired for generated supervised actors, and the SBI conformance gate prevents non-owned types from crossing the actor boundary at declaration time.
What Shipped
Section titled “What Shipped”SBI conformance gate (E2530)
Section titled “SBI conformance gate (E2530)”message declarations with fields that are not SBI-conformant now
produce E2530 at compile time:
message Bad { Set { ptr: *u8 }, // E2530: pointer-typed field is not SBI-conformant}Only owned, by-value, and recursively SBI-conformant field types are accepted. This is the SPEC-021 v0.6.4 §3.1 assertion — memory is the message, and every field must be safely transferable across actor boundaries without shared mutable state.
The gate uses three-layer detection: node kind, preceding-star token heuristic, and source-span type name resolution. This works around the parser storing inner type nodes in variant edges rather than wrapper nodes.
The $sbi_conformant(T) comptime builtin is available for manual
checks.
Named variant receive patterns
Section titled “Named variant receive patterns”Receive arms now accept named unit-variant patterns from the message protocol:
receive do Cmd.Tick => do count += 1 end, Cmd.Stop => do return 0 end, else => do count = count end,endPreviously, receive arms only matched raw integer tags. Named variants
route through integer Equal comparison (not Union_Tag_Check) via an
isReceiveContext flag set in the handler triple’s lowering context.
Receive payload destructuring
Section titled “Receive payload destructuring”Payload-bearing variants can be destructured directly in receive arms:
message Cmd { Tick, Set { value: u64 }, Stop,}
actor Counter(msg: Cmd) do var count: u64 = 0
receive do Cmd.Tick => do count += 1 end, Cmd.Set { value } => do count += value end, Cmd.Stop => do return 0 end, else => do count = count end, endendThe Cmd.Set { value } arm binds the value field into arm scope from
the typed payload slot. The parser accepts shorthand struct syntax
{ field } without requiring : or =.
The slot-array infrastructure (sender, handler, binding, runtime) was
already fully implemented in Phase B. The isReceiveContext flag was
the only missing piece — it routes payload extraction through
janus_actor_state_slot_load(msg_ptr, field_index+1) instead of the
union-tag path.
Receive guards and supervised timeout arms
Section titled “Receive guards and supervised timeout arms”Receive arms now accept when guards and after N => timeout arms:
receive do Cmd.Set { value } when value >= 0 as u64 => do count += value end, after 0 => do count = count end,endThe guard expression can read payload bindings introduced by the pattern.
after N => is a receive timeout arm. Generated supervised actors now get an
Actor_timeout(actor) helper, and the generated Actor_start_supervised*
wrappers register that helper with the local actor runtime. Delivered messages
still call the normal handler; an empty mailbox at the timeout boundary calls
the timeout helper.
Send safety integration
Section titled “Send safety integration”Stage 0 audit confirmed that SPEC-029 send-safety enforcement is fully wired:
- Profile gate:
effect_graph.Profile.enforcesIsoTagSendSafety()returnstruefor:clusterand:sovereignonly.:core,:service, and:computeare exempt. - Parser attachment:
ref_capflows from type nodes through thecapability_bindingsmap to the send-safety checker. - Erasure: Capability metadata never reaches QTJIR — architecturally zero-cost per SPEC-029 §3.3.
Integration fixture verifies: val u64 payload sends via ActorRef[Cmd]
compile and run; ref payloads are rejected with E2801.
Verification
Section titled “Verification”./scripts/zb test-cluster-actors # cluster aggregate./scripts/zb test-cap-send-safety # profile gate fixtures./scripts/zb test-cluster-receive-destructure # named variants + payload./scripts/zb test-cluster-supervised-after-timeout # generated timeout handler./scripts/zb test-cluster-msg-non-sbi-reject # E2530 compile-fail./scripts/zb test-cluster-actor-payload # payload smoke./scripts/zb test-cluster-send-safety-payload # send safety × payloadCurrent Limits
Section titled “Current Limits”- Slot type is still
u64: All actorvars and payload fields becomeu64slots. Heterogeneous typed state remains future work. - Message ABI is
i64for tags: Unit variants lower to theiri64tag. Payload values transfer through boxed slot arrays.
Canonical Actor Shape (Updated)
Section titled “Canonical Actor Shape (Updated)”{.profile: cluster.}
use std.cluster.local as cluster
message Cmd { Tick, Set { value: u64 }, Stop,}
@mailbox(capacity: 4)actor Counter(msg: Cmd) do var count: u64 = 0
receive do Cmd.Tick => do count += 1 end, Cmd.Set { value } when value >= 0 as u64 => do count += value end, Cmd.Stop => do return 0 end, else => do count = count end, after 0 => do count = count end, endend
pub func send_set(actor_ref: ActorRef[Cmd]) -> i32 do let payload: val u64 = 42 as u64 actor_ref.send(Cmd.Set { value: payload }) return 0end
pub func main() -> i32 do let sys = cluster.local_new(42 as u64, cluster.STRATEGY_ONE_FOR_ONE, 1 as u64) let actor_id = Counter_start_supervised(sys, 0 as u64, cluster.POLICY_PERMANENT)
// Low-level supervised bridge sends scalar tags. cluster.local_try_send(sys, 0 as u64, 0 as i64) // Tick tag cluster.local_try_send(sys, 0 as u64, 1 as i64) // Set tag
cluster.local_shutdown(sys) cluster.local_destroy(sys) return 0end