Actors

Each VM task in Lambda OS is a self-contained Lambda C VM instance with its own stack, global area and heap. Tasks are actors: isolated state, message passing, pinned to a core. Native Zig tasks — the network stack, a control loop — run alongside them.

Node roles

Nodes have roles.

An orchestrator node (x86_64, riscv64, aarch64) holds many actors. It spawns them at runtime, supervises them, and rebuilds them when they die.

An edge node (an MMU-less MCU) holds one or two actors and coordinates over the network.

The machinery for holding and managing many actors on one chip lives only on the orchestrator, by design. Putting it on a 264 KB part would run that machinery where it earns nothing — the very thing distributed actors exist to avoid.

The actor API

The typed mailbox and send_remote are on every target. The rest is scoped.

x86_64riscv64aarch64MMU-less MCU
send / recv / msg_from / msg_a / msg_b
msg_tag
send_remote
spawn (dynamic creation)
monitor / die (supervision)
register_name / whereis (local name registry)
has_msg (non-blocking peek)
node_ip / send_node / reply_node

Typed request/reply. A message is {from, tag, a, b}. send(pid, tag, a, b) stamps the sender's pid automatically, so a handler can answer with send(msg_from(), …) without knowing who called. recv() returns the tag for dispatch.

Dynamic spawn and supervision (the orchestrator). spawn(role) creates a new actor at runtime from inside a running FFI. monitor(pid) asks to be told when an actor dies; die() notifies the monitors with a reserved MSG_DOWN tag and terminates. A supervisor that receives MSG_DOWN re-spawns the worker in the freed slot. The system self-heals.

An edge node does not have this. Instead it has frame-level fault recovery.

Verified on real silicon (a Pico W) by injecting a fault mid-flight: the mission actor stores into a sibling's context, the MPU denies it, the kernel skips the frame, and the delivery still completes. The control loop and the sibling actors never notice.

Cross-node supervision — a monitor on another node noticing an edge node's death and re-provisioning it — is not implemented.

Frame-driven execution

The scheduler re-runs each script's main() once per tick. Globals in the global area persist across calls; the operand stack resets every frame. So a script does not write an internal unbounded loop. It does a slice of work per frame and returns.

The frame is the unit of cooperative progress, and the preempting timer is the guarantee that a badly-behaved frame cannot hang the machine. On x86, riscv64, aarch64 and the MMU-less MCUs a per-task instruction-count watchdog also bounds a single frame.

Concurrency without a global lock

The VM keeps process-wide run-state in plain globals. Rather than serialize the whole VM run behind one lock, the scheduler saves and restores that run-state per task on every context switch.

The consequence: a VM frame is fully preemptible, with no interrupt-disabled section around it. The interrupt-masked windows are short ones around genuinely shared data — the actor mailbox, the console, the NIC transmit path, task creation. Per-task stack, global area and heap make everything else race-free.

On SMP, per-core VM globals are separated by a per-CPU base: the GS base on x86, the thread pointer on riscv64, TPIDRRO_EL0 on aarch64, the core-id register on the RP2040's two cores. The VM runs on every core on those four. The RP2350 still shares one block, so its VM stays on one core.

Location transparency: messages across machines

A remote message arrives in a local mailbox and is read by the same recv() as a local one.

On real silicon. An unprivileged, PMP-confined actor on a Raspberry Pi Pico 2 W (RP2350, no MMU) sends a typed message over real Wi-Fi and a real LAN to an actor on an x86 Lambda OS. That actor receives it in its ordinary mailbox and replies, and the value it computes comes back into the sending actor's own mailbox.

In that image, the network stack runs as a privileged task beside the confined actors. Isolation and distribution hold in the same image.

How far the transparency goes

The receive path is transparent. A remote message is handled by the same mailbox, the same recv(), and the same tag dispatch as a local one.

The reply path is not. Because the sender is on another node, msg_from() returns -1 — so an ordinary send(msg_from(), …) cannot answer it. The reply goes through reply_node(), which uses the return address the kernel recorded from the incoming packet.

A script that answers remote senders knows it is doing so. Location transparency is complete on receive and partial on reply.

What has actually been shown

Microsecond coordination across nodes: not implemented

Actor message delivery is best-effort. Acting on a common clock across nodes — genuinely microsecond-coordinated operation, a swarm holding formation — is not implemented.

It would need a shared time base (PTP over a wired link, or GPS/PPS discipline per node when wireless) plus local time-triggered execution. It cannot be validated in emulation: two guests cannot hold a common clock, each being independently perturbed by its host.