swactor-development-history/PROCESS_PRIMITIVES.md

33 KiB
Raw Permalink Blame History

Process Abstraction for Swactor — Development History

Design and implementation record for the "process" abstraction layer built on top of swactor's actor primitives. This work ran across items 1–9 and added 9 extension traits, 3 registries, and ~70 scenario tests.

Context

Swactor is a distributed actor runtime with local primitives (spawn, send, stop, monitor, supervise) and distributed primitives (SWIM membership, Kademlia directory, cluster-wide naming, content-addressed datastore). The goal was to design a "process" abstraction that sits on top of these primitives, making the experience of running code on a swactor network feel closer to what an OS process feels like -- with access to an API for requesting resources and querying system state.


Part 1: OS Process Mapping

Already strong (direct OS equivalents exist)

OS Concept: PID Swactor Equivalent: ActorAddress (32-byte random) Where: src/actor.rs ──────────────────────────────────────── OS Concept: fork+exec Swactor Equivalent: ctx.spawn(), Runtime::spawn() Where: src/actor.rs, src/runtime.rs ──────────────────────────────────────── OS Concept: exit(0) Swactor Equivalent: ctx.stop_self() Where: src/actor.rs ──────────────────────────────────────── OS Concept: kill(pid, SIGTERM) Swactor Equivalent: ctx.stop_actor(addr) Where: src/actor.rs ──────────────────────────────────────── OS Concept: SIGCHLD / waitpid Swactor Equivalent: ctx.monitor() -> Down, ctx.watch() -> ActorExited Where: crates/std/src/ctx_ext.rs ──────────────────────────────────────── OS Concept: IPC (message queues) Swactor Equivalent: Typed message passing (local + cross-worker + cross-runtime) Where: src/actor.rs, src/transport.rs ──────────────────────────────────────── OS Concept: Service names Swactor Equivalent: NameRegistry (local), ClusterRegistry (cluster CRDT) Where: crates/std/src/name_registry.rs, crates/distribution/src/registry.rs ──────────────────────────────────────── OS Concept: Process groups Swactor Equivalent: GroupRegistry (join/leave/publish/members) Where: crates/std/src/ctx_ext.rs ──────────────────────────────────────── OS Concept: init/systemd Swactor Equivalent: Supervisor with restart strategies Where: crates/std/src/supervisor.rs ──────────────────────────────────────── OS Concept: Scheduler Swactor Equivalent: Worker pool with load-aware placement + per-actor message budgets Where: src/worker.rs, src/delivery.rs ──────────────────────────────────────── OS Concept: Machine identity Swactor Equivalent: NodeId (ed25519 public key) Where: crates/distribution/src/types.rs ──────────────────────────────────────── OS Concept: Cluster membership Swactor Equivalent: SWIM protocol Where: crates/distribution/src/swim/ ──────────────────────────────────────── OS Concept: /proc, top, ps Swactor Equivalent: RuntimeStats, StatsHook, Dashboard, Investigate protocol Where: src/stats.rs, crates/dashboard/

Implemented during this work

OS Concept: System introspection from inside Swactor Equivalent: CtxSystem (worker_id, num_workers, total_actors, uptime_ms) + SystemInfo Where: src/actor.rs, crates/std/src/ctx_ext.rs ──────────────────────────────────────── OS Concept: Per-actor introspection Swactor Equivalent: CtxSelfStats (messages_processed, mailbox_depth, message_type_counts) Where: src/actor.rs, src/worker.rs, crates/std/src/ctx_ext.rs ──────────────────────────────────────── OS Concept: Process lineage (getppid) Swactor Equivalent: CtxLineage (ctx.parent(), ctx.supervisor()) Where: src/actor.rs, src/worker.rs, crates/std/src/ctx_ext.rs, crates/std/src/supervisor_registry.rs ──────────────────────────────────────── OS Concept: Process environment (environ/getenv) Swactor Equivalent: CtxEnvironment (ctx.env::(), ctx.environment(), SpawnBuilder for overrides) Where: src/actor.rs, src/worker.rs, crates/std/src/ctx_ext.rs ──────────────────────────────────────── OS Concept: Well-known environment keys (spawn metadata) Swactor Equivalent: SpawnTimestamp(u64) injected by StdExtension on_spawn hook; LogicalName(String) injected by spawn_named (ctx and runtime level) Where: src/actor.rs, src/extension.rs, src/worker.rs, crates/std/src/extension.rs, crates/std/src/ctx_ext.rs, crates/std/src/runtime_ext.rs, src/runtime.rs ──────────────────────────────────────── OS Concept: Service discovery Swactor Equivalent: ServiceRegistry + CtxResources (ctx.resource::() -> Option) Where: src/actor.rs, crates/std/src/service_registry.rs, crates/std/src/ctx_ext.rs, crates/std/src/runtime_ext.rs, crates/std/src/extension.rs ──────────────────────────────────────── OS Concept: Resource request API (typed handles) Swactor Equivalent: ResourceHandle trait + CtxHandles (ctx.handle::() -> Option) Where: crates/std/src/resource_handle.rs, crates/std/src/ctx_ext.rs ──────────────────────────────────────── OS Concept: Exit codes / rich exit values Swactor Equivalent: ExitValue(Arc<dyn Any + Send + Sync>), ctx.stop_with(value), StopReason::Completed, ExitReason::Completed. Exit values propagated via Down/ActorExited. Where: src/actor.rs, src/worker.rs, crates/std/src/extension.rs, crates/std/src/watch_registry.rs ──────────────────────────────────────── OS Concept: Parent-child hierarchy + orphan handling Swactor Equivalent: ChildrenRegistry tracks parent->children. On parent death, unsupervised children are killed (StopSignal). Supervised children are left to their supervisor. Cascades naturally across generations via tick-based cleanup. Where: crates/std/src/children_registry.rs, crates/std/src/extension.rs ──────────────────────────────────────── OS Concept: Suspend/resume (SIGSTOP/SIGCONT) Swactor Equivalent: ctx.suspend_self(), ctx.resume(target) with auth (self or supervisor only). Suspended actors queue messages but don't process them. ResumeSignal via transfer queue for cross-worker resume. Where: src/actor.rs, src/worker.rs, src/runtime.rs, crates/std/src/ctx_ext.rs ──────────────────────────────────────── OS Concept: Capability model / sandboxing Swactor Equivalent: CapabilitySet stored in actor's Environment. Enforced at Ctx level (send, spawn, stop_actor, monitor, resource). Opt-in: actors without a CapabilitySet are unrestricted. Where: src/actor.rs, crates/std/src/ctx_ext.rs

Still partially there

OS Concept: Resource limits What Exists: Mailbox capacity + message budget What's Missing: No per-actor memory/CPU/fd limits ──────────────────────────────────────── OS Concept: Auth/permissions What Exists: Datastore ACL + node-level peer auth + actor-level CapabilitySet What's Missing: Cluster-level capability propagation (local-only today)


Part 2: Design Primitives

The design followed the existing extension pattern: new capabilities were added as extension traits on Ctx<'_>, backed by registries in the extension system. This preserved backwards compatibility and kept the core minimal.

2.1 System Queries (CtxSystem)

What it enables: An actor can ask about the system it's running in.

Implemented queries (available via ctx.system_info() or the CtxSystem extension trait):

  • ctx.worker_id() -> usize -- which worker thread am I on?
  • ctx.num_workers() -> usize -- how many worker threads exist?
  • ctx.total_actors() -> usize -- live actors across all workers
  • ctx.uptime_ms() -> u64 -- milliseconds since runtime creation

Implementation: SystemInfo struct in src/actor.rs. ContextInner::system_info() implemented on both Runtime (for spawn-time context) and WorkerContext (for handler context). Data flows through TickContext (worker_stats + created_at fields in src/delivery.rs). The CtxSystem extension trait in crates/std/src/ctx_ext.rs provides ergonomic per-field accessors.

Future cluster-level queries (not yet implemented):

  • What is my node's identity (NodeId)?
  • How many cluster nodes are alive?
  • Who are the cluster members?

These require the distribution crate's DistributedNode state to be exposed through the extension system. The CtxSystem trait can be extended with these when the distribution integration is ready.

2.2 Process Environment (CtxEnvironment)

What it enables: Typed configuration that flows from parent to child at spawn time.

Properties:

  • Inherited: When actor A spawns actor B via ctx.spawn(), B gets A's environment (Arc clone)
  • Overridable: ctx.spawn_builder(actor).env(Key(val)).finish() lazily clones the parent's map on first override (copy-on-write), leaving the common case (no overrides) allocation-free
  • Immutable after spawn: Set at creation, read-only thereafter. Mutable config goes through messages.
  • Typed values: TypeId-keyed (like http::Extensions), not string-to-string
  • Runtime-spawned actors start with an empty environment

Implementation: Environment is Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>> -- clone is an Arc bump (zero allocation). EnvironmentBuilder provides from_env() for copy-on-write overrides (cloning individual entries is cheap since values are also Arc-wrapped). The spawn channel was replaced with a SpawnRequest struct (addr, actor, parent, env) to avoid further tuple growth. ActorSlot stores env, and Ctx receives it at both construction sites (tick_all and cleanup_dead). SpawnBuilder provides the ergonomic override API. The CtxEnvironment extension trait in crates/std/src/ctx_ext.rs provides the import path, following the same pattern as CtxLineage (no StdExtension dependency required). Python crate spawns with Environment::new(). 6 scenario tests in tests/std_extension.rs cover: inheritance, empty for runtime-spawned, grandchild chain, override-one-inherit-others, readable in on_stop, and sibling independence.

Well-known keys:

  • SpawnTimestamp(u64): Injected by StdExtension's on_spawn hook. Milliseconds since runtime creation, same time base as SystemInfo::uptime_ms. Opt-in at runtime level (present when StdExtension is installed). Read via ctx.env::().
  • LogicalName(String): Injected by spawn_named() at both ctx and Runtime levels. Inherited by children via normal environment inheritance. Read via ctx.env::().
  • ServiceBinding(ActorAddress): Injected by ServiceRegistry's inject_into() hook during on_spawn. Registered at runtime level via rt.register_service::(addr). Read via ctx.resource::() (CtxResources trait). Overridable per-subtree via spawn_builder.
  • CapabilitySet: Granted at spawn time (via environment or spawn_builder). Inherited by children. Enforced at Ctx level. See section 2.7.

Analogy: Unix environ -- inherited by default, augmented at fork/exec time, readable via getenv().

2.3 Service Discovery (CtxResources)

What it enables: Actors can discover system services by type, not by knowing raw addresses.

How it differs from NameRegistry: NameRegistry maps strings to addresses. CtxResources maps service marker types to addresses. Looking up "datastore" by name gives you a raw ActorAddress and you must know what messages it accepts. ctx.resource::() gives you the address of the service registered under that marker type.

Implementation: Three layers compose the feature:

  1. Core type: ServiceBinding(ActorAddress) in src/actor.rs -- a generic environment key parameterized by a zero-sized marker type. Any struct satisfying 'static + Send + Sync works as a marker (no special Service trait required, consistent with Environment's existing API).

  2. Registry + injection: ServiceRegistry in crates/std/src/service_registry.rs stores registered bindings as RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>> (same thread-safety pattern as SupervisorRegistry). StdExtension's on_spawn hook calls inject_into() before adding SpawnTimestamp -- this merges all registered bindings into the actor's environment, skipping keys already present (preserves per-subtree overrides set via spawn_builder). Helper methods on Environment (contains_type_id) and EnvironmentBuilder (set_raw) support type-erased injection without knowing concrete types at compile time.

  3. Read API: CtxResources trait in crates/std/src/ctx_ext.rs provides ctx.resource::() -> Option, a thin wrapper around ctx.env::<ServiceBinding>().map(|b| b.addr). Does NOT require StdExtension -- reads from core environment (same pattern as CtxEnvironment). When a CapabilitySet is present, resource() checks check_service::() and returns None if denied. RuntimeResources trait in crates/std/src/runtime_ext.rs provides rt.register_service::(addr) for startup-time registration.

Key design decisions:

  • No Service marker trait: S: 'static + Send + Sync is sufficient. Any zero-size struct works.
  • "Skip if present" injection: The registry doesn't overwrite env keys set by spawn_builder, enabling per-subtree service overrides (e.g., test doubles, staging vs production services).
  • No cleanup on service actor death: A dead service's binding stays in the registry (stale address). Sends to it will fail. Service lifecycle management is a higher-level concern.

6 scenario tests in tests/std_extension.rs cover: discovery by marker type, child inherits binding from parent, multiple services each accessible by marker, unregistered returns None, overridable via spawn_builder, accessible in on_start and on_stop lifecycle hooks.

Well-known services that could be registered (when swactor-node is updated):

  • Storage -- content-addressed datastore (currently wired manually in swactor-node)
  • Directory -- actor location resolution (currently locked inside DistributedNode)
  • Cluster -- membership/topology info (currently snapshot-only for dashboard)
  • Metrics -- runtime stats (currently StatsHook push-only)

2.4 Resource Handles (CtxHandles)

What it enables: Domain-specific typed proxies that wrap service addresses and provide ergonomic APIs.

The pattern: A handle wraps (service_address, self_address) and provides methods that construct and send the right messages, embedding self_addr as reply_to. Responses arrive as normal messages in the actor's handle().

Implementation: The ResourceHandle trait in crates/std/src/resource_handle.rs defines the contract:

  • type Service: 'static + Send + Sync -- the marker type used for service discovery
  • from_parts(service_addr, self_addr) -> Self -- construct from addresses
  • service_addr() -> ActorAddress -- the underlying service address
  • self_addr() -> ActorAddress -- the actor's own address (for reply_to)

The CtxHandles extension trait in crates/std/src/ctx_ext.rs provides ctx.handle::() -> Option, which looks up ServiceBinding<H::Service> from the actor's environment and constructs the handle. Returns None if the service is not registered (consistent with ctx.resource(), ctx.where_is()).

Handle methods take &self + &Ctx (not stored &Ctx -- avoids lifetime issues with &mut self in handlers). Example: impl MyHandle { pub fn do_work(&self, ctx: &Ctx, data: Vec) -> Result<(), Error> { ctx.send(self.service_addr(), MyMsg::DoWork { data, reply_to: self.self_addr() }) } }

Key design tension: Handles can't block (no await in swactor). The response arrives asynchronously as a message. This is inherent to the actor model and not something to "fix" -- the handle just makes the send side ergonomic.

5 scenario tests: handle wraps service and sends ergonomically, returns None when service not registered, inherits service binding from parent, constructible in on_start, two actors with same handle type each get responses at their own address.

2.5 Process Lineage (CtxLineage)

What it enables: Actors know their ancestry.

Implemented queries:

  • ctx.parent() -> Option (who spawned me?) Returns Some(spawner_addr) for actor-spawned children, None for Runtime::spawn(). Available in handle(), on_start(), and on_stop().
  • ctx.supervisor() -> Option (who supervises me, if anyone?) Returns Some(supervisor_addr) for supervised children, None for unsupervised actors. Gracefully returns None when StdExtension is absent (no panic).

Implementation (parent): The spawn channel uses a SpawnRequest struct (addr, actor, parent, env) -- the original 3-tuple was replaced when CtxEnvironment was added. When Ctx::spawn is called, the spawning actor's self_addr is passed as Some(parent). Runtime::spawn passes None. The parent is stored in ActorSlot::parent_addr and threaded into Ctx::self_parent_addr at both construction sites (tick_all and cleanup_dead). 4 scenario tests cover: child knows parent, runtime-spawned has no parent, grandchild sees immediate parent (not grandparent), and parent is visible in on_stop.

Implementation (supervisor): SupervisorRegistry in crates/std/src/supervisor_registry.rs stores a child_addr -> supervisor_addr map (RwLock<AddrMap>). Supervisor::start_child calls register(self_addr, child_addr) after spawning and monitoring. cleanup() removes entries where the dead address is either child or supervisor (O(n) scan for supervisor death, acceptable since supervisor death is rare and the map is small). CtxLineage::supervisor() downcasts the extension gracefully (returns None if StdExtension is absent). 5 scenario tests cover: supervised child knows supervisor, unsupervised actor returns None, supervisor survives child restart, grandchild not supervised but parent is, OneForAll restart re-registers all children.

The CtxLineage extension trait in crates/std/src/ctx_ext.rs provides the ergonomic import path.

Orphan handling was implemented as part of item 8 (Lifecycle Enrichment) -- see section 2.8.

2.6 Self-Introspection (CtxSelfStats)

What it enables: Actors can see their own operational metrics.

Implemented queries (available directly on Ctx or via the CtxSelfStats extension trait):

  • ctx.messages_processed() -> u64 -- total successfully processed before current tick
  • ctx.mailbox_depth() -> usize -- messages queued at start of current tick (pre-dequeue)
  • ctx.message_type_counts() -> &[(&str, u64)] -- per-type counts, sorted descending

Implementation: Stats are snapshotted from ActorSlot fields into Ctx before each tick_all iteration (src/worker.rs). The snapshot captures the state before any messages are dequeued in the current tick, giving actors a consistent view. The same snapshot is provided during on_stop callbacks in cleanup_dead. The CtxSelfStats extension trait in crates/std/src/ctx_ext.rs provides the ergonomic import path.

The Vec allocation for type counts is bounded (max 32 entries from ActorSlot's msg_type_counts cap) and negligible relative to handle_any cost.

2.7 Capability Model (CapabilitySet + CtxCapabilities)

What it enables: Controlled access to system resources and other actors. Primarily important for sandboxing untrusted code (wasm actors in crates/bin-runner/).

Approach: A single CapabilitySet stored in the actor's Environment. When present, enforcement is active -- the actor can only perform operations granted by the set. When absent, the actor is unrestricted (backward compatible). Capabilities inherit from parent to child via normal environment inheritance.

Capability grants (all in CapabilitySet):

  • with_send(addr) -- send any message type to a specific address
  • with_send_typed::(addr) -- send only messages of type M to a specific address
  • with_spawn() -- permission to spawn new actors
  • with_service::() -- permission to access system service S via ctx.resource::()
  • with_monitor(addr) -- permission to monitor a specific actor

Enforcement points (all in src/actor.rs Ctx methods or crates/std/src/ctx_ext.rs):

  • ctx.send::(addr, msg) -- checks check_send::(addr); self-send always allowed
  • ctx.spawn() / SpawnBuilder::finish() -- checks check_spawn()
  • ctx.stop_actor(addr) -- checks check_send_addr(addr) (stop is a send of StopSignal)
  • ctx.monitor(addr) -- checks check_monitor(addr); returns Result<MonitorRef, Error>
  • ctx.resource::() -- checks check_service::(); returns None if denied

Key design decisions:

  • Opt-in: No CapabilitySet in environment means unrestricted. Zero behavioral change for existing actors. The only cost is an Option check (env.get::()) at each enforcement point.
  • Enforcement at Ctx level only: The core ContextInner::send_any is not gated. This means extension code (supervisors, timers, etc.) that calls send_any directly bypasses capability checks, which is intentional -- system infrastructure is trusted.
  • Dual send granularity: with_send(addr) grants all message types to an address. with_send_typed::(addr) grants only type M. The check tries address-only first, then typed. This allows coarse grants for trusted peers and fine-grained grants for untrusted actors.
  • Self-send always allowed: A restricted actor can always send to its own address. This prevents capabilities from breaking actors that use self-messaging patterns (timers, state machines).
  • monitor() returns Result: Changed from -> MonitorRef to -> Result<MonitorRef, Error>. This was a breaking change to all callers (supervisor.rs, router.rs, test files), fixed mechanically by adding ? or .unwrap().

Builder API: Fluent (CapabilitySet::new().with_send(addr).with_spawn()) and mutable (caps.grant_send(addr)) variants. Mutable methods return &mut Self for chaining.

Introspection: CtxCapabilities extension trait in crates/std/src/ctx_ext.rs provides:

  • ctx.capabilities() -> Option<&CapabilitySet> -- access the raw set
  • ctx.is_restricted() -> bool -- quick check

Implementation locations:

  • src/actor.rs: CapabilitySet struct, builder methods, check methods, Ctx::capabilities() helper, enforcement in send/spawn/stop_actor/SpawnBuilder::finish
  • src/lib.rs: CapabilitySet re-export
  • crates/std/src/ctx_ext.rs: CtxCapabilities trait, monitor() enforcement, resource() enforcement
  • crates/std/src/lib.rs: CtxCapabilities re-export

11 scenario tests in tests/std_extension.rs cover: unrestricted actor sends freely (backward compat), restricted actor denied send, restricted actor allowed send, typed send grant (Ping allowed / Pong denied), spawn denied, spawn allowed, capability inheritance (child inherits parent's CapabilitySet), monitor denied, service access denied, self-send always allowed, stop requires send permission.

2.8 Lifecycle Enrichment

Rich exit values: ExitValue(Arc<dyn Any + Send + Sync>) is an opaque typed wrapper. Actors stop with ctx.stop_with(value) which stores the value and triggers StopReason::Completed. The value is propagated through Down (monitors) and ActorExited (watchers) via the exit_value: Option field. Manual PartialEq/Eq on ExitValue (always false -- opaque blob), so Down/ActorExited compare by addr+reason only.

Implementation: StopWithSignal(ExitValue) is a sentinel message intercepted in tick_all (like StopSignal). ActorSlot gains exit_value: Option. cleanup_dead returns Vec<(ActorAddress, StopReason, Option)> with StopReason::Completed when exit_value is present. The on_actor_death extension hook receives and propagates exit values to monitors/watchers.

7 scenario tests: stop_with value received in Down, received in ActorExited, normal stop has None, panic has None, multiple monitors receive cloned value, stop_with from on_start, supervisor receives rich exit in handle_down (graceful handoff pattern).

Orphan handling: ChildrenRegistry tracks parent -> set of children. Populated in on_spawn when a parent is present. On parent death (on_actor_death), unsupervised children receive StopSignal. Supervised children are left to their supervisor. Cascades naturally: parent dies -> children killed next tick -> grandchildren killed the tick after that. StopSignal made pub (was pub(crate)) to enable this -- it's not Message (not Clone) so can't be sent via ctx.send().

4 scenario tests: unsupervised children killed on parent death, supervised children not killed, cascading cleanup across generations, runtime-spawned actors unaffected.

Suspend/resume: ActorSlot gains a suspended: bool flag. Suspended actors queue messages but don't process them (tick_all skips them). ctx.suspend_self() sets the flag via a suspend_requests buffer. ResumeSignal is intercepted in deliver() to clear the flag. StopSignal/StopWithSignal are also intercepted for suspended actors (so stop_actor works on them). Cross-worker resume sends ResumeSignal via the transfer queue.

Authorization: CtxLifecycle extension trait provides ctx.suspend_self() (always allowed) and ctx.resume(target) which checks: target == self (self-resume) OR caller is the target's supervisor via SupervisorRegistry. Returns Err if unauthorized.

5 scenario tests: suspended actor queues then resume processes, supervisor can resume, non-supervisor cannot resume, suspended actor can be stopped, cross-worker resume via runtime.

Graceful handoff: Built on rich exit values. An outgoing actor stops with its state via ctx.stop_with(state); the supervisor receives it in handle_down's Down message and can pass it to the replacement's constructor. Enables zero-downtime upgrades. No additional mechanism needed -- the pattern composes from existing primitives.


Part 3: How These Compose

The primitives form a layered system:

Layer 3: Integration (swactor-node wires services at startup) Layer 2: Process (CapabilitySet, ProcessBuilder) Layer 1: Std (CtxSystem, CtxEnvironment, CtxLineage, CtxSelfStats, Well-known env keys, SupervisorRegistry, CtxResources, CtxHandles, CtxLifecycle, ChildrenRegistry, CtxCapabilities) Layer 0: Core (SystemInfo, Ctx self-stats, parent tracking, Environment + SpawnRequest, on_spawn hook, spawn_with_env, ServiceBinding, suspend flag, rich exit, orphan handling, CapabilitySet)

A "process" in swactor is an actor that has:

  1. An identity (ActorAddress) and a name (NameRegistry)
  2. A parent and supervisor it can query (CtxLineage)
  3. An environment inherited from its spawner, with well-known keys (CtxEnvironment)
  4. Access to system services through discovery (CtxResources)
  5. The ability to query the system it lives in (CtxSystem)
  6. Awareness of its own operational state (CtxSelfStats)
  7. Typed resource handles for ergonomic service interaction (CtxHandles)
  8. Rich lifecycle support including typed exit values, orphan handling, and suspend/resume
  9. Controlled permissions for what it can access (CapabilitySet)

What stayed the same: The core actor model (message passing, mailboxes, workers, tick-based execution) was unchanged. ActorInterface, Ctx, Runtime remained the foundation. The process abstraction was additive -- existing actors continued to work exactly as before.


Part 4: Implementation Sequence

Each item was implemented and merged in dependency order. Earlier items established the infrastructure (Environment, extension hooks) that later items built on.

  1. CtxSystem + CtxSelfStats -- Exposed existing internal data to actors. SystemInfo struct, ContextInner::system_info(), Ctx self-stats snapshot fields. Extension traits CtxSystem and CtxSelfStats in swactor-std. Covered by 3 scenario tests.

  2. CtxLineage (parent tracking) -- Option threaded through the spawn path. ContextInner::spawn_any gained a parent parameter. ActorSlot stores parent_addr. Ctx exposes parent(). CtxLineage extension trait in swactor-std. 4 scenario tests. Python crate updated to pass parent on spawn.

  3. CtxEnvironment (process environment) -- Typed key-value map inherited from parent to child at spawn time. Environment is Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>> -- clone is an Arc bump. EnvironmentBuilder supports copy-on-write overrides via from_env(). The spawn channel 3-tuple was replaced with a SpawnRequest struct (addr, actor, parent, env) to stop tuple growth. ActorSlot stores env. Ctx gains env::(), environment(), and spawn_builder(). SpawnBuilder lazily clones the parent's map on first .env() call. CtxEnvironment extension trait in swactor-std (no StdExtension dependency). Python crate spawns with Environment::new(). 6 scenario tests: inheritance, empty for runtime-spawned, grandchild chain, override-one-inherit-others, readable in on_stop, sibling independence.

  4. Well-known environment keys -- SpawnTimestamp(u64) and LogicalName(String) types in src/actor.rs, exported from src/lib.rs. SpawnTimestamp is opt-in at runtime level: injected by StdExtension's on_spawn hook (new RuntimeExtension::on_spawn hook with default no-op in src/extension.rs). Worker::drain_spawns now takes &TickContext and calls on_spawn for each spawn request, passing uptime_ms to avoid exposing the pub(crate) Instant type. LogicalName is injected by spawn_named at both ctx level (via spawn_builder + env override) and runtime level (via new Runtime::spawn_with_env method). LogicalName inherits to children automatically via normal environment inheritance. 7 scenario tests.

  5. Supervisor lineage (ctx.supervisor()) -- SupervisorRegistry in crates/std/src/supervisor_registry.rs stores child_addr -> supervisor_addr as RwLock<AddrMap>. Supervisor::start_child calls register() after spawning and monitoring. cleanup() removes entries for dead actors (both as child and as supervisor). CtxLineage::supervisor() gracefully returns None when StdExtension is absent (downcasts via as_any, no panic). Distinct from parent() because not every parent is a supervisor. get_ext made pub(crate) so supervisor.rs can access it. 5 scenario tests.

  6. Service Registry + CtxResources -- Actors discover system services by type (ctx.resource::()) rather than by raw address. ServiceBinding(ActorAddress) is a generic environment key parameterized by a marker type. ServiceRegistry in StdExtension stores bindings and injects them into every actor's environment via on_spawn (skipping keys already present to preserve spawn_builder overrides). CtxResources trait provides ctx.resource::() sugar. RuntimeResources trait provides rt.register_service::(addr). 6 scenario tests.

  7. Resource Handles (CtxHandles) -- ResourceHandle trait + CtxHandles extension trait. ctx.handle::() -> Option constructs typed proxies from ServiceBinding<H::Service> in the actor's environment. Handle methods take &self + &Ctx for ergonomic domain-specific APIs. 5 scenario tests.

  8. Lifecycle enrichment -- Three sub-features: a) Rich exit values: ExitValue(Arc<dyn Any + Send + Sync>), ctx.stop_with(value), StopReason::Completed, ExitReason::Completed. Propagated through Down/ActorExited. 7 scenario tests. b) Orphan handling: ChildrenRegistry tracks parent->children. Unsupervised children killed on parent death. Supervised children left to their supervisor. Natural cascade. 4 scenario tests. c) Suspend/resume: ActorSlot::suspended flag, ctx.suspend_self(), ctx.resume(target) with auth (self or supervisor only). ResumeSignal for cross-worker resume. 5 scenario tests.

  9. Capability model (CapabilitySet) -- Per-actor permission set stored in the Environment. Grants: with_send(addr), with_send_typed::(addr), with_spawn(), with_service::(), with_monitor(addr). Enforced at Ctx level in send, spawn, stop_actor, monitor, and resource. Opt-in: actors without a CapabilitySet are unrestricted (zero behavioral change). Self-send always allowed. monitor() changed from -> MonitorRef to -> Result<MonitorRef, Error> (breaking change, fixed mechanically in supervisor.rs, router.rs, and all test files). CtxCapabilities extension trait for introspection. 11 scenario tests.