swactor/crates/dashboard/ACTOR_PANEL_SPEC.md
Zachery Aaron Shores-Chmielewski ef9e1c98a3 refactor: mvp-system is now a standalone app, myelin
Promote the `mvp-system` workspace library crate to a standalone application at `apps/myelin`, rebranding the MVP system along with its binaries, node image, and spec.

- workspace `Cargo.toml`: swap member `crates/mvp-system` -> `apps/myelin` and drop `apps` from `exclude` so the app joins the workspace
- `apps/myelin/Cargo.toml`: declare package `myelin` with `autobins = false` and explicit `[[bin]]` targets `myelin-worker`, `myelin-orchestrator`, `myelin-chat`
- `apps/myelin/src`: move the whole `mvp-system` source tree and rebrand module surfaces (`chat/mod.rs`, `prompt/mod.rs`); add `bin/chat.rs` (`myelin::run_chat_from_args`) and delete the old `mvp_chat.rs`
- `apps/myelin/node-image`: relocate the worker image assets from `apps/mvp-node/` (Dockerfile, Dockerfile.base, tinygrad_worker.py, entrypoint, e2e script) and rename `MVP_SYSTEM_SPEC.md` -> `MYELIN_SPEC.md`
- `xtask`: rewrite build/reference paths for the rename (~1000-line churn); add `crates/dashboard/ACTOR_PANEL_SPEC.md`

Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-08-01 14:06:10 +04:00

8.8 KiB

Actor Panel — v1 Spec (DRAFT)

Status: DRAFT, v1 scope. Converged scope for the first menu item of the swactor dashboard: live observation of actors, their messages, mailbox, and throughput. North star is tokio-console, scoped down to ship fast and iterate.


1. Goal

An actor-first roster plus a per-actor dossier, focused on identity, lifecycle, mailbox, and message throughput. Streaming-feel, single-runtime in focus with a runtime selector. Replaces the worker page's worker-centric framing with actors as the primary axis; workers become a column and a filter.

One sentence: a live console over the actor population and what each actor is doing with its messages.

2. Non-goals (deferred backlog)

Explicitly out of v1, queued for later iterations:

  • Tree lens (spawn/supervision hierarchy) — needs parent.
  • Flow/message graph (who talks to whom).
  • All relationship facets: parent, children, senders/receivers, monitors.
  • Per-actor arrival rate (saturation is derived from mailbox growth instead).
  • Death-event capture with StopReason + ExitValue (dead actors get a stale badge from the last retained snapshot only).
  • Rich lifecycle transition timeline (v1 shows current state + age).
  • SSE streaming transport (v1 polls, matching the existing pages).
  • Actor state inspection (GetActorState — security-gated, tier-3).
  • Per-actor busy/poll handler timing.

3. Roster (primary view)

Sortable, filterable live table. Default sort: mailbox depth descending (hot actors bubble up). Matches the existing worker page's poll cadence (750 ms) and runtime selector.

Column Source Notes
name · addr LogicalName + ActorAddress (short) short hex identity + human label
type actor_type_name new — the actor's Rust type
state derived from lifecycle flags (see §6) up from poisoned-only; colored badge
mailbox mailbox_depth + growth color trend color: steady / rising / runaway
msg/s derived from messages_processed delta processed rate
processed messages_processed lifetime total
last msg last_msg_type last handled message type
worker worker_id placement; also a filter
age from spawn_time new — alive duration

Filters: by state (e.g. "show all poisoned"), by type, by worker, by name/address substring.

4. Dossier (row click)

Focus panel with three facets, all about the actor itself — no relationships.

4.1 Lifecycle & identity

Full address, actor_type, the message_type it accepts, spawn age, current state. If the actor is dead, show the last-known state as stale (no StopReason/ExitValue in v1 — deferred).

4.2 Mailbox dynamics

Depth-over-time chart + growth rate. Saturation signal. Backed by a per-actor history ring buffer the view maintains (bounded VecDeque, same pattern as the existing HistorySample in worker_view.rs). No core change — the view folds each incoming snapshot into the buffer.

4.3 Message diet (signature feature)

message_type_counts rendered as a sorted bar list (top-N) with counts, plus the last message. Data already exists. This is the column tokio-console cannot have (tasks are opaque); swactor actors are message-typed, so "what does this actor do" is answered by what it eats. Lean into it visually.

5. Summary strip

Throughput at the runtime level, above the roster. Same card shape as the worker view, actor-centric:

actors · total msg/s · total mailbox · poisoned · uptime

6. Lifecycle state derivation

A single display state derived from the four flags, in priority order:

poisoned               → "poisoned"   (red)
else stopping          → "stopping"   (orange)
else suspended         → "suspended"  (yellow)
else !started          → "new"        (blue)
else                   → "running"    (green)

swactor's flags are the states — cleaner than tokio-console's running/idle.

7. Data model — the one core prerequisite

The live feed is runtime.actors, emitted by DatastreamStatsHook (crates/datastream/src/endpoint.rs) from ActorSnapshot (src/stats.rs:123), built in ActorPool::mailbox_depths_into (src/worker.rs:995). ActorSlot (src/worker.rs:558) already holds every field v1 needs; the change is purely additive in the snapshot, keeping the read-only push contract.

Enrich ActorSnapshot with

Field Type Source on ActorSlot / actor
started bool slot.started
suspended bool slot.suspended
stopping bool slot.stopping
actor_type &'static str slot.actor.metadata().actor_type_name
message_type &'static str slot.actor.metadata().message_type_name
spawn_time Option<u64> slot.env → SpawnTimestamp (ms since runtime creation)

poisoned already present. Propagate through RuntimeActorSnapshotRecord (endpoint.rs) so the JSON wire payload carries the new keys.

parent is excluded — only needed for the tree lens (deferred).

name continues via the existing merge

name is a swactor-std registry concern, not core. It already flows through the runtime.stats → actor_details (ActorInfo.name) path and is merged by the existing worker view. v1 reuses that merge; no new core plumbing for names.

8. View-side state (dashboard)

New view module, mirroring SwactorWorkerView's structure but actor-centric.

Per-actor view state

struct ActorState {
    address: String,
    name: Option<String>,
    actor_type: Option<String>,
    message_type: Option<String>,
    // lifecycle
    started: bool,
    suspended: bool,
    stopping: bool,
    poisoned: bool,
    spawn_time: Option<u64>,
    // throughput
    mailbox_depth: u32,
    mailbox_growth: f64,                  // depth/s, derived from history
    messages_processed: u64,
    msg_per_sec: f64,                     // derived via assign_u64_rate
    last_msg_type: Option<String>,
    message_type_counts: Vec<(String, u64)>,
    worker_id: Option<u32>,
    history: VecDeque<HistorySample>,     // per-actor mailbox chart buffer
    last_update: Option<Instant>,
}

apply_json reuses the tolerant multi-alias field helpers already in worker_view.rs (u32_field, string_field, assign_u64_rate, parse_message_type_counts) so the new keys land gracefully across versions.

Snapshot JSON contract

Same envelope as the worker view:

{
  "runtimes": [
    {
      "stream": { "key": "...", "node": "...", "life": 0 },
      "live": true,
      "last_seen_ms_ago": 42,
      "summary": { "actors": 0, "msg_per_sec": 0.0, "mailbox_depth": 0, "poisoned": 0, "uptime_ms": 0 },
      "actors": [ { /* ActorState fields */ } ]
    }
  ]
}

Keyed by stream_key = "{node}#{life}" (one entry per runtime stream), matching the worker view so the runtime selector is shared.

9. Transport & feel (kept cheap for v1)

  • Poll-first, 750 ms, matching worker_page.rs. SSE is an iteration upgrade.
  • State-badge color carries the alarm; no flash/animation machinery.
  • Dead actors: last snapshot retained with a stale badge (now - last_seen > LIVE_TTL), no death-event capture.
  • LIVE_TTL ~8 s (match the worker view).

10. File map

File Change
src/stats.rs add fields to ActorSnapshot (§7)
src/worker.rs (mailbox_depths_into) populate new fields from ActorSlot
crates/datastream/src/endpoint.rs (RuntimeActorSnapshotRecord) serialize new fields
crates/dashboard/src/swactor/actor_view.rs new — ActorPanelView: DashboardView
crates/dashboard/src/swactor/actor_page.rs new — ACTOR_HTML const
crates/dashboard/src/swactor/mod.rs pub fn actor_view(), register
crates/dashboard/src/server.rs / root_page.rs list first in the menu

11. Open decisions

  1. Menu first: render the index dynamically from ViewRegistry::descriptors() (today the static root HTML ignores it) vs. hardcode the actor link ahead of workers. Recommend dynamic — scales as views grow.
  2. Type cardinality display: with many instances per type, do we also offer a type-aggregated rollup (OrleansDashboard-style) in v1, or instance-only? Recommend instance-only for v1; rollup is a fast follow.