Commit Graph
133 Commits
Author SHA1 Message Date
smarm-agent 0017c5b9a1 fix(runtime): consume wake-pipe bytes only under the drain lock
Lost-wakeup: schedule_loop's phase-1 drain uses drain_lock.try_lock(), and
try_lock losers skip the completion drain entirely. Both schedulers park on
one shared wake pipe and, until now, drained ALL its bytes right after their
idle poll_wake returned — outside the drain lock. A loser could therefore
eat the byte announcing a completion the winner had not seen (the winner was
already past drain_completions when the epoll thread pushed it), and both
threads would park with the completion stranded. Because the bridge eventfd
is registered EPOLLONESHOT, the kernel had already disarmed it at
epoll_wait, so no later write could re-fire it: the runtime slept until an
unrelated timer deadline forced another phase-1 pass.

Fix: drain_wake_pipe() moves inside the drain guard, immediately before
drain_completions(); the two post-poll drains in the Pop::Idle arms are
removed. Producers push their completion before writing the byte, so a byte
consumed under the guard always has its completion visible to the drain that
follows. An unconsumed byte keeps the (level-triggered) idle poll returning
instantly, so a try_lock loser spins briefly until the winner releases —
it can no longer sleep through stranded work.

Found via smarm_beam's ingress-cap drain barrier flaking under CPU load
(5/25 loaded suite runs wedged; mid-wedge stacks showed both schedulers in
poll_wake with an FdReady stranded and the eventfd disarmed). Post-fix:
60/60 loaded runs green, tight 5.8-6.8s timing band, no stall tail.
Root-cause notes: smarm_beam outputs/flake-rootcause-egress-overload.md.
2026-07-11 16:13:46 +00:00
smarm-agent 6c2b7e91cf channel: drop queued messages when the Receiver drops
A queued Envelope::Call was stranded until the last Sender dropped, so a
caller parked in gen_server::call was never released with ServerDown when a
*named* server was request_stop'd — the registry's inbox Sender clone (lazy
prune) kept the channel Arc, and the queued reply_tx, alive indefinitely.

Receiver::Drop now drains the queue (items dropped after releasing the lock,
since a reply_tx drop reaches a different channel's lock + the scheduler),
restoring the documented ServerDown guarantee on every teardown path.

Adds tests/stop_with_queued_call.rs: deterministic pure-smarm reproducer.
2026-06-24 20:53:27 +00:00
smarm-agent 3e9c33377c gen_statem: disambiguate module/macro intra-doc links 2026-06-20 19:51:33 +00:00
smarm-agent e54c67c431 supervisor: rewrite docs for users; relocate internals to items 2026-06-20 19:51:32 +00:00
smarm-agent 3e321eaaf3 pg: rewrite docs for users; relocate internals to items
Reframe the module doc in the gen_server house style: lead with what a
process group is and when to reach for one, the auto-eviction-on-death
behaviour, groups-vs-registry, and a running-context note. Keep the
runnable example; add an ignored dispatch/worker-pool example.

Move implementation reasoning to where a maintainer stands: lock
discipline onto the ProcessGroups store, the eager-cleanup rationale onto
reap_group, the join finalize-race detail into join's body comment.

Drop all RFC references and the stray phase marker, and lead the public
read/select fn docs with what the caller gets. Demote the pub(crate)
assert_type intra-doc link in pick_as to plain code, clearing a
pre-existing broken-link warning.
2026-06-20 18:51:00 +00:00
smarm-agent c415f14dd0 ci: deny unwrap_used/expect_used on the library target
Add [lints.clippy] unwrap_used = "deny", expect_used = "deny" plus a tracked
pre-commit hook running `cargo clippy --lib -- -D warnings`. Library code may
not hide a panic behind unwrap/expect; panic!/unreachable! stay un-linted as
the explicit sanctioned form. Gate is the library target only — tests and
examples are not gated. A fresh clone must run
`git config core.hooksPath .githooks` to enable the hook.
2026-06-20 17:47:44 +00:00
smarm-agent 33177a0c48 library + trace: rewrite panic sites as explicit match+panic
Apply the same explicit match+panic shape to the library layer (channel,
gen_server, gen_statem). Extend it to the smarm-trace-gated code that the
default `cargo clippy --lib` does not see: the GLOBAL lock-poison sites in
trace.rs and the current_pid sites inside te!() in channel.rs. Keep the
current_pid match inside the te!() argument so non-trace builds evaluate
nothing extra on the recv-wake hot path. const-init the trace thread-local.
2026-06-20 17:47:39 +00:00
smarm-agent a875fa8285 core: rewrite panic sites as explicit match+panic
Replace implicit unwrap()/expect() in the lock-ordered core with explicit
match arms. Lock-poison sites use one uniform message
("smarm: <lock> lock poisoned (core corrupt): {e}"); invariant sites panic
with a descriptive message naming the violated invariant. No behaviour
change: each rewrite preserves the prior panic-on-bad-arm semantics. Also
clears the accompanying clippy hygiene in these files (redundant_closure,
len_without_is_empty, too_many_arguments, unnecessary_sort_by,
missing_safety_doc, nonminimal_bool/unnecessary_unwrap).
2026-06-20 17:47:33 +00:00
smarm-agent 531571bfa5 gen_statem: postpone events for replay after a transition
A `=> postpone` row (cast/call/info) defers the current event untouched,
to be replayed after the next real transition. `handle` is now two-phase:
a borrow-only postpone pre-pass that hands the event back as
`Step::Postponed(ev)`, then the existing consuming `match (state, event)`.
The loop owns a FIFO queue, drained in the new state ahead of further
intake; a replayed event may postpone again. A postponed `call` keeps its
Reply, so a later state answers it.

`handle` returns `Step` (Postponed / Transitioned / Stayed) so the loop
can see both deferral and transition without reading the state cell.
`Resolution::Postpone` is removed: postpone is a pre-dispatch routing
decision, not a consuming-dispatch outcome.
2026-06-20 13:02:15 +00:00
smarm-agent acf67fef06 gen_statem: state and named timeouts, info events
Add the two timeout flavours, both surfacing as ordinary events matched
in on-state arms:

- cx.state_timeout(d): fires a state_timeout event after d in the current
  state, auto-reset by the loop on every real transition.
- cx.timeout(name, d): fires a timeout(name) event after d, surviving state
  changes, keyed by name, with cx.cancel_timeout(name).

Both ride the existing timer min-heap via send_after_to onto a new per-loop
system channel, selected above the inbox so a fire can't be starved by inbox
traffic. A local-id stamp on each fire lets a reset/cancel that loses the race
discard a stale fire. The macro grows an info: clause and folds three internal
Ev variants (Info, StateTimeout, Timeout) alongside cast/call, with new row
keywords info / state_timeout / timeout. Unmatched info silently drops (the
gen_server default); state/named timeouts have no default, so a state that can
see one must handle it or the match is non-exhaustive.

Rename the hand-written expansion-target example fused -> expanded, retire the
deprecated Switch demo machine (its round-trip / enter / panic-down coverage
moves onto the timer machine), and refresh the macro docs to the door machine.

cargo build --all-targets warning-free; cargo test green.
2026-06-20 12:22:45 +00:00
smarm bfa513cd6d doc: rework gen_server module docs completely 2026-06-20 13:41:27 +02:00
smarm-agent 0cf6b80396 gen_statem: GenStatem* type prefix + cleanup
- rename StatemRef/StatemCallError/StatemSendError -> GenStatem*
- move the inline unit test out of src; consolidate the Switch coverage
  onto a single macro-driven harness in tests/gen_statem.rs
- drop the redundant hand-written Switch test machine and the two
  untracked rejected-direction probes (succ_enums, typed_edges)
- rename examples statem_{fused,macro}.rs -> gen_statem_{fused,macro}.rs
- strip RFC/chunk/spike provenance and fix the mislabeled "throwaway"
  example header and dead cross-references
2026-06-20 10:48:33 +00:00
smarm-agent 3e316066c3 gen_server: prefix public types with Gen (GenServerRef, GenServerCtx, GenServerName, GenServerBuilder, NamedGenServerBuilder) 2026-06-20 10:48:33 +00:00
smarm f646c5cd72 cleanup some LLM crud 2026-06-20 12:22:33 +02:00
smarm 07867b91f6 doc: links and tweaks 2026-06-20 12:11:16 +02:00
smarm-agent 8d1605638e statem: add gen_statem! authoring macro (RFC 017 chunk 1)
Declarative macro_rules! that fuses the hand-written statem surface into
one invocation: emits the unified event enum, the machine struct + state
cell, start, the Machine impl (dispatch + stay/transition apply-tail), and
the enter dispatch. User keeps the meaningful types, the per-state
successor enums, and the free handler fns.

Pure sugar: every safety property is a property of the emitted code,
checked by rustc, so a declarative macro carries (almost) the proc-macro
guarantee set:
  1. forgotten (state,event) pair -> E0004 (total match, no injected _)
  2. conflicting row -> unreachable_patterns (macro self-denies; HARD only
     in-crate, suppressed cross-crate by in_external_macro -- documented)
  3. orphan handler -> dead_code (handlers are user free fns)
  4. out-of-set target -> E0599 (per-state successor enums)

Hygiene: bodies can't see the macro's self/cx, so the caller names them
via `context(data, prev, cx)` (shared call-site hygiene).

No separate transitions{} block: the match IS the table, successor enums
ARE the per-state target sets (fused variant, diverges from RFC
edge-lint).

- examples/statem_macro.rs: Door machine via the macro (parallel to the
  hand-written examples/statem_fused.rs; diff the two to see the delta).
- in-crate test exercises a machine + anchors the in-crate #2 guarantee.
2026-06-20 09:55:01 +00:00
smarm-agent acc37c5fc9 RFC 017 chunk 1: gen_statem primitives (no macro yet)
Runtime support layer for gen_statem, built against existing public API
(channel + scheduler::spawn), sibling to gen_server. No macro: per review,
build the primitives first and hand-write the Switch example to evaluate
whether a statem! macro earns its place before committing to one.

- src/statem.rs: Machine trait (on_start/handle), Resolution<S> with
  From<S>, Cx (on_unhandled), Reply<T> move-only reply handle, StatemRef
  (send/call), spawn + inbox loop. Real time only; Postpone + Cx timeout
  arming are in the type surface but not yet acted on (chunks 2-3).
- examples/statem_switch.rs: the RFC Switch machine hand-written against the
  primitives, tagged USER vs MACRO to mark what a macro would generate.
  Asserts the RFC end-state (flips=1, enters=3).
- tests/statem.rs: call/cast round-trip, enter-on-start/transition-not-stay,
  panicking-handler -> Down.

Reply<T> included (the call helper needs a handle type; keeps the example
true to the RFC surface) but isolated and trivially removable if we drop it.
2026-06-19 19:52:02 +00:00
smarm-agent 0d6fc970a7 scheduler: gate send_after_to runtime tests out of the loom build
These three RFC-015 tests call run() (the real runtime), so under
--cfg loom they construct loom atomics outside a loom::model block and
panic with "cannot access Loom execution state from outside a Loom
model". They are unit tests of runtime behavior, not state-machine
models. Gate the module #[cfg(all(test, not(loom)))], matching the
existing run_queue::tests precedent: they still run under normal
cargo test, and the loom build is now 28/28 clean.
2026-06-19 14:48:23 +00:00
smarm-agent e9c39bff46 roadmap: introspection & observability shipped as RFC 016
Chunks 1-4 all landed; mark the introspection block done (was 'Needs an
RFC'), matching the send_after in-place SHIPPED treatment above it.
2026-06-19 10:20:37 +00:00
smarm-agent 2d15834b24 RFC 016 Chunk 4: observer example with ps-style + tree dump
A runnable examples/observer.rs (required-features = ["observer"]) that
stands up a named service + two parked workers, starts the observer, and
renders a snapshot as a ps-style table and the parentage forest indented.
The observer appears in its own dump, caught running while it serves the
snapshot call — transport over the same read every consumer sees.

Complements the runnable doctest already on observer::start.
2026-06-19 10:20:09 +00:00
smarm-agent 6df4cd4a0b RFC 016 Chunk 4: observer gen_server (feature-gated)
A thin GenServer consumer of the Chunk-1 read primitive — the live
observer process (D12). ObserverRequest/ObserverReply are the wire
contract (D11); the version rides along on the snapshot/tree payloads,
which already carry SNAPSHOT_FORMAT_VERSION (D1). Behind the new
`observer` Cargo feature, off by default (D10): the primitive stays
always-on, only the transport is gated. Cast is Infallible, so the
server takes no async traffic and handle_cast is statically unreachable.

Gated integration test proves each verb relays exactly what the
corresponding primitive returns (snapshot/tree/actor_info), incl. a
forged-pid None and a live Parked classification.
2026-06-19 10:18:54 +00:00
smarm-agent 48d47c45c9 RFC 016 Chunk 2c: approximate per-actor time-budget (reductions-like)
Accumulate on-CPU cycles per actor as ActorInfo.budget_cycles, behind
the off-by-default budget-accounting feature (D6) — a reductions-style
work metric for relative comparison across runs.

- Approximate by design (per Mark): charge now - slice-start once at the
  yield point, reusing the timestamp reset_timeslice already sets, so
  one RDTSC per resume not two. Wake-slot resumes inherit the slice and
  so slightly over-attribute the chain's time to the woken actor — noise
  that averages out; we trade exactness for half the hot-path cost.
- Field/ActorInfo member are unconditional (keeps the snapshot shape
  stable across the feature flag, D1); only the accumulation is gated,
  so default builds are byte-identical and pay nothing. Reads return 0
  when off. Single-writer Relaxed like the other counters; reset in
  reset_counters (D7).

Matrix: default + feature-on + rq-mpmc + rq-striped + release + trace all
green; loom unaffected (feature off under --cfg loom).
2026-06-19 08:47:58 +00:00
smarm-agent e93b3120ec RFC 016 Chunk 2b: per-actor messages-received counter
Tally each dequeued message against the receiving actor, surfaced as
ActorInfo.messages_received (the 'is this actor draining slower than its
mailbox fills' signal).

- messages-received, not sent (D4): the receiver counts on its own
  thread, so it's a single-writer Relaxed load+store on a hot Slot
  AtomicU64, no atomic RMW (D5); reuses the stashed *const Slot from 2a.
- Incremented at all six channel dequeue-success sites (recv,
  recv_timeout x2, recv_match, try_recv_match, try_recv) via
  preempt::note_message_received; no-op outside an actor (null slot).
- Resets with overruns in reset_counters across the three lifecycle
  sites (D7).

Matrix: debug default + rq-mpmc + rq-striped + release green; loom
slot_state/run_queue models pass (the 3 send_after_to loom failures are
pre-existing at fc014c4 — plain #[test]s under --cfg loom, not Chunk 2).
2026-06-19 07:34:32 +00:00
smarm-agent 354eef9f88 RFC 016 Chunk 2a: per-actor timeslice overrun counter
Tally overruns at the slice-expiry site in preempt.rs (the RFC 006
signal), surfaced as ActorInfo.overruns.

- Counter is a hot-region AtomicU64 on Slot, single-writer Relaxed
  load+store (no atomic RMW), read Relaxed by the snapshot (D5).
- Reached from the rare expiry branch via a stashed *const Slot in a
  preempt thread-local, set/cleared on the same resume/return boundary
  as CURRENT_STOP — one TLS load, no runtime lookup. Shared infra for
  the messages-received counter next.
- Reset in all three slot-lifecycle sites: Slot::vacant, reclaim_slot,
  install_actor (standing invariant, D7) — per-incarnation counts.
2026-06-19 07:18:03 +00:00
smarm-agent 7ef915c81e RFC 016 Chunk 3: parentage tree view
tree() / tree_from() fold a Chunk-1 snapshot into a forest by grouping
each actor under its parent pid — one O(n) pass, no new reads. tree_from
is public so a held (or synthetic) snapshot can be folded without a
second scan.

- D8: actors parented at ROOT_PID are genuine roots; an actor whose
  recorded parent is absent from the snapshot is re-rooted under the
  sentinel and flagged orphaned, so the forest stays total. take()-on-
  place doubles as a guard against re-entering a node.
- D9: the edge is parent/spawned-by, documented as not necessarily
  supervision.
2026-06-19 06:33:21 +00:00
smarm-agent c66691943d RFC 016 Chunk 1: runtime introspection read primitive
snapshot() / actor_info() return owned ActorInfo over the slab: pid,
names, fine-grained scheduling state, parent edge, trap flag, mailbox
depth, and monitor/link/joiner counts. Pure reads, no hot-path change.

- ActorState maps the packed slot word (no new storage); introspect.rs.
- D1: RuntimeSnapshot carries SNAPSHOT_FORMAT_VERSION from day one.
- D2: per-slot tearing (ps semantics); actor_info coherent per actor.
- D3: mailbox depth included. Registry channels now stored behind an
  ErasedSender trait (downcast for clone_sender + type-erased
  queued_len); depth summed over published channels under the registry
  Leaf (Leaf -> Channel), kept out of the cold-lock pass so no two
  Leaves are ever held at once. Depth covers published channels only.
- Done slots surface as root-less tombstones.
2026-06-19 06:31:10 +00:00
smarm-agent fc014c4e54 roadmap: gen_server time patterns shipped (RFC 015); substrate status recorded 2026-06-18 19:31:06 +00:00
smarm-agent f454a9195a gen_server: docs — disambiguate client call deadline from server idle timeout (RFC 015 §7) 2026-06-18 19:20:12 +00:00
smarm-agent 1df85e2384 gen_server: drop-guard drains live timers + debug_assert no leak (RFC 015 §4.7) 2026-06-18 19:19:15 +00:00
smarm-agent 8c8af55928 gen_server: idle/receive timeout via select_timeout/recv_timeout + handle_idle (RFC 015 §4.4) 2026-06-18 19:17:20 +00:00
smarm-agent c0cfa01f37 gen_server: tick_every periodic sugar — loop-driven re-arm via factory + Sys::Tick (RFC 015 §4.3) 2026-06-18 19:13:52 +00:00
smarm-agent 401a1465d5 gen_server: TimerHandle arm_after/cancel + Sys::Timer dispatch (RFC 015 §4.3, §5) 2026-06-18 18:58:00 +00:00
smarm-agent 57eadb5c6c gen_server: type Timer + handle_timer/handle_idle; fold control into Sys channel (RFC 015 §4.5, §6) 2026-06-18 18:54:24 +00:00
smarm-agent 47d75d1ead timer: send_after_to — channel-targeting send_after sibling (RFC 015 §5) 2026-06-18 18:50:59 +00:00
smarm-agent 61520bf2cc timer: send_after / cancel_timer message-delivery substrate
Add a cancellable message-delivery timer on the existing timer.rs min-heap,
the substrate the gen_server time idioms (idle/receive timeout, periodic tick,
debounce/backoff) need.

- Reason::Send { fire }: a type-erased delivery thunk. send_after (Pid<A>, via
  send_to) and send_after_named (Name<M>, via send) capture dest+msg and
  resolve the address on fire, not at arm time, so a dead target / restarted
  name is observed when it fires; a failed resolve or closed inbox is dropped
  (Erlang erlang:send_after semantics).
- Cancellation via an "armed" set keyed on the entry seq, exposed as an opaque
  TimerId. Only Send timers use it; Sleep/WaitTimeout keep their inert-stale
  behaviour untouched. pop_due fires a Send only while still armed and removes
  it, so cancel returns true iff it landed before the fire (the race signal).
  cancel is unscoped: any holder of the id can cancel (e.g. racing two servers
  and cancelling the slow path).
- peek_deadline contract documented as "<= true next deadline" so a future
  hierarchical timing wheel can back Timers without touching send_after or the
  scheduler idle path. call_timeout left on recv_timeout (blast radius).

Tests: Timers-level (fire/cancel/race/clear/ordering) plus scheduler-level
delivery for both Name<M> and Pid<A>, cancel-prevents-delivery, and silent
drop on unresolved name / dead pid. Green on rq-mutex/rq-mpmc/rq-striped.
2026-06-18 14:28:54 +00:00
smarm-agent 5cb7f6a491 roadmap: 013/014 shipped, gen_server time patterns up next
Move typed addressable mailboxes (RFC 013 directory rework + RFC 014 typed
addressing/producers/naming/root-exit teardown) into Shipped, with the final
phase's delivered surface enumerated. Retarget Up next to the gen_server
time-related patterns: send_after/cancel_timer substrate + the OTP time idioms
(idle/receive timeout, periodic tick/heartbeat, debounce/backoff) on the v0.8
server loop. Fold the duplicate Later send_after bullet into Up next, and mark
the "app actor blocks AllDone" Look-into as addressed by the root-exit teardown.
2026-06-18 12:32:12 +00:00
smarm-agent ecb0835aa7 examples: typed_actor, named_genserver, worker_pool for the new surface
typed_actor and named_genserver land as written (the target-ergonomics spec):
identity-bound Pid<A> vs durable Name<M>, and a gen_server addressed by a
durable ServerName. worker_pool is reworked from the spec into a self-draining
program: workers retire on a sentinel and report their tally, and the
dispatcher waits the pool out — so it terminates on its own rather than leaning
on root-exit teardown, while still exercising the full typed surface
(spawn_addr / join / pick_as / members_as / dispatch) alongside the untyped
escape hatch (members / pick + send_dyn).

All three build unchanged-against-spec (typed_actor, named_genserver) and run
to completion.
2026-06-18 11:02:35 +00:00
smarm-agent a866e34b52 mailbox: typed-path producers, by-name gen_servers, root-exit teardown (RFC 014)
Implements the new addressing surface the examples in examples/ specify:

- spawn_addr::<A>(FnOnce(Receiver<A::Msg>)) -> Pid<A>: the typed-path
  producer. Makes the inbox, spawns the body with its receiver, and publishes
  the sender from the PARENT side (registry::install_for) before returning the
  pid, so an immediate send_to always resolves (no race on the body installing
  itself). Detached, like ServerBuilder::start.

- lookup_as / pick_as / members_as: re-type an erased pid as Pid<A> over the
  heterogeneous registry/pg stores, sharing one helper (pid::assert_type).
  Unchecked but sound: routing is by message TypeId, so a wrong A degrades to
  SendError::NoChannel on the next send, never a misdelivery.

- dispatch::<A>(group, msg) -> Result<Pid<A>, SendError<A::Msg>>: pick_as +
  send_to, with the message handed back on failure. New SendError::NoMember
  variant for the empty/all-dead group case.

- gen_server naming: ServerName<G> backed internally by the registry's existing
  typed-channel store keyed by TypeId::of::<Envelope<G>>() — Envelope stays
  private, no separate directory. Type-state NamedServerBuilder<G> keeps the
  current infallible ServerBuilder::start() untouched; its start() is fallible
  (parent-side register, NameTaken). Free call/cast/whereis_server resolve per
  use. ServerRef::shutdown (+ free shutdown) is the sys-style synchronous stop.

- root-exit teardown: the run's initial actor is recorded as root; when it
  finalizes it flags root_exited, and the scheduler's idle verdict then stops
  the parked-forever remainder (a one-shot RootDrain sweep). Deferred to the
  idle point on purpose: the run queue drains first, so actors with queued work
  finish rather than unwinding on the stop. This lets a named-server daemon (or
  any pinned actor) wind the run down instead of hanging on live_actors > 0.

request_stop is refactored to a request_stop_inner core so the sweep can drive
it from inside the runtime without re-borrowing the thread-local.

Lib + tests + examples build warning-free; full suite green.
2026-06-18 11:02:27 +00:00
smarm-agent 4c56938f0b mailbox: Phase 4b — send_dyn bare-pid escape hatch (RFC 014 §4.6)
The explicit fallible fallback for when only an untyped Pid is in hand (off a
Down, out of a future members()), so the typed send_to / send-by-name stay the
default and callers don't grow workarounds around a missing primitive.

- send_dyn::<M>(Pid, msg): one line over the shared send_to_pid core. Same
  identity-bound, no-redirect liveness as send_to (Dead once the incarnation is
  gone); the difference is the explicit M, so NoChannel is a real runtime
  outcome here rather than the debug_assert it collapses to on the typed paths.
- Takes Pid<Erased> specifically: a typed Pid<A> must .erase() to reach it, so
  opting into the fallible downcast stays visible at the call site.

tests/registry.rs: send_dyn_delivers_and_reports_wrong_type (right type
delivers; live actor with no channel for the asked type -> NoChannel) and
send_dyn_to_dead_pid_is_dead. Full suite + order-checker green, warning-free.

This completes the RFC 014 send surface: Name<M> (re-resolving), Pid<A>
(typed direct), send_dyn (bare-pid). send_after (§6) can now target any of
them — its Dest question is answered.
2026-06-17 11:45:48 +00:00
smarm-agent 3cec3ba1a1 mailbox: Phase 4a — direct identity-bound send to Pid<A> (RFC 014 §4.2)
Make a Pid<A> messageable: install + send_to, the direct addressing mode that
dies with the actor and never redirects (the counterpart to the re-resolving
Name).

- install::<A: Addressable>(tx: Sender<A::Msg>) -> Pid<A>: opt-in, lazy,
  nameless publish (RFC 014 §5). Files the actor's inbox into the by_index
  mailbox table under TypeId::of::<A::Msg>() and re-types self's identity as
  Pid<A>. Infallible: no name to collide on, self is always live in run().
- register and install now share publish_channel (the insert-or-extend-mailbox
  step factored out); register additionally binds the name. Byte-identical
  storage, so name- and pid-addressing populate the same table.
- send_to::<A>(Pid<A>, A::Msg): resolve by raw pid, deliver with NO redirect.
  The stored mailbox must be this exact incarnation (generation included) and
  live, else SendError::Dead — even when the slot now holds a different live
  actor, which is left untouched. Resolution clones the Sender under the Leaf
  lock, releases, then sends (Leaf -> Channel), as name send / pg / finalize.
- send_to_pid is the shared raw-pid core; send_dyn (§4.6) lands on it next.
- SendError gains Dead(M), the pid-path counterpart to name-path Unresolved;
  into_inner / Display / Debug updated. Name send never yields Dead, pid send
  never yields Unresolved — disjoint by construction, documented on the enum.

tests/registry.rs: install_then_send_to_pid_delivers; and the load-bearing
send_to_does_not_redirect_after_takeover (slot reused by a new incarnation, the
stale Pid<A> send returns Dead and the new occupant gets only its own message).
Full suite + order-checker green, warning-free across all targets.
2026-06-17 11:43:16 +00:00
smarm-agent f5fbb5b144 mailbox: Phase 3 — typed Pid<A> over a raw inner identity (RFC 014)
Fold the made-up Addr<A> back into the pid, where it belonged. The typed
handle IS the pid now.

- RawPid { index, generation }: today's Pid, renamed — the raw numbers, the key
  for identity-only plumbing and the heterogeneous monitor/link/pg tables.
- Pid<A = Erased>: RawPid + phantom actor type. Both an identity and a direct,
  identity-bound address; when A: Addressable a send delivers A::Msg to exactly
  this incarnation (next phase). Hand-written impls (eq/hash/fmt on the raw
  only, no A bounds); fn()->A phantom keeps it Copy+Send+Sync for any A.
- Erased: uninhabited, deliberately NOT Addressable, so a typed send to a
  Pid<Erased> won't compile — that's what send_dyn (section 4.6) will be for. It
  is the default type arg, so every plain "Pid" written today still compiles as
  Pid<Erased>; that, plus the fact that nothing destructures pid fields, is why
  an identity change touching 18 files cascaded to zero internal edits.
- Addr<A> deleted; Name<M> / the Phase-2 registry unchanged (they key on raw).

Per the call to make the consumers take typed pids: the user-facing identity
APIs — monitor, link, unlink, request_stop, spawn_under(supervisor), pg::join,
pg::leave — are now generic over A and erase at the boundary; their bodies are
byte-for-byte unchanged. Pure plumbing (unpark, set_current_pid,
register_supervisor_channel) stays erased — it only ever sees internal identity.

Phase 1's Addr tests become Pid<A> tests (identity/copy/erase/Debug, Send+Sync).
Full suite + order-checker green, warning-free across all targets.
2026-06-16 22:09:27 +00:00
smarm-agent 48bdbada2b mailbox: Phase 2 — registry resolves name/pid to a messageable actor (RFC 014)
Rip out the old name<->pid bimap; rebuild registry.rs as the live mailbox
directory. A name resolves to a SINGLE actor (many-per-label is pg's job); that
actor owns a SET of typed channels (channels are typed, so no single mailbox),
keyed by message TypeId. Resolution: name -> pid (one actor) -> Mailbox ->
channel for type M. Same name + different type parameter selects different
channels of the one actor, so capability separation (§4.7) needs no new type.

- register<M>(Name<M>, Sender<M>): captures the current actor's channel under a
  name (one step, per decision). Adds channels cumulatively; NameTaken only vs a
  different LIVE holder; stale/dangling bindings pruned on contact.
- send<M>(Name<M>, msg): the payoff — resolve, clone the Sender UNDER the Leaf
  lock, release, then send (Leaf -> Channel; a send can unpark). Returns the msg
  on Unresolved / NoChannel / Closed.
- whereis -> single live pid; unregister frees only the name (mailbox/other
  names survive). name_of (reverse lookup) dropped — deferred to introspection.
- Contained erasure: each channel is Box<dyn Any+Send> filed under TypeId::of M
  and downcast to its own keying type, so the downcast can't fail on good data
  (debug-asserted via the stored type_name). Phantom M on Name re-imposes type.
- One Leaf RawMutex (the fold), so a name send stays on a single Leaf — two
  Leaves at once is a hard panic. runtime.rs field/init unchanged (same Registry
  name + new()).

tests/registry.rs rewritten for the new semantics: send-by-name delivery,
many typed channels on one actor, same-name/different-type routing, NameTaken,
takeover across slot reuse, Unresolved/NoChannel. Also fold in a Phase 1 fixup:
the phantom test key was an enum whose variants tripped dead-code under the test
build (only caught now that I run -D warnings on --tests). Full suite +
order-checker green, warning-free across all targets.
2026-06-16 19:17:45 +00:00
smarm-agent 48bc636552 mailbox: Phase 1 — typed-address tokens (RFC 014)
Add the value-token layer for typed addressing, ahead of the handle table:

- Addressable { type Msg }: minimal actor-type -> message-type hook (decision
  (a)). Raw actors are closures over channels; GenServer is multi-message and
  stays addressed via ServerRef. This gives the single-message actors a key.
- Addr<A>: direct, identity-bound address (RFC's Pid<A>, renamed to avoid the
  collision with the non-generic Pid identity). Plain Pid + PhantomData<fn()->A>.
- Name<A>: durable, re-resolving address; const-constructible (Name::new).

Both tokens are Copy + Send + Sync regardless of key (phantom is fn()->T), with
hand-written trait impls so no spurious T: Copy/Eq bounds leak in. No behavior
yet; send/resolve/storage land in later phases. Addr::new is #[cfg(test)] until
Phase 2 has a real (non-test) caller. Unit tests cover identity/copy/eq/debug
and Send+Sync.
2026-06-16 18:33:32 +00:00
smarm-agent ae9f0e864a docs(roadmap): RFC 013 (typed addressable mailboxes) up-next
- add RFC 013 as the up-next item, sequenced before send_after
- mark process groups shipped (RFC 012, b78311b..56f2fc5)
- note send_after now depends on 013 for Dest; cross-link clustering name-addressing
2026-06-16 14:04:11 +00:00
smarm-agent 56f2fc573c pg: canonical usage doctest on the module
A plain-actor example showing the one thing process groups are for: a live
membership view that self-heals on death. Two workers join a group; one dies
and is evicted by the death hook with no deregistration call; one leaves
voluntarily. Runs under cargo test --doc.

(Deliberately not a worker-pool-with-dispatch example: pg is membership and
liveness, not a message router — routing a job to a picked pid needs a
pid->handle map that pg does not provide, so a dispatch example would document
the adapter, not pg.)
2026-06-15 20:05:46 +00:00
smarm-agent 3262c9527b pg: Phase 3 — generation-checked liveness backstop on reads (RFC 012)
members/pick now filter through a lock-free, generation-checked slot-word
read (live(), identical to the registry's guard) so a member that is already
dead but whose Down has not yet been drained is never *returned*. Eviction
stays the monitor's job (reap_group); this is the belt-and-braces read guard.

- members_where / first_member_where take an is_live oracle and replace the
  unconditional first_member; members_of becomes test-only (raw enumeration).
- Unit test: the backstop hides a member the monitor has not yet reaped, and
  does not itself evict (storage still holds it until reap).

Full suite green, warning-free.
2026-06-15 19:53:10 +00:00
smarm-agent 6464c3e92b pg: Phase 2 — liveness & the monitor death hook (RFC 012)
Wire eviction to actor death via the monitor subsystem, using drain-on-
contact (no scheduler hot-path hook).

- Each membership now carries its own Monitor alongside the group entry;
  join() installs monitor(pid) (registration races finalize_actor under the
  cold lock, as every monitor does) and a redundant join tears its extra
  monitor back down.
- reap_group: every group op drains the touched group's monitors with a
  non-blocking try_recv (a delivered Down or closed channel = dead) and, on
  the first death, sweeps that pid out of *every* group via remove_where —
  so a death vanishes from all its groups on next contact.
- Lock discipline: monitor()/demonitor() (cold = Leaf) run outside the group
  lock; try_recv (Channel) runs under it, permitted by the Leaf->Channel
  order; evicted/rejected monitors are dropped after the lock releases so no
  receiver-drop wakeup runs under it. Validated by the debug-build lock-order
  checker passing under the integration tests.
- remove_where now returns the evicted monitors (for deferred drop) and
  preserves intra-group insertion order.
- Tests: unit (synthetic monitors) for reap live/dead/closed + sweep;
  integration (real actors) for death-vanishes-from-every-group, pick on an
  all-dead group, peer survival, leave isolation, dead-at-join eviction.

Outstanding: a miri pass (cargo +nightly miri test --lib pg::) is untried —
first build exceeds the sandbox window and the futex RawMutex may need a miri
shim; the mechanical Leaf->Channel checker is the primary soundness guard.
2026-06-15 17:24:48 +00:00
smarm-agent b78311bc88 pg: Phase 1 — identity & storage substrate (RFC 012)
Add a process-groups module (name -> multiset<Member>) sibling to the
registry, plus the cluster-shaped identity it is keyed on.

- NodeId/Incarnation u32 newtypes; Member { node, incarnation, pid } at
  final (BEAM NEW_PID_EXT-shaped) layout.
- Config::node_id/incarnation builder setters beside wake_slot, defaulting
  to a fixed single-node value; threaded through RuntimeInner::new.
- process_groups: RawMutex<ProcessGroups> on RuntimeInner, Leaf class,
  mirroring the registry field.
- remove_where: the one dumb predicate-eviction primitive (no liveness wired
  yet; first caller is the Phase 2 death hook).
- Public surface (join/leave/members/pick) in pre-liveness/pre-monitor form,
  Pid-shaped; node/incarnation filled from runtime identity.
- Structural unit tests on synthetic members: idempotent join, multiset
  semantics across groups, generation-distinct members, leave + empty-group
  pruning, predicate sweep, incarnation-sweep shape.
2026-06-15 16:41:30 +00:00
smarm-agent 4d4a2a6c9b roadmap: ship v0.9, promote Process groups to up-next
- Compact v0.9 (Wake-path latency) into Shipped; record the RFC 004 spinning
  excision as a deviation (preserved on branch rfc-004-spinning).
- Drop the v0.7 entry, folding its reference into the Shipped history pointer.
- Promote Process groups from Later into the up-next slot v0.9 vacated;
  Per-switch cost now leads Later/Highest priority.
2026-06-15 12:39:30 +00:00
smarm-agent 6566e2f613 roadmap: remove RFC 004 spinning sections (3. Spinning workers, 4. RFC 004 bench + interaction pass)
The spinning experiment is excised from master. Drops the two dedicated v0.9
sections describing it. Inline RFC-004 mentions in the v0.9 intro and the
queue-topology decision record are intentionally left for a manual short-term
-plan pass.
2026-06-15 11:22:21 +00:00