Skip to main content

vertep_runtime/
node.rs

1//! A runtime node: activations, turns, the outbox, timers, supervision and migrations.
2//!
3//! A [`Node`] owns the in-memory activations of one process. Its scheduler is synchronous and
4//! deterministic: [`Node::pump`] drains finished background turns, fires due timers, runs one
5//! turn for every actor with a deliverable message and deactivates idle actors;
6//! [`Node::run_until_idle`] pumps until nothing is left to do at the current time, and
7//! [`Node::run`] is the async loop that sleeps until the next timer or wake-up. Datamodels need
8//! not be `Send`, so a node stays on one thread (`tokio::task::LocalSet`); everything outside
9//! it talks to it through a [`NodeHandle`], which is `Send + Sync`.
10//!
11//! One turn of a virtual actor:
12//!
13//! 1. receive the oldest message not deferred by the active states;
14//! 2. drop it if its message id is already journaled, if it is a second reply to a request, or
15//!    if it is a timer that was cancelled;
16//! 3. stamp the time, run the start macrostep (first entry only) and one macrostep;
17//! 4. append the entry with compare-and-set at the expected position, carrying the
18//!    current-state delta and the fencing token; on a conflict step down without dispatching;
19//! 5. acknowledge the message, then dispatch the recorded sends (the outbox), arm timers, start
20//!    and cancel invocations, record dead letters, and write a snapshot every N entries.
21
22use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
23use std::panic::AssertUnwindSafe;
24use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
25use std::sync::{Arc, Mutex, MutexGuard, RwLock};
26use std::time::Duration;
27
28use futures::FutureExt;
29use oxrdf::vocab::xsd;
30use oxrdf::{GraphName, Literal, NamedNode, Quad, Term, Triple};
31use vertep_core::datamodel::{DatamodelRegistry, Environment, Event, EventType, Value};
32use vertep_core::ir::Chart;
33use vertep_core::session::{InvokeRequest, SCXML_EVENT_PROCESSOR, SCXML_INVOKE_TYPE};
34use vertep_rdf::ChartVersion;
35use vertep_store::{AppendError, AppendMeta, FencingToken, Position, StateDelta, StoreError};
36
37use crate::actor::{
38    Actor, ActorFactory, ActorFailure, ActorFn, ActorInput, ActorStart, ChartActor, FnActor,
39    OutTarget, Outgoing, START_EVENT, Turn, TurnOutput, platform_event,
40};
41use crate::address::{Address, hex};
42use crate::chart::{
43    ChartInfo, ChartRegistry, Migration, RegisterError, RestartMode, RestartPolicy,
44    UnhandledPolicy, migrated_session, static_invoke_sources, unmapped_configurations,
45};
46use crate::journal::{
47    DecodeError, EntryBody, EntryData, InputRecord, SendRecord, SnapshotData, TimerRecord,
48};
49use crate::machine::{Ctx, Machine, MachineError};
50use crate::mailbox::{
51    Envelope, Mailbox, MailboxError, MailboxFactory, MemoryMailbox, MemoryMailboxes,
52};
53use crate::placement::{Placement, Route};
54use crate::shell::Shell;
55use crate::storage::{Storage, select, text};
56use crate::transport::{Directory, Resolver, Transport, Transports};
57use crate::vocab::{self as vt, node};
58
59/// Where a node simulates a crash (for tests of the journal protocol): the node stops at that
60/// point of the next turn and does nothing afterwards, as if the process died.
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum CrashPoint {
63    /// After the macrostep ran, before its entry is appended.
64    BeforeAppend,
65    /// After the entry is appended, before the message is acknowledged and sends dispatched.
66    AfterAppend,
67}
68
69/// Node settings.
70#[derive(Clone, Debug)]
71pub struct NodeConfig {
72    /// A virtual actor idle this long, with nothing deliverable, is deactivated.
73    pub idle_timeout_ms: i64,
74    /// A snapshot is written whenever the journal position is a multiple of this; 0 disables
75    /// snapshots.
76    pub snapshot_interval: u64,
77    /// The fencing token every append carries.
78    pub fencing: Option<FencingToken>,
79    /// How long an actor whose turn failed waits before it is activated again.
80    pub failure_backoff_ms: i64,
81    /// Simulated crash point (tests).
82    pub crash_at: Option<CrashPoint>,
83}
84
85impl Default for NodeConfig {
86    fn default() -> Self {
87        NodeConfig {
88            idle_timeout_ms: 60_000,
89            snapshot_interval: 100,
90            fencing: None,
91            failure_backoff_ms: 1_000,
92            crash_at: None,
93        }
94    }
95}
96
97/// Why an actor could not be activated.
98#[derive(Clone, Debug, PartialEq)]
99pub enum ActivationError {
100    /// A replayed macrostep produced a different configuration digest or different effects.
101    ReplayDivergence {
102        /// The actor.
103        actor: Address,
104        /// The diverging position.
105        position: u64,
106    },
107    /// A recorded chart version is not in the registry.
108    MissingVersion {
109        /// The actor.
110        actor: Address,
111        /// The version IRI.
112        version: String,
113    },
114    /// No chart type is registered for an actor with an empty journal.
115    NoChart(Address),
116    /// A journal record is malformed.
117    Decode(DecodeError),
118    /// The store failed.
119    Store(StoreError),
120    /// The interpreter failed.
121    Machine(String),
122    /// A turn's append failed for another reason than a conflict or a fencing token.
123    Append(AppendError),
124}
125
126impl core::fmt::Display for ActivationError {
127    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128        match self {
129            ActivationError::ReplayDivergence { actor, position } => {
130                write!(f, "replay divergence for {actor} at position {position}")
131            }
132            ActivationError::MissingVersion { actor, version } => {
133                write!(f, "chart version {version} of {actor} is not registered")
134            }
135            ActivationError::NoChart(a) => write!(f, "no chart type is registered for {a}"),
136            ActivationError::Decode(e) => write!(f, "{e}"),
137            ActivationError::Store(e) => write!(f, "{e}"),
138            ActivationError::Machine(e) => write!(f, "{e}"),
139            ActivationError::Append(e) => write!(f, "{e}"),
140        }
141    }
142}
143
144impl std::error::Error for ActivationError {}
145
146/// Why a migration or fork was refused.
147#[derive(Clone, Debug, PartialEq)]
148pub enum MigrateError {
149    /// No migration with that IRI is declared.
150    UnknownMigration(String),
151    /// The actor does not run the migration's source version.
152    WrongVersion {
153        /// The version the actor runs.
154        actual: String,
155        /// The migration's source version.
156        expected: String,
157    },
158    /// Reachable configurations the migration does not map.
159    Unmapped(Vec<BTreeSet<String>>),
160    /// Too many reachable configurations to check.
161    TooManyConfigurations(usize),
162    /// The datamodel transform failed; nothing was appended.
163    TransformFailed(String),
164    /// The actor could not be activated.
165    Activation(ActivationError),
166    /// The actor has no state yet (empty journal).
167    NotStarted,
168    /// The target actor of a fork already has a journal.
169    Exists(Address),
170    /// The append failed.
171    Append(AppendError),
172}
173
174impl core::fmt::Display for MigrateError {
175    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
176        match self {
177            MigrateError::UnknownMigration(m) => write!(f, "unknown migration {m}"),
178            MigrateError::WrongVersion { actual, expected } => {
179                write!(f, "actor runs {actual}, migration starts from {expected}")
180            }
181            MigrateError::Unmapped(c) => {
182                let list: Vec<String> = c
183                    .iter()
184                    .map(|s| format!("{{{}}}", s.iter().cloned().collect::<Vec<_>>().join(", ")))
185                    .collect();
186                write!(f, "unmapped configurations: {}", list.join(" "))
187            }
188            MigrateError::TooManyConfigurations(n) => {
189                write!(f, "more than {n} reachable configurations")
190            }
191            MigrateError::TransformFailed(e) => write!(f, "datamodel transform failed: {e}"),
192            MigrateError::Activation(e) => write!(f, "{e}"),
193            MigrateError::NotStarted => f.write_str("the actor has no state yet"),
194            MigrateError::Exists(a) => write!(f, "{a} already has a journal"),
195            MigrateError::Append(e) => write!(f, "{e}"),
196        }
197    }
198}
199
200impl std::error::Error for MigrateError {}
201
202/// Why a chart version could not be removed.
203#[derive(Clone, Debug, PartialEq, Eq)]
204pub enum RemoveError {
205    /// Non-final actors still run the version.
206    InUse(Vec<Address>),
207    /// The store failed while checking.
208    Store(String),
209}
210
211impl core::fmt::Display for RemoveError {
212    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
213        match self {
214            RemoveError::InUse(a) => write!(
215                f,
216                "version in use by {}",
217                a.iter().map(Address::as_str).collect::<Vec<_>>().join(", ")
218            ),
219            RemoveError::Store(e) => write!(f, "{e}"),
220        }
221    }
222}
223
224impl std::error::Error for RemoveError {}
225
226/// Why a message could not be delivered.
227#[derive(Clone, Debug, PartialEq, Eq)]
228pub enum DeliveryError {
229    /// The target is not an address or no resolver knows it.
230    Unresolvable(String),
231    /// The transport refused the message.
232    Transport(String),
233}
234
235impl core::fmt::Display for DeliveryError {
236    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
237        match self {
238            DeliveryError::Unresolvable(t) => write!(f, "cannot resolve {t}"),
239            DeliveryError::Transport(e) => write!(f, "{e}"),
240        }
241    }
242}
243
244impl std::error::Error for DeliveryError {}
245
246/// Why an external ask failed.
247#[derive(Clone, Debug, PartialEq, Eq)]
248pub enum AskError {
249    /// No reply arrived in time.
250    Timeout,
251    /// The request could not be delivered.
252    Delivery(DeliveryError),
253}
254
255impl core::fmt::Display for AskError {
256    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
257        match self {
258            AskError::Timeout => f.write_str("ask timed out"),
259            AskError::Delivery(e) => write!(f, "{e}"),
260        }
261    }
262}
263
264impl std::error::Error for AskError {}
265
266/// An actor's state at a journal position.
267#[derive(Clone, Debug, PartialEq)]
268pub struct ActorState {
269    /// The journal position the state is after.
270    pub position: u64,
271    /// The chart version.
272    pub version: String,
273    /// Active state ids in document order.
274    pub configuration: Vec<String>,
275    /// The datamodel snapshot.
276    pub datamodel: Value,
277    /// Whether the session terminated.
278    pub terminated: bool,
279}
280
281/// A recorded dead letter.
282#[derive(Clone, Debug, PartialEq, Eq)]
283pub struct DeadLetter {
284    /// Its IRI.
285    pub iri: String,
286    /// The intended receiver.
287    pub receiver: String,
288    /// The sender, when known.
289    pub sender: Option<String>,
290    /// The event name.
291    pub event: String,
292    /// The payload as JSON text.
293    pub payload: String,
294    /// Why it is a dead letter.
295    pub reason: String,
296    /// When it was recorded.
297    pub recorded_at: i64,
298}
299
300/// Counters for observing a node.
301#[derive(Clone, Debug, Default)]
302pub struct NodeStats {
303    /// Activations created, per address.
304    pub activations: BTreeMap<Address, u64>,
305    /// Deactivations.
306    pub deactivations: u64,
307    /// Turns that appended an entry.
308    pub turns: u64,
309    /// Macrosteps re-run by replay.
310    pub replayed: u64,
311    /// The highest number of concurrently running worker turns seen, per address.
312    pub worker_concurrency: BTreeMap<Address, usize>,
313    /// Child restarts: (child, time scheduled for).
314    pub restarts: Vec<(Address, i64)>,
315}
316
317/// A stateless worker pool.
318#[derive(Clone)]
319struct WorkerSpec {
320    pool: usize,
321    handler: ActorFn,
322}
323
324/// An ask waiting for its reply.
325struct AskMailbox {
326    tx: Mutex<Option<tokio::sync::oneshot::Sender<Event>>>,
327}
328
329impl Mailbox for AskMailbox {
330    fn enqueue(&self, env: Envelope) -> Result<(), MailboxError> {
331        if let Some(tx) = self.tx.lock().ok().and_then(|mut g| g.take()) {
332            let _ = tx.send(env.event);
333        }
334        Ok(())
335    }
336    fn receive(&self, _: &[String]) -> Option<Envelope> {
337        None
338    }
339    fn ack(&self, _: &str) -> Result<(), MailboxError> {
340        Ok(())
341    }
342    fn release(&self, _: &str) {}
343    fn release_all(&self) {}
344    fn pending(&self) -> Vec<Envelope> {
345        Vec::new()
346    }
347    fn pending_deliverable(&self, _: &[String]) -> usize {
348        0
349    }
350}
351
352/// Accepts and drops everything (late replies to finished asks).
353struct DropMailbox;
354
355impl Mailbox for DropMailbox {
356    fn enqueue(&self, _: Envelope) -> Result<(), MailboxError> {
357        Ok(())
358    }
359    fn receive(&self, _: &[String]) -> Option<Envelope> {
360        None
361    }
362    fn ack(&self, _: &str) -> Result<(), MailboxError> {
363        Ok(())
364    }
365    fn release(&self, _: &str) {}
366    fn release_all(&self) {}
367    fn pending(&self) -> Vec<Envelope> {
368        Vec::new()
369    }
370    fn pending_deliverable(&self, _: &[String]) -> usize {
371        0
372    }
373}
374
375const ASK_PREFIX: &str = "urn:vertep:ask:";
376
377#[derive(Default)]
378struct Hosted {
379    workers: BTreeMap<Address, WorkerSpec>,
380    children: BTreeSet<Address>,
381    asks: BTreeMap<Address, Arc<AskMailbox>>,
382}
383
384struct Completion {
385    address: Address,
386    generation: u64,
387    message_id: Option<String>,
388    origin: Option<String>,
389    correlation: Option<String>,
390    result: Result<TurnOutput, ActorFailure>,
391}
392
393#[derive(Default)]
394struct Ready {
395    queue: VecDeque<Address>,
396    set: HashSet<Address>,
397}
398
399/// The part of a node shared with handles and background turns.
400struct Shared {
401    storage: Arc<dyn Storage>,
402    shell: Shell,
403    directory: Directory,
404    transports: Transports,
405    default_mailboxes: Arc<dyn MailboxFactory>,
406    scheme_mailboxes: BTreeMap<String, Arc<dyn MailboxFactory>>,
407    mailboxes: Mutex<HashMap<Address, Arc<dyn Mailbox>>>,
408    charts: RwLock<ChartRegistry>,
409    hosted: RwLock<Hosted>,
410    ready: Mutex<Ready>,
411    completions: Mutex<VecDeque<Completion>>,
412    jobs: AtomicUsize,
413    wake: tokio::sync::Notify,
414    counter: AtomicU64,
415    nonce: u64,
416    shutdown: AtomicBool,
417    /// Where virtual actors and workers live, on a clustered node.
418    placement: Option<Arc<dyn Placement>>,
419}
420
421fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
422    m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
423}
424
425#[derive(Clone, Copy, PartialEq, Eq)]
426enum Kind {
427    Ask,
428    Worker,
429    Child,
430    Virtual,
431}
432
433impl Shared {
434    fn charts(&self) -> std::sync::RwLockReadGuard<'_, ChartRegistry> {
435        self.charts
436            .read()
437            .unwrap_or_else(std::sync::PoisonError::into_inner)
438    }
439
440    fn charts_mut(&self) -> std::sync::RwLockWriteGuard<'_, ChartRegistry> {
441        self.charts
442            .write()
443            .unwrap_or_else(std::sync::PoisonError::into_inner)
444    }
445
446    fn hosted(&self) -> std::sync::RwLockReadGuard<'_, Hosted> {
447        self.hosted
448            .read()
449            .unwrap_or_else(std::sync::PoisonError::into_inner)
450    }
451
452    fn hosted_mut(&self) -> std::sync::RwLockWriteGuard<'_, Hosted> {
453        self.hosted
454            .write()
455            .unwrap_or_else(std::sync::PoisonError::into_inner)
456    }
457
458    fn kind(&self, a: &Address) -> Option<Kind> {
459        {
460            let h = self.hosted();
461            if h.asks.contains_key(a) || a.as_str().starts_with(ASK_PREFIX) {
462                return Some(Kind::Ask);
463            }
464            if h.workers.contains_key(a) {
465                return Some(Kind::Worker);
466            }
467            if h.children.contains(a) {
468                return Some(Kind::Child);
469            }
470        }
471        if self.charts().type_of(a.as_str()).is_some() {
472            return Some(Kind::Virtual);
473        }
474        if self
475            .storage
476            .journal()
477            .head(a.node())
478            .is_ok_and(|h| h.get() > 0)
479        {
480            return Some(Kind::Virtual);
481        }
482        None
483    }
484
485    /// The mailbox of a local address, created on first use.
486    fn local_mailbox(&self, a: &Address) -> Option<(Arc<dyn Mailbox>, Kind)> {
487        let kind = self.kind(a)?;
488        if kind == Kind::Ask {
489            let m: Arc<dyn Mailbox> = match self.hosted().asks.get(a) {
490                Some(m) => m.clone(),
491                None => Arc::new(DropMailbox),
492            };
493            return Some((m, kind));
494        }
495        let mut g = lock(&self.mailboxes);
496        if let Some(m) = g.get(a) {
497            return Some((m.clone(), kind));
498        }
499        let m = match kind {
500            Kind::Child | Kind::Worker => Arc::new(MemoryMailbox::default()) as Arc<dyn Mailbox>,
501            _ => self
502                .scheme_mailboxes
503                .get(a.scheme())
504                .unwrap_or(&self.default_mailboxes)
505                .create(a),
506        };
507        g.insert(a.clone(), m.clone());
508        Some((m, kind))
509    }
510
511    fn mark_ready(&self, a: &Address) {
512        let mut r = lock(&self.ready);
513        if r.set.insert(a.clone()) {
514            r.queue.push_back(a.clone());
515        }
516        drop(r);
517        self.wake.notify_one();
518    }
519
520    /// Delivers `env` with the transport for `type_`.
521    fn deliver(&self, env: Envelope, type_: &str) -> Result<(), DeliveryError> {
522        let to =
523            Address::parse(&env.to).map_err(|_| DeliveryError::Unresolvable(env.to.clone()))?;
524        let transport: Arc<dyn Transport> = self
525            .transports
526            .get(type_)
527            .or_else(|| self.transports.get(SCXML_EVENT_PROCESSOR))
528            .cloned()
529            .ok_or_else(|| DeliveryError::Transport(format!("no transport for {type_}")))?;
530        if let Some(m) = self.placed_elsewhere(&to, &env) {
531            return transport
532                .deliver(m.as_ref(), env)
533                .map_err(|e| DeliveryError::Transport(e.0));
534        }
535        self.deliver_with(env, &to, transport.as_ref())
536    }
537
538    /// The mailbox that carries messages for `to` to another node, when the placement says
539    /// another node hosts it. Asks and invoked children always live here.
540    fn placed_elsewhere(&self, to: &Address, env: &Envelope) -> Option<Arc<dyn Mailbox>> {
541        let p = self.placement.as_ref()?;
542        let kind = self.kind(to);
543        if matches!(kind, Some(Kind::Ask | Kind::Child)) {
544            return None;
545        }
546        match p.route(to, kind == Some(Kind::Virtual), Some(env)) {
547            Route::Remote(m) => Some(m),
548            Route::Local | Route::Hold => None,
549        }
550    }
551
552    /// Delivers `env` to `to` on this node (or through the directory), without asking the
553    /// placement.
554    fn deliver_with(
555        &self,
556        env: Envelope,
557        to: &Address,
558        transport: &dyn Transport,
559    ) -> Result<(), DeliveryError> {
560        let to = to.clone();
561        if let Some((m, kind)) = self.local_mailbox(&to) {
562            transport
563                .deliver(m.as_ref(), env)
564                .map_err(|e| DeliveryError::Transport(e.0))?;
565            if kind != Kind::Ask {
566                self.mark_ready(&to);
567            }
568            return Ok(());
569        }
570        let m = self
571            .directory
572            .resolve(&to)
573            .map_err(|_| DeliveryError::Unresolvable(to.as_str().to_owned()))?;
574        transport
575            .deliver(m.as_ref(), env)
576            .map_err(|e| DeliveryError::Transport(e.0))
577    }
578
579    fn fresh_id(&self, prefix: &str) -> String {
580        let n = self.counter.fetch_add(1, Ordering::Relaxed);
581        format!(
582            "{prefix}{}-{:016x}-{n}",
583            self.shell.clock.now_ms(),
584            self.nonce
585        )
586    }
587
588    fn complete(&self, c: Completion) {
589        lock(&self.completions).push_back(c);
590        self.jobs.fetch_sub(1, Ordering::SeqCst);
591        self.wake.notify_one();
592    }
593}
594
595/// A handle on a node for callers outside the actor system: send, ask, register resolvers,
596/// shut down. It is `Send + Sync` and cheap to clone.
597#[derive(Clone)]
598pub struct NodeHandle {
599    shared: Arc<Shared>,
600}
601
602impl std::fmt::Debug for NodeHandle {
603    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
604        f.debug_struct("NodeHandle").finish_non_exhaustive()
605    }
606}
607
608impl NodeHandle {
609    /// Sends `event` to `to` from outside the actor system; returns the message id.
610    ///
611    /// # Errors
612    ///
613    /// [`DeliveryError`] when `to` cannot be resolved.
614    pub fn send(&self, to: &Address, event: Event) -> Result<String, DeliveryError> {
615        let id = self.shared.fresh_id("urn:vertep:message:");
616        self.send_envelope(Envelope::new(id.clone(), to, event))?;
617        Ok(id)
618    }
619
620    /// Delivers a complete envelope (with its own message id).
621    ///
622    /// # Errors
623    ///
624    /// [`DeliveryError`] when its receiver cannot be resolved.
625    pub fn send_envelope(&self, env: Envelope) -> Result<(), DeliveryError> {
626        self.shared.deliver(env, SCXML_EVENT_PROCESSOR)
627    }
628
629    /// Sends `event` to `to` and waits for its `vt:reply`, at most `timeout` on the node's
630    /// clock. For callers outside the actor system only (HTTP handlers, tests); charts never
631    /// block. A reply arriving after the timeout is dropped.
632    ///
633    /// # Errors
634    ///
635    /// [`AskError::Timeout`], or [`AskError::Delivery`] when `to` cannot be resolved.
636    // @tg: implements:: [[openspec:messaging#External Blocking Ask]]
637    pub async fn ask(
638        &self,
639        to: &Address,
640        event: Event,
641        timeout: Duration,
642    ) -> Result<Event, AskError> {
643        let me = Address::parse(&self.shared.fresh_id(ASK_PREFIX))
644            .map_err(|e| AskError::Delivery(DeliveryError::Unresolvable(e.text)))?;
645        let (tx, rx) = tokio::sync::oneshot::channel();
646        self.shared.hosted_mut().asks.insert(
647            me.clone(),
648            Arc::new(AskMailbox {
649                tx: Mutex::new(Some(tx)),
650            }),
651        );
652        let mut ev = event;
653        ev.origin = Some(me.as_str().to_owned());
654        ev.origintype = Some(SCXML_EVENT_PROCESSOR.to_owned());
655        ev.originverified = false;
656        let mut env = Envelope::new(format!("{me}/message"), to, ev);
657        env.correlation = Some(format!("{me}/request"));
658        if let Err(e) = self.send_envelope(env) {
659            self.shared.hosted_mut().asks.remove(&me);
660            return Err(AskError::Delivery(e));
661        }
662        let deadline = self
663            .shared
664            .shell
665            .clock
666            .now_ms()
667            .saturating_add(i64::try_from(timeout.as_millis()).unwrap_or(i64::MAX));
668        let sleep = self.shared.shell.sleep.sleep_until(deadline);
669        let r = futures::future::select(rx, sleep).await;
670        self.shared.hosted_mut().asks.remove(&me);
671        match r {
672            futures::future::Either::Left((Ok(ev), _)) => Ok(ev),
673            _ => Err(AskError::Timeout),
674        }
675    }
676
677    /// Registers a resolver for addresses of `scheme`.
678    pub fn register_resolver(&self, scheme: &str, resolver: Arc<dyn Resolver>) {
679        self.shared.directory.register(scheme, resolver);
680    }
681
682    /// Binds `address` to the virtual actor of `chart_type` with `key`: the actor is hosted
683    /// here and activated on message like `<chart_type>/<key>`, with `address` as its subject.
684    /// Used for actors whose address is a DID.
685    pub fn bind_address(&self, address: &Address, chart_type: &str, key: &str) {
686        self.shared
687            .charts_mut()
688            .bind(address.as_str(), chart_type, key);
689    }
690
691    /// Tells the node that the mailbox of the local actor `a` received messages from outside
692    /// the node (a relay pickup), so the actor is activated and receives them. False when `a`
693    /// is not hosted here.
694    pub fn notify(&self, a: &Address) -> bool {
695        match self.shared.local_mailbox(a) {
696            Some((_, kind)) if kind != Kind::Ask => {
697                self.shared.mark_ready(a);
698                true
699            }
700            _ => false,
701        }
702    }
703
704    /// Asks [`Node::run`] to return.
705    pub fn shutdown(&self) {
706        self.shared.shutdown.store(true, Ordering::SeqCst);
707        self.shared.wake.notify_one();
708    }
709
710    /// The node's clock reading.
711    pub fn now_ms(&self) -> i64 {
712        self.shared.shell.clock.now_ms()
713    }
714
715    /// Completes once the node has new work (a delivered message, a finished background turn,
716    /// a shutdown request), for shells that drive the node with [`Node::run_until_idle`]. A
717    /// signal sent while nobody waits is kept for the next waiter.
718    pub fn wakeup(&self) -> crate::shell::BoxFuture<()> {
719        let shared = self.shared.clone();
720        Box::pin(async move { shared.wake.notified().await })
721    }
722
723    /// Delivers `env` to an actor hosted on this node without asking the placement: what a
724    /// cluster does with a message another node sent here for an actor this node owns.
725    ///
726    /// # Errors
727    ///
728    /// [`DeliveryError`] when its receiver cannot be resolved.
729    pub fn deliver_here(&self, env: Envelope) -> Result<(), DeliveryError> {
730        let to =
731            Address::parse(&env.to).map_err(|_| DeliveryError::Unresolvable(env.to.clone()))?;
732        let transport = self
733            .shared
734            .transports
735            .get(SCXML_EVENT_PROCESSOR)
736            .cloned()
737            .ok_or_else(|| DeliveryError::Transport("no local transport".into()))?;
738        self.shared.deliver_with(env, &to, transport.as_ref())
739    }
740
741    /// Records `env` in `receiver`'s dead-letter graph with `reason`, at the node's time.
742    pub fn record_dead_letter(&self, receiver: &Address, env: &Envelope, reason: &str) {
743        self.shared
744            .dead_letter(receiver, env, reason, self.shared.shell.clock.now_ms());
745    }
746
747    /// The number of messages waiting in the local mailbox of `a` (none when `a` has no
748    /// mailbox here).
749    pub fn pending(&self, a: &Address) -> usize {
750        lock(&self.shared.mailboxes).get(a).map_or(0, |m| m.len())
751    }
752}
753
754/// Builds a [`Node`].
755pub struct NodeBuilder {
756    storage: Arc<dyn Storage>,
757    shell: Shell,
758    config: NodeConfig,
759    registry: Arc<dyn DatamodelRegistry + Send + Sync>,
760    mailboxes: Arc<dyn MailboxFactory>,
761    scheme_mailboxes: BTreeMap<String, Arc<dyn MailboxFactory>>,
762    transports: Transports,
763    placement: Option<Arc<dyn Placement>>,
764}
765
766impl std::fmt::Debug for NodeBuilder {
767    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
768        f.debug_struct("NodeBuilder")
769            .field("config", &self.config)
770            .finish_non_exhaustive()
771    }
772}
773
774impl NodeBuilder {
775    /// Datamodels the node's charts may name (`null` is always available).
776    pub fn datamodels(mut self, r: Arc<dyn DatamodelRegistry + Send + Sync>) -> Self {
777        self.registry = r;
778        self
779    }
780
781    /// The mailbox implementation for every address without a scheme-specific one.
782    pub fn mailboxes(mut self, f: Arc<dyn MailboxFactory>) -> Self {
783        self.mailboxes = f;
784        self
785    }
786
787    /// The mailbox implementation for addresses of `scheme`.
788    pub fn mailboxes_for_scheme(mut self, scheme: &str, f: Arc<dyn MailboxFactory>) -> Self {
789        self.scheme_mailboxes.insert(scheme.to_owned(), f);
790        self
791    }
792
793    /// Registers a transport besides the local one.
794    pub fn transport(mut self, t: Arc<dyn Transport>) -> Self {
795        self.transports.register(t);
796        self
797    }
798
799    /// Node settings.
800    pub fn config(mut self, c: NodeConfig) -> Self {
801        self.config = c;
802        self
803    }
804
805    /// The placement of a clustered node: where virtual actors and workers live. Without one
806    /// the node hosts every virtual actor it knows the chart type of.
807    pub fn placement(mut self, p: Arc<dyn Placement>) -> Self {
808        self.placement = Some(p);
809        self
810    }
811
812    /// Builds the node. On its first [`Node::pump`] (after the chart types are registered) it
813    /// recovers what a previous process left: pending timers, undispatched sends and stored
814    /// mailbox messages.
815    pub fn build(self) -> Node {
816        let nonce = {
817            use std::hash::{BuildHasher, Hasher};
818            let mut h = std::collections::hash_map::RandomState::new().build_hasher();
819            h.write_u64(self.shell.clock.now_ms() as u64);
820            h.finish()
821        };
822        let mut factories = vec![self.mailboxes.clone()];
823        factories.extend(self.scheme_mailboxes.values().cloned());
824        let shared = Arc::new(Shared {
825            storage: self.storage,
826            shell: self.shell,
827            directory: Directory::default(),
828            transports: self.transports,
829            default_mailboxes: self.mailboxes,
830            scheme_mailboxes: self.scheme_mailboxes,
831            mailboxes: Mutex::new(HashMap::new()),
832            charts: RwLock::new(ChartRegistry::default()),
833            hosted: RwLock::new(Hosted::default()),
834            ready: Mutex::new(Ready::default()),
835            completions: Mutex::new(VecDeque::new()),
836            jobs: AtomicUsize::new(0),
837            wake: tokio::sync::Notify::new(),
838            counter: AtomicU64::new(0),
839            nonce,
840            shutdown: AtomicBool::new(false),
841            placement: self.placement,
842        });
843
844        Node {
845            shared,
846            config: self.config,
847            registry: self.registry,
848            kinds: BTreeMap::new(),
849            actors: BTreeMap::new(),
850            timers: BTreeMap::new(),
851            timer_keys: HashMap::new(),
852            volatile: BTreeMap::new(),
853            parked: BTreeMap::new(),
854            errors: BTreeMap::new(),
855            migrations: BTreeMap::new(),
856            seq: 0,
857            crashed: false,
858            recovered: false,
859            stats: NodeStats::default(),
860            factories,
861            outbox: HashMap::new(),
862        }
863    }
864}
865
866struct Armed {
867    rec: TimerRecord,
868    occurrence: u64,
869}
870
871enum Volatile {
872    Deliver(Box<Envelope>),
873    /// A recorded send of the entry at the position that its transport refused: retried until
874    /// delivered, and owed by the outbox until then.
875    Resend(Address, u64, Box<SendRecord>),
876    Restart(Address, u64),
877}
878
879struct VirtualActivation {
880    machine: Option<Machine>,
881    first: Arc<ChartInfo>,
882    chart_type: Option<(String, String)>,
883    head: u64,
884    seen: HashSet<String>,
885    state: HashSet<Triple>,
886    last_active: i64,
887}
888
889impl VirtualActivation {
890    fn deferred(&self) -> Vec<String> {
891        self.machine
892            .as_ref()
893            .map(|m| m.info.deferred(&m.session))
894            .unwrap_or_default()
895    }
896
897    fn info(&self) -> &Arc<ChartInfo> {
898        self.machine.as_ref().map_or(&self.first, |m| &m.info)
899    }
900}
901
902struct ChildActivation {
903    parent: Address,
904    invokeid: String,
905    make: Arc<dyn Fn() -> Result<Box<dyn Actor>, ActorFailure> + Send + Sync>,
906    actor: Box<dyn Actor>,
907    busy: bool,
908    generation: u64,
909    policy: RestartPolicy,
910    failures: Vec<i64>,
911    out_seq: u64,
912    suppress: bool,
913}
914
915struct WorkerPool {
916    spec: WorkerSpec,
917    busy: usize,
918}
919
920enum Activation {
921    Virtual(Box<VirtualActivation>),
922    Child(Box<ChildActivation>),
923    Worker(WorkerPool),
924}
925
926/// A runtime node. See the module documentation.
927pub struct Node {
928    shared: Arc<Shared>,
929    config: NodeConfig,
930    registry: Arc<dyn DatamodelRegistry + Send + Sync>,
931    kinds: BTreeMap<String, ActorFactory>,
932    actors: BTreeMap<Address, Activation>,
933    timers: BTreeMap<(i64, String), Armed>,
934    timer_keys: HashMap<String, (i64, String)>,
935    volatile: BTreeMap<(i64, u64), Volatile>,
936    parked: BTreeMap<Address, i64>,
937    errors: BTreeMap<Address, ActivationError>,
938    migrations: BTreeMap<Address, VecDeque<(String, String)>>,
939    seq: u64,
940    crashed: bool,
941    recovered: bool,
942    stats: NodeStats,
943    factories: Vec<Arc<dyn MailboxFactory>>,
944    outbox: HashMap<Address, Outbox>,
945}
946
947/// What one actor's outbox still owes in this process. The durable mark
948/// (`vt:dispatchedThrough`) moves only over entries whose sends were all delivered, so a send a
949/// transport refused is dispatched again after a crash.
950#[derive(Default)]
951struct Outbox {
952    /// The highest journal position whose sends were dispatched.
953    tip: u64,
954    /// Positions with sends a transport refused and that are not delivered yet: how many each.
955    owed: BTreeMap<u64, usize>,
956}
957
958impl std::fmt::Debug for Node {
959    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
960        f.debug_struct("Node")
961            .field("active", &self.actors.keys().collect::<Vec<_>>())
962            .field("timers", &self.timers.len())
963            .finish_non_exhaustive()
964    }
965}
966
967/// The outcome of loading an actor's journal.
968struct Loaded {
969    machine: Option<Machine>,
970    head: u64,
971    seen: HashSet<String>,
972    entries: Vec<EntryData>,
973}
974
975fn is_scxml_invoke(t: &str) -> bool {
976    t == SCXML_INVOKE_TYPE || t == "http://www.w3.org/TR/scxml" || t == "scxml"
977}
978
979impl Node {
980    /// A builder for a node on `storage` with the shell services `shell`.
981    pub fn builder(storage: Arc<dyn Storage>, shell: Shell) -> NodeBuilder {
982        NodeBuilder {
983            storage,
984            shell,
985            config: NodeConfig::default(),
986            registry: Arc::new(vertep_core::null::NullRegistry),
987            mailboxes: Arc::new(MemoryMailboxes),
988            scheme_mailboxes: BTreeMap::new(),
989            transports: Transports::local(),
990            placement: None,
991        }
992    }
993
994    /// A handle for callers outside the actor system.
995    pub fn handle(&self) -> NodeHandle {
996        NodeHandle {
997            shared: self.shared.clone(),
998        }
999    }
1000
1001    /// Counters.
1002    pub fn stats(&self) -> &NodeStats {
1003        &self.stats
1004    }
1005
1006    /// The storage.
1007    pub fn storage(&self) -> &Arc<dyn Storage> {
1008        &self.shared.storage
1009    }
1010
1011    /// The node's clock reading.
1012    pub fn now_ms(&self) -> i64 {
1013        self.shared.shell.clock.now_ms()
1014    }
1015
1016    /// Whether `a` has an activation on this node.
1017    pub fn is_active(&self, a: &Address) -> bool {
1018        self.actors.contains_key(a)
1019    }
1020
1021    /// Activates virtual actor `a` now, without waiting for a message (a no-op when active).
1022    ///
1023    /// # Errors
1024    ///
1025    /// [`ActivationError`] when its journal cannot be replayed.
1026    pub fn activate_actor(&mut self, a: &Address) -> Result<(), ActivationError> {
1027        if self.actors.contains_key(a) {
1028            return Ok(());
1029        }
1030        let now = self.now_ms();
1031        self.activate(a, now)
1032    }
1033
1034    /// The last activation error of `a`.
1035    pub fn last_error(&self, a: &Address) -> Option<&ActivationError> {
1036        self.errors.get(a)
1037    }
1038
1039    /// The registered chart version `iri`.
1040    pub fn chart(&self, iri: &str) -> Option<Arc<ChartInfo>> {
1041        self.shared.charts().get(iri)
1042    }
1043
1044    /// The latest version of chart type `ty`.
1045    pub fn latest(&self, ty: &str) -> Option<Arc<ChartInfo>> {
1046        self.shared.charts().latest(ty)
1047    }
1048
1049    /// The messages waiting for `a`, in order.
1050    pub fn mailbox(&self, a: &Address) -> Vec<Envelope> {
1051        self.shared
1052            .local_mailbox(a)
1053            .map(|(m, _)| m.pending())
1054            .unwrap_or_default()
1055    }
1056
1057    // ----- registration ---------------------------------------------------------------------
1058
1059    /// Registers `chart` as the latest version of chart type `chart_type`; virtual actors
1060    /// addressed `<chart_type>/<key>` run it. Existing actors keep their version.
1061    ///
1062    /// # Errors
1063    ///
1064    /// [`RegisterError`] when the type is not an IRI, the chart is invalid, or an `<invoke>`
1065    /// targets a virtual actor.
1066    // @tg: implements:: [[openspec:chart-versioning#Pin Until Final]]
1067    // @tg: implements:: [[openspec:actor-model#Virtual Actors Are Not Invokable]]
1068    pub fn register_type(
1069        &mut self,
1070        chart_type: &str,
1071        chart: Chart,
1072    ) -> Result<ChartVersion, RegisterError> {
1073        Address::parse(chart_type).map_err(|_| RegisterError::BadType(chart_type.to_owned()))?;
1074        let mut srcs = Vec::new();
1075        static_invoke_sources(&chart.states, &mut srcs);
1076        {
1077            let charts = self.shared.charts();
1078            for (state, src) in srcs {
1079                let own = src
1080                    .strip_prefix(chart_type)
1081                    .is_some_and(|r| r.starts_with('/'));
1082                if own || charts.type_of(&src).is_some() {
1083                    return Err(RegisterError::InvokesVirtualActor { state, src });
1084                }
1085            }
1086        }
1087        let info = ChartInfo::new(chart, Some(chart_type.to_owned()))?;
1088        self.admit_chart(&info)?;
1089        Ok(self.shared.charts_mut().insert(info).version.clone())
1090    }
1091
1092    /// Registers a chart version without a type (for invoked charts and migrations).
1093    ///
1094    /// # Errors
1095    ///
1096    /// [`RegisterError`] when the chart is invalid or its datamodel refuses it.
1097    pub fn register_chart(&mut self, chart: Chart) -> Result<ChartVersion, RegisterError> {
1098        let info = ChartInfo::new(chart, None)?;
1099        self.admit_chart(&info)?;
1100        Ok(self.shared.charts_mut().insert(info).version.clone())
1101    }
1102
1103    /// Refuses a chart its datamodel refuses to instantiate (a typed CEL chart with type
1104    /// errors, [`DatamodelRegistry::check`]), so no actor of it is ever activated. A datamodel
1105    /// the node does not register is not an error here: activation reports it.
1106    // @tg: implements:: [[openspec:cel-datamodel#Static Type Checking of CEL]]
1107    fn admit_chart(&self, info: &ChartInfo) -> Result<(), RegisterError> {
1108        let chart = info.compiled.chart();
1109        self.registry
1110            .check(chart)
1111            .map_err(|problems| RegisterError::Refused {
1112                datamodel: chart.datamodel.clone(),
1113                problems,
1114            })
1115    }
1116
1117    /// Declares a migration.
1118    pub fn register_migration(&mut self, m: Migration) {
1119        self.shared.charts_mut().add_migration(m);
1120    }
1121
1122    /// Registers an actor kind that `<invoke type="...">` can start.
1123    pub fn register_actor_kind(&mut self, invoke_type: &str, factory: ActorFactory) {
1124        self.kinds.insert(invoke_type.to_owned(), factory);
1125    }
1126
1127    /// Registers a pool of `pool` stateless workers at `address`, each running `handler`.
1128    // @tg: implements:: [[openspec:actor-model#Stateless Worker Lifecycle]]
1129    pub fn register_worker(&mut self, address: Address, pool: usize, handler: ActorFn) {
1130        self.shared.hosted_mut().workers.insert(
1131            address,
1132            WorkerSpec {
1133                pool: pool.max(1),
1134                handler,
1135            },
1136        );
1137    }
1138
1139    /// Sends `event` to `to` from outside the actor system.
1140    ///
1141    /// # Errors
1142    ///
1143    /// [`DeliveryError`] when `to` cannot be resolved.
1144    pub fn send(&self, to: &Address, event: Event) -> Result<String, DeliveryError> {
1145        self.handle().send(to, event)
1146    }
1147
1148    /// Delivers a complete envelope.
1149    ///
1150    /// # Errors
1151    ///
1152    /// [`DeliveryError`] when its receiver cannot be resolved.
1153    pub fn send_envelope(&self, env: Envelope) -> Result<(), DeliveryError> {
1154        self.handle().send_envelope(env)
1155    }
1156
1157    /// Removes chart version `iri`, unless a non-final actor still uses it.
1158    ///
1159    /// # Errors
1160    ///
1161    /// [`RemoveError::InUse`] listing those actors.
1162    pub fn remove_version(&mut self, iri: &str) -> Result<(), RemoveError> {
1163        let j = self.shared.storage.journal();
1164        let mut users = BTreeSet::new();
1165        for (a, act) in &self.actors {
1166            if let Activation::Virtual(v) = act
1167                && let Some(m) = &v.machine
1168                && m.info.version.as_str() == iri
1169                && !m.session.is_terminated()
1170            {
1171                users.insert(a.clone());
1172            }
1173        }
1174        for a in j.actors().map_err(|e| RemoveError::Store(e.to_string()))? {
1175            let head = j.head(&a).map_err(|e| RemoveError::Store(e.to_string()))?;
1176            let recs = j
1177                .read(&a, head)
1178                .map_err(|e| RemoveError::Store(e.to_string()))?;
1179            let Ok(addr) = Address::parse(a.as_str()) else {
1180                continue;
1181            };
1182            if let Some(r) = recs.last()
1183                && let Ok(e) = EntryData::from_record(&addr, r)
1184                && e.version == iri
1185                && !e.effects().is_some_and(|x| x.terminated)
1186            {
1187                users.insert(addr);
1188            }
1189        }
1190        if !users.is_empty() {
1191            return Err(RemoveError::InUse(users.into_iter().collect()));
1192        }
1193        self.shared.charts_mut().remove(iri);
1194        Ok(())
1195    }
1196
1197    // ----- scheduling -----------------------------------------------------------------------
1198
1199    /// One scheduling round; true when anything happened.
1200    pub fn pump(&mut self) -> bool {
1201        if self.crashed {
1202            return false;
1203        }
1204        if !self.recovered {
1205            self.recovered = true;
1206            self.recover();
1207        }
1208        let now = self.now_ms();
1209        let unparked: Vec<Address> = self
1210            .parked
1211            .iter()
1212            .filter(|(_, t)| **t <= now)
1213            .map(|(a, _)| a.clone())
1214            .collect();
1215        for a in unparked {
1216            self.parked.remove(&a);
1217            self.shared.mark_ready(&a);
1218        }
1219        let mut progress = self.drain_completions(now);
1220        progress |= self.fire_timers(now);
1221        progress |= self.fire_volatile(now);
1222        progress |= self.process_ready(now);
1223        progress |= self.deactivate_idle(now);
1224        progress
1225    }
1226
1227    /// Pumps until nothing is left to do at the current time; returns the rounds that did
1228    /// something.
1229    pub fn run_until_idle(&mut self) -> usize {
1230        let mut n = 0;
1231        while n < 1_000_000 && self.pump() {
1232            n += 1;
1233        }
1234        n
1235    }
1236
1237    /// Runs until idle and no background turn is running.
1238    pub async fn settle(&mut self) {
1239        loop {
1240            self.run_until_idle();
1241            if self.crashed
1242                || (self.shared.jobs.load(Ordering::SeqCst) == 0
1243                    && lock(&self.shared.completions).is_empty())
1244            {
1245                return;
1246            }
1247            self.shared.wake.notified().await;
1248        }
1249    }
1250
1251    /// The node's event loop: runs until idle, then sleeps until the next timer, idle
1252    /// deadline or wake-up, until [`NodeHandle::shutdown`].
1253    pub async fn run(&mut self) {
1254        loop {
1255            self.run_until_idle();
1256            if self.crashed || self.shared.shutdown.load(Ordering::SeqCst) {
1257                return;
1258            }
1259            let notified = self.shared.wake.notified();
1260            match self.next_deadline() {
1261                Some(t) => {
1262                    let sleep = self.shared.shell.sleep.sleep_until(t);
1263                    futures::future::select(Box::pin(notified), sleep).await;
1264                }
1265                None => notified.await,
1266            }
1267        }
1268    }
1269
1270    /// The earliest time something is due (a timer, a failed actor's back-off, an idle
1271    /// deadline), for shells that drive the node with [`Node::run_until_idle`] themselves.
1272    pub fn next_deadline(&self) -> Option<i64> {
1273        let mut d: Vec<i64> = Vec::new();
1274        d.extend(self.timers.keys().next().map(|k| k.0));
1275        d.extend(self.volatile.keys().next().map(|k| k.0));
1276        d.extend(self.parked.values().min().copied());
1277        for a in self.actors.values() {
1278            if let Activation::Virtual(v) = a {
1279                d.push(v.last_active.saturating_add(self.config.idle_timeout_ms));
1280            }
1281        }
1282        d.into_iter().min()
1283    }
1284
1285    fn ctx(&self, a: &Address) -> (Vec<String>, Vec<String>, Value) {
1286        let mut invoke_types: Vec<String> = self.kinds.keys().cloned().collect();
1287        invoke_types.push(SCXML_INVOKE_TYPE.to_owned());
1288        (
1289            self.shared.transports.send_types(),
1290            invoke_types,
1291            self.shared.transports.ioprocessors(a),
1292        )
1293    }
1294
1295    fn park(&mut self, a: &Address, now: i64) {
1296        self.parked.insert(
1297            a.clone(),
1298            now.saturating_add(self.config.failure_backoff_ms),
1299        );
1300    }
1301
1302    fn crash(&mut self) {
1303        self.crashed = true;
1304    }
1305
1306    fn process_ready(&mut self, now: i64) -> bool {
1307        let batch: Vec<Address> = {
1308            let mut r = lock(&self.shared.ready);
1309            r.set.clear();
1310            r.queue.drain(..).collect()
1311        };
1312        let mut progress = false;
1313        for a in batch {
1314            if self.crashed {
1315                break;
1316            }
1317            if self.parked.contains_key(&a) {
1318                continue;
1319            }
1320            let Some(kind) = self.shared.kind(&a) else {
1321                continue;
1322            };
1323            let did = match kind {
1324                Kind::Ask => false,
1325                Kind::Worker => self.worker_turns(&a, now),
1326                Kind::Child => self.child_next(&a, now),
1327                Kind::Virtual => self.virtual_turn(&a, now),
1328            };
1329            progress |= did;
1330            if did && self.has_work(&a, kind) {
1331                self.shared.mark_ready(&a);
1332            }
1333        }
1334        progress
1335    }
1336
1337    fn has_work(&self, a: &Address, kind: Kind) -> bool {
1338        if self.migrations.get(a).is_some_and(|q| !q.is_empty()) {
1339            return true;
1340        }
1341        let Some((m, _)) = self.shared.local_mailbox(a) else {
1342            return false;
1343        };
1344        let deferred = match self.actors.get(a) {
1345            Some(Activation::Virtual(v)) => v.deferred(),
1346            Some(Activation::Child(c)) if c.busy => return false,
1347            Some(Activation::Worker(w)) if w.busy >= w.spec.pool => return false,
1348            _ => Vec::new(),
1349        };
1350        let _ = kind;
1351        m.has_deliverable(&deferred)
1352    }
1353
1354    // ----- virtual actors -------------------------------------------------------------------
1355
1356    /// Restores `a` from its latest valid snapshot and the entries after it, up to `upto`.
1357    // @tg: implements:: [[openspec:event-journal#Deterministic Replay]]
1358    // @tg: implements:: [[openspec:chart-versioning#Replay With Recorded Version]]
1359    fn load(&mut self, a: &Address, upto: Option<u64>) -> Result<Loaded, ActivationError> {
1360        let j = self.shared.storage.journal();
1361        let recs = j
1362            .read(a.node(), Position::ZERO)
1363            .map_err(ActivationError::Store)?;
1364        let mut entries = Vec::with_capacity(recs.len());
1365        for r in &recs {
1366            if upto.is_some_and(|u| r.position.get() > u) {
1367                break;
1368            }
1369            entries.push(EntryData::from_record(a, r).map_err(ActivationError::Decode)?);
1370        }
1371        let head = entries.len() as u64;
1372        let seen: HashSet<String> = entries.iter().map(|e| e.message_id.clone()).collect();
1373        let (sends, invokes, ioproc) = self.ctx(a);
1374        let registry = self.registry.clone();
1375        let ctx = Ctx {
1376            registry: registry.as_ref(),
1377            ioprocessors: ioproc,
1378            send_types: sends,
1379            invoke_types: invokes,
1380        };
1381        let shared = self.shared.clone();
1382        let charts = |v: &str| shared.charts().get(v);
1383        let mut replayed = 0u64;
1384        let missing = |v: &str| ActivationError::MissingVersion {
1385            actor: a.clone(),
1386            version: v.to_owned(),
1387        };
1388        // the latest valid snapshot at or before the head
1389        let mut machine: Option<Machine> = None;
1390        let mut from = 1u64;
1391        for s in self.snapshots(a) {
1392            if s.position > head || s.position == 0 {
1393                continue;
1394            }
1395            let e = &entries[(s.position - 1) as usize];
1396            if e.version != s.version || e.digest != s.digest {
1397                continue;
1398            }
1399            let Some(info) = charts(&s.version) else {
1400                continue;
1401            };
1402            if let Ok(m) = Machine::from_snapshot(a, info, &ctx, &s) {
1403                machine = Some(m);
1404                from = s.position + 1;
1405                break;
1406            }
1407        }
1408        for e in entries.iter().skip((from - 1) as usize) {
1409            match &e.body {
1410                EntryBody::Fork { snapshot, .. } => {
1411                    let info =
1412                        charts(&snapshot.version).ok_or_else(|| missing(&snapshot.version))?;
1413                    let mut m = Machine::from_snapshot(a, info, &ctx, snapshot)
1414                        .map_err(|x| ActivationError::Machine(x.to_string()))?;
1415                    m.session.sessionid = a.as_str().to_owned();
1416                    machine = Some(m);
1417                }
1418                EntryBody::Migration { after, .. } => {
1419                    let info = charts(&after.version).ok_or_else(|| missing(&after.version))?;
1420                    machine = Some(
1421                        Machine::from_snapshot(a, info, &ctx, after)
1422                            .map_err(|x| ActivationError::Machine(x.to_string()))?,
1423                    );
1424                }
1425                EntryBody::Input { input, effects } => {
1426                    let info = charts(&e.version).ok_or_else(|| missing(&e.version))?;
1427                    let env = Environment {
1428                        now_ms: e.timestamp_ms,
1429                        seed: crate::address::seed_of(&e.iri()),
1430                    };
1431                    let (mut m, start, trace) = match machine.take() {
1432                        Some(m) => {
1433                            if m.info.version.as_str() != e.version {
1434                                return Err(ActivationError::ReplayDivergence {
1435                                    actor: a.clone(),
1436                                    position: e.position,
1437                                });
1438                            }
1439                            (m, Vec::new(), None)
1440                        }
1441                        None => {
1442                            let (m, eff, tr) = Machine::start(a, info.clone(), &ctx, env)
1443                                .map_err(|x| ActivationError::Machine(x.to_string()))?;
1444                            (m, eff, Some(tr))
1445                        }
1446                    };
1447                    let policy = info.unhandled.unwrap_or(UnhandledPolicy::DeadLetter);
1448                    let applied = m
1449                        .apply(
1450                            input,
1451                            e.position,
1452                            e.timestamp_ms,
1453                            policy,
1454                            start,
1455                            trace.as_ref(),
1456                        )
1457                        .map_err(|x| ActivationError::Machine(x.to_string()))?;
1458                    replayed += 1;
1459                    let same_sends = applied.effects.sends.len() == effects.sends.len()
1460                        && applied
1461                            .effects
1462                            .sends
1463                            .iter()
1464                            .zip(&effects.sends)
1465                            .all(|(x, y)| x.send_id == y.send_id && x.event.name == y.event.name);
1466                    if m.digest() != e.digest || !same_sends {
1467                        return Err(ActivationError::ReplayDivergence {
1468                            actor: a.clone(),
1469                            position: e.position,
1470                        });
1471                    }
1472                    machine = Some(m);
1473                }
1474            }
1475        }
1476        self.stats.replayed += replayed;
1477        Ok(Loaded {
1478            machine,
1479            head,
1480            seen,
1481            entries,
1482        })
1483    }
1484
1485    /// Every snapshot record of `a`, newest first.
1486    fn snapshots(&self, a: &Address) -> Vec<SnapshotData> {
1487        let q = format!(
1488            "SELECT ?s ?n ?v ?d ?ss ?dm ?rt WHERE {{ GRAPH ?s {{ ?s a <{snap}> ; <{actor}> {a} ; \
1489             <{pos}> ?n ; <{ver}> ?v ; <{dig}> ?d ; <{ses}> ?ss ; <{dms}> ?dm ; <{rts}> ?rt }} }}",
1490            snap = vt::SNAPSHOT,
1491            actor = vt::SNAPSHOT_OF,
1492            a = a.node(),
1493            pos = vt::POSITION,
1494            ver = vt::CHART_VERSION,
1495            dig = vt::CONFIGURATION_DIGEST,
1496            ses = vt::SESSION_STATE,
1497            dms = vt::DATAMODEL_STATE,
1498            rts = vt::RUNTIME_STATE,
1499        );
1500        let Ok(sol) = select(self.shared.storage.graph(a.node()), &q) else {
1501            return Vec::new();
1502        };
1503        let mut out = Vec::new();
1504        for i in 0..sol.len() {
1505            let g = |v: &str| sol.get(i, v).and_then(text);
1506            if let (Some(s), Some(n), Some(v), Some(d), Some(ss), Some(dm), Some(rt)) = (
1507                g("s"),
1508                g("n").and_then(|n| n.parse::<u64>().ok()),
1509                g("v"),
1510                g("d"),
1511                g("ss"),
1512                g("dm"),
1513                g("rt"),
1514            ) {
1515                out.push(SnapshotData {
1516                    iri: s,
1517                    actor: a.as_str().to_owned(),
1518                    position: n,
1519                    version: v,
1520                    digest: d,
1521                    session: ss,
1522                    datamodel: dm,
1523                    runtime: rt,
1524                });
1525            }
1526        }
1527        out.sort_by_key(|x| std::cmp::Reverse(x.position));
1528        out
1529    }
1530
1531    fn outbox_marks(&self, a: &Address) -> (u64, HashSet<String>) {
1532        let q = format!(
1533            "SELECT ?s ?p ?o WHERE {{ GRAPH {g} {{ ?s ?p ?o }} }}",
1534            g = a.outbox_graph()
1535        );
1536        let mut through = 0;
1537        let mut fired = HashSet::new();
1538        if let Ok(sol) = select(self.shared.storage.graph(a.node()), &q) {
1539            for i in 0..sol.len() {
1540                let p = sol.get(i, "p").and_then(text).unwrap_or_default();
1541                if p == vt::DISPATCHED_THROUGH {
1542                    through = through.max(
1543                        sol.get(i, "o")
1544                            .and_then(text)
1545                            .and_then(|n| n.parse().ok())
1546                            .unwrap_or(0),
1547                    );
1548                } else if p == vt::DISPATCHED
1549                    && let Some(s) = sol.get(i, "s").and_then(text)
1550                {
1551                    fired.insert(s);
1552                }
1553            }
1554        }
1555        (through, fired)
1556    }
1557
1558    /// Activates `a`: replays its journal, re-arms its timers, dispatches sends left
1559    /// undispatched and rebuilds running invocations.
1560    // @tg: implements:: [[openspec:actor-model#Virtual Actor Activation]]
1561    fn activate(&mut self, a: &Address, now: i64) -> Result<(), ActivationError> {
1562        let loaded = self.load(a, None)?;
1563        let chart_type = self.shared.charts().type_of(a.as_str());
1564        let first = match &loaded.machine {
1565            Some(m) => m.info.clone(),
1566            None => chart_type
1567                .as_ref()
1568                .and_then(|(t, _)| self.shared.charts().latest(t))
1569                .ok_or_else(|| ActivationError::NoChart(a.clone()))?,
1570        };
1571        let (through, fired) = self.outbox_marks(a);
1572        let mut machine = loaded.machine;
1573        if let Some(m) = &mut machine {
1574            m.rt.timers.retain(|k, _| !fired.contains(k));
1575        }
1576        let ct = chart_type.as_ref().map(|(t, k)| (t.as_str(), k.as_str()));
1577        let state = machine
1578            .as_ref()
1579            .map(|m| m.state_triples(ct))
1580            .unwrap_or_default();
1581        // re-arm timers
1582        if let Some(m) = &machine {
1583            for t in m.rt.timers.values() {
1584                if !self.timer_keys.contains_key(&t.iri) {
1585                    self.arm(t.clone(), now, true);
1586                }
1587            }
1588        }
1589        let invocations: Vec<_> = machine
1590            .as_ref()
1591            .map(|m| m.rt.invocations.values().cloned().collect())
1592            .unwrap_or_default();
1593        *self.stats.activations.entry(a.clone()).or_default() += 1;
1594        self.errors.remove(a);
1595        self.actors.insert(
1596            a.clone(),
1597            Activation::Virtual(Box::new(VirtualActivation {
1598                machine,
1599                first,
1600                chart_type,
1601                head: loaded.head,
1602                seen: loaded.seen,
1603                state,
1604                last_active: now,
1605            })),
1606        );
1607        // the outbox: sends of entries after the last dispatched position
1608        for e in loaded.entries.iter().filter(|e| e.position > through) {
1609            if let Some(eff) = e.effects() {
1610                self.dispatch_sends(a, e.position, &eff.sends, now);
1611            }
1612        }
1613        if loaded.head > through {
1614            self.advance_outbox(a, loaded.head);
1615        }
1616        // invoked children live inside their parent: rebuild them
1617        for log in invocations {
1618            self.start_child(a, &log.request, &log.events, now);
1619        }
1620        Ok(())
1621    }
1622
1623    /// Moves the durable outbox mark of `a` forward to `n` (never back: another node sharing
1624    /// the store may have moved it further).
1625    fn mark_through(&self, a: &Address, n: u64) {
1626        let g = self.shared.storage.graph(a.node());
1627        let graph = GraphName::NamedNode(a.outbox_graph());
1628        let p = node(vt::DISPATCHED_THROUGH);
1629        let old: Vec<Quad> = crate::storage::graph_quads(g, &a.outbox_graph())
1630            .unwrap_or_default()
1631            .into_iter()
1632            .filter(|q| q.predicate == p)
1633            .collect();
1634        let current = old
1635            .iter()
1636            .filter_map(|q| match &q.object {
1637                Term::Literal(l) => l.value().parse::<u64>().ok(),
1638                _ => None,
1639            })
1640            .max()
1641            .unwrap_or(0);
1642        if current >= n {
1643            return;
1644        }
1645        let _ = g.remove_quads(&old);
1646        let _ = g.insert_quads(&[Quad::new(
1647            a.node().clone(),
1648            p,
1649            Literal::new_typed_literal(n.to_string(), xsd::INTEGER),
1650            graph,
1651        )]);
1652    }
1653
1654    fn mark_fired(&self, scheduler: &str, timer: &str) {
1655        let Ok(a) = Address::parse(scheduler) else {
1656            return;
1657        };
1658        let _ = self
1659            .shared
1660            .storage
1661            .graph(a.node())
1662            .insert_quads(&[Quad::new(
1663                NamedNode::new_unchecked(timer),
1664                node(vt::DISPATCHED),
1665                Literal::new_typed_literal("true", xsd::BOOLEAN),
1666                a.outbox_graph(),
1667            )]);
1668    }
1669
1670    /// One turn of virtual actor `a`.
1671    // @tg: implements:: [[openspec:actor-model#Turn Is One Macrostep]]
1672    // @tg: implements:: [[openspec:actor-model#Actor Failure Isolation]]
1673    // @tg: implements:: [[openspec:event-journal#Compare-And-Set Append]]
1674    // @tg: implements:: [[openspec:event-journal#Message Deduplication]]
1675    // @tg: implements:: [[openspec:event-journal#Inputs Only From The Journal]]
1676    // @tg: implements:: [[openspec:messaging#Duplicate Reply Suppression]]
1677    fn virtual_turn(&mut self, a: &Address, now: i64) -> bool {
1678        if let Some(progress) = self.placement_turn(a, now) {
1679            return progress;
1680        }
1681        if !self.actors.contains_key(a)
1682            && let Err(e) = self.activate(a, now)
1683        {
1684            self.errors.insert(a.clone(), e);
1685            self.park(a, now);
1686            return false;
1687        }
1688        if let Some(q) = self.migrations.get_mut(a)
1689            && let Some((to, m)) = q.pop_front()
1690        {
1691            self.self_migrate(a, &to, &m, now);
1692            return true;
1693        }
1694        let Some((mailbox, _)) = self.shared.local_mailbox(a) else {
1695            return false;
1696        };
1697        let Some(Activation::Virtual(mut va)) = self.actors.remove(a) else {
1698            return false;
1699        };
1700        let deferred = va.deferred();
1701        let Some(env) = mailbox.receive(&deferred) else {
1702            self.actors.insert(a.clone(), Activation::Virtual(va));
1703            return false;
1704        };
1705        va.last_active = now;
1706        // dedup and drops: acknowledged without a macrostep
1707        let drop_reason = self.drop_reason(&va, &env);
1708        if let Some(reason) = drop_reason {
1709            if let Some(dl) = reason {
1710                self.dead_letter(a, &env, dl, now);
1711            }
1712            let _ = mailbox.ack(&env.message_id);
1713            self.actors.insert(a.clone(), Activation::Virtual(va));
1714            return true;
1715        }
1716        let position = va.head + 1;
1717        let entry_iri = a.entry_iri(position);
1718        let input = InputRecord {
1719            event: Event {
1720                type_: EventType::External,
1721                ..env.event.clone()
1722            },
1723            message_id: env.message_id.clone(),
1724            correlation: env.correlation.clone(),
1725            timer: env.timer.clone(),
1726        };
1727        let (sends, invokes, ioproc) = self.ctx(a);
1728        let registry = self.registry.clone();
1729        let info = va.info().clone();
1730        let policy = info.unhandled.unwrap_or(UnhandledPolicy::DeadLetter);
1731        let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
1732            let ctx = Ctx {
1733                registry: registry.as_ref(),
1734                ioprocessors: ioproc,
1735                send_types: sends,
1736                invoke_types: invokes,
1737            };
1738            let (mut m, start, trace) = match va.machine.take() {
1739                Some(m) => (m, Vec::new(), None),
1740                None => {
1741                    let env = Environment {
1742                        now_ms: now,
1743                        seed: crate::address::seed_of(&entry_iri),
1744                    };
1745                    let (m, eff, tr) = Machine::start(a, info.clone(), &ctx, env)?;
1746                    (m, eff, Some(tr))
1747                }
1748            };
1749            let applied = m.apply(&input, position, now, policy, start, trace.as_ref());
1750            Ok::<_, MachineError>((m, applied))
1751        }));
1752        let (machine, applied) = match result {
1753            Ok(Ok((m, Ok(applied)))) => (m, applied),
1754            Ok(Ok((_, Err(MachineError::Terminated)))) => {
1755                self.dead_letter(a, &env, "terminated", now);
1756                let _ = mailbox.ack(&env.message_id);
1757                // the activation is stale (its machine was consumed): reload on next message
1758                self.stats.deactivations += 1;
1759                return true;
1760            }
1761            _ => {
1762                // a failed or panicking turn: nothing appended, message redelivered later
1763                mailbox.release(&env.message_id);
1764                self.stats.deactivations += 1;
1765                self.park(a, now);
1766                return true;
1767            }
1768        };
1769        let ct = va
1770            .chart_type
1771            .as_ref()
1772            .map(|(t, k)| (t.as_str(), k.as_str()));
1773        let new_state = machine.state_triples(ct);
1774        let delta = state_delta(&va.state, &new_state);
1775        let data = EntryData {
1776            actor: a.clone(),
1777            position,
1778            version: machine.info.version.as_str().to_owned(),
1779            message_id: env.message_id.clone(),
1780            timestamp_ms: now,
1781            digest: machine.digest(),
1782            body: EntryBody::Input {
1783                input,
1784                effects: applied.effects.clone(),
1785            },
1786        };
1787        if self.config.crash_at == Some(CrashPoint::BeforeAppend) {
1788            self.crash();
1789            return true;
1790        }
1791        if let Err(e) = self.append(a, va.head, &data, delta.clone()) {
1792            // compare-and-set lost, fenced or failed: step down, dispatch nothing
1793            mailbox.release(&env.message_id);
1794            self.stats.deactivations += 1;
1795            if !(e.is_conflict() || e.is_fenced()) {
1796                self.errors.insert(a.clone(), ActivationError::Append(e));
1797                self.park(a, now);
1798            }
1799            return true;
1800        }
1801        self.stats.turns += 1;
1802        if self.config.crash_at == Some(CrashPoint::AfterAppend) {
1803            self.crash();
1804            return true;
1805        }
1806        let _ = mailbox.ack(&env.message_id);
1807        va.head = position;
1808        va.seen.insert(env.message_id.clone());
1809        va.state = new_state;
1810        let snapshot = (self.config.snapshot_interval > 0
1811            && position % self.config.snapshot_interval == 0)
1812            .then(|| machine.snapshot(&a.snapshot_iri(position), position));
1813        va.machine = Some(machine);
1814        self.actors.insert(a.clone(), Activation::Virtual(va));
1815        if let Some(s) = snapshot {
1816            self.write_snapshot(a, &s);
1817        }
1818        self.after_append(
1819            a,
1820            &data,
1821            applied.invokes,
1822            applied.cancel_invokes,
1823            applied.migrations,
1824            &env,
1825            now,
1826        );
1827        true
1828    }
1829
1830    /// `Some(reason)` when the message is acknowledged without a turn; the inner reason, when
1831    /// set, also records a dead letter.
1832    fn drop_reason(&self, va: &VirtualActivation, env: &Envelope) -> Option<Option<&'static str>> {
1833        if va.seen.contains(&env.message_id) {
1834            return Some(None);
1835        }
1836        let rt = va.machine.as_ref().map(|m| &m.rt);
1837        if let Some(t) = &env.timer
1838            && rt.is_some_and(|rt| rt.cancelled.contains(t))
1839        {
1840            return Some(None);
1841        }
1842        if let Some(c) = &env.correlation {
1843            let answered = rt.is_some_and(|rt| rt.answered.contains(c));
1844            let open = rt.is_some_and(|rt| rt.requests.contains_key(c));
1845            if env.event.name == vt::REPLY_EVENT && !open {
1846                return Some(if answered { None } else { Some("late-reply") });
1847            }
1848            if env.event.name == vt::TIMEOUT_EVENT && !open {
1849                return Some(None);
1850            }
1851        }
1852        if va
1853            .machine
1854            .as_ref()
1855            .is_some_and(|m| m.session.is_terminated())
1856        {
1857            return Some(Some("terminated"));
1858        }
1859        None
1860    }
1861
1862    fn append(
1863        &self,
1864        a: &Address,
1865        expected: u64,
1866        data: &EntryData,
1867        delta: StateDelta,
1868    ) -> Result<(), AppendError> {
1869        let s = &self.shared.storage;
1870        let caps = s.journal().capabilities();
1871        let entry = data.to_entry();
1872        let mut meta = AppendMeta::none();
1873        if let Some(f) = &self.config.fencing {
1874            meta = meta.with_fencing(f.clone());
1875        }
1876        if caps.state_in_append && !delta.is_empty() {
1877            meta = meta.with_state(delta.clone());
1878        }
1879        s.journal()
1880            .append(a.node(), Position::new(expected), &[entry], &meta)?;
1881        let notes = data.annotations();
1882        if !notes.is_empty() {
1883            let _ = vertep_store::reification::write_annotations(s.graph(a.node()), &notes);
1884        }
1885        if !caps.state_in_append && !delta.is_empty() {
1886            let g = s.graph(a.node());
1887            let gn = GraphName::NamedNode(a.node().clone());
1888            let r: Vec<Quad> = delta
1889                .retract
1890                .iter()
1891                .map(|t| t.clone().in_graph(gn.clone()))
1892                .collect();
1893            let i: Vec<Quad> = delta
1894                .assert
1895                .iter()
1896                .map(|t| t.clone().in_graph(gn.clone()))
1897                .collect();
1898            let _ = g.remove_quads(&r);
1899            let _ = g.insert_quads(&i);
1900        }
1901        Ok(())
1902    }
1903
1904    fn write_snapshot(&self, a: &Address, s: &SnapshotData) {
1905        let g = NamedNode::new_unchecked(&s.iri);
1906        let quads: Vec<Quad> = crate::journal::snapshot_triples(s)
1907            .into_iter()
1908            .map(|t| t.in_graph(g.clone()))
1909            .collect();
1910        let _ = self.shared.storage.graph(a.node()).insert_quads(&quads);
1911    }
1912
1913    /// The transactional outbox and the other effects of an appended entry.
1914    // @tg: implements:: [[openspec:event-journal#Transactional Outbox]]
1915    #[allow(clippy::too_many_arguments)]
1916    fn after_append(
1917        &mut self,
1918        a: &Address,
1919        data: &EntryData,
1920        invokes: Vec<InvokeRequest>,
1921        cancel_invokes: Vec<String>,
1922        migrations: Vec<(String, String)>,
1923        env: &Envelope,
1924        now: i64,
1925    ) {
1926        let Some(eff) = data.effects() else { return };
1927        self.dispatch_sends(a, data.position, &eff.sends, now);
1928        if !eff.sends.is_empty() {
1929            self.advance_outbox(a, data.position);
1930        }
1931        for c in &eff.cancels {
1932            self.disarm(c);
1933        }
1934        for t in &eff.timers {
1935            self.arm(t.clone(), now, false);
1936        }
1937        if eff.unhandled == Some(UnhandledPolicy::DeadLetter) {
1938            self.dead_letter(a, env, "unhandled", now);
1939        }
1940        for id in cancel_invokes {
1941            self.cancel_child(&Address::invoked(a, &id));
1942        }
1943        for req in invokes {
1944            self.start_child(a, &req, &[], now);
1945        }
1946        if !migrations.is_empty() {
1947            self.migrations
1948                .entry(a.clone())
1949                .or_default()
1950                .extend(migrations);
1951            self.shared.mark_ready(a);
1952        }
1953    }
1954
1955    fn communication_error(&self, to: &Address, about: &str, send_id: &str) {
1956        let mut ev = platform_event("error.communication", Value::Undefined);
1957        ev.sendid = Some(send_id.to_owned());
1958        let env = Envelope::new(format!("{about}/error"), to, ev);
1959        let _ = self.shared.deliver(env, SCXML_EVENT_PROCESSOR);
1960    }
1961
1962    /// Dispatches the recorded sends of the entry of `from` at `position`; the sends a transport
1963    /// refused stay owed until a retry delivers them.
1964    fn dispatch_sends(&mut self, from: &Address, position: u64, sends: &[SendRecord], now: i64) {
1965        let refused = sends
1966            .iter()
1967            .filter(|s| !self.dispatch_send(from, position, s, now))
1968            .count();
1969        let o = self.outbox.entry(from.clone()).or_default();
1970        o.tip = o.tip.max(position);
1971        if refused > 0 {
1972            *o.owed.entry(position).or_default() += refused;
1973        }
1974    }
1975
1976    /// Moves the durable outbox mark of `a` to `upto`, or to just before its first entry with
1977    /// an owed send ([[openspec:event-journal#Transactional Outbox]]: the mark passes only
1978    /// delivered sends, as in the Lean journal model's `outbox_safe`).
1979    fn advance_outbox(&mut self, a: &Address, upto: u64) {
1980        let o = self.outbox.entry(a.clone()).or_default();
1981        o.tip = o.tip.max(upto);
1982        let through = match o.owed.keys().next() {
1983            Some(&p) => p.saturating_sub(1).min(o.tip),
1984            None => o.tip,
1985        };
1986        if through > 0 {
1987            self.mark_through(a, through);
1988        }
1989    }
1990
1991    /// A retried send of the entry of `from` at `position` was delivered (or dead-lettered).
1992    fn settle_send(&mut self, from: &Address, position: u64) {
1993        let Some(o) = self.outbox.get_mut(from) else {
1994            return;
1995        };
1996        if let Some(n) = o.owed.get_mut(&position) {
1997            *n = n.saturating_sub(1);
1998            if *n == 0 {
1999                o.owed.remove(&position);
2000            }
2001        }
2002        let tip = o.tip;
2003        self.advance_outbox(from, tip);
2004    }
2005
2006    /// Dispatches one recorded send of the entry of `from` at `position`: resolves its
2007    /// recipient and delivers it with its send IRI as message id; an unresolvable recipient
2008    /// becomes `error.communication` for the sender and a dead letter. A send its transport
2009    /// refuses is retried after the failure backoff; `false` then, as it is not delivered yet.
2010    fn dispatch_send(&mut self, from: &Address, position: u64, s: &SendRecord, now: i64) -> bool {
2011        let env = Envelope {
2012            message_id: s.iri.clone(),
2013            to: s.recipient.clone().unwrap_or_default(),
2014            event: s.event.clone(),
2015            correlation: s.correlation.clone(),
2016            timer: None,
2017        };
2018        let type_ = if s.type_.starts_with(vt::REQUEST_SEND_TYPE) {
2019            SCXML_EVENT_PROCESSOR
2020        } else {
2021            s.type_.as_str()
2022        };
2023        match self.shared.deliver(env.clone(), type_) {
2024            Ok(()) => {}
2025            Err(DeliveryError::Unresolvable(t)) => {
2026                self.communication_error(from, &s.iri, &s.send_id);
2027                let receiver = Address::parse(&t).unwrap_or_else(|_| from.clone());
2028                self.dead_letter(&receiver, &env, "unresolvable", now);
2029            }
2030            Err(DeliveryError::Transport(_)) => {
2031                // retried after the failure backoff, and owed by the outbox until delivered
2032                self.seq += 1;
2033                self.volatile.insert(
2034                    (now.saturating_add(self.config.failure_backoff_ms), self.seq),
2035                    Volatile::Resend(from.clone(), position, Box::new(s.clone())),
2036                );
2037                return false;
2038            }
2039        }
2040        true
2041    }
2042
2043    /// Records a dead letter in the receiver's dead-letter graph.
2044    // @tg: implements:: [[openspec:messaging#Queryable Dead Letters]]
2045    fn dead_letter(&self, receiver: &Address, env: &Envelope, reason: &str, now: i64) {
2046        self.shared.dead_letter(receiver, env, reason, now);
2047    }
2048}
2049
2050impl Shared {
2051    /// Records a dead letter in the receiver's dead-letter graph.
2052    fn dead_letter(&self, receiver: &Address, env: &Envelope, reason: &str, now: i64) {
2053        let g = receiver.dead_letter_graph();
2054        let d = receiver.dead_letter_iri(&env.message_id);
2055        let q = |p: &str, o: Term| Quad::new(d.clone(), node(p), o, g.clone());
2056        let mut quads = vec![
2057            q(vt::RDF_TYPE, node(vt::DEAD_LETTER).into()),
2058            q(vt::RECEIVER, receiver.node().clone().into()),
2059            q(
2060                vt::EVENT_NAME,
2061                Literal::new_simple_literal(&env.event.name).into(),
2062            ),
2063            q(
2064                vt::PAYLOAD,
2065                Literal::new_simple_literal(crate::json::to_text(&env.event.data)).into(),
2066            ),
2067            q(vt::REASON, Literal::new_simple_literal(reason).into()),
2068            q(
2069                vt::RECORDED_AT,
2070                Literal::new_typed_literal(now.to_string(), xsd::INTEGER).into(),
2071            ),
2072            q(
2073                vt::MESSAGE_ID,
2074                Literal::new_simple_literal(&env.message_id).into(),
2075            ),
2076        ];
2077        if let Some(o) = &env.event.origin {
2078            quads.push(q(vt::SENDER, Literal::new_simple_literal(o).into()));
2079        }
2080        let _ = self.storage.graph(receiver.node()).insert_quads(&quads);
2081    }
2082}
2083
2084impl Node {
2085    /// The dead letters recorded for `receiver`, by SPARQL over its dead-letter graph.
2086    ///
2087    /// # Errors
2088    ///
2089    /// The store's query errors.
2090    pub fn dead_letters(&self, receiver: &Address) -> Result<Vec<DeadLetter>, StoreError> {
2091        let q = format!(
2092            "SELECT ?d ?sender ?name ?payload ?reason ?at WHERE {{ GRAPH {g} {{ ?d a <{dl}> ; \
2093             <{rcv}> {r} ; <{en}> ?name ; <{pl}> ?payload ; <{rs}> ?reason ; <{at}> ?at . \
2094             OPTIONAL {{ ?d <{snd}> ?sender }} }} }} ORDER BY ?at ?d",
2095            g = receiver.dead_letter_graph(),
2096            dl = vt::DEAD_LETTER,
2097            rcv = vt::RECEIVER,
2098            r = receiver.node(),
2099            en = vt::EVENT_NAME,
2100            pl = vt::PAYLOAD,
2101            rs = vt::REASON,
2102            at = vt::RECORDED_AT,
2103            snd = vt::SENDER,
2104        );
2105        let sol = select(self.shared.storage.graph(receiver.node()), &q)?;
2106        let mut out = Vec::new();
2107        for i in 0..sol.len() {
2108            let g = |v: &str| sol.get(i, v).and_then(text);
2109            out.push(DeadLetter {
2110                iri: g("d").unwrap_or_default(),
2111                receiver: receiver.as_str().to_owned(),
2112                sender: g("sender"),
2113                event: g("name").unwrap_or_default(),
2114                payload: g("payload").unwrap_or_default(),
2115                reason: g("reason").unwrap_or_default(),
2116                recorded_at: g("at").and_then(|x| x.parse().ok()).unwrap_or_default(),
2117            });
2118        }
2119        Ok(out)
2120    }
2121
2122    fn deactivate_idle(&mut self, now: i64) -> bool {
2123        let idle: Vec<Address> = self
2124            .actors
2125            .iter()
2126            .filter_map(|(a, act)| match act {
2127                Activation::Virtual(v)
2128                    if now.saturating_sub(v.last_active) >= self.config.idle_timeout_ms =>
2129                {
2130                    Some((a, v))
2131                }
2132                _ => None,
2133            })
2134            .filter(|(a, v)| {
2135                let busy = self
2136                    .shared
2137                    .local_mailbox(a)
2138                    .is_some_and(|(m, _)| m.has_deliverable(&v.deferred()));
2139                let children = v
2140                    .machine
2141                    .as_ref()
2142                    .is_some_and(|m| !m.rt.invocations.is_empty());
2143                !busy && !children && self.migrations.get(*a).is_none_or(|q| q.is_empty())
2144            })
2145            .map(|(a, _)| a.clone())
2146            .collect();
2147        for a in &idle {
2148            self.deactivate(a);
2149        }
2150        !idle.is_empty()
2151    }
2152
2153    /// Deactivates `a`, keeping its mailbox: its state is recoverable from the journal.
2154    // @tg: implements:: [[openspec:actor-model#Virtual Actor Deactivation]]
2155    pub fn deactivate(&mut self, a: &Address) {
2156        if self.actors.remove(a).is_some() {
2157            self.stats.deactivations += 1;
2158            if let Some((m, _)) = self.shared.local_mailbox(a) {
2159                m.release_all();
2160            }
2161        }
2162    }
2163
2164    /// The virtual actors active on this node.
2165    pub fn active_actors(&self) -> Vec<Address> {
2166        self.actors
2167            .iter()
2168            .filter(|(_, act)| matches!(act, Activation::Virtual(_)))
2169            .map(|(a, _)| a.clone())
2170            .collect()
2171    }
2172
2173    /// Drains the virtual actors `which` selects, for a shard handover: runs until idle (every
2174    /// macrostep in progress finishes and its outbox is dispatched), then deactivates them.
2175    /// Returns the actors deactivated. Their messages that arrive afterwards follow the
2176    /// placement.
2177    pub fn drain(&mut self, which: &dyn Fn(&Address) -> bool) -> Vec<Address> {
2178        self.run_until_idle();
2179        let drained: Vec<Address> = self
2180            .active_actors()
2181            .into_iter()
2182            .filter(|a| which(a))
2183            .collect();
2184        for a in &drained {
2185            self.deactivate(a);
2186        }
2187        drained
2188    }
2189
2190    /// Applies the placement before a turn of virtual actor `a`. `None` when `a` lives here
2191    /// (or the node has no placement); otherwise the actor is deactivated here, and its
2192    /// waiting messages either move to the node that hosts it or stay held.
2193    fn placement_turn(&mut self, a: &Address, now: i64) -> Option<bool> {
2194        let p = self.shared.placement.clone()?;
2195        match p.route(a, true, None) {
2196            Route::Local => None,
2197            Route::Hold => {
2198                self.deactivate(a);
2199                self.park(a, now);
2200                Some(false)
2201            }
2202            Route::Remote(remote) => {
2203                self.deactivate(a);
2204                let Some((m, _)) = self.shared.local_mailbox(a) else {
2205                    return Some(false);
2206                };
2207                let mut moved = false;
2208                for env in m.pending() {
2209                    let id = env.message_id.clone();
2210                    if remote.enqueue(env).is_ok() {
2211                        let _ = m.ack(&id);
2212                        moved = true;
2213                    } else {
2214                        self.park(a, now);
2215                        break;
2216                    }
2217                }
2218                Some(moved)
2219            }
2220        }
2221    }
2222
2223    // ----- timers ---------------------------------------------------------------------------
2224
2225    /// Arms a timer; with `catch_up`, a reminder whose occurrences are overdue fires once for
2226    /// all of them.
2227    fn arm(&mut self, rec: TimerRecord, now: i64, catch_up: bool) {
2228        self.disarm(&rec.iri);
2229        let mut occurrence = 0;
2230        if catch_up
2231            && let Some(r) = &rec.reminder
2232            && rec.due_ms <= now
2233            && r.period_ms > 0
2234        {
2235            occurrence = u64::try_from((now - rec.due_ms) / r.period_ms).unwrap_or(0);
2236        }
2237        let key = (rec.due_of(occurrence), rec.iri.clone());
2238        self.timer_keys.insert(rec.iri.clone(), key.clone());
2239        self.timers.insert(key, Armed { rec, occurrence });
2240    }
2241
2242    fn disarm(&mut self, iri: &str) {
2243        if let Some(k) = self.timer_keys.remove(iri) {
2244            self.timers.remove(&k);
2245        }
2246    }
2247
2248    /// Fires every due timer in due-time order, as messages with ids derived from the timer.
2249    // @tg: implements:: [[openspec:durable-timers#Timer Fires As Input]]
2250    // @tg: implements:: [[openspec:durable-timers#Timers Reactivate Virtual Actors]]
2251    // @tg: implements:: [[openspec:durable-timers#Missed Timers After Downtime]]
2252    fn fire_timers(&mut self, now: i64) -> bool {
2253        let mut progress = false;
2254        while let Some(((due, _), _)) = self.timers.first_key_value() {
2255            if *due > now {
2256                break;
2257            }
2258            let Some((_, armed)) = self.timers.pop_first() else {
2259                break;
2260            };
2261            progress = true;
2262            let rec = armed.rec;
2263            self.timer_keys.remove(&rec.iri);
2264            let env = Envelope {
2265                message_id: rec.message_id(armed.occurrence),
2266                to: rec.recipient.clone(),
2267                event: rec.event.clone(),
2268                correlation: rec.correlation.clone(),
2269                timer: Some(rec.iri.clone()),
2270            };
2271            match self.shared.deliver(env.clone(), SCXML_EVENT_PROCESSOR) {
2272                Ok(()) => {}
2273                Err(DeliveryError::Unresolvable(t)) => {
2274                    if let Ok(s) = Address::parse(&rec.actor) {
2275                        self.communication_error(&s, &rec.iri, &rec.send_id);
2276                        let receiver = Address::parse(&t).unwrap_or_else(|_| s.clone());
2277                        self.dead_letter(&receiver, &env, "unresolvable", now);
2278                    }
2279                }
2280                Err(DeliveryError::Transport(_)) => {
2281                    // try again after the failure backoff
2282                    let key = (
2283                        now.saturating_add(self.config.failure_backoff_ms),
2284                        rec.iri.clone(),
2285                    );
2286                    self.timer_keys.insert(rec.iri.clone(), key.clone());
2287                    self.timers.insert(
2288                        key,
2289                        Armed {
2290                            rec,
2291                            occurrence: armed.occurrence,
2292                        },
2293                    );
2294                    continue;
2295                }
2296            }
2297            if let Some(r) = &rec.reminder {
2298                // the next occurrence after now: missed occurrences collapse into this firing
2299                let mut next = armed.occurrence + 1;
2300                if r.period_ms > 0 && rec.due_of(next) <= now {
2301                    next = u64::try_from((now - rec.due_ms) / r.period_ms).unwrap_or(0) + 1;
2302                }
2303                let key = (rec.due_of(next), rec.iri.clone());
2304                self.timer_keys.insert(rec.iri.clone(), key.clone());
2305                self.timers.insert(
2306                    key,
2307                    Armed {
2308                        rec,
2309                        occurrence: next,
2310                    },
2311                );
2312            } else {
2313                self.mark_fired(&rec.actor, &rec.iri);
2314                if let Ok(s) = Address::parse(&rec.actor)
2315                    && s.as_str() != rec.recipient
2316                    && let Some(Activation::Virtual(v)) = self.actors.get_mut(&s)
2317                    && let Some(m) = &mut v.machine
2318                {
2319                    m.rt.timers.remove(&rec.iri);
2320                }
2321            }
2322        }
2323        progress
2324    }
2325
2326    fn fire_volatile(&mut self, now: i64) -> bool {
2327        let mut progress = false;
2328        while let Some(((due, _), _)) = self.volatile.first_key_value() {
2329            if *due > now {
2330                break;
2331            }
2332            let Some((_, v)) = self.volatile.pop_first() else {
2333                break;
2334            };
2335            progress = true;
2336            match v {
2337                Volatile::Deliver(env) => {
2338                    if self
2339                        .shared
2340                        .deliver((*env).clone(), SCXML_EVENT_PROCESSOR)
2341                        .is_err()
2342                    {
2343                        self.seq += 1;
2344                        self.volatile.insert(
2345                            (now.saturating_add(self.config.failure_backoff_ms), self.seq),
2346                            Volatile::Deliver(env),
2347                        );
2348                    }
2349                }
2350                Volatile::Resend(from, position, s) => {
2351                    if self.dispatch_send(&from, position, &s, now) {
2352                        self.settle_send(&from, position);
2353                    }
2354                }
2355                Volatile::Restart(a, generation) => self.restart_child(&a, generation, now),
2356            }
2357        }
2358        progress
2359    }
2360
2361    // ----- recovery -------------------------------------------------------------------------
2362
2363    /// After a restart: arm pending timers, dispatch undispatched sends and deliver stored
2364    /// mailbox messages.
2365    fn recover(&mut self) {
2366        let now = self.now_ms();
2367        let storage = self.shared.storage.clone();
2368        let q_timers = format!(
2369            "SELECT ?env WHERE {{ GRAPH ?a {{ ?t <{actor}> ?a ; <{env}> ?env }} }}",
2370            actor = vt::ACTOR,
2371            env = vt::ENVELOPE
2372        );
2373        let q_fired = format!(
2374            "SELECT ?t WHERE {{ GRAPH ?g {{ ?t <{d}> ?x }} }}",
2375            d = vt::DISPATCHED
2376        );
2377        let mut fired = HashSet::new();
2378        let mut pending = Vec::new();
2379        for g in storage.graphs() {
2380            if let Ok(sol) = select(g, &q_fired) {
2381                fired.extend(sol.values("t").iter().filter_map(text));
2382            }
2383            if let Ok(sol) = select(g, &q_timers) {
2384                for t in sol.values("env") {
2385                    if let Some(rec) =
2386                        text(&t).and_then(|j| serde_json::from_str::<TimerRecord>(&j).ok())
2387                    {
2388                        pending.push(rec);
2389                    }
2390                }
2391            }
2392        }
2393        for rec in pending {
2394            if !fired.contains(&rec.iri) {
2395                self.arm(rec, now, true);
2396            }
2397        }
2398        // undispatched sends
2399        if let Ok(actors) = storage.journal().actors() {
2400            for n in actors {
2401                let Ok(a) = Address::parse(n.as_str()) else {
2402                    continue;
2403                };
2404                let head = storage.journal().head(&n).map(|h| h.get()).unwrap_or(0);
2405                let (through, _) = self.outbox_marks(&a);
2406                if head <= through {
2407                    continue;
2408                }
2409                let Ok(recs) = storage.journal().read(&n, Position::new(through + 1)) else {
2410                    continue;
2411                };
2412                for r in recs {
2413                    if let Ok(e) = EntryData::from_record(&a, &r)
2414                        && let Some(eff) = e.effects()
2415                    {
2416                        self.dispatch_sends(&a, e.position, &eff.sends, now);
2417                    }
2418                }
2419                self.advance_outbox(&a, head);
2420            }
2421        }
2422        for f in self.factories.clone() {
2423            for a in f.recover() {
2424                self.shared.mark_ready(&a);
2425            }
2426        }
2427    }
2428
2429    // ----- invoked actors -------------------------------------------------------------------
2430
2431    fn is_virtual(&self, src: &str) -> bool {
2432        self.shared.charts().type_of(src).is_some()
2433    }
2434
2435    /// Starts the child of `parent` for invocation `req`, replaying `events` (recorded in the
2436    /// parent's journal) without delivering its outputs.
2437    // @tg: implements:: [[openspec:actor-model#Invoked Actor Lifecycle]]
2438    fn start_child(
2439        &mut self,
2440        parent: &Address,
2441        req: &InvokeRequest,
2442        events: &[(Event, i64)],
2443        now: i64,
2444    ) {
2445        let child = Address::invoked(parent, &req.invokeid);
2446        let fail = |node: &mut Node, cause: String| {
2447            let ev = platform_event(
2448                "error.execution",
2449                Value::Object(vec![
2450                    ("invokeid".into(), Value::String(req.invokeid.clone())),
2451                    ("cause".into(), Value::String(cause)),
2452                ]),
2453            );
2454            let env = Envelope::new(format!("{child}/start-error"), parent, ev);
2455            let _ = node.shared.deliver(env, SCXML_EVENT_PROCESSOR);
2456        };
2457        if let Some(src) = &req.src
2458            && self.is_virtual(src)
2459        {
2460            fail(
2461                self,
2462                format!("{src} is a virtual actor and cannot be invoked"),
2463            );
2464            return;
2465        }
2466        let start = ActorStart {
2467            address: child.clone(),
2468            parent: parent.clone(),
2469            invokeid: req.invokeid.clone(),
2470            src: req.src.clone(),
2471            content: req.content.clone(),
2472            params: req.params.clone(),
2473        };
2474        let make: Arc<dyn Fn() -> Result<Box<dyn Actor>, ActorFailure> + Send + Sync> =
2475            if is_scxml_invoke(&req.type_) {
2476                let info = match self.child_chart(req) {
2477                    Ok(i) => i,
2478                    Err(e) => {
2479                        fail(self, e);
2480                        return;
2481                    }
2482                };
2483                let registry = self.registry.clone();
2484                let io = self.shared.transports.ioprocessors(&child);
2485                let data: Vec<(String, Value)> = match &req.params {
2486                    Value::Object(kv) => kv.clone(),
2487                    _ => Vec::new(),
2488                };
2489                let addr = child.clone();
2490                Arc::new(move || {
2491                    Ok(Box::new(ChartActor::new(
2492                        addr.clone(),
2493                        info.clone(),
2494                        registry.clone(),
2495                        data.clone(),
2496                        io.clone(),
2497                    )) as Box<dyn Actor>)
2498                })
2499            } else if let Some(f) = self.kinds.get(&req.type_).cloned() {
2500                let start = start.clone();
2501                Arc::new(move || f(&start))
2502            } else {
2503                fail(self, format!("no actor kind handles {}", req.type_));
2504                return;
2505            };
2506        let actor = match make() {
2507            Ok(a) => a,
2508            Err(e) => {
2509                fail(self, e.0);
2510                return;
2511            }
2512        };
2513        let policy = self
2514            .actors
2515            .get(parent)
2516            .and_then(|p| match p {
2517                Activation::Virtual(v) => {
2518                    Some(v.info().restart_policy(&req.state, index_of(v, req)))
2519                }
2520                _ => None,
2521            })
2522            .unwrap_or_default();
2523        self.shared.hosted_mut().children.insert(child.clone());
2524        self.seq += 1;
2525        let generation = self.seq;
2526        self.actors.insert(
2527            child.clone(),
2528            Activation::Child(Box::new(ChildActivation {
2529                parent: parent.clone(),
2530                invokeid: req.invokeid.clone(),
2531                make,
2532                actor,
2533                busy: false,
2534                generation,
2535                policy,
2536                failures: Vec::new(),
2537                out_seq: 0,
2538                suppress: !events.is_empty(),
2539            })),
2540        );
2541        let mut start_ev = Event::named(START_EVENT, EventType::External);
2542        start_ev.data = req.params.clone();
2543        self.child_input(&child, start_ev, None, None, now);
2544        for (e, ts) in events {
2545            self.child_input(&child, e.clone(), None, None, *ts);
2546        }
2547        if let Some(Activation::Child(c)) = self.actors.get_mut(&child) {
2548            c.suppress = false;
2549        }
2550    }
2551
2552    fn child_chart(&mut self, req: &InvokeRequest) -> Result<Arc<ChartInfo>, String> {
2553        if let Some(src) = &req.src {
2554            let charts = self.shared.charts();
2555            return charts
2556                .latest(src)
2557                .or_else(|| charts.get(src))
2558                .ok_or_else(|| format!("no chart registered for {src}"));
2559        }
2560        let Some(Value::String(xml)) = &req.content else {
2561            return Err("an scxml invoke needs src or inline content".into());
2562        };
2563        let opts = vertep_scxml::ReadOptions::new().with_datamodels(&vertep_scxml::AnyDatamodel);
2564        let parsed = vertep_scxml::read(xml, &opts).map_err(|e| format!("{e:?}"))?;
2565        let info = ChartInfo::new(parsed.chart, None).map_err(|e| e.to_string())?;
2566        Ok(self.shared.charts_mut().insert(info))
2567    }
2568
2569    fn cancel_child(&mut self, child: &Address) {
2570        if self.actors.remove(child).is_some() {
2571            self.shared.hosted_mut().children.remove(child);
2572            lock(&self.shared.mailboxes).remove(child);
2573        }
2574    }
2575
2576    /// Runs one input through a child, catching panics.
2577    fn child_input(
2578        &mut self,
2579        child: &Address,
2580        event: Event,
2581        message_id: Option<String>,
2582        origin_correlation: Option<(Option<String>, Option<String>)>,
2583        now: i64,
2584    ) {
2585        let Some(Activation::Child(c)) = self.actors.get_mut(child) else {
2586            return;
2587        };
2588        let input = ActorInput {
2589            event: event.clone(),
2590            message_id: message_id.clone().unwrap_or_default(),
2591            received_at_ms: now,
2592        };
2593        let generation = c.generation;
2594        let turn = std::panic::catch_unwind(AssertUnwindSafe(|| c.actor.turn(input)))
2595            .unwrap_or_else(|_| Turn::Ready(Err(ActorFailure("panic".into()))));
2596        let (origin, correlation) = origin_correlation.unwrap_or((event.origin.clone(), None));
2597        match turn {
2598            Turn::Ready(result) => {
2599                self.child_done(
2600                    Completion {
2601                        address: child.clone(),
2602                        generation,
2603                        message_id,
2604                        origin,
2605                        correlation,
2606                        result,
2607                    },
2608                    now,
2609                );
2610            }
2611            Turn::Pending(fut) => {
2612                c.busy = true;
2613                let shared = self.shared.clone();
2614                let address = child.clone();
2615                shared.jobs.fetch_add(1, Ordering::SeqCst);
2616                let job = async move {
2617                    let result = AssertUnwindSafe(fut)
2618                        .catch_unwind()
2619                        .await
2620                        .unwrap_or_else(|_| Err(ActorFailure("panic".into())));
2621                    shared.complete(Completion {
2622                        address,
2623                        generation,
2624                        message_id,
2625                        origin,
2626                        correlation,
2627                        result,
2628                    });
2629                };
2630                self.shared.shell.spawn.spawn(Box::pin(job));
2631            }
2632        }
2633    }
2634
2635    fn child_next(&mut self, child: &Address, now: i64) -> bool {
2636        let Some(Activation::Child(c)) = self.actors.get(child) else {
2637            return false;
2638        };
2639        if c.busy {
2640            return false;
2641        }
2642        let Some((m, _)) = self.shared.local_mailbox(child) else {
2643            return false;
2644        };
2645        let Some(env) = m.receive(&[]) else {
2646            return false;
2647        };
2648        let origin = env.event.origin.clone();
2649        self.child_input(
2650            child,
2651            env.event,
2652            Some(env.message_id),
2653            Some((origin, env.correlation)),
2654            now,
2655        );
2656        true
2657    }
2658
2659    fn drain_completions(&mut self, now: i64) -> bool {
2660        let done: Vec<Completion> = lock(&self.shared.completions).drain(..).collect();
2661        let progress = !done.is_empty();
2662        for c in done {
2663            match self.actors.get(&c.address) {
2664                Some(Activation::Child(_)) => self.child_done(c, now),
2665                Some(Activation::Worker(_)) => self.worker_done(c, now),
2666                _ => {}
2667            }
2668        }
2669        progress
2670    }
2671
2672    fn child_done(&mut self, c: Completion, now: i64) {
2673        let Some(Activation::Child(ca)) = self.actors.get_mut(&c.address) else {
2674            return;
2675        };
2676        if ca.generation != c.generation {
2677            return;
2678        }
2679        ca.busy = false;
2680        if let Some(id) = &c.message_id
2681            && let Some((m, _)) = self.shared.local_mailbox(&c.address)
2682        {
2683            let _ = m.ack(id);
2684        }
2685        let parent = ca.parent.clone();
2686        let invokeid = ca.invokeid.clone();
2687        let suppress = ca.suppress;
2688        match c.result {
2689            Ok(out) => {
2690                if !suppress {
2691                    for o in out.sends {
2692                        self.child_send(
2693                            &c.address,
2694                            &parent,
2695                            &invokeid,
2696                            o,
2697                            c.origin.as_deref(),
2698                            c.correlation.as_deref(),
2699                            now,
2700                        );
2701                    }
2702                }
2703                if let Some(data) = out.done {
2704                    let restart = matches!(
2705                        self.actors.get(&c.address),
2706                        Some(Activation::Child(x)) if x.policy.mode == RestartMode::Always
2707                    );
2708                    if restart {
2709                        self.child_failed(&c.address, "completed".into(), now);
2710                    } else {
2711                        if !suppress {
2712                            let mut ev = Event::named(
2713                                format!("done.invoke.{invokeid}"),
2714                                EventType::External,
2715                            );
2716                            ev.data = data;
2717                            ev.invokeid = Some(invokeid.clone());
2718                            ev.origin = Some(c.address.as_str().to_owned());
2719                            ev.origintype = Some(SCXML_EVENT_PROCESSOR.to_owned());
2720                            ev.originverified = false;
2721                            let env = Envelope::new(
2722                                format!("{}/{}/done", c.address, c.generation),
2723                                &parent,
2724                                ev,
2725                            );
2726                            let _ = self.shared.deliver(env, SCXML_EVENT_PROCESSOR);
2727                        }
2728                        self.cancel_child(&c.address);
2729                    }
2730                } else {
2731                    self.shared.mark_ready(&c.address);
2732                }
2733            }
2734            Err(f) => self.child_failed(&c.address, f.0, now),
2735        }
2736    }
2737
2738    #[allow(clippy::too_many_arguments)]
2739    fn child_send(
2740        &mut self,
2741        child: &Address,
2742        parent: &Address,
2743        invokeid: &str,
2744        o: Outgoing,
2745        origin: Option<&str>,
2746        correlation: Option<&str>,
2747        now: i64,
2748    ) {
2749        let mut ev = o.event;
2750        ev.origin = Some(child.as_str().to_owned());
2751        ev.origintype = Some(SCXML_EVENT_PROCESSOR.to_owned());
2752        ev.originverified = false;
2753        let to = match o.target {
2754            OutTarget::Parent => {
2755                ev.invokeid = Some(invokeid.to_owned());
2756                Some(parent.clone())
2757            }
2758            OutTarget::Origin => origin.and_then(|x| Address::parse(x).ok()),
2759            OutTarget::Itself => Some(child.clone()),
2760            OutTarget::Address(a) => Some(a),
2761            OutTarget::Invalid(_) => None,
2762        };
2763        let generation = match self.actors.get_mut(child) {
2764            Some(Activation::Child(c)) => {
2765                c.out_seq += 1;
2766                format!("{}/{}", c.generation, c.out_seq)
2767            }
2768            _ => {
2769                self.seq += 1;
2770                format!("w/{}", self.seq)
2771            }
2772        };
2773        let Some(to) = to else {
2774            self.communication_error(
2775                child,
2776                &format!("{child}/{generation}"),
2777                ev.sendid.as_deref().unwrap_or(""),
2778            );
2779            return;
2780        };
2781        let mut env = Envelope::new(
2782            format!("{child}/{}-{generation}", self.shared.nonce),
2783            &to,
2784            ev,
2785        );
2786        if env.event.name == vt::REPLY_EVENT {
2787            env.correlation = correlation.map(str::to_owned);
2788        }
2789        if o.delay_ms > 0 {
2790            self.seq += 1;
2791            self.volatile.insert(
2792                (
2793                    now.saturating_add(i64::try_from(o.delay_ms).unwrap_or(i64::MAX)),
2794                    self.seq,
2795                ),
2796                Volatile::Deliver(Box::new(env)),
2797            );
2798        } else if let Err(DeliveryError::Unresolvable(_)) =
2799            self.shared.deliver(env.clone(), SCXML_EVENT_PROCESSOR)
2800        {
2801            self.dead_letter(&to, &env, "unresolvable", now);
2802        }
2803    }
2804
2805    /// Applies the child's `vt:restart` policy after a failure.
2806    fn child_failed(&mut self, child: &Address, cause: String, now: i64) {
2807        let Some(Activation::Child(ca)) = self.actors.get_mut(child) else {
2808            return;
2809        };
2810        let p = ca.policy;
2811        let give_up = if p.mode == RestartMode::Never {
2812            true
2813        } else {
2814            ca.failures.retain(|t| *t > now.saturating_sub(p.window_ms));
2815            ca.failures.push(now);
2816            ca.failures.len() as u32 > p.limit
2817        };
2818        let parent = ca.parent.clone();
2819        let invokeid = ca.invokeid.clone();
2820        let generation = ca.generation;
2821        if give_up {
2822            let ev = platform_event(
2823                "error.execution",
2824                Value::Object(vec![
2825                    ("invokeid".into(), Value::String(invokeid.clone())),
2826                    ("cause".into(), Value::String(cause)),
2827                ]),
2828            );
2829            let mut ev = ev;
2830            ev.invokeid = Some(invokeid);
2831            let env = Envelope::new(format!("{child}/{generation}/failed"), &parent, ev);
2832            let _ = self.shared.deliver(env, SCXML_EVENT_PROCESSOR);
2833            self.cancel_child(child);
2834        } else {
2835            let n = ca.failures.len() as u32;
2836            ca.busy = true;
2837            let at = now.saturating_add(p.backoff(n));
2838            self.seq += 1;
2839            self.volatile
2840                .insert((at, self.seq), Volatile::Restart(child.clone(), generation));
2841            self.stats.restarts.push((child.clone(), at));
2842        }
2843    }
2844
2845    fn restart_child(&mut self, child: &Address, generation: u64, now: i64) {
2846        let Some(Activation::Child(ca)) = self.actors.get_mut(child) else {
2847            return;
2848        };
2849        if ca.generation != generation {
2850            return;
2851        }
2852        match (ca.make)() {
2853            Ok(actor) => {
2854                self.seq += 1;
2855                ca.actor = actor;
2856                ca.generation = self.seq;
2857                ca.busy = false;
2858                let ev = Event::named(START_EVENT, EventType::External);
2859                self.child_input(child, ev, None, None, now);
2860            }
2861            Err(e) => self.child_failed(child, e.0, now),
2862        }
2863    }
2864
2865    // ----- stateless workers ----------------------------------------------------------------
2866
2867    fn worker_turns(&mut self, a: &Address, now: i64) -> bool {
2868        if !self.actors.contains_key(a) {
2869            let Some(spec) = self.shared.hosted().workers.get(a).cloned() else {
2870                return false;
2871            };
2872            *self.stats.activations.entry(a.clone()).or_default() += 1;
2873            self.actors
2874                .insert(a.clone(), Activation::Worker(WorkerPool { spec, busy: 0 }));
2875        }
2876        let Some((m, _)) = self.shared.local_mailbox(a) else {
2877            return false;
2878        };
2879        let mut progress = false;
2880        loop {
2881            let Some(Activation::Worker(pool)) = self.actors.get_mut(a) else {
2882                return progress;
2883            };
2884            if pool.busy >= pool.spec.pool {
2885                return progress;
2886            }
2887            let Some(env) = m.receive(&[]) else {
2888                return progress;
2889            };
2890            pool.busy += 1;
2891            let busy = pool.busy;
2892            let c = self.stats.worker_concurrency.entry(a.clone()).or_default();
2893            *c = (*c).max(busy);
2894            let mut actor = FnActor::new(a.clone(), pool.spec.handler.clone());
2895            let input = ActorInput {
2896                event: env.event.clone(),
2897                message_id: env.message_id.clone(),
2898                received_at_ms: now,
2899            };
2900            let shared = self.shared.clone();
2901            let completion = |result| Completion {
2902                address: a.clone(),
2903                generation: 0,
2904                message_id: Some(env.message_id.clone()),
2905                origin: env.event.origin.clone(),
2906                correlation: env.correlation.clone(),
2907                result,
2908            };
2909            match actor.turn(input) {
2910                Turn::Ready(r) => {
2911                    shared.jobs.fetch_add(1, Ordering::SeqCst);
2912                    shared.complete(completion(r));
2913                }
2914                Turn::Pending(fut) => {
2915                    shared.jobs.fetch_add(1, Ordering::SeqCst);
2916                    let mut c = completion(Ok(TurnOutput::default()));
2917                    let s2 = shared.clone();
2918                    self.shared.shell.spawn.spawn(Box::pin(async move {
2919                        c.result = AssertUnwindSafe(fut)
2920                            .catch_unwind()
2921                            .await
2922                            .unwrap_or_else(|_| Err(ActorFailure("panic".into())));
2923                        s2.complete(c);
2924                    }));
2925                }
2926            }
2927            progress = true;
2928        }
2929    }
2930
2931    fn worker_done(&mut self, c: Completion, now: i64) {
2932        if let Some(Activation::Worker(pool)) = self.actors.get_mut(&c.address) {
2933            pool.busy = pool.busy.saturating_sub(1);
2934        }
2935        if let (Some(id), Some((m, _))) = (&c.message_id, self.shared.local_mailbox(&c.address)) {
2936            let _ = m.ack(id);
2937        }
2938        if let Ok(out) = c.result {
2939            for o in out.sends {
2940                let mut ev = o.event;
2941                ev.origin = Some(c.address.as_str().to_owned());
2942                ev.origintype = Some(SCXML_EVENT_PROCESSOR.to_owned());
2943                ev.originverified = false;
2944                let to = match o.target {
2945                    OutTarget::Origin => c.origin.as_deref().and_then(|x| Address::parse(x).ok()),
2946                    OutTarget::Address(a) => Some(a),
2947                    _ => None,
2948                };
2949                let Some(to) = to else { continue };
2950                self.seq += 1;
2951                let mut env = Envelope::new(
2952                    format!("{}/{}-{}", c.address, self.shared.nonce, self.seq),
2953                    &to,
2954                    ev,
2955                );
2956                if env.event.name == vt::REPLY_EVENT {
2957                    env.correlation = c.correlation.clone();
2958                }
2959                if let Err(DeliveryError::Unresolvable(_)) =
2960                    self.shared.deliver(env.clone(), SCXML_EVENT_PROCESSOR)
2961                {
2962                    self.dead_letter(&to, &env, "unresolvable", now);
2963                }
2964            }
2965        }
2966        self.shared.mark_ready(&c.address);
2967    }
2968
2969    // ----- migrations, forks and time travel ------------------------------------------------
2970
2971    fn self_migrate(&mut self, a: &Address, to: &str, migration: &str, now: i64) {
2972        let current = match self.actors.get(a) {
2973            Some(Activation::Virtual(v)) => v.info().version.as_str().to_owned(),
2974            _ => return,
2975        };
2976        if current == to {
2977            return;
2978        }
2979        if let Err(e) = self.migrate_at(a, migration, now) {
2980            let ev = platform_event(
2981                "error.execution",
2982                Value::Object(vec![
2983                    ("migration".into(), Value::String(migration.to_owned())),
2984                    ("cause".into(), Value::String(e.to_string())),
2985                ]),
2986            );
2987            let env = Envelope::new(format!("{a}/migration-refused/{}", self.seq), a, ev);
2988            self.seq += 1;
2989            let _ = self.shared.deliver(env, SCXML_EVENT_PROCESSOR);
2990        }
2991    }
2992
2993    /// Migrates actor `a` with the declared migration `migration`: checks that every
2994    /// reachable configuration of the old version is mapped, transforms the datamodel and
2995    /// appends one migration entry holding the snapshot before, the migration IRI and the
2996    /// snapshot after. Nothing is appended when any step fails.
2997    ///
2998    /// # Errors
2999    ///
3000    /// [`MigrateError`].
3001    // @tg: implements:: [[openspec:chart-versioning#Explicit Migration]]
3002    // @tg: implements:: [[openspec:chart-versioning#Migration Compatibility Check]]
3003    pub fn migrate(&mut self, a: &Address, migration: &str) -> Result<u64, MigrateError> {
3004        let now = self.now_ms();
3005        self.migrate_at(a, migration, now)
3006    }
3007
3008    fn migrate_at(&mut self, a: &Address, migration: &str, now: i64) -> Result<u64, MigrateError> {
3009        if !self.actors.contains_key(a) {
3010            self.activate(a, now).map_err(MigrateError::Activation)?;
3011        }
3012        let m = self
3013            .shared
3014            .charts()
3015            .migration(migration)
3016            .ok_or_else(|| MigrateError::UnknownMigration(migration.to_owned()))?;
3017        let to = self.shared.charts().get(m.to.as_str()).ok_or_else(|| {
3018            MigrateError::Activation(ActivationError::MissingVersion {
3019                actor: a.clone(),
3020                version: m.to.as_str().to_owned(),
3021            })
3022        })?;
3023        let Some(Activation::Virtual(va)) = self.actors.get(a) else {
3024            return Err(MigrateError::NotStarted);
3025        };
3026        let Some(old) = &va.machine else {
3027            return Err(MigrateError::NotStarted);
3028        };
3029        if old.info.version != m.from {
3030            return Err(MigrateError::WrongVersion {
3031                actual: old.info.version.as_str().to_owned(),
3032                expected: m.from.as_str().to_owned(),
3033            });
3034        }
3035        let unmapped = unmapped_configurations(&m, &old.info, &to)
3036            .map_err(MigrateError::TooManyConfigurations)?;
3037        if !unmapped.is_empty() {
3038            return Err(MigrateError::Unmapped(unmapped));
3039        }
3040        let cfg: BTreeSet<String> = old.active().into_iter().collect();
3041        let new_cfg = m
3042            .map(&cfg, &to)
3043            .ok_or_else(|| MigrateError::Unmapped(vec![cfg.clone()]))?;
3044        let value = (m.transform)(old.dm.snapshot()).map_err(MigrateError::TransformFailed)?;
3045        let position = va.head + 1;
3046        let e = a.entry_iri(position);
3047        let before = old.snapshot(
3048            &NamedNode::new_unchecked(format!("{}/before", e.as_str())),
3049            va.head,
3050        );
3051        let (sends, invokes, ioproc) = self.ctx(a);
3052        let ctx = Ctx {
3053            registry: self.registry.as_ref(),
3054            ioprocessors: ioproc,
3055            send_types: sends,
3056            invoke_types: invokes,
3057        };
3058        let session = migrated_session(&old.session, &new_cfg, &to);
3059        let mut probe = SnapshotData {
3060            iri: format!("{}/after", e.as_str()),
3061            actor: a.as_str().to_owned(),
3062            position,
3063            version: to.version.as_str().to_owned(),
3064            digest: String::new(),
3065            session: serde_json::to_string(&session).unwrap_or_default(),
3066            datamodel: crate::json::to_text(&value),
3067            runtime: serde_json::to_string(&old.rt).unwrap_or_default(),
3068        };
3069        let new = Machine::from_snapshot(a, to.clone(), &ctx, &probe)
3070            .map_err(|x| MigrateError::TransformFailed(x.to_string()))?;
3071        probe.digest = new.digest();
3072        let ct = va
3073            .chart_type
3074            .as_ref()
3075            .map(|(t, k)| (t.as_str(), k.as_str()));
3076        let new_state = new.state_triples(ct);
3077        let delta = state_delta(&va.state, &new_state);
3078        let data = EntryData {
3079            actor: a.clone(),
3080            position,
3081            version: to.version.as_str().to_owned(),
3082            message_id: format!("{a}/migration/{position}"),
3083            timestamp_ms: now,
3084            digest: probe.digest.clone(),
3085            body: EntryBody::Migration {
3086                migration: m.iri.clone(),
3087                from: m.from.as_str().to_owned(),
3088                before,
3089                after: probe,
3090            },
3091        };
3092        self.append(a, va.head, &data, delta)
3093            .map_err(MigrateError::Append)?;
3094        if let Some(Activation::Virtual(va)) = self.actors.get_mut(a) {
3095            va.machine = Some(new);
3096            va.head = position;
3097            va.seen.insert(data.message_id.clone());
3098            va.state = new_state;
3099        }
3100        Ok(position)
3101    }
3102
3103    /// Forks `source` at `position` under `new`: the new actor's journal starts with a fork
3104    /// entry referencing the source and holding its state there; the source is unchanged.
3105    ///
3106    /// # Errors
3107    ///
3108    /// [`MigrateError::Exists`] when `new` has a journal, or replay errors.
3109    // @tg: implements:: [[openspec:event-journal#Journal Read Access]]
3110    // @tg: implements:: [[openspec:chart-versioning#Fork Before Migration]]
3111    pub fn fork(
3112        &mut self,
3113        source: &Address,
3114        position: u64,
3115        new: &Address,
3116    ) -> Result<(), MigrateError> {
3117        let j = self.shared.storage.journal();
3118        if j.head(new.node()).map(|h| h.get()).unwrap_or(0) > 0 {
3119            return Err(MigrateError::Exists(new.clone()));
3120        }
3121        let loaded = self
3122            .load(source, Some(position))
3123            .map_err(MigrateError::Activation)?;
3124        let Some(mut m) = loaded.machine else {
3125            return Err(MigrateError::NotStarted);
3126        };
3127        m.address = new.clone();
3128        m.session.sessionid = new.as_str().to_owned();
3129        let e = new.entry_iri(1);
3130        let snap = m.snapshot(
3131            &NamedNode::new_unchecked(format!("{}/state", e.as_str())),
3132            position,
3133        );
3134        let state = m.state_triples(None);
3135        let delta = state_delta(&HashSet::new(), &state);
3136        let data = EntryData {
3137            actor: new.clone(),
3138            position: 1,
3139            version: m.info.version.as_str().to_owned(),
3140            message_id: format!("{new}/fork"),
3141            timestamp_ms: self.now_ms(),
3142            digest: snap.digest.clone(),
3143            body: EntryBody::Fork {
3144                source: source.as_str().to_owned(),
3145                position,
3146                snapshot: snap,
3147            },
3148        };
3149        self.append(new, 0, &data, delta)
3150            .map_err(MigrateError::Append)
3151    }
3152
3153    /// The state of `a` after journal position `position`, rebuilt by replay; the live actor
3154    /// and the journal are not touched.
3155    ///
3156    /// # Errors
3157    ///
3158    /// [`ActivationError`] when the journal cannot be replayed.
3159    pub fn state_at(&mut self, a: &Address, position: u64) -> Result<ActorState, ActivationError> {
3160        let loaded = self.load(a, Some(position))?;
3161        Ok(actor_state(loaded.machine.as_ref(), loaded.head))
3162    }
3163
3164    /// The current state of `a`: the live activation's, or rebuilt by replay.
3165    ///
3166    /// # Errors
3167    ///
3168    /// [`ActivationError`] when the journal cannot be replayed.
3169    pub fn state(&mut self, a: &Address) -> Result<ActorState, ActivationError> {
3170        if let Some(Activation::Virtual(v)) = self.actors.get(a) {
3171            return Ok(actor_state(v.machine.as_ref(), v.head));
3172        }
3173        let loaded = self.load(a, None)?;
3174        Ok(actor_state(loaded.machine.as_ref(), loaded.head))
3175    }
3176
3177    /// Whether `a` would accept `event` in its current configuration: the event enables at
3178    /// least one transition or an active state defers it. Guards are evaluated on a copy of the
3179    /// datamodel; nothing changes and nothing is journaled. An actor without a journal has no
3180    /// configuration yet and accepts nothing.
3181    ///
3182    /// # Errors
3183    ///
3184    /// [`ActivationError`] when the journal cannot be replayed.
3185    pub fn can(&mut self, a: &Address, event: &Event) -> Result<bool, ActivationError> {
3186        if let Some(Activation::Virtual(v)) = self.actors.get(a)
3187            && let Some(m) = &v.machine
3188        {
3189            return Ok(m.can(self.registry.as_ref(), event));
3190        }
3191        let loaded = self.load(a, None)?;
3192        Ok(loaded
3193            .machine
3194            .as_ref()
3195            .is_some_and(|m| m.can(self.registry.as_ref(), event)))
3196    }
3197
3198    /// The decoded journal of `a`.
3199    ///
3200    /// # Errors
3201    ///
3202    /// Store or decoding errors.
3203    pub fn journal(&self, a: &Address) -> Result<Vec<EntryData>, ActivationError> {
3204        let recs = self
3205            .shared
3206            .storage
3207            .journal()
3208            .read(a.node(), Position::ZERO)
3209            .map_err(ActivationError::Store)?;
3210        recs.iter()
3211            .map(|r| EntryData::from_record(a, r).map_err(ActivationError::Decode))
3212            .collect()
3213    }
3214
3215    /// The pending durable timers armed on this node, in due order.
3216    pub fn pending_timers(&self) -> Vec<(i64, TimerRecord)> {
3217        self.timers
3218            .iter()
3219            .map(|((due, _), a)| (*due, a.rec.clone()))
3220            .collect()
3221    }
3222
3223    /// The datamodel registry.
3224    pub fn datamodels(&self) -> &Arc<dyn DatamodelRegistry + Send + Sync> {
3225        &self.registry
3226    }
3227
3228    /// A fresh, unique id with `prefix`.
3229    pub fn fresh_id(&self, prefix: &str) -> String {
3230        self.shared.fresh_id(prefix)
3231    }
3232
3233    /// The digest of `text` as short hex (exposed for tests and tools).
3234    pub fn short_hex(text: &str) -> String {
3235        hex(text.as_bytes())
3236    }
3237}
3238
3239fn index_of(v: &VirtualActivation, req: &InvokeRequest) -> usize {
3240    v.machine
3241        .as_ref()
3242        .and_then(|m| {
3243            m.session
3244                .invocations
3245                .iter()
3246                .find(|i| i.invokeid == req.invokeid)
3247                .map(|i| i.index)
3248        })
3249        .unwrap_or(0)
3250}
3251
3252fn actor_state(m: Option<&Machine>, head: u64) -> ActorState {
3253    match m {
3254        Some(m) => ActorState {
3255            position: head,
3256            version: m.info.version.as_str().to_owned(),
3257            configuration: m.active(),
3258            datamodel: m.dm.snapshot(),
3259            terminated: m.session.is_terminated(),
3260        },
3261        None => ActorState {
3262            position: head,
3263            version: String::new(),
3264            configuration: Vec::new(),
3265            datamodel: Value::Undefined,
3266            terminated: false,
3267        },
3268    }
3269}
3270
3271/// The delta from `old` to `new`, in a stable order.
3272fn state_delta(old: &HashSet<Triple>, new: &HashSet<Triple>) -> StateDelta {
3273    let mut retract: Vec<Triple> = old.difference(new).cloned().collect();
3274    let mut assert: Vec<Triple> = new.difference(old).cloned().collect();
3275    retract.sort_by_cached_key(ToString::to_string);
3276    assert.sort_by_cached_key(ToString::to_string);
3277    StateDelta { retract, assert }
3278}