Skip to main content

rustev_runtime/
lib.rs

1//! Rustev's runtime (spec 003).
2//!
3//! Drives the pure core's staged evaluation through [`DecisionBackend`]s
4//! under explicit bounds: a monotonic [`Clock`] and one end-to-end deadline,
5//! bounded admission and concurrency, cost [`Ledger`]s that reserve before
6//! dispatch, retries and fallback only as the plan's execution policy
7//! declares them, cancellation that records exactly what the backend
8//! acknowledged, and a run record delivered under a [`SinkPolicy`].
9//!
10//! The runtime spawns no task per decision, request or attempt: a decision
11//! runs on the caller's task. The one spawned task is the delivery worker of
12//! the queued sink policies (spec 003, 3.4.3).
13#![forbid(unsafe_code)]
14
15mod acquisition;
16mod capture;
17pub mod clock;
18mod driver;
19mod dynamic;
20mod generation;
21#[cfg(test)]
22mod generation_acceptance;
23pub use generation::{GenerationEvent, GenerationStream};
24pub mod ledger;
25mod live;
26mod optimization;
27mod progress;
28mod shared;
29mod store;
30pub use store::{
31    Invalidated, PutAnswer, PutTerms, RuntimeStore, StoreCall, StoreFailure, StoreSelector, Stored,
32};
33mod sink;
34pub use progress::{ProgressObserver, StepProgress};
35mod wait;
36
37use std::collections::{BTreeMap, BTreeSet};
38use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
39use std::sync::{Arc, Mutex};
40
41use rustev_contract::execution::{
42    AttemptTimeout, CostPolicy, FailureClass, RetryPolicy, StepExecution,
43};
44use rustev_contract::ids::ArtifactId;
45use rustev_contract::judgment::Judgment;
46use rustev_contract::optimization::{DecisionCacheTier, OptimizationPolicy};
47use rustev_contract::plan::{PlanExecution, PlanStepDetail};
48use rustev_contract::replay::Capture;
49use rustev_contract::run::{
50    CoreEvidence, CostMode, CostModel, MAX_REMOTE_EVIDENCE, RecordedExecution, RemoteEvidence,
51    RequestRecord, RunRecord, Termination, Timing,
52};
53use rustev_contract::snapshot::Snapshot;
54use rustev_contract::time::Timestamp;
55use rustev_contract::{Identified, schema};
56use rustev_core::Compiled;
57use rustev_core::evaluate::Evaluation;
58use rustev_core::seams::{CancelSignal, DecisionBackend, EvidenceSink};
59use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc, oneshot};
60
61pub use capture::{CaptureConfig, CaptureConfigError, MAX_CAPTURE_BYTES};
62pub use clock::{Clock, Instant, ManualClock, TokioClock};
63/// Dynamic questions (spec 019): the only execution entry point.
64pub use dynamic::{DynamicAsked, DynamicBounds, DynamicDelivery, ask_dynamic};
65pub use ledger::Ledger;
66pub use live::{DispatchAnswer, LiveAuthorizer, LiveRefusal, LiveRequest, LiveResult};
67pub use optimization::{InvalidationResult, PersistentInvalidation};
68pub use sink::{DeliveryFailure, DeliveryTicket, SinkPolicy, SinkStats};
69
70use crate::optimization::InvalidationSelector;
71use crate::sink::{Job, Stats};
72use crate::wait::{Raced, race};
73
74/// Bounds of one runtime (spec 003, 3.4).
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct RuntimeConfig {
77    /// Decisions admitted at once.
78    pub max_in_flight: usize,
79    /// Decisions waiting for admission.
80    pub max_queued: usize,
81    /// Requests in progress within one decision.
82    pub max_parallel_requests: usize,
83    pub sink: SinkPolicy,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum ConfigError {
88    /// A bound that must be positive is 0.
89    Zero(&'static str),
90    DuplicateBackend(String),
91    InvalidSource(String),
92}
93
94/// Why a plan cannot run on this runtime (spec 003, 3.5.3 and 3.6.5).
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum PrepareError {
97    SourceBinding(String),
98    /// The plan binds a backend id that is not registered.
99    MissingBackend(String),
100    /// The registered backend's descriptor is not the one the plan bound.
101    DescriptorMismatch {
102        backend_id: String,
103    },
104    /// A hard budget, and a reachable backend without enforceable bounds.
105    HardBudgetUnenforceable {
106        backend_id: String,
107        model: CostModel,
108    },
109    /// A plan 3 and an evidence sink that does not accept `rustev.run/3`
110    /// (spec 032, 3.11.2).
111    SinkVersionUnsupported {
112        schema: &'static str,
113    },
114    /// A persistent tier, and a runtime built without a store (spec 044,
115    /// 3.4.1).
116    StoreMissing,
117    /// The store's contract is not the one the plan states.
118    StoreContractMismatch,
119}
120
121/// Why a decision was not admitted (spec 003, 3.4.1). Nothing was dispatched
122/// or spent, and there is no run record.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum Rejection {
125    Overloaded,
126    QueueDeadline,
127    Cancelled,
128    InvalidRequest(String),
129}
130
131#[derive(Debug)]
132pub enum DecideError {
133    /// A pure generation invariant failed; no text result is returned.
134    GenerationFailed(String),
135    Rejected(Rejection),
136    /// Work and spending happened; the record did not reach the sink. The
137    /// judgment inside `record` is evidence, not a result to act on.
138    EvidenceNotDelivered {
139        record: Arc<RunRecord>,
140        failure: DeliveryFailure,
141    },
142}
143
144/// One decision to run.
145#[derive(Debug, Clone)]
146pub struct DecisionRequest {
147    /// Non-empty, at most 256 bytes; the delivery key.
148    pub decision_id: String,
149    pub snapshot: Snapshot,
150    /// Domain time, supplied; never read from a clock.
151    pub evaluation_time: Timestamp,
152    /// Can only tighten the plan's `deadline_ms`.
153    pub deadline_ms: Option<u64>,
154    /// Opaque complete authorized-scope handle, including any tenant and
155    /// principal isolation the host requires; passed through to backends and
156    /// included in every reusable-work key.
157    pub principal_handle: Vec<u8>,
158}
159
160#[derive(Debug, Clone, PartialEq)]
161pub enum Completion {
162    Judged(Box<Judgment>),
163    Cancelled,
164}
165
166#[derive(Debug)]
167pub enum Delivery {
168    Acknowledged { receipt: String },
169    Queued(DeliveryTicket),
170    Dropped { dropped_total: u64 },
171}
172
173#[derive(Debug)]
174pub struct Decided {
175    pub text_result: Option<rustev_contract::generation_result::TextResult>,
176    pub completion: Completion,
177    pub record: Arc<RunRecord>,
178    pub delivery: Delivery,
179    /// The decision-result cache entry the decision was served from, its
180    /// judgment recomputed by the core (spec 044, 3.6.6).
181    pub served_from_cache: Option<ServedFromCache>,
182    /// The persistent tier's answer to this decision's entry (spec 044,
183    /// 3.6.4): `Ok` when stored, otherwise the diagnostic of an admission
184    /// not made, a store failure under either `on_store_failure` included
185    /// (3.4.2). The entry names the delivered run record's digest, so it
186    /// is written after delivery and its answer is here, not in that
187    /// record. `None` when nothing was to be written.
188    pub decision_cache_write: Option<Result<(), String>>,
189}
190
191/// The entry a decision was served from (spec 044, 3.6.6), as its run
192/// record's `decision_cache` member names it.
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct ServedFromCache {
195    pub entry_id: String,
196    /// The digest of the run record of the decision that wrote the entry.
197    pub source_run_record: rustev_contract::ids::ContentDigest,
198}
199
200/// What [`Runtime::decide_with_capture`] captured (spec 004, 5.3).
201#[derive(Debug, Clone, PartialEq)]
202pub enum CaptureOutcome {
203    /// The decision was rejected: nothing ran, and there is nothing to
204    /// retain or replay.
205    NotAdmitted,
206    Captured(Capture),
207}
208
209/// The normal decide result, and beside it the capture, also when evidence
210/// delivery failed.
211#[derive(Debug)]
212pub struct CapturedDecision {
213    pub result: Result<Decided, DecideError>,
214    pub capture: CaptureOutcome,
215}
216
217/// Live counts, for operators and tests. In memory only.
218#[derive(Debug, Clone, PartialEq, Eq, Default)]
219pub struct Gauges {
220    pub in_flight: usize,
221    pub queued: usize,
222    pub rejected: u64,
223    pub abandoned: u64,
224    /// Attempts in flight per backend.
225    pub backend_in_flight: BTreeMap<String, usize>,
226    pub sink: SinkStats,
227}
228
229#[derive(Clone)]
230pub(crate) struct Registered {
231    pub backend: Arc<dyn DecisionBackend>,
232    pub permits: Arc<Semaphore>,
233    pub max: usize,
234    /// Who implements it (spec 033, 3.9): `upstream` unless registered as
235    /// the host's own.
236    pub origin: rustev_contract::actionability::Origin,
237}
238
239pub(crate) struct Inner {
240    pub clock: Arc<dyn Clock>,
241    pub config: RuntimeConfig,
242    pub backends: BTreeMap<String, Registered>,
243    pub sources: BTreeMap<String, acquisition::RegisteredSource>,
244    pub sink: Arc<dyn EvidenceSink>,
245    pub queue: Option<mpsc::Sender<Job>>,
246    pub stats: Arc<Stats>,
247    pub shared: Option<Arc<Ledger>>,
248    /// The host's store behind the persistent tier (spec 044, 3.4).
249    pub store: Option<Arc<dyn RuntimeStore>>,
250    /// Persistent selections invalidated in the memory tiers and not yet
251    /// in the store: a namespace, or a scope in it (spec 044, 3.10.3).
252    unavailable: Mutex<BTreeSet<(String, Option<String>)>>,
253    optimizer: optimization::Optimizer,
254    admission: Arc<Semaphore>,
255    queued: AtomicUsize,
256    rejected: AtomicU64,
257    abandoned: AtomicU64,
258    observer_panics: AtomicU64,
259    /// Decision ids submitted and not yet finished, so no two live
260    /// decisions share attempt ids (spec 003, 3.9.2).
261    live: Mutex<BTreeSet<String>>,
262}
263
264impl Inner {
265    /// Whether a persistent selection covering this key awaits its store
266    /// invalidation (spec 044, 3.10.3).
267    pub(crate) fn persistent_unavailable(&self, namespace: &str, scope_id: &str) -> bool {
268        let marks = self.unavailable.lock().unwrap_or_else(|e| e.into_inner());
269        marks.contains(&(namespace.to_string(), None))
270            || marks.contains(&(namespace.to_string(), Some(scope_id.to_string())))
271    }
272}
273
274pub struct Runtime {
275    inner: Arc<Inner>,
276}
277
278pub struct RuntimeBuilder {
279    clock: Arc<dyn Clock>,
280    sink: Arc<dyn EvidenceSink>,
281    config: RuntimeConfig,
282    backends: Vec<(
283        Arc<dyn DecisionBackend>,
284        usize,
285        rustev_contract::actionability::Origin,
286    )>,
287    shared: Option<Arc<Ledger>>,
288    sources: Vec<(Arc<dyn rustev_core::seams::AcquisitionSource>, usize)>,
289    store: Option<Arc<dyn RuntimeStore>>,
290}
291
292impl RuntimeBuilder {
293    /// Install a source by its spec 032 descriptor with a per-source attempt bound.
294    pub fn acquisition_source(
295        mut self,
296        source: Arc<dyn rustev_core::seams::AcquisitionSource>,
297        max_concurrency: usize,
298    ) -> Self {
299        self.sources.push((source, max_concurrency));
300        self
301    }
302
303    /// Register a backend with its own attempt concurrency limit.
304    pub fn backend(mut self, backend: Arc<dyn DecisionBackend>, max_concurrency: usize) -> Self {
305        self.backends.push((
306            backend,
307            max_concurrency,
308            rustev_contract::actionability::Origin::Upstream,
309        ));
310        self
311    }
312
313    /// Register a backend the host implements itself (spec 033, 3.9.1): its
314    /// failures are caused by the host, not an upstream service. A host
315    /// relaying a failure from its own upstream may still report it as
316    /// `upstream`; nothing reports the caller.
317    pub fn host_backend(
318        mut self,
319        backend: Arc<dyn DecisionBackend>,
320        max_concurrency: usize,
321    ) -> Self {
322        self.backends.push((
323            backend,
324            max_concurrency,
325            rustev_contract::actionability::Origin::Host,
326        ));
327        self
328    }
329
330    /// The host's store, for plans whose policy enables the persistent tier
331    /// (spec 044, 3.4.1).
332    pub fn store(mut self, store: Arc<dyn RuntimeStore>) -> Self {
333        self.store = Some(store);
334        self
335    }
336
337    /// A ledger shared by every decision, reserved after each decision's own.
338    pub fn shared_ledger(mut self, ledger: Arc<Ledger>) -> Self {
339        self.shared = Some(ledger);
340        self
341    }
342
343    /// Build the runtime. Under a queued sink policy this spawns the delivery
344    /// worker, so it must run inside a Tokio runtime.
345    pub fn build(self) -> Result<Runtime, ConfigError> {
346        let c = self.config;
347        for (name, v) in [
348            ("max_in_flight", c.max_in_flight),
349            ("max_parallel_requests", c.max_parallel_requests),
350        ] {
351            if v == 0 {
352                return Err(ConfigError::Zero(name));
353            }
354        }
355        let mut backends = BTreeMap::new();
356        for (b, max, origin) in self.backends {
357            if max == 0 {
358                return Err(ConfigError::Zero("backend max_concurrency"));
359            }
360            let id = b.descriptor().backend_id.clone();
361            let r = Registered {
362                backend: b,
363                permits: Arc::new(Semaphore::new(max)),
364                max,
365                origin,
366            };
367            if backends.insert(id.clone(), r).is_some() {
368                return Err(ConfigError::DuplicateBackend(id));
369            }
370        }
371        let mut sources = BTreeMap::new();
372        for (source, max) in self.sources {
373            if max == 0 {
374                return Err(ConfigError::Zero("source max_concurrency"));
375            }
376            let descriptor = source.descriptor().clone();
377            if descriptor.id().is_err() {
378                return Err(ConfigError::InvalidSource(descriptor.name));
379            }
380            let name = descriptor.name.clone();
381            if sources
382                .insert(
383                    name.clone(),
384                    acquisition::RegisteredSource {
385                        source,
386                        descriptor,
387                        permits: Arc::new(Semaphore::new(max)),
388                    },
389                )
390                .is_some()
391            {
392                return Err(ConfigError::InvalidSource(name));
393            }
394        }
395        let stats = Arc::new(Stats::default());
396        let queue = match c.sink {
397            SinkPolicy::FailDecision { .. } => None,
398            SinkPolicy::Backpressure {
399                max_pending,
400                delivery_timeout_ms,
401                ..
402            }
403            | SinkPolicy::DropCounted {
404                max_pending,
405                delivery_timeout_ms,
406            } => {
407                if max_pending == 0 {
408                    return Err(ConfigError::Zero("max_pending"));
409                }
410                let (tx, rx) = mpsc::channel(max_pending);
411                tokio::spawn(sink::worker(
412                    rx,
413                    self.sink.clone(),
414                    self.clock.clone(),
415                    delivery_timeout_ms,
416                    stats.clone(),
417                ));
418                Some(tx)
419            }
420        };
421        Ok(Runtime {
422            inner: Arc::new(Inner {
423                clock: self.clock,
424                config: c,
425                backends,
426                sources,
427                sink: self.sink,
428                queue,
429                stats,
430                shared: self.shared,
431                store: self.store,
432                unavailable: Mutex::new(BTreeSet::new()),
433                optimizer: optimization::Optimizer::default(),
434                admission: Arc::new(Semaphore::new(c.max_in_flight)),
435                queued: AtomicUsize::new(0),
436                rejected: AtomicU64::new(0),
437                abandoned: AtomicU64::new(0),
438                observer_panics: AtomicU64::new(0),
439                live: Mutex::new(BTreeSet::new()),
440            }),
441        })
442    }
443}
444
445/// A backend a request can be served by, in fallback order.
446#[derive(Clone)]
447pub(crate) struct Target {
448    pub backend_id: String,
449    pub artifact: ArtifactId,
450    pub registered: Registered,
451}
452
453pub(crate) struct StepRuntime {
454    pub targets: Vec<Target>,
455    pub extraction: Option<rustev_contract::extraction::ExtractionLimits>,
456    pub retry: RetryPolicy,
457    pub timeout: AttemptTimeout,
458    pub fallback_on: Vec<FailureClass>,
459}
460
461/// A compiled plan checked against this runtime's backends.
462pub struct PreparedPlan {
463    compiled: Arc<Compiled>,
464    pub(crate) steps: BTreeMap<String, StepRuntime>,
465    pub(crate) cost: CostPolicy,
466    execution: RecordedExecution,
467    pub(crate) optimization: Option<OptimizationPolicy>,
468}
469
470impl PreparedPlan {
471    pub fn compiled(&self) -> &Compiled {
472        &self.compiled
473    }
474
475    /// Identified cache namespace for host invalidation and audit.
476    pub fn optimization_namespace(&self) -> Option<String> {
477        self.optimization
478            .as_ref()
479            .map(|policy| optimization::namespace(&self.compiled.id, policy))
480    }
481}
482
483struct QueueSlot<'a>(&'a AtomicUsize);
484
485impl Drop for QueueSlot<'_> {
486    fn drop(&mut self) {
487        self.0.fetch_sub(1, Ordering::SeqCst);
488    }
489}
490
491struct Live<'a> {
492    set: &'a Mutex<BTreeSet<String>>,
493    id: String,
494}
495
496impl Drop for Live<'_> {
497    fn drop(&mut self) {
498        self.set
499            .lock()
500            .unwrap_or_else(|e| e.into_inner())
501            .remove(&self.id);
502    }
503}
504
505struct Abandon<'a> {
506    counter: &'a AtomicU64,
507    armed: bool,
508}
509
510impl Drop for Abandon<'_> {
511    fn drop(&mut self) {
512        if self.armed {
513            self.counter.fetch_add(1, Ordering::SeqCst);
514        }
515    }
516}
517
518impl Runtime {
519    pub fn builder(
520        clock: Arc<dyn Clock>,
521        sink: Arc<dyn EvidenceSink>,
522        config: RuntimeConfig,
523    ) -> RuntimeBuilder {
524        RuntimeBuilder {
525            clock,
526            sink,
527            config,
528            backends: vec![],
529            sources: vec![],
530            shared: None,
531            store: None,
532        }
533    }
534
535    pub fn gauges(&self) -> Gauges {
536        let i = &self.inner;
537        Gauges {
538            in_flight: i.config.max_in_flight - i.admission.available_permits(),
539            queued: i.queued.load(Ordering::SeqCst),
540            rejected: i.rejected.load(Ordering::SeqCst),
541            abandoned: i.abandoned.load(Ordering::SeqCst),
542            backend_in_flight: i
543                .backends
544                .iter()
545                .map(|(k, r)| (k.clone(), r.max - r.permits.available_permits()))
546                .collect(),
547            sink: i.stats.snapshot(),
548        }
549    }
550
551    /// The registered backend's descriptor and static cost model, for a
552    /// host's checks before any dispatch (spec 043, 3.7.1).
553    pub fn backend_terms(
554        &self,
555        backend_id: &str,
556    ) -> Option<(&rustev_contract::descriptor::BackendDescriptor, CostModel)> {
557        let r = self.inner.backends.get(backend_id)?;
558        Some((r.backend.descriptor(), r.backend.cost_model()))
559    }
560
561    /// Check a compiled plan against the registered backends (spec 003,
562    /// 3.5.3 and 3.6.5).
563    pub fn prepare(&self, compiled: Compiled) -> Result<PreparedPlan, PrepareError> {
564        if compiled.plan.schema == schema::PLAN_V3 && !self.inner.sink.accepts(schema::RUN_V3) {
565            return Err(PrepareError::SinkVersionUnsupported {
566                schema: schema::RUN_V3,
567            });
568        }
569        let (policy, fallbacks, execution) = match &compiled.plan.execution {
570            PlanExecution::None => (None, vec![], RecordedExecution::None),
571            PlanExecution::Declared(d) => (
572                Some(d.policy.clone()),
573                d.fallbacks.clone(),
574                RecordedExecution::Declared(d.id.clone()),
575            ),
576        };
577        let cost = policy
578            .as_ref()
579            .map(|p| p.cost)
580            .unwrap_or(CostPolicy::Unlimited);
581        let optimization = policy.as_ref().and_then(|p| p.optimization.clone());
582        if let Some(persistent) = optimization
583            .as_ref()
584            .and_then(|o| o.persistent_cache.as_ref())
585        {
586            let Some(store) = &self.inner.store else {
587                return Err(PrepareError::StoreMissing);
588            };
589            if *store.contract() != persistent.store_contract {
590                return Err(PrepareError::StoreContractMismatch);
591            }
592        }
593        let target = |backend_id: &str, descriptor: &rustev_contract::ids::DescriptorId| {
594            let r = self
595                .inner
596                .backends
597                .get(backend_id)
598                .ok_or_else(|| PrepareError::MissingBackend(backend_id.to_string()))?;
599            if r.backend.descriptor().id().ok().as_ref() != Some(descriptor) {
600                return Err(PrepareError::DescriptorMismatch {
601                    backend_id: backend_id.to_string(),
602                });
603            }
604            let hard = matches!(cost, CostPolicy::Hard { .. })
605                || self
606                    .inner
607                    .shared
608                    .as_ref()
609                    .is_some_and(|l| l.mode() == CostMode::Hard);
610            let model = r.backend.cost_model();
611            if hard && model != CostModel::Bounded {
612                return Err(PrepareError::HardBudgetUnenforceable {
613                    backend_id: backend_id.to_string(),
614                    model,
615                });
616            }
617            Ok(r.clone())
618        };
619        // Fact-only sources describe snapshot evidence and need no runtime adapter.
620        for binding in compiled
621            .plan
622            .acquisitions
623            .iter()
624            .flatten()
625            .flat_map(|a| &a.sources)
626        {
627            let Some(source) = self.inner.sources.get(&binding.source.name) else {
628                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
629            };
630            if source.descriptor.id().ok().as_ref() != Some(&binding.source.id) {
631                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
632            }
633            let op = source
634                .descriptor
635                .operations
636                .iter()
637                .flatten()
638                .find(|o| o.name == binding.operation);
639            let hard = matches!(cost, CostPolicy::Hard { .. })
640                || self
641                    .inner
642                    .shared
643                    .as_ref()
644                    .is_some_and(|l| l.mode() == CostMode::Hard);
645            if op.is_none()
646                || (hard
647                    && !op.is_some_and(|o| {
648                        matches!(
649                            o.cost,
650                            rustev_contract::acquisition::OperationCost::Bounded { .. }
651                        )
652                    }))
653            {
654                return Err(PrepareError::SourceBinding(binding.source.name.clone()));
655            }
656        }
657        let mut steps = BTreeMap::new();
658        for s in &compiled.plan.steps {
659            let (backend_id, artifact, descriptor, extraction) = match &s.detail {
660                PlanStepDetail::Semantic(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
661                PlanStepDetail::Generate(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
662                PlanStepDetail::Extract(b) => (
663                    &b.backend_id,
664                    &b.artifact,
665                    &b.descriptor,
666                    Some(b.limits.clone()),
667                ),
668                PlanStepDetail::Rank(b) => (&b.backend_id, &b.artifact, &b.descriptor, None),
669                _ => continue,
670            };
671            let mut targets = vec![Target {
672                backend_id: backend_id.clone(),
673                artifact: artifact.clone(),
674                registered: target(backend_id, descriptor)?,
675            }];
676            let mut fb: Vec<_> = fallbacks.iter().filter(|f| f.step == s.id).collect();
677            fb.sort_by_key(|f| f.target);
678            for f in fb {
679                targets.push(Target {
680                    backend_id: f.backend_id.clone(),
681                    artifact: f.artifact.clone(),
682                    registered: target(&f.backend_id, &f.descriptor)?,
683                });
684            }
685            let declared: Option<&StepExecution> = policy
686                .as_ref()
687                .and_then(|p| p.steps.iter().find(|x| x.step == s.id));
688            steps.insert(
689                s.id.clone(),
690                StepRuntime {
691                    targets,
692                    extraction,
693                    retry: declared
694                        .map(|d| d.retry.clone())
695                        .unwrap_or_else(RetryPolicy::none),
696                    timeout: declared
697                        .map(|d| d.attempt_timeout)
698                        .unwrap_or(AttemptTimeout::None),
699                    fallback_on: declared.map(|d| d.fallback.on.clone()).unwrap_or_default(),
700                },
701            );
702        }
703        for s in &compiled.plan.steps {
704            if let PlanStepDetail::Generate(g) = &s.detail
705                && let Some(b) = &g.output_screener
706            {
707                for field in &g.output.fields {
708                    if matches!(
709                        field.ty,
710                        rustev_contract::generation::GeneratedType::Text { .. }
711                    ) {
712                        steps.insert(
713                            rustev_contract::generation::output_screen_step(&s.id, &field.name),
714                            StepRuntime {
715                                targets: vec![Target {
716                                    backend_id: b.backend_id.clone(),
717                                    artifact: b.artifact.clone(),
718                                    registered: target(&b.backend_id, &b.descriptor)?,
719                                }],
720                                extraction: None,
721                                retry: RetryPolicy::none(),
722                                timeout: AttemptTimeout::None,
723                                fallback_on: vec![],
724                            },
725                        );
726                    }
727                }
728            }
729        }
730        Ok(PreparedPlan {
731            compiled: Arc::new(compiled),
732            steps,
733            cost,
734            execution,
735            optimization,
736        })
737    }
738
739    /// Invalidate every reusable result in this plan's namespace, for any
740    /// correction, revocation, erasure, artifact withdrawal or policy
741    /// change the host observed. The generation advances before entries are
742    /// removed, so a result captured earlier is never stored or served. A
743    /// plan with a persistent tier also has its namespace marked
744    /// unavailable in this process until [`Runtime::invalidate`] completes
745    /// for it (spec 044, 3.10.3).
746    pub fn invalidate_plan(&self, plan: &PreparedPlan) -> Option<InvalidationResult> {
747        let namespace = plan.optimization_namespace()?;
748        Some(self.invalidate_memory(plan, namespace, None))
749    }
750
751    /// Invalidate reusable results for one opaque authorized scope, as for
752    /// an erasure or a scope revocation, as [`Runtime::invalidate_plan`]
753    /// does for a namespace.
754    pub fn invalidate_scope(
755        &self,
756        plan: &PreparedPlan,
757        principal_handle: &[u8],
758    ) -> Option<InvalidationResult> {
759        let namespace = plan.optimization_namespace()?;
760        Some(self.invalidate_memory(
761            plan,
762            namespace,
763            Some(optimization::scope_id(principal_handle)),
764        ))
765    }
766
767    /// Invalidate the plan's namespace, or one authorized scope of it, in
768    /// every tier (spec 044, 3.10.3): the memory tier at once, then the
769    /// store, whose generation advances. The selection is unavailable to
770    /// the persistent tier until the store answers; a failure leaves it
771    /// unavailable, and a later call reconciles it. Invalidation is not
772    /// carried to other processes (3.10.4).
773    pub async fn invalidate(
774        &self,
775        plan: &PreparedPlan,
776        principal_handle: Option<&[u8]>,
777    ) -> Option<InvalidationResult> {
778        let namespace = plan.optimization_namespace()?;
779        let scope = principal_handle.map(optimization::scope_id);
780        let mut result = self.invalidate_memory(plan, namespace.clone(), scope.clone());
781        let Some(store) = self
782            .inner
783            .store
784            .as_ref()
785            .filter(|_| result.persistent.is_some())
786        else {
787            return Some(result);
788        };
789        let selector = match &scope {
790            None => StoreSelector::Namespace,
791            Some(scope) => StoreSelector::Scope(scope.clone()),
792        };
793        let call = StoreCall {
794            remaining_ms: None,
795            cancel: CancelSignal::new(),
796        };
797        match store.invalidate(&namespace, selector, call).await {
798            Ok(done) => {
799                let mut marks = self.unavailable();
800                match &scope {
801                    // A namespace covers every scope in it.
802                    None => marks.retain(|(n, _)| *n != namespace),
803                    Some(scope) => {
804                        marks.remove(&(namespace.clone(), Some(scope.clone())));
805                    }
806                }
807                result.persistent = Some(PersistentInvalidation::Done {
808                    generation: done.generation,
809                    removed: done.removed,
810                });
811                // A scope stays unavailable while its namespace awaits its
812                // own store invalidation.
813                result.complete = !marks.contains(&(namespace.clone(), None));
814            }
815            Err(failure) => {
816                result.persistent = Some(PersistentInvalidation::Failed {
817                    detail: failure.detail,
818                });
819            }
820        }
821        Some(result)
822    }
823
824    fn unavailable(&self) -> std::sync::MutexGuard<'_, BTreeSet<(String, Option<String>)>> {
825        self.inner
826            .unavailable
827            .lock()
828            .unwrap_or_else(|e| e.into_inner())
829    }
830
831    /// The memory tier's invalidation, with the selection marked
832    /// unavailable to the persistent tier when the plan has one.
833    fn invalidate_memory(
834        &self,
835        plan: &PreparedPlan,
836        namespace: String,
837        scope: Option<String>,
838    ) -> InvalidationResult {
839        let selector = match &scope {
840            None => InvalidationSelector::Namespace {
841                namespace: namespace.clone(),
842            },
843            Some(scope_id) => InvalidationSelector::Scope {
844                namespace: namespace.clone(),
845                scope_id: scope_id.clone(),
846            },
847        };
848        let mut result = self.inner.optimizer.invalidate(&selector);
849        let persistent = plan
850            .optimization
851            .as_ref()
852            .is_some_and(|o| o.persistent_cache.is_some());
853        if persistent && self.inner.store.is_some() {
854            self.unavailable().insert((namespace, scope));
855            result.persistent = Some(PersistentInvalidation::Pending);
856            result.complete = false;
857        }
858        result
859    }
860
861    async fn admit(
862        &self,
863        deadline: Instant,
864        cancel: &CancelSignal,
865    ) -> Result<OwnedSemaphorePermit, Rejection> {
866        let i = &self.inner;
867        if let Ok(p) = i.admission.clone().try_acquire_owned() {
868            return Ok(p);
869        }
870        let mut n = i.queued.load(Ordering::SeqCst);
871        loop {
872            if n >= i.config.max_queued {
873                return Err(Rejection::Overloaded);
874            }
875            match i
876                .queued
877                .compare_exchange(n, n + 1, Ordering::SeqCst, Ordering::SeqCst)
878            {
879                Ok(_) => break,
880                Err(now) => n = now,
881            }
882        }
883        let _slot = QueueSlot(&i.queued);
884        let mut acquire = Box::pin(i.admission.clone().acquire_owned());
885        let mut timer = i.clock.sleep_until(deadline);
886        match race(&mut acquire, cancel, &mut timer).await {
887            Raced::Done(Ok(p)) if i.clock.now() < deadline => Ok(p),
888            Raced::Done(Ok(_)) | Raced::Expired => Err(Rejection::QueueDeadline),
889            Raced::Done(Err(_)) => Err(Rejection::Overloaded),
890            Raced::Cancelled => Err(Rejection::Cancelled),
891        }
892    }
893
894    /// Run one decision (spec 003). `cancel` is the caller's cancellation.
895    pub async fn decide(
896        &self,
897        plan: &PreparedPlan,
898        req: DecisionRequest,
899        cancel: &CancelSignal,
900    ) -> Result<Decided, DecideError> {
901        self.run(plan, req, cancel, None, None, None, None, &BTreeMap::new())
902            .await
903    }
904
905    /// [`Runtime::decide`] with the host's key dimensions (spec 044, 3.5
906    /// item 3): one scope handle per dimension the plan's
907    /// `decision_cache` declares. A decision missing a declared dimension,
908    /// or naming one the plan does not declare, is neither served from nor
909    /// admitted to the decision cache, and runs or is refused under the
910    /// policy's `admission`. A plan without the decision cache ignores
911    /// them.
912    pub async fn decide_keyed(
913        &self,
914        plan: &PreparedPlan,
915        req: DecisionRequest,
916        dimensions: &BTreeMap<String, rustev_contract::scope::Handle>,
917        cancel: &CancelSignal,
918    ) -> Result<Decided, DecideError> {
919        self.run(plan, req, cancel, None, None, None, None, dimensions)
920            .await
921    }
922
923    /// Run with a value-free observer. The ordinary entry point has no callbacks.
924    pub async fn decide_observed(
925        &self,
926        plan: &PreparedPlan,
927        req: DecisionRequest,
928        cancel: &CancelSignal,
929        observer: &dyn ProgressObserver,
930    ) -> Result<Decided, DecideError> {
931        self.run(
932            plan,
933            req,
934            cancel,
935            None,
936            None,
937            Some(observer),
938            None,
939            &BTreeMap::new(),
940        )
941        .await
942    }
943
944    /// Contained progress callback panics across this runtime's decisions.
945    pub fn observer_panics(&self) -> u64 {
946        self.inner.observer_panics.load(Ordering::Relaxed)
947    }
948
949    /// [`Runtime::decide`] with opt-in bounded capture of every supplied
950    /// value (spec 004, 5.3). An invalid configuration is refused before
951    /// admission. Dropping the future returns nothing, as for `decide`.
952    pub async fn decide_with_capture(
953        &self,
954        plan: &PreparedPlan,
955        req: DecisionRequest,
956        cancel: &CancelSignal,
957        capture: &CaptureConfig,
958    ) -> Result<CapturedDecision, CaptureConfigError> {
959        self.decide_capturing(plan, req, cancel, capture, None)
960            .await
961    }
962
963    async fn decide_capturing(
964        &self,
965        plan: &PreparedPlan,
966        req: DecisionRequest,
967        cancel: &CancelSignal,
968        capture: &CaptureConfig,
969        streaming: Option<&generation::Streaming>,
970    ) -> Result<CapturedDecision, CaptureConfigError> {
971        // Spec 036, 3.15.1 admits every plan 3 decision; generation capture
972        // (spec 037, 3.15.3) shares the acquisition and semantic supply order.
973        capture.check()?;
974        let decision_id = req.decision_id.clone();
975        let mut capturing = capture::Capturing::new(capture);
976        let result = self
977            .run(
978                plan,
979                req,
980                cancel,
981                Some(&mut capturing),
982                None,
983                None,
984                streaming,
985                &BTreeMap::new(),
986            )
987            .await;
988        let capture = match &result {
989            Err(DecideError::Rejected(_) | DecideError::GenerationFailed(_)) => {
990                CaptureOutcome::NotAdmitted
991            }
992            Ok(Decided { record, .. }) | Err(DecideError::EvidenceNotDelivered { record, .. }) => {
993                CaptureOutcome::Captured(
994                    capturing.finish(&decision_id, record.termination == Termination::Cancelled),
995                )
996            }
997        };
998        Ok(CapturedDecision { result, capture })
999    }
1000
1001    /// Start an opt-in field stream. Poll the returned future to drive execution;
1002    /// the receiver may be polled independently and never backpressures it.
1003    pub fn decide_streaming<'a>(
1004        &'a self,
1005        plan: &'a PreparedPlan,
1006        req: DecisionRequest,
1007        cancel: &'a CancelSignal,
1008        stream_buffer: usize,
1009    ) -> Result<
1010        (
1011            impl std::future::Future<Output = Result<Decided, DecideError>> + Send + 'a,
1012            GenerationStream,
1013        ),
1014        rustev_core::generation_eval::GenerationError,
1015    > {
1016        let (streaming, receiver) = generation::Streaming::new(plan, stream_buffer)?;
1017        Ok((
1018            async move {
1019                self.run(
1020                    plan,
1021                    req,
1022                    cancel,
1023                    None,
1024                    None,
1025                    None,
1026                    Some(&streaming),
1027                    &BTreeMap::new(),
1028                )
1029                .await
1030            },
1031            receiver,
1032        ))
1033    }
1034
1035    #[allow(clippy::too_many_arguments)]
1036    async fn run(
1037        &self,
1038        plan: &PreparedPlan,
1039        req: DecisionRequest,
1040        cancel: &CancelSignal,
1041        mut capture: Option<&mut capture::Capturing<'_>>,
1042        live: Option<&live::LiveCtx<'_>>,
1043        observer: Option<&dyn ProgressObserver>,
1044        streaming: Option<&generation::Streaming>,
1045        dimensions: &BTreeMap<String, rustev_contract::scope::Handle>,
1046    ) -> Result<Decided, DecideError> {
1047        let i = &*self.inner;
1048        // The runtime waits on its own child of the caller's signal, so a
1049        // long-lived caller signal holds no waker of this decision after it
1050        // ends (spec 003, 3.4.3).
1051        let cancel = &cancel.child();
1052        // Dropping this future at any point after here, queued or admitted,
1053        // counts as abandoned (spec 003, 3.4.5).
1054        let mut abandon = Abandon {
1055            counter: &i.abandoned,
1056            armed: true,
1057        };
1058        let mut reject = |r: Rejection| {
1059            abandon.armed = false;
1060            i.rejected.fetch_add(1, Ordering::SeqCst);
1061            DecideError::Rejected(r)
1062        };
1063        let submitted = i.clock.now();
1064        if req.decision_id.is_empty() || req.decision_id.len() > 256 {
1065            return Err(reject(Rejection::InvalidRequest(
1066                "a decision id is 1 to 256 bytes".into(),
1067            )));
1068        }
1069        let compiled: &Compiled = &plan.compiled;
1070        let ev = match Evaluation::start(compiled, &req.snapshot, req.evaluation_time) {
1071            Ok(ev) => ev,
1072            Err(e) => return Err(reject(Rejection::InvalidRequest(e.to_string()))),
1073        };
1074        if !i
1075            .live
1076            .lock()
1077            .unwrap_or_else(|e| e.into_inner())
1078            .insert(req.decision_id.clone())
1079        {
1080            return Err(reject(Rejection::InvalidRequest(
1081                "a decision with this id is already running".into(),
1082            )));
1083        }
1084        let _live = Live {
1085            set: &i.live,
1086            id: req.decision_id.clone(),
1087        };
1088        let deadline_ms = compiled
1089            .plan
1090            .definition
1091            .limits
1092            .deadline_ms
1093            .min(req.deadline_ms.unwrap_or(u64::MAX));
1094        let deadline = submitted.saturating_add(deadline_ms);
1095        let _permit = match self.admit(deadline, cancel).await {
1096            Ok(p) => p,
1097            Err(r) => return Err(reject(r)),
1098        };
1099        let queued_ms = i.clock.now().saturating_sub(submitted);
1100        let observing = observer.map(|observer| progress::Observing {
1101            observer,
1102            panics: &i.observer_panics,
1103        });
1104        if let Some(o) = &observing {
1105            o.call(|o| o.admitted());
1106        }
1107
1108        let ledger = Ledger::new(plan.cost);
1109        let ev = std::sync::Mutex::new(ev);
1110        let deferred_cache = Mutex::new(vec![]);
1111        // The persistent tier (spec 044, 3.4); live re-execution observes
1112        // fresh backend work and reads no tier.
1113        let persistent = plan
1114            .optimization
1115            .as_ref()
1116            .and_then(|o| o.persistent_cache.as_ref())
1117            .filter(|_| i.store.is_some() && live.is_none());
1118        let mut pending = store::Pending::default();
1119        // The decision-result cache reads its tier after admission and
1120        // before the core starts (spec 044, 3.6.3).
1121        let mut decision = optimization::decision::DecisionRun::start(
1122            &i.optimizer,
1123            plan,
1124            &req,
1125            dimensions,
1126            live.is_some(),
1127        );
1128        let decision_policy = plan
1129            .optimization
1130            .as_ref()
1131            .and_then(|o| o.decision_cache.as_ref());
1132        // Then its persistent tier (spec 044, 3.3), on the decision's task.
1133        let decision_store = match (persistent, i.store.as_deref()) {
1134            (Some(persistent), Some(store)) => Some(store::StoreCtx {
1135                rt: i,
1136                store,
1137                persistent,
1138                cancel,
1139                deadline,
1140            }),
1141            _ => None,
1142        };
1143        if let (Some(d), Some(calls), Some(policy), Some(cache)) = (
1144            decision.as_mut(),
1145            decision_store.as_ref(),
1146            plan.optimization.as_ref(),
1147            decision_policy,
1148        ) {
1149            d.read_store(&i.optimizer, policy, cache, calls, req.evaluation_time)
1150                .await;
1151        }
1152        let has_generation = compiled
1153            .plan
1154            .steps
1155            .iter()
1156            .any(|s| matches!(s.detail, PlanStepDetail::Generate(_)));
1157        let (mut requests, acquisitions, mut cancelled) = {
1158            let cx = driver::DecisionCtx {
1159                rt: i,
1160                plan,
1161                deadline,
1162                submitted,
1163                cancel,
1164                ledger: &ledger,
1165                decision_id: &req.decision_id,
1166                principal: &req.principal_handle,
1167                evaluation_time: req.evaluation_time,
1168                ev: Some(&ev),
1169                generation: None,
1170                step_ledger: None,
1171                stream: None,
1172                item_clock: None,
1173                deferred_cache: has_generation.then_some(&deferred_cache),
1174                persistent: persistent.map(|_| &pending),
1175                decision: decision.as_ref(),
1176                live,
1177                observing: observing.as_ref(),
1178            };
1179            driver::run_requests(&cx, capture.as_deref_mut()).await
1180        };
1181        let ev = ev.into_inner().unwrap_or_else(|e| e.into_inner());
1182        // A volatile input the core did not accept from the new snapshot is
1183        // a failed refresh (spec 044, 3.6.5).
1184        let unresolved_volatile: Vec<String> = decision_policy
1185            .filter(|_| decision.as_ref().is_some_and(|d| d.hit.is_some()))
1186            .map(|p| {
1187                p.volatile
1188                    .iter()
1189                    .filter(|v| !v.starts_with("step:"))
1190                    .filter(|v| ev.status_of(&format!("input:{v}")).is_err())
1191                    .cloned()
1192                    .collect()
1193            })
1194            .unwrap_or_default();
1195        let stage_cx = generation::StageContext {
1196            rt: i,
1197            plan,
1198            deadline,
1199            submitted,
1200            cancel,
1201            ledger: &ledger,
1202            decision_id: &req.decision_id,
1203            principal: &req.principal_handle,
1204            evaluation_time: req.evaluation_time,
1205            streaming,
1206        };
1207        let mut text_result = None;
1208        let mut writes = vec![];
1209        let mut judged = false;
1210        // Spec 037 3.13.1 permits Complete only after final verification and
1211        // defines no failure event. Both cancellation paths close the stream
1212        // without Complete: cancellation has no text result (3.12.6).
1213        let (completion, core) = if cancelled {
1214            (Completion::Cancelled, CoreEvidence::NotProduced)
1215        } else {
1216            let stage = ev
1217                .begin_generation(&req.decision_id)
1218                .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1219            let stage = Mutex::new(stage);
1220            // Spec 003 3.9.1 permits only judged or cancelled records; spec 037
1221            // 3.12.6 requires generation to complete before judged. An invariant
1222            // failure has no specified run-record termination, so this path
1223            // returns GenerationFailed without delivery or cache writes. Ordinary
1224            // backend failures resolve through backstops and still deliver evidence
1225            // (037 3.10.2, 3.12.7, 3.15.2); they do not take this error path.
1226            let (generation_requests, pending_writes, was_cancelled) =
1227                generation::run(&stage_cx, &stage, capture)
1228                    .await
1229                    .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1230            requests.extend(generation_requests);
1231            cancelled = was_cancelled;
1232            if cancelled {
1233                (Completion::Cancelled, CoreEvidence::NotProduced)
1234            } else {
1235                let (judgment, evidence) = {
1236                    let stage = stage.lock().unwrap_or_else(|e| e.into_inner());
1237                    let (mut judgment, evidence, mut text) = stage
1238                        .finish()
1239                        .map_err(|e| DecideError::GenerationFailed(e.to_string()))?;
1240                    if let Some(text) = &mut text {
1241                        stage.verify_ownership(&mut judgment, text);
1242                        if let Some(streaming) = streaming {
1243                            streaming.finish(text);
1244                        }
1245                    }
1246                    if let Some(o) = &observing {
1247                        o.remaining(&evidence, &requests, &compiled.plan);
1248                    }
1249                    text_result = text;
1250                    (judgment, evidence)
1251                };
1252                // Persistent writes follow the evidence, whose digest each
1253                // entry binds, and precede the run record, which records
1254                // every admission not made (spec 044, 3.4).
1255                if let (Some(persistent), Some(store), Some(policy)) =
1256                    (persistent, i.store.as_deref(), plan.optimization.as_ref())
1257                {
1258                    let written = store::write_pending(
1259                        std::mem::take(&mut pending),
1260                        store::Writing {
1261                            rt: i,
1262                            store,
1263                            policy,
1264                            persistent,
1265                            cancel,
1266                            deadline,
1267                            created: req.evaluation_time,
1268                            evidence: rustev_contract::Document::record_digest(&evidence).ok(),
1269                        },
1270                    )
1271                    .await;
1272                    for ((step, instance), diagnostic) in written {
1273                        if let Some(o) = requests
1274                            .iter_mut()
1275                            .find(|r| r.step == step && r.instance == instance)
1276                            .and_then(|r| r.optimization.as_mut())
1277                        {
1278                            o.diagnose(diagnostic);
1279                        }
1280                    }
1281                }
1282                writes = deferred_cache
1283                    .into_inner()
1284                    .unwrap_or_else(|e| e.into_inner());
1285                writes.extend(pending_writes);
1286                judged = true;
1287                (
1288                    Completion::Judged(Box::new(judgment)),
1289                    CoreEvidence::Recorded(Box::new(evidence)),
1290                )
1291            }
1292        };
1293        let elapsed_ms = i.clock.now().saturating_sub(submitted);
1294        let plan_3 = plan.compiled.plan.schema == schema::PLAN_V3;
1295        // A plan 2 decision's record is `rustev.run/2` exactly when the sink
1296        // selected it (spec 020, 3.2).
1297        let run_2 = !plan_3 && i.sink.run_schema() == schema::RUN_V2;
1298        let (remote_evidence, remote_evidence_truncated) = if run_2 {
1299            let (list, truncated) = remote_evidence(i, &requests);
1300            (Some(list), truncated)
1301        } else {
1302            (None, false)
1303        };
1304        // A judged miss may be admitted once its record exists, since the
1305        // entry names the record's digest (spec 044, 3.6.4).
1306        let admission = match (&decision, decision_policy) {
1307            (Some(d), Some(policy)) if judged => match d.admissible(&requests, policy) {
1308                Ok(admit) => admit,
1309                Err(diagnostic) => {
1310                    d.diagnose(diagnostic);
1311                    None
1312                }
1313            },
1314            _ => None,
1315        };
1316        let served_from_cache =
1317            decision
1318                .as_ref()
1319                .and_then(|d| d.hit.as_ref())
1320                .map(|hit| ServedFromCache {
1321                    entry_id: hit.entry_id.clone(),
1322                    source_run_record: hit.entry.source_run_record.clone(),
1323                });
1324        let mut record = RunRecord {
1325            acquisitions: (!acquisitions.is_empty()).then_some(acquisitions),
1326            decision_cache: decision
1327                .as_ref()
1328                .map(|d| d.record(&requests, &unresolved_volatile)),
1329            // A plan 3 decision's record holds evidence 2 (spec 032, 3.10.3).
1330            schema: if plan_3 {
1331                schema::RUN_V3
1332            } else if run_2 {
1333                schema::RUN_V2
1334            } else {
1335                schema::RUN
1336            }
1337            .into(),
1338            decision_id: req.decision_id.clone(),
1339            plan_id: compiled.id.clone(),
1340            execution: plan.execution.clone(),
1341            termination: match completion {
1342                Completion::Judged(_) => Termination::Judged,
1343                Completion::Cancelled => Termination::Cancelled,
1344            },
1345            core,
1346            timing: Timing {
1347                queued_ms,
1348                elapsed_ms,
1349                deadline_ms,
1350                deadline_expired: i.clock.now() >= deadline,
1351            },
1352            requests,
1353            cost: ledger.summary(),
1354            remote_evidence,
1355            remote_evidence_truncated,
1356        };
1357        fit_remote_evidence(&mut record);
1358        let record = Arc::new(record);
1359        let delivery = self.deliver(record.clone()).await;
1360        abandon.armed = false;
1361        match delivery {
1362            Ok(delivery) => {
1363                generation::write_cache(&stage_cx, writes);
1364                let mut decision_cache_write = None;
1365                if let (Some(d), Some(policy), Some((key, requests)), Ok(source)) = (
1366                    &decision,
1367                    decision_policy,
1368                    admission,
1369                    rustev_contract::Document::record_digest(&*record),
1370                ) {
1371                    if let (Some(calls), Some(generations)) = (&decision_store, d.captured)
1372                        && policy.tiers.contains(&DecisionCacheTier::Persistent)
1373                    {
1374                        use rustev_contract::Document;
1375                        let digests = match (&completion, &record.core) {
1376                            (Completion::Judged(j), CoreEvidence::Recorded(e)) => {
1377                                j.record_digest().ok().zip(e.record_digest().ok())
1378                            }
1379                            _ => None,
1380                        };
1381                        let volatile = req
1382                            .snapshot
1383                            .entries
1384                            .iter()
1385                            .filter(|e| policy.volatile.contains(&e.field))
1386                            .cloned()
1387                            .collect();
1388                        decision_cache_write = Some(match (digests, plan.optimization.as_ref()) {
1389                            (Some((judgment, evidence)), Some(optimization)) => {
1390                                store::write_decision(
1391                                    calls,
1392                                    store::DecisionWrite {
1393                                        key: &key,
1394                                        policy: optimization,
1395                                        requests: &requests,
1396                                        volatile,
1397                                        judgment,
1398                                        run_record: source.clone(),
1399                                        evidence,
1400                                        generations,
1401                                        created: req.evaluation_time,
1402                                        ttl_ms: policy.ttl_ms,
1403                                    },
1404                                )
1405                                .await
1406                            }
1407                            _ => {
1408                                Err("persistent decision cache entry has no canonical form".into())
1409                            }
1410                        });
1411                    }
1412                    // A refused admission (a generation advanced, or an
1413                    // equal digest over other material) is a miss for the
1414                    // next decision; this record is already delivered.
1415                    if policy.tiers.contains(&DecisionCacheTier::Memory) {
1416                        let _ = i.optimizer.insert_decision(
1417                            policy,
1418                            key,
1419                            optimization::decision::DecisionEntry {
1420                                requests,
1421                                source_run_record: source,
1422                                created: req.evaluation_time,
1423                            },
1424                            d.generation,
1425                        );
1426                    }
1427                }
1428                Ok(Decided {
1429                    text_result,
1430                    completion,
1431                    record,
1432                    delivery,
1433                    served_from_cache,
1434                    decision_cache_write,
1435                })
1436            }
1437            Err(failure) => Err(DecideError::EvidenceNotDelivered { record, failure }),
1438        }
1439    }
1440
1441    async fn deliver(&self, record: Arc<RunRecord>) -> Result<Delivery, DeliveryFailure> {
1442        let i = &*self.inner;
1443        match i.config.sink {
1444            SinkPolicy::FailDecision { timeout_ms } => {
1445                sink::deliver_once(&*i.sink, &*i.clock, &record, timeout_ms, &i.stats)
1446                    .await
1447                    .map(|receipt| Delivery::Acknowledged { receipt })
1448            }
1449            SinkPolicy::Backpressure {
1450                enqueue_timeout_ms, ..
1451            } => {
1452                let Some(tx) = &i.queue else {
1453                    return Err(stopped());
1454                };
1455                let mut reserve = Box::pin(tx.reserve());
1456                let mut timer = i
1457                    .clock
1458                    .sleep_until(i.clock.now().saturating_add(enqueue_timeout_ms));
1459                match race(&mut reserve, &CancelSignal::new(), &mut timer).await {
1460                    Raced::Done(Ok(permit)) => {
1461                        let (done, rx) = oneshot::channel();
1462                        i.stats.queued.fetch_add(1, Ordering::SeqCst);
1463                        permit.send(Job { record, done });
1464                        Ok(Delivery::Queued(DeliveryTicket(rx)))
1465                    }
1466                    Raced::Done(Err(_)) => Err(stopped()),
1467                    Raced::Expired | Raced::Cancelled => {
1468                        i.stats.failed.fetch_add(1, Ordering::SeqCst);
1469                        Err(DeliveryFailure {
1470                            detail: format!("no delivery slot within {enqueue_timeout_ms} ms"),
1471                            uncertain: false,
1472                        })
1473                    }
1474                }
1475            }
1476            SinkPolicy::DropCounted { .. } => {
1477                let Some(tx) = &i.queue else {
1478                    return Err(stopped());
1479                };
1480                let (done, rx) = oneshot::channel();
1481                i.stats.queued.fetch_add(1, Ordering::SeqCst);
1482                match tx.try_send(Job { record, done }) {
1483                    Ok(()) => Ok(Delivery::Queued(DeliveryTicket(rx))),
1484                    Err(mpsc::error::TrySendError::Closed(_)) => {
1485                        i.stats.queued.fetch_sub(1, Ordering::SeqCst);
1486                        Err(stopped())
1487                    }
1488                    Err(mpsc::error::TrySendError::Full(_)) => {
1489                        i.stats.queued.fetch_sub(1, Ordering::SeqCst);
1490                        let total = i.stats.dropped.fetch_add(1, Ordering::SeqCst) + 1;
1491                        Ok(Delivery::Dropped {
1492                            dropped_total: total,
1493                        })
1494                    }
1495                }
1496            }
1497        }
1498    }
1499
1500    /// A later observed charge for an attempt whose cost was unknown, applied
1501    /// to the shared ledger (spec 003, 3.5.5). Delivered records are never
1502    /// rewritten.
1503    pub fn reconcile(&self, attempt_id: &str, observed_units: u64) -> bool {
1504        self.inner
1505            .shared
1506            .as_ref()
1507            .is_some_and(|l| l.reconcile(attempt_id, observed_units))
1508    }
1509}
1510
1511fn stopped() -> DeliveryFailure {
1512    DeliveryFailure {
1513        detail: "the delivery worker stopped".into(),
1514        uncertain: false,
1515    }
1516}
1517
1518/// The remote evidence list of a `rustev.run/2` record (spec 020, 3.2): at
1519/// most one entry per remote attempt, in attempt order, taken from the
1520/// backend that ran it, with the step, instance and final remote state the
1521/// record holds. Beyond [`MAX_REMOTE_EVIDENCE`] entries the list is cut and
1522/// says so.
1523fn remote_evidence(i: &Inner, requests: &[RequestRecord]) -> (Vec<RemoteEvidence>, bool) {
1524    let mut out = Vec::new();
1525    let mut truncated = false;
1526    for r in requests {
1527        for a in &r.attempts {
1528            let Some(reg) = i.backends.get(&a.backend_id) else {
1529                continue;
1530            };
1531            let Some(mut e) = reg.backend.take_remote_evidence(&a.attempt_id) else {
1532                continue;
1533            };
1534            if out.len() >= MAX_REMOTE_EVIDENCE {
1535                truncated = true;
1536                continue;
1537            }
1538            e.attempt_id = a.attempt_id.clone();
1539            e.step = r.step.clone();
1540            e.instance = r.instance.clone();
1541            e.remote_state = a.remote;
1542            out.push(e.bounded());
1543        }
1544    }
1545    (out, truncated)
1546}
1547
1548/// Keep a `rustev.run/2` record within its parse bounds: remote evidence
1549/// entries leave from the end, and the record says so, until it fits.
1550fn fit_remote_evidence(record: &mut RunRecord) {
1551    use rustev_contract::Document;
1552    let max = rustev_contract::limits::RECORD_V1.max_bytes;
1553    while record
1554        .remote_evidence
1555        .as_ref()
1556        .is_some_and(|l| !l.is_empty())
1557        && record.record_canonical().is_ok_and(|b| b.len() > max)
1558    {
1559        if let Some(l) = &mut record.remote_evidence {
1560            l.pop();
1561        }
1562        record.remote_evidence_truncated = true;
1563    }
1564}
1565
1566mod spend;
1567pub use spend::{
1568    LiveSpend, SpendAttempt, SpendBinding, SpendGate, SpendRefusal, SpendTicket, SpendUnit,
1569};