Commit Graph
143 Commits
Author SHA1 Message Date
Claude (sandbox) a2d0b7af18 feat(causal): fidelity footer in render_summary
One unconditional footer line whenever there are results:
"note: impacts are lower bounds — undershoot grows with speedup pct;
rankings unaffected" — surfacing the RFC 007 Validation fidelity statement
where users actually look, instead of only in the RFC. Wording pinned by
the summary test.
2026-07-13 11:11:20 +00:00
Claude (sandbox) a4647f368a feat(causal): wall-anchored send_after — user-facing timer opt-out (RFC 007)
send_after_wall / send_after_named_wall (+ Timers::insert_send_wall) arm a
message-delivery timer that opts out of the RFC 007 virtual-time shift and
fires at its raw deadline regardless of injected delay — the Send-reason
sibling of sleep_wall, closing the jar item whose substrate efbc254 landed.
For deadlines that reflect the outside world (protocol timeouts, wall-clock
schedules) rather than workload pacing. cancel_timer is anchor-agnostic and
unchanged; without the feature the API exists and is identical to send_after.

The gen_server timer layer (send_after_to, RFC 015 §5) deliberately stays
virtual-only — an opt-out there means new options on the gen_server/statem
timeout API, out of scope for now.

Tests: wall send fires at raw deadline while a virtual sibling shifts;
cancel on a wall send with debt outstanding; featureless delivery/cancel
smokes through the public named API. 15/15 causal, 34/34 binaries both
feature configs, lib clippy clean both.
2026-07-13 11:10:11 +00:00
Claude (sandbox) fec760a3c0 docs(causal): record the reserve-shortfall verdict in the demo header
Occupancy probe on 24 cores: δ = 0.3µs/item (0.1% of the serialized
path); wide guard leaves the @50% cell unchanged. The +84-vs-+100
shortfall is controller-side (injected 327ms of the ideal 350ms over
the 700ms window, plus ~3% real-rate dip during experiments), not
unguarded stage time. urus's ~70µs/request remainder remains the
guard-placement case; the occupancy probe discriminates the two.
2026-07-13 09:48:24 +00:00
Claude (sandbox) d5b6a8f66f feat(causal): pipeline demo modes for the reserve-shortfall experiment
SMARM_CAUSAL_MODE selects the reserve stage's guard placement:
work (default, unchanged) | wide (guard over recv+work+send, the whole
serialized per-item path) | occupancy (no experiments; per-segment
timing of reserve's loop at baseline, reporting the unguarded
remainder δ and the impact ceiling it implies).

Discriminates the two candidate explanations for the demo's +84-vs-+100
@50% shortfall: physical recv/send time outside the guard (occupancy
sees δ≈30µs, wide recovers ~2x) vs. injection-side credit loss
(occupancy sees δ≈0, wide caps at ~+84 too — guard cadence identical).
2026-07-13 09:40:27 +00:00
Claude (sandbox) efbc254634 feat(causal): wall-anchored timers — controller windows keep fixed wall length (RFC 007)
New timer anchor: insert_sleep_wall / scheduler::sleep_wall (exported) opt a
Sleep entry out of the RFC 007 virtual-time shift, so it fires at its raw
deadline regardless of injected delay. Featureless config is unchanged (the
API exists but is identical to sleep).

The causal controller's window/cooldown sleeps and the tsc_hz calibration
sleep use it on the actor path (the OS-thread path was already wall). This
fixes the controller's own sleeps dilating under its own injection —
experiment windows stretched ~2x at 50% speedup (337ms -> 646ms injected/
window). Deltas were rate-normalized so results were unbiased; this fixes
sweep cost, not bias. The general wall-anchored-timer-semantics jar item
(user-facing opt-out) remains open; this lands the substrate.

Test: wall_timer_ignores_injected_delay — wall entry fires at raw deadline
while a virtual sibling in the same heap shifts. 13/13 causal, 34/34
binaries both feature configs.
2026-07-13 07:44:51 +00:00
Claude (sandbox) 04dbac1f4b feat(causal): timer-heap virtual time — deadlines chase injected delay (RFC 007)
Injected delays dilate virtual time for the workload, but timer deadlines
stayed wall-anchored: a sleep or receive-timeout fired early in virtual
terms, so timeout/retry behaviour sped up relative to the dilated world
(v1 known gap #1).

Every heap Entry now carries delay_stamp — the global delay ledger at
(re-)queue time, cfg-gated on smarm-causal. pop_due converts any debt
accrued since the stamp to wall time (tsc_hz) and shifts the effective
deadline; a not-yet-due entry is re-queued at the shifted deadline with a
fresh stamp, so it keeps chasing delay injected while it waits. seq is
preserved across re-queues, keeping send_after cancellation identity
intact (cancelled entries are discarded before any shift). Zero debt is
byte-identical to the old path; peek_deadline may under-report, costing
one spurious scheduler wake per injected chunk (documented).

This also makes the park-gated resume credit *correct* rather than
forgiving for sleepers: a sleeping actor now physically pays its debt by
sleeping longer, so the on_resume fast-forward reflects real payment
(sleeping_actor_pays_injected_delay pins this end-to-end through the
runtime).

New test hooks: inject_delay_cycles_for_test (deterministic ledger
driver, eagerly TSC-calibrating so conversion never stalls a scheduler
loop) and cycles_to_duration. The ledger is process-global, so the
delta-sensitive causal tests now serialize on a shared test mutex — they
were racy under the parallel test harness before this, in principle.
2026-07-13 07:31:08 +00:00
Claude (sandbox) d496914d40 fix(causal): flush target-site samples at guard boundaries (RFC 007)
Samples were taken only when maybe_preempt's cold block happened to fire
in-site, so the interval between the last check and SiteGuard drop was
discarded on every site entry. Measured live on the 24-core box:
22-29us lost per entry, a constant attribution efficiency of ~0.93-0.94,
which under-reported every impact (+83.5% where theory says +100%; the
observed shortfall fits 1/(1-pct*eff)-1 at both 25% and 50%).

SiteGuard enter/drop now call site_transition(): leaving the target site
flushes the pending interval into the ledger (sample-only, never spins,
so safe under no-preempt regions); entering the target site re-arms the
sample clock so pre-site time is never attributed (the symmetric
over-attribution). Winner attribution is factored into attribute(),
shared by the cold check and the flush, with the same interval clamps.

Adds examples/causal_attrib_probe.rs (ground-truth in-site time vs
ledger attribution, the probe that confirmed the leak) and the
site_boundaries_flush_tail regression test (a site entry that never
hits a cold check must still be attributed). Also gates causal_probe
on smarm-causal in Cargo.toml - it never was, so featureless builds
of the examples were broken.
2026-07-12 19:35:06 +00:00
Claude (sandbox) 2668f4018f feat(causal): native causal profiling behind smarm-causal (RFC 007 v1)
causal_site! scoped site guards per actor slot, progress! throughput
points, and a Coz-style virtual-speedup engine hooked into
maybe_preempt's amortized cold block: target-site samples grow a global
delay ledger; bystanders spin-absorb their debt at the next causal
check, with timeslice extension so injected delay is not charged
against the slice.

Resume credit (Coz's blocked-thread rule) is gated on a causal_parked
slot bit set only by a real park: crediting on every resume made any
yield-cadence actor delay-immune and every experiment inert (found
live on a 24-core run — dead-flat deltas across all sites).

Report normalization uses a measured TSC frequency (~50ms calibration
on first use) instead of the crate-wide 3 GHz assumption, which
uniformly inflated impact numbers on a 3.7 GHz box. impact_pct() is
the machine-readable form of the summary for programmatic checks.

examples/causal_pipeline.rs burns fixed *work* (calibrated LCG loop),
not fixed wall time — a timed busy-wait absorbs injected delay into
its own budget and reads as a no-op. Self-checking: exits nonzero if
causal separation fails; skips the verdict below 4 cores. Validated
on a 24-core box: reserve (true bottleneck) +29.3%@25/+83.5%@50;
serialize and background-compaction ~0%.

Known v1 gaps (jar): timer-heap deadlines unshifted, no-check!/no-alloc
actors undelayable, multi-scheduler coherence best-effort Relaxed,
off-CPU blame punted, Instant::now() uncorrected.

Zero-cost with the feature off; clippy -D warnings clean both ways;
full suite green with and without smarm-causal.
2026-07-12 19:10:04 +00:00
smarm-agent 1c90a4ef5e fix(registry): by_name stores the full holder Pid — a dead name heals under slot reuse
Root cause of soak20 signature 2 (refcount_test.exs 'watcher crash',
110235x fast {:error, :server_down} probes over the full await window):
by_name mapped name -> slot *index*, so a name whose holder died (no stop
path unregisters; prune is lazy) and whose slot was then re-tenanted read
as live-held: register failed NameTaken{holder: <unrelated tenant>} (which
the bridge macro's generated start() swallows -> start_server/1 reports :ok
for a server that never came up), while name resolution reached the
tenant's mailbox, missed on the message TypeId and failed fast WITHOUT
pruning — the wedge self-sustained for the tenant's lifetime. Name-
addressed send additionally judged liveness on the slot's *current*
mailbox pid, so a same-typed tenant would have received the message
(misdelivery) and a differently typed one a misleading NoChannel.

Fix: by_name: HashMap<&'static str, Pid> — every reader judges the
*stored* holder with the generation-checked live(), so a recycled slot's
tenant no longer impersonates a dead holder, and every touch (register /
whereis / resolve / send) prunes and heals a stale name. prune(index)
becomes prune_holder(pid): names bound to the holder go; the mailbox goes
only while still the holder's own (a tenant's replacement mailbox is left
untouched). Introspection matches names to mailboxes by full pid, so a
stale name never annotates a slot's new tenant.

Deterministic regression test added first and shown to fail pre-fix
(tests/stale_name_slot_reuse.rs: tiny slab forces re-tenanting; old slot
(1,0) died, tenant (1,1) took the index; register -> NameTaken pre-fix).
Post-fix it asserts the healed contract: whereis -> None (pruned), call ->
ServerDown, re-register -> Ok. Suite 33 ok-binaries, clippy gate clean.

In the wild the window opened at every splice_test teardown:
Splice.terminate -> exit_server('subtree') left the name bound; width 20
raised the re-tenant probability. Downstream (smarm_beam): install_child's
unregister-before-register workaround becomes dead code (removed there);
the #[smarm_server] macro's swallowed register error becomes truthful
idempotency (a NameTaken now really is a live holder).
2026-07-12 07:23:30 +00:00
smarm-agent f6641cd266 runtime: name scheduler threads smarm-sched-{slot}
Extra scheduler threads (slots 1..N-1) are now spawned via thread::Builder
with the name smarm-sched-{slot}, so they are identifiable in
/proc/<pid>/task/*/comm, stack dumps and debuggers. Thread 0 keeps its
caller-given name (an embedder names the thread that calls run — smarm_beam
names it smarm-runtime). A refused spawn still panics, matching the previous
thread::spawn semantics.

Motivation: the §17 scheduler-width knob in smarm_beam asserts the *live*
width by counting these named threads, and a 20-scheduler soak needs the
threads tellable apart in wedge captures.
2026-07-11 19:17:52 +00:00
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