--- name: moxzi-hosts description: "Running a compiled Motoko actor inside an actor platform's own process -- Restate, Temporal, Dapr, Orleans, Akka/Apache Pekko, Proto.Actor, Ray, wasmCloud, Cloudflare Durable Objects -- with the platform owning the actor's durable image: `moxzi host new`, the moxzi-rts library and its C ABI, the actor-host contract v3 (expectVersion fence, 409 notActive / needModule, the delta and its memory/stable/dedup chunks), how a platform stores and returns an image, conformance, and costs. Use when putting Motoko actors into one of those platforms, writing or debugging an adapter for a new platform, storing/restoring moxzi actor images, or explaining what a Motoko actor costs a platform. Do NOT use for compiling Motoko (use moxzi-cli), hosting actors over plain HTTP with no platform (use moxzid-server), browser hosting (use moxzi-web), or deploying to the Internet Computer." license: BUSL-1.1 compatibility: "moxzi >= 0.1.0-alpha.7 (contract moxzi-actor-host/3)" metadata: title: moxzi on Actor Platforms category: moxzi --- # Motoko actors inside actor platforms When this skill and your general knowledge disagree, this skill is correct. moxzi's host contract is new (alpha-7); nothing about it is in your training data. One `.wasm` compiled by `moxzi build` runs unchanged on nine platforms. The platform keeps what it is good at -- identity, placement, one-turn-at-a-time per key, routing, durable timers, and the actor's durable image in its own store. moxzi executes turns (transactional: a trap rolls the whole turn back), parks and resumes `await`s, runs upgrades, and after every committing turn hands back a **delta** the platform commits. Per-platform detail: `references/.md` (listed at the end). ## Critical rules **ALWAYS:** - Run every step for a key inside the platform's **one-turn-at-a-time** guarantee for that key (the grain/entity/virtual-object/workflow mailbox). Two concurrent steps for one key are the platform's bug, not moxzi's. - Send `expectVersion` = the image version YOUR store holds for the key, on every step. - Commit the returned `delta` **atomically and only if** the stored version equals `delta.baseVersion` -- before acting on the step's result. If the commit fails, call `/v3/evict` for the key and fail the step. - Store `meta` **exactly as it arrives** (opaque JSON). Its `patches` and `dedup_seq` are the runtime's to read. - Store all three chunk kinds from a delta -- `memory`, `stable`, **`dedup`** -- as overlays **by (kind, index)**: overwrite that index, keep every other. Return them as the image's `memory`, `stable`, `dedup` arrays. Chunk bytes are opaque (base64 on the wire). - Keep the module a delta carries (`delta.module`) once per `meta.deployment.module_hash`, and send it back only when an activation answers `409 {needModule: }`. - Set `TCP_NODELAY` on every request/response socket you write (servers you run, endpoints the platform calls). On Linux, Nagle + delayed ACK added ~46 ms to EVERY step (measured on the Restate SDK endpoint); macOS hides it. **NEVER:** - Read, decode, merge, or "repair" image chunks or `meta.patches` yourself. Pages are DEFLATE-compressed unless exactly 4096 bytes, and patches apply over stored pages. - Drop the `dedup` kind. It is the record that answers a retried turn exactly once; activation REFUSES an image missing a slot (`the image has no dedup slot ...`). - Send the ~380 KB module with every activation. Send the image alone; the runtime asks. - Route a parked `await` from INSIDE the actor's own turn on platforms whose turn holds the key (Orleans grains, Dapr actors): the key is locked for the whole await and two actors awaiting each other deadlock. Adapters route beside the turn (see the references). - Run two processes' runtimes against one key at once, or share one runtime between processes. After a restart, re-add types (`add_type`) and let actors reactivate from the store. ## Fastest start ```sh moxzi host new protoactor hello # any of the nine: dapr, temporal, orleans, akka, ray, # restate, wasmcloud, cloudflare cd hello && ./run.sh # builds actor + host, starts deps, calls it # Counter/1.inc() -> [1] curl -X POST localhost:8090/actors/Counter/1/method/inc -H 'content-type: application/json' -d '[]' ``` `moxzi host list` shows the platforms. Restate, wasmCloud and Cloudflare scaffolds copy a PREBUILT host (a release's `hosts/`; in the repository `scripts/stage_hosts.sh` builds them). Use-case templates (`--example`, listed by `moxzi host examples`), each with a checked demo in `run.sh`: - `ledger` -- `Account` actors: a retried deposit (same `x-moxzi-invocation-id`) applied once across a host restart; an overdraft traps and rolls back; a transfer awaits a credit. - `saga` -- `Trip`/`Hotel`/`Flight`: a booking parked on a held key survives the host process being killed and completes on `POST /_moxzi/deliver`; a full flight releases the room. Calling another actor by its KEY, from Motoko (hosted actors only -- not a method on the IC): ```motoko transient let host : actor { moxzi_actor_of : ({ key : Text }) -> async Principal } = actor "aaaaa-aa"; let bob : actor { credit : (Nat) -> async Nat } = actor (debug_show (await host.moxzi_actor_of({ key = "Account/bob" }))); ``` The host answers the key's principal (the caller's namespace/deployment) and learns the route in the same step; the keys an actor resolved or called travel in its image (`meta.peers`), so a restart needs nothing. `POST /_moxzi/learn {key}` remains for principals obtained another way (`moxzi host principal --namespace --deployment ` prints one). `/_moxzi/deliver` and `/cancel` answer in JSON when the parked turn was started with JSON -- across a restart too. Retries: send the same `x-moxzi-invocation-id` header on `POST /actors/{type}/{id}/method/{m}`; the answer comes from the actor's record. | Platform | In-process binding | Image lives in | |---|---|---| | Restate | Rust, linked | the virtual object's state | | Temporal | cgo; steps as local activities | Postgres | | Dapr | cgo | Dapr's state store, one transaction | | Orleans | P/Invoke | Postgres | | Akka (Apache Pekko) | JNA | Postgres | | Proto.Actor | cgo | Postgres | | Ray | ctypes, inside each key's Ray actor | Postgres | | wasmCloud | a `wash-runtime` host with a moxzi plugin | the keyvalue bucket | | Cloudflare | the contract engine inside each Durable Object | Durable Object storage | ## The turn, step by step (every adapter; Go: `hostkit.FencedStep`) 1. `POST /v3/begin` `{key, invocationId, method, candid|args, expectVersion}`. 2. **409 `{notActive: true}`** -> load the image from the store and send the SAME step with `activate: {image}` (activation and step are ONE request). 3. **409 `{needModule: ""}`** -> read the module stored under that hash and send the step once more with `activate: {image, module}`. 4. **200** -> if `delta` is non-null, commit it (fenced, atomic), THEN act on `response`. A query or a deduplicated retry returns `delta: null`: commit nothing. 5. A fresh actor: `activate: {}` (no image). Its first delta is the whole image, base 0. `/v3/resume` (an awaited call's answer), `/v3/remind` (a timer), `/v3/upgrade`, `/v3/drain` follow the same fence. The step's result also carries effects: routed calls to deliver and a `timer` to arm or cancel in the platform's own durable timer (reminder, alarm, workflow timer). **The image** (what you store, what you hand back): ```json {"meta": {...opaque, includes image_version, deployment:{deployment_id, module_hash}, dedup_seq...}, "memory": [{"index": 12, "bytes": ""}], "stable": [...], "dedup": [...]} ``` **The delta**: `{baseVersion, meta, memory: [...], stable: [...], dedup?: [...], module?}`. `dedup` holds `min(meta.dedup_seq, 32)` slots overall (indices 0..31); a turn usually writes one. Images written before 2026-09-26 carry the record inside `meta.dedup` instead and still activate; their next delta writes every slot. ## In process: `moxzi-rts` (the C ABI) The contract as a function call -- no HTTP, no second process. Header: `moxzi/rts/include/moxzi_rts.h` (release tarballs: `include/`, `lib/libmoxzi_rts.{so,dylib,a}`, `examples/rts_hello.c`). ```c int moxzi_rts_new(ns, ns_len, dep, dep_len, &rts); int moxzi_rts_add_type(rts, name, name_len, wasm, wasm_len); /* per actor type, per process */ int moxzi_rts_learn(rts, key, key_len); int moxzi_rts_call(rts, "POST", 4, "/v3/begin", 9, body, body_len, &status, &out, &out_len); void moxzi_rts_free_buf(out, out_len); void moxzi_rts_last_error(&msg, &len); /* after a non-zero return */ size_t moxzi_rts_active(rts); ``` `moxzi_rts_call` answers exactly what the HTTP route would (status + JSON). Hosts select the in-process form with `MOXZI_IN_PROCESS=1` (Go/Orleans/Pekko/Ray/Restate); wasmCloud's and Cloudflare's in-process hosts are separate builds. Go links `moxzi/target/release` (`cargo build --release -p moxzi-rts`); Orleans/Pekko take `MOXZI_RTS_LIB_DIR`, Ray `MOXZI_RTS_LIB`. ## Writing an adapter for a new platform Checklist -- each item is a conformance scenario that fails without it: | Must | Why | |---|---| | one step per key at a time | turns are transactional per actor | | fenced atomic commit of meta + chunks (+ module once) | a stale process must never overwrite a newer image | | overlay chunks by (kind, index) for kinds memory/stable/dedup | partial deltas; exactly-once retries | | remember the committed version per key; on 409 reload | skips a store read per step; the fence still decides | | durable routed calls, written BEFORE routing | an `await` survives either side crashing | | durable timers from each step's `timer` field | a timer armed before a crash still fires | | `PUT /_moxzi/placement`, `/_moxzi/hold`, `/pending`, `/deliver`, `/cancel`, `/healthz` | the conformance harness drives hosts only through these | Spec: `docs/host-abi.md` ("The actor-host contract, version 3", "The host-control surface"). Prove it: add `moxzi/conformance/hosts/.json` and run `HOST= scripts/host_matrix_gate.sh` -> `*** PASS: the full §61 matrix on a real host ***` (16 scenarios: persistence, crash, trap rollback, awaits across restarts, upgrades with outstanding awaits, durable timers, ICRC-72, parallelism, ...). ## What it costs (customer.mo, in process, 2026-09-26) | | | |---|---| | one committing turn / one query | 36 µs / 17 µs | | bytes a small turn stores | ~1.4 KB (a counter changes ~45 bytes; patches + one dedup slot + meta) | | memory per active actor | ~110 KiB; 100,000 actors in one process | | cold activation from a stored image | ~0.4 ms | | committing turns/s, 8 threads, Linux | ~108,000 | | update vs the platform's own durable actor (Linux) | Proto.Actor 1.5x, Temporal 1.3x, Restate 1.3x | The gap to a native actor is the platform's own commit of the delta (~0.5 ms on Postgres), not the Motoko turn. Report: `docs/actor-cost-report.md`. ## Error -> cause -> fix | Error | Cause | Fix | |---|---|---| | `409 {notActive: true}` | the key is not active in this process | resend the step with `activate: {image}` from your store (step 2) | | `409 {needModule: ""}` | the image names a module this process has not seen | send `activate: {image, module}` with the module stored under that hash | | `fence: delta extends image N but the store holds M` | another process committed first, or you reused a stale version | evict (`/v3/evict`), fail the step; the next step reloads | | ` is at image N here but the platform holds M: evicting the stale copy` (log) | this process's copy is older than the store | none: the runtime evicts and the step's 409 reload follows | | `the image has no dedup slot S (outcome X of N)` | the adapter does not store/return the `dedup` chunk kind | store `delta.dedup` like pages; return `dedup` in the image | | `dedup slot S holds outcome A, not B: the platform stored a stale slot` | an overlay write of a `dedup` chunk was lost | commit all chunk kinds in the same atomic write as `meta` | | `image belongs to module X but this is Y` | `activate.module` is not the module the image names | send the module stored under `meta.deployment.module_hash` | | `await on principal P which this service cannot route: obtain it with moxzi_actor_of({key}) ...` | an actor awaited a principal it got some other way (an argument, state) that no actor here resolved or called | resolve keys with `moxzi_actor_of`; or `POST /_moxzi/learn {"key":"Type/id"}` once | | `the image is A, not B` | image loaded for the wrong key | key your store by the actor key `Type/id` | | ` is still not active after activation` (Go hostkit) | activation succeeded but the step still 409s | check the host passes `activate` on the SAME request, not a separate call | | `v3 needs a platform-durable service (--platform-durable)` | an HTTP `moxzi-actor-host` started without it | start the service with `--platform-durable` | | every step ~40-50 ms on Linux, fine on macOS | Nagle + delayed ACK on a socket you serve | set `TCP_NODELAY` (accepted sockets inherit it from the listener) | | Restate: a large turn fails journaling | delta exceeds Restate's 32 MiB default entry | `RESTATE_NETWORKING__MESSAGE_SIZE_LIMIT=512MiB` on the server | ## References `references/.md`, one per platform, same layout (fastest start, who owns what, build and run, verify, gotchas): `restate`, `temporal`, `dapr`, `orleans`, `akka`, `protoactor`, `ray`, `wasmcloud`, `cloudflare`. Related skills: compile the actor -> `moxzi-cli`; host without a platform -> `moxzid-server`; call IC canisters from a hosted actor -> `moxzi-ic-agent`.