# Phase 1 actor prototype ## Execution inspection The image API and `inspect_execution` protocol operation expose a read-only, bounded snapshot of an active invocation's head Rune VM frames and the exact installed source/implementation identities. The current frame maps the next instruction; saved caller return addresses map back to their call instructions. While a native await is pending, the snapshot retains the last safe instruction boundary and is marked awaiting. It does not borrow the active VM or expose stack values across actor boundaries. At most 64 innermost frames are copied; depth/truncation remain explicit. Unknown mappings are nullable. VS Code can navigate these frames in immutable loaded-source documents. Shared working edits and pending temporary replacements cannot change an active frame's source. Idle/failed actors have no active stack; failures still discard the invocation, so this is not post-mortem debugging. Shutdown hooks use the same mapping. Pause retains a stable active snapshot; restart/kill discard it. Native Rust frames and hidden nested-future stacks are not inspected. Variables and durable stack storage remain planned. ## Source stepping and breakpoints The trusted image API supports actor-local breakpoints in the **installed** implementation, independent of shared working text and pending temporary edits. Setting breakpoints replaces the actor's list (maximum 128 zero-based lines). The result reports sorted instruction addresses for each requested line; an empty address list explicitly means unverified. Rune debug spans, line transitions, function entries, and labeled control-flow targets determine line entries; lines are never silently moved to a nearby executable location. Normal ticks check those addresses before execution and pause only the matching actor. A debugger pause retains the reserved message, invocation-local draft, and buffered sends; no transition commits early. Resume skips the stopped address once so it can progress, but revisiting a loop breakpoint can stop again. Other actors still receive their ordinary scheduler slices. `step_source` requires a paused **active** invocation and an expected loaded implementation identity. It polls only that actor until the next mapped line or call-depth change, a breakpoint, pending async operation, completion, failure, or the instruction budget. It is step-into, not step-over/out. Budgets range from 1 to 10,000 (VS Code uses 1,000); same-line loops and unmapped instructions are bounded rather than allowed to run indefinitely. Native calls are still not preemptible. A pending async poll returns immediately with `awaiting`; another explicit step may poll it again. Native/hidden future stacks remain outside source stepping. The actor stays paused even after successful handler completion, with normal state/send commit and no automatic start of the next message. Resume then restores idle/running/awaiting status as appropriate. Failure retains the failed reservation, discards buffered effects, and cannot be resumed without repair. Stepping a successful shutdown hook removes the actor normally. Inspection includes debugger configuration and the latest stop reason. Restart/cancel clear the stop reason but retain breakpoints on the same loaded code; implementation installation clears all breakpoints rather than applying stale line mappings. Removal discards debugger state. Steps and breakpoint configuration changes broadcast `image_changed`. These are explicit actor controls, not a DAP session or VS Code gutter breakpoints. CLI debugger commands, conditions, hit counts, step-over/out, locals, and post-mortem stacks are not implemented. ## Actor repair and logging `set-state ` replaces committed state without touching the mailbox. `set-messages {"reserved":[],"queued":[]}` replaces the reserved and queued messages without changing state. The reserved array has zero or one element (so a null-valued message is distinguishable from no reservation). Both operations require paused or failed status and no active invocation. Idle actors must be paused first; an active invocation must finish or be cancelled through the runtime API. Kill removes the actor rather than making it editable. All value and capacity checks finish before any changes are applied. Edits do not resume/restart an actor or change its implementation/incarnation. Unchanged messages at unchanged positions retain sender metadata; replaced messages are host-originated. Removing the failed reservation avoids retrying it. `ctx.log(level, message)` accepts a lowercase severity string and message string. `ctx.trace`, `ctx.debug`, `ctx.info`, `ctx.warn`, `ctx.error`, and `ctx.fatal` delegate to it. Trace < debug < info < warn < error < fatal; default info. `log-level ` changes the minimum accepted severity, including during suspended execution. Lower-severity entries are neither stored nor sent. Accepted messages longer than 512 UTF-8 bytes fail explicitly. Logs use an image-wide increasing sequence and include actor identity, level, and message. Each actor retains its last 1,000 accepted entries across restart, in memory only. Logging is immediate and does not roll back if a handler fails, is cancelled, or is killed. This is diagnostic output, not a transactional actor send. `logs [-f|--follow] [actor...]` is available in both CLI modes. REPL names resolve to references before the request; an empty filter means all actors. History is read non-destructively in pages, preserving sequence order. Follow requests register a per-connection server-side filter and receive only matching future log events; unsubscribed connections receive no log events. Ctrl+C unsubscribes, returning to the REPL or gracefully closing the one-shot connection. Live log buffers are bounded; lagging subscriptions fail explicitly rather than silently losing entries. History still only contains the retained tail, not durable logs. Start the server-owned, in-memory image: ```sh mise run start ``` Then connect from another terminal with `mise run repl`, or use `cargo run -p runa-cli -- --connect repl` for a custom listener. The CLI requires a subcommand: `repl` starts the interactive session, while image operations such as `ps`, `spawn`, and `send` run once and exit. Use `runa-cli --help` or `runa-cli --help` for argument details. One-shot JSON values must be quoted as a single shell argument; actor names and `let` bindings exist only within a REPL session. The server (`runa`) defaults to `127.0.0.1:3737`; `--listen` or `RUNA_LISTEN` select another loopback address. The REPL (`runa-cli`) accepts `--connect` or `RUNA_CONNECT`, defaulting to the same address. Explicit flags override environment variables. Both executables provide Clap-generated `--help` and `--version`. Quitting the REPL leaves the image running. Reconnecting clients can list and inspect the same actors; local bindings and history do not survive a REPL restart. Server exit still loses the entire image. The WebSocket server loads [counter.rn](../runa-std/actors/counter.rn) into the image's working `source/actors/counter.rn`. Commands include `spawn`, `send`, `tick`, `inspect`, `ps`, `events`, `stop`, `kill`, `restart`, `pause`, `resume`, `reload`, `become`, `sources`, `write`, `commit`, `history`, and `rollback`; enter `help` to see usage. Paths exclude the root and extension: `spawn actors/counter 1`. Spawn compiles the current working source, not a committed revision. `write ` updates working source using source-identity conflict checks. `commit ` captures it without compilation, and generates a readable revision ID. `history ` lists snapshots; `rollback ` restores working source without making a commit. Saving, committing, spawning, and reloading are distinct operations. The CLI sends versioned requests to the loopback-only WebSocket listener using the `runa-debug` subprotocol. VS Code uses the same protocol and automatically refreshes on non-destructive server change notifications. These notifications do not tick the image or drain the REPL's explicit `events` queue. There is no stdio transport; all external clients use WebSockets. Example: ```text runa> let sender = spawn actors/counter 0 sender = @ runa> let receiver = spawn actors/counter 10 receiver = @ runa> send sender {"op": "send", "value": 5, "target": {"$actor": "receiver"}} message queued runa> tick 100 runa> inspect sender runa> inspect receiver ``` The sender commits state 1 and sends a structured message; the receiver commits state 15. Actor references are 21-character NanoIDs; names are local REPL bindings. Tab completes commands, behaviors, and bound actor references, while Up/Down navigate command history. `tick [instruction-budget]` explicitly advances the scheduler. The default per-actor slice budget is 32 instructions. State and messages use JSON literals in the REPL, preserving whitespace inside strings. `spawn ` and `let = spawn ` require explicit initial state; `send ` takes one message value. For example, `send receiver {"op": "add", "value": 7}` adds to the bundled counter. Its supported operations are `add` and `send`; unknown operations fail explicitly. The sole-key object `{"$actor": "receiver"}` resolves a local binding to a distinct actor-reference value. A literal `@` or raw NanoID string can be used in place of the binding. Ordinary strings are never implicitly converted to references. To encode a literal record that would otherwise match a marker, wrap it in `{"$record": {...}}`. The wrapper escapes only that record; nested values still use the normal conversion rules. Tab completion also offers references inside a `"$actor"` string. ## Current contracts - `runa-actor` owns actor references, mailbox and implementation IDs, structured messages, snapshots, statuses and lifecycle events. - `runa-core` owns stable source/implementation identities, actor references, and the portable owned value model shared by the runtime and protocol. - `runa-code` preserves original source, derives identities, caches compiled artifacts, mutable path-based working source, and independent commit snapshots. - `runa-rune` owns compilation and typed bounded invocation execution. - `runa-runtime` owns implementation registration, queues, private committed state, scheduling, message effects and lifecycle coordination. - `runa-image-api` composes code publication and runtime operations behind a trusted in-process service boundary. - `runa-protocol` defines versioned transport-neutral request, response, and event DTOs; it does not dispatch requests or access runtime state. - `runa` dispatches those requests through `runa-image-api` and maps results to protocol replies; its executable accepts JSON text messages over loopback WebSockets, with a 1 MiB request limit. - `runa-client` provides a correlated, bounded loopback WebSocket client. - `runa-cli` provides an interactive CLI using the WebSocket client, without an embedded runtime. Connection errors are explicit; requests are never auto-retried. The image API is process-local and intended for trusted callers. It has no transport, authentication, or authorization layer; do not expose it directly to untrusted clients. Its install operation also supports the runtime's explicit host-provided state-migration closure. Revision source retrieval returns the exact saved text. An implementation is published once under an ID. Actors reference that exact implementation. Implementation IDs use source content and the compile fingerprint; actor references are random NanoIDs and mailbox IDs remain process-local counters. Incarnation counters use checked increments. Source files have canonical case-sensitive relative paths, without `source/` or `.rn`, rejecting traversal and empty components. Writes require the expected current source identity (null means create). Conflicting saves do not overwrite another client's work. Commits are version control only, independent of compilation and actor installation; invalid Rune source can be committed. Commit IDs are generated from a readable lowercase alphabet, not caller-chosen revision numbers. Source and history remain in memory until server exit. The older named behavior registry survives as an internal compatibility helper, but its publish/history/install commands are not part of the v5 wire contract. The runtime's low-level `install` operation switches an actor's implementation only when it is idle, has no active invocation, and has no reserved input. Queued messages may remain; they will use the newly installed implementation. Active or awaiting handlers keep their original implementation, and installation attempts during execution fail explicitly so the caller can retry at the next handler boundary. `install` preserves the current structured state unchanged. Client-facing `reload` and `become ` instead queue installation for the next handler boundary, keeping active frames immutable. Reload compiles the latest working source at the actor's current path; become chooses another path. Compilation failure preserves its current implementation and source binding. The image API also owns actor-specific temporary source. Reading current behavior returns that temporary text when present, otherwise the immutable loaded artifact (not newer working source). Saving compiles and schedules a safe-boundary install, preserving state and reserved/queued messages. Failed actors can replace their implementation but remain failed until an explicit restart. Temporary text survives restart, is cleared on successful reload/become scheduling, and is removed with the actor. Copying it back to shared working source requires matching original and temporary source identities; it neither commits nor reloads other actors. Source rename preserves commit history and, with explicit authorization, atomically rewrites every installed and pending actor path binding without changing loaded implementations. Delete rejects any nonfailed actor using either binding, including paused actors, and tells the caller to stop actors first. Failed actors may keep loaded artifacts after deleting their source; reload requires recreating that path. Source mutations and temporary text are in-memory only. The read-only `check_source` image service compiles submitted text using the same native modules as actor execution, returning structured Rune errors/warnings and optional source ranges. Rune's byte spans are converted to zero-based UTF-16 editor coordinates. Checks do not execute scripts, mutate source/history/actors, or retain compile artifacts. Normal compile failures also preserve structured diagnostics through the protocol error envelope while retaining terminal diagnostics for non-validation compilation. This is compilation checking only, not runtime source mapping or state-schema validation. Runtime callers that need a state transformation can use `install_with_migration`, which receives the committed state and computes its replacement before changing either state or implementation. The operation requires the same idle safe point; if the migration returns an error, neither value changes and no installation event is emitted. A successful changed state emits `StateMigrated` as well as `BehaviorInstalled`. This prototype accepts a host-provided migration closure only: it does not infer schema compatibility or execute migration code from Rune source. The closure receives an owned copy of the structured state; an invalid result also leaves the original state and implementation unchanged. Schema/version policy remains future work. `pause` prevents an actor from being scheduled on subsequent worker ticks and retains its active invocation, reserved message, and prior `Running`, `Awaiting`, or `Idle` status. `resume` restores that status; an awaiting invocation resumes only if its future has been woken. These operations take effect between calls to `tick` and cannot interrupt a currently executing Rune slice or native callback. They provide cooperative scheduling control, not preemption. Actor snapshots include basic execution telemetry while an invocation is active: completed VM instruction-step count, Rune instruction pointer, and call-frame depth. Paused invocations retain the same telemetry. These values are execution-local diagnostics only; they do not map to source locations or expose locals, and disappear when the invocation completes or is cancelled. Runtime invocation cancellation (not exposed as a CLI/REPL command) is distinct from `stop`: it drops an actor's active invocation, moves its reserved message to the front of the mailbox, and returns the actor to `Idle`. The input is therefore eligible for retry on the next tick, ahead of messages that were already queued. Committed state is unchanged and no transition effects are applied. Cancellation is also available for paused invocations. It does not undo external side effects already performed by native callbacks, so retries may repeat such effects. The source identity hashes the exact original UTF-8 bytes with the versioned `runa-source:v1` domain. Whitespace/comment canonicalization is intentionally not attempted; textually different source has a different identity. The implementation identity includes the source ID, pinned Rune version, compiler settings/features, registered native-module manifest, runtime ABI version, and Cargo target triple. The native-module manifest and runtime ABI identifier must be updated when those compile inputs change. The cache is in-memory only. Invalid source is retained in the current `CodeStore` for diagnostics, but does not produce a compiled artifact. Cache identity is not a claim that Rune bytecode is portable or safe to persist. `CodeStore::compile_with_module` requires an explicit stable module identity; callers must change it when registered native functions or their contracts change. The server creates a fresh `CodeStore` when its image starts. State and messages are owned `Value` trees: null, boolean, signed 64-bit integer, UTF-8 string, list, string-keyed record, or actor reference. Public actor references are 21-character URL-safe NanoIDs. They are generated when actors are spawned and remain stable across restart; stop/kill removes the actor. Mailbox IDs are separate internal counters. Rune handlers have this interface: ```text handle(ctx, state, sender, message) -> new_state on_shutdown(state) -> new_state // optional ``` Both functions may omit the returned state. A unit result (`()`, `return;`, or falling through without a value) keeps the previously committed state, not the invocation's mutable draft. Non-unit results replace state as before. Successful handlers still acknowledge the reserved message and commit validated context sends atomically. Invalid sends still fail the transition. A unit shutdown result finishes shutdown without changing state before the actor is removed. Unit cannot reset state to null anymore; host state editing can still set null explicitly. The runtime uses internal envelopes containing sender and data, then passes them separately to the handler. Host-originated messages have unit/null sender. Actors choose their own message data. For example: ```rune pub fn handle(ctx, state, sender, message) { state.items.push(message.text); ctx.send(sender, #{accepted: true}); state } ``` The runtime allocates a fresh Rune object graph for each state and message input, and copies portable data back out before committing. `ctx.send` copies message data and records the executing actor as sender. The read-only `ctx.this` field returns the current actor's typed reference; `ctx.send(ctx.this, message)` sends a normal transactional message to itself. Assigning `ctx.this` is not supported and fails execution. `ctx.send_from(sender, recipient, message)` permits forwarding the original sender (including unit for host-originated input). A Rune handler can mutate its invocation-local draft but cannot mutate committed state, the reserved input, or another actor's data. Copies made for separate effect messages do not share mutable Rune objects. Null maps to Rune unit `()`, lists to Rune vectors, records to anonymous objects (`#{...}`), and references to an opaque native `runa::ActorReference`. Effect targets must be references, not lookalike strings. References support equality and cloning in Rune and can be retained and forwarded through state/messages; this is not authentication. Floats, nonempty tuples, options, closures, futures, custom Rune structs, and other native objects are not portable actor data and fail conversion. Cyclic or excessively deep Rune output also fails without committing. Each state/message is limited to depth 32 (root depth 0), 4,096 value nodes (including the root), and 1 MiB of UTF-8 string/key bytes. A transition permits at most 4,096 send effects. Transport line-size limits apply separately. These are conversion limits, not a Rune heap quota or hostile-code sandbox. Schema/version policy remains future work. Actor contexts expose only ordinary data sends, never privileged system messages. ## Scheduling and lifecycle `tick(nonzero_budget)` gives each runnable actor one bounded slice in actor reference order. FIFO queues are bounded, including the reserved message in capacity accounting. A queued message is reserved when its invocation starts. Subsequent messages stay queued, including while the actor awaits. A wake marks an awaiting actor runnable; the caller still needs to drive ticks. Successful handlers validate every send target and its aggregate queue capacity before updating state, acknowledging the reserved message, and enqueueing effects. Invalid effects fail the actor without partial sends or a state commit. This is a **logical, single-worker in-memory atomicity guarantee**: observers using the runtime see either the transition before commit or after commit, not an intermediate state/effect set. It is not crash-safe or durable. External side effects performed by native callbacks are outside this boundary and cannot be rolled back. Runtime errors retain the last committed state and reserved message, set an observable failed status, and emit a failure event. The failed message is not silently acknowledged or automatically retried. External sends return errors for unknown, unavailable, or full actors. System messages are host-only commands, separate from ordinary data messages: stop, kill, restart, pause, resume, reload, and become. A record containing `kind: "stop"` sent by an actor is just application data. Graceful stop finishes the active handler, then runs optional `on_shutdown(state)` with cooperative instruction slices and commits a non-unit returned state, or retains committed state for a unit return. It does not drain queued input. A missing hook stops normally; a failing hook marks the actor failed and retains its last committed state. Stop unpauses a paused actor so shutdown can progress; explicit scheduler ticks are still needed. Successful shutdown removes the actor, its state, queued/reserved messages, and retained logs/threshold. There is no persistent stopped actor. The `Stopped` lifecycle event records removal; subsequent inspection, sends, or restart return unknown actor. Failed actors (including shutdown failures) remain inspectable and repairable. Kill drops the invocation immediately between slices, skips shutdown, and removes the actor, including failed actors and all retained state/messages/logs. Restart is a separate operation that preserves its actor reference, mailbox ID, implementation ID, committed state, and queued-message order; it increments its incarnation and puts the reserved message at the front of the queue. It can also restart a running actor, dropping the current invocation first without removing the actor. It does not reload working source. Repeated failure of the same reserved message can therefore block later messages until an operator changes the implementation or otherwise resolves it; there is no automatic poison-message policy or supervision. ### Transition and retry guarantees | Situation | State | Reserved input | Effects | Status | | --- | --- | --- | --- | --- | | Handler returns a valid transition | New state commits | Acknowledged | All validated sends enqueue | `Idle` | | Handler returns unit / no state | Last committed state retained; draft discarded | Acknowledged | All validated sends enqueue | `Idle` | | Rune execution or transition decoding fails | Last committed state retained | Retained | None from the returned transition | `Failed` | | Any target is unknown, stopped, failed, or over capacity | Last committed state retained | Retained | No sends from the transition enqueue | `Failed` | | Graceful stop | Discarded after successful shutdown | Active input acknowledged; queued input discarded | Valid active-handler sends commit | Removed after shutdown | | Kill during an invocation | Discarded | Discarded | None from incomplete invocation | Removed | | Restart while alive or failed | Retained until retried | Moved to queue front | None until retry completes | `Idle` | | Pause between ticks | Last committed state retained | Retained | None while paused | `Paused` | | Resume a paused invocation | Retained until invocation completes | Retained | None until completion | Prior runnable/awaiting status | | Cancel an invocation | Last committed state retained | Moved to queue front | None from incomplete transition | `Idle` | Mailbox capacity counts queued plus reserved messages. Before applying a transition, the runtime validates all effects, including aggregate sends to a single target. For a self-send, the completing input is acknowledged as part of the same transition, freeing its reserved slot before the new effects are enqueued. An invalid target or over-capacity effect rejects the entire returned transition. These are process-local guarantees only. A host crash loses state, mailboxes, reservations and events. External/native callback effects may already have occurred even when the Rune invocation later fails or is stopped. Lifecycle tests verify that failed and cancelled reservations retry from the last committed state, that restart preserves reserved-first FIFO order, and that cancellation followed by installation retries queued input in order with the new implementation. Tests also cover that a running actor's queued messages are not processed until the reservation finishes. Transition tests cover malformed results, runtime failures, invalid targets, aggregate capacity, and successful message effects. ## Limits This is not yet a full image: no persistent behavior registry or mailboxes, transactional outbox, supervisors, GUI, or automatic image bootstrap. The behavior registry and implementation installation are process-local; installation requires an idle handler boundary and does not upgrade active execution. The example counter script is bundled through `include_str!`. Messages are not accepted by failed or stopping actors, or removed references. The bundled counter defaults unit/null initial state to zero when it handles its first message. `#{op: "inc"}` increments and logs the state, and queues another increment to itself. Explicit integer initial state is preserved. No handler runs at spawn, so a no-state counter snapshot stays null until its first message. Native functions must be nonblocking. Dropping a future proves cleanup only for the controlled test operation, not rollback of arbitrary external effects. Instruction slices cannot interrupt a native callback. The [execution decision](decisions/0001-worker-local-execution.md) records the scheduling trade-offs. Heap profiling and source-level debugging remain open.