Skip to main content

areev_run/
runner.rs

1//! The driver loop (§5.3): start, resume, respond, cancel, verify. All
2//! store writes happen HERE, on the driver thread, in the order the pure
3//! core commands them — intents before dispatches, results before their
4//! bookkeeping, checkpoints as commanded. Parallelism lives exclusively in
5//! the executor pool.
6
7use crate::clock::Clock;
8use crate::executor::{DispatchDone, DispatchJob, HostToolExecutor, Pool};
9use crate::journal::{self, JournalView};
10use crate::manifest::{BudgetsSpec, RunManifest};
11use crate::{err_run, RunSession, VerifyReport, VerifyStep};
12use areev_cal::AreevFacade;
13use areev_core::authz::HARNESS_NS;
14use areev_core::error::Hash;
15use areev_core::types::{Fact, Grain, Observation};
16use areev_run_core::{
17    step, Command, DecisionRecord, EffectOutcome, EventIn, FailCause, JournalKey, PlanGraph,
18    RunError, RunOutcome, SchedulerState, StepEnv,
19};
20use serde_json::{json, Value};
21use sha2::Digest;
22use std::collections::BTreeMap;
23use std::sync::Arc;
24
25/// What to do with a dangling intent found on resume (§5.3 step 4): the
26/// pinned default re-dispatches under the SAME idempotency key and journals
27/// the redelivery; hosts that cannot deduplicate set `Fail`. No third mode.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum OnDangling {
30    #[default]
31    Redispatch,
32    Fail,
33}
34
35/// A deterministic crash-injection point — the §5.5 gate-2 mechanism
36/// (deterministic injection, never sleeps): the driver aborts at a precise
37/// spot in the intent→result protocol, leaving exactly the on-disk state a
38/// real crash there would leave. Production callers never set this.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum CrashPoint {
41    /// Abort immediately after writing the nth intent (1-based), before its
42    /// dispatch: the effect never fired; the intent dangles.
43    AfterIntent(u32),
44    /// Abort after the nth effect COMPLETED but before its result is
45    /// journaled: the effect fired in the real world; the intent dangles —
46    /// the exact window §5.3's redelivery contract exists for.
47    BeforeResult(u32),
48}
49
50/// Start/resume options.
51#[derive(Default)]
52pub struct RunOptions {
53    pub budgets: BudgetsSpec,
54    pub ask_ttl_sec: Option<i64>,
55    pub workers: usize,
56    pub on_dangling: OnDangling,
57    /// Per-LLM-call `max_tokens` for abstract nodes — frozen into the
58    /// manifest at start (§6.7's per-dispatch reservation). None = 1024.
59    pub llm_max_tokens: Option<u32>,
60    /// Crash-injection for the §5.5 gates. `None` in production.
61    pub inject_crash: Option<CrashPoint>,
62}
63
64/// The runtime driver: a host over one facade.
65pub struct Runner {
66    pub facade: Arc<AreevFacade>,
67    pub clock: Arc<dyn Clock>,
68    pub executor: Arc<dyn HostToolExecutor>,
69    /// The tool-calling model for abstract nodes; None = abstract nodes
70    /// refuse at load (`RUN-E006`).
71    pub llm: Option<Arc<dyn areev_llm::ToolCallLlm>>,
72    /// §6.10 streaming subscriber; None = no events. Observational only —
73    /// a slow observer delays (and eventually loses) events, never the run.
74    pub observer: Option<Arc<dyn crate::stream::RunObserver>>,
75    pub ns: String,
76    pub principal: String,
77}
78
79/// Journal-shaped transcript messages → the tool-calling seam's shape.
80/// Pure translation, on the driver thread; the pool never parses state.
81fn seam_messages(input: &Value) -> Vec<areev_llm::ChatMessage> {
82    let Some(msgs) = input.get("messages").and_then(|m| m.as_array()) else {
83        return Vec::new();
84    };
85    msgs.iter()
86        .filter_map(|m| {
87            let role = m.get("role")?.as_str()?;
88            match role {
89                "user" => Some(areev_llm::ChatMessage::User(
90                    m.get("content")
91                        .map(|c| {
92                            c.as_str().map(str::to_string).unwrap_or_else(|| c.to_string())
93                        })
94                        .unwrap_or_default(),
95                )),
96                "assistant" => Some(areev_llm::ChatMessage::Assistant {
97                    text: m.get("text").and_then(|t| t.as_str()).map(str::to_string),
98                    tool_calls: m
99                        .get("tool_calls")
100                        .and_then(|c| c.as_array())
101                        .map(|arr| {
102                            arr.iter()
103                                .map(|c| areev_llm::ToolCallOut {
104                                    id: c
105                                        .get("id")
106                                        .and_then(|v| v.as_str())
107                                        .unwrap_or_default()
108                                        .to_string(),
109                                    name: c
110                                        .get("name")
111                                        .and_then(|v| v.as_str())
112                                        .unwrap_or_default()
113                                        .to_string(),
114                                    arguments: c
115                                        .get("arguments")
116                                        .cloned()
117                                        .unwrap_or(Value::Null),
118                                    arguments_raw: None,
119                                })
120                                .collect()
121                        })
122                        .unwrap_or_default(),
123                }),
124                "tool" => Some(areev_llm::ChatMessage::ToolResult {
125                    tool_call_id: m
126                        .get("tool_call_id")
127                        .and_then(|v| v.as_str())
128                        .unwrap_or_default()
129                        .to_string(),
130                    content: m
131                        .get("content")
132                        .map(|c| {
133                            c.as_str().map(str::to_string).unwrap_or_else(|| c.to_string())
134                        })
135                        .unwrap_or_default(),
136                    is_error: m.get("is_error").and_then(|v| v.as_bool()).unwrap_or(false),
137                }),
138                _ => None,
139            }
140        })
141        .collect()
142}
143
144/// The pure §6.11 argument validator over the manifest's strict schemas:
145/// non-strict (or schema-less) tools pass; strict ones run
146/// `validate_instance`, and the violation kind becomes the re-prompt text.
147fn make_validate_args(
148    schemas: &BTreeMap<String, Value>,
149) -> impl Fn(&str, &Value) -> std::result::Result<(), String> + '_ {
150    move |tool: &str, args: &Value| match schemas.get(tool) {
151        Some(schema) => areev_core::types::validate_instance(args, schema)
152            .map_err(|k| format!("schema violation: {}", k.as_str())),
153        None => Ok(()),
154    }
155}
156
157fn builtin_eval(edge: &areev_run_core::PlanEdge, state: &Value) -> bool {
158    edge.cond
159        .as_ref()
160        .map(|c| areev_run_core::cond::eval(c, state))
161        .unwrap_or(true)
162}
163
164/// Name the top-level fields where two scheduler-state JSONs disagree — the
165/// RUN-E009 diagnostic. "Diverges" without saying WHERE would send whoever
166/// reads the report straight back to a debugger.
167fn diff_fields(stored: &Value, replayed: &Value) -> String {
168    let (Some(a), Some(b)) = (stored.as_object(), replayed.as_object()) else {
169        return "state shape differs".into();
170    };
171    let mut diffs: Vec<String> = Vec::new();
172    for key in a.keys().chain(b.keys()) {
173        if a.get(key) != b.get(key) && !diffs.iter().any(|d| d.starts_with(key.as_str())) {
174            diffs.push(format!(
175                "{key}: stored={} replayed={}",
176                a.get(key).map(|v| v.to_string()).unwrap_or_else(|| "∅".into()),
177                b.get(key).map(|v| v.to_string()).unwrap_or_else(|| "∅".into()),
178            ));
179        }
180    }
181    diffs.join("; ")
182}
183
184fn idempotency_key(key: &JournalKey, input: &Value) -> String {
185    let mut h = sha2::Sha256::new();
186    h.update(key.idempotency_prefix().as_bytes());
187    h.update([0x1f]);
188    h.update(input.to_string().as_bytes());
189    let out = h.finalize();
190    let mut s = String::with_capacity(64);
191    for b in out {
192        use std::fmt::Write;
193        let _ = write!(s, "{b:02x}");
194    }
195    s
196}
197
198impl Runner {
199    /// D5 enforcement at the driver boundary. The facade session's rights
200    /// decide (owner unless the host bound a principal — the single-user
201    /// path never meets authorization, same as every other surface); a
202    /// bound session needs the run verb granted in the file.
203    fn check_run_verb(&self, verb: areev_core::authz::Verb) -> Result<(), RunError> {
204        self.facade
205            .authz()
206            .check(verb, &self.ns)
207            .map_err(|e| RunError::Unauthorized { what: e.to_string() })
208    }
209
210    /// `respond` authorizes the RESPONDER, not the session: on a governed
211    /// session (non-owner), the responder principal's own grants must cover
212    /// `run.respond` — an approval boundary where "whoever holds the
213    /// process" can approve is not an approval boundary.
214    fn check_responder(&self, responder: &str) -> Result<(), RunError> {
215        if self.facade.authz().is_owner() {
216            return Ok(());
217        }
218        let session = self
219            .facade
220            .principal_session(responder)
221            .map_err(err_run)?;
222        session
223            .authz()
224            .check(areev_core::authz::Verb::RunRespond, &self.ns)
225            .map_err(|e| RunError::Unauthorized { what: e.to_string() })
226    }
227
228    fn load_plan(&self, plan_hash: &Hash) -> Result<PlanGraph, RunError> {
229        let wf = self
230            .facade
231            .with_store(|m| m.get(plan_hash))
232            .map_err(|e| RunError::UnresolvedRef { what: format!("workflow: {e}") })?
233            .to_workflow()
234            .map_err(|e| RunError::InvalidPlan { why: e.to_string() })?;
235        PlanGraph::build(&wf)
236    }
237
238    fn load_manifest(&self, run_id: &str) -> Result<RunManifest, RunError> {
239        self.facade.with_store(|m| RunManifest::load(m, run_id))
240    }
241
242    /// The §6.11 argument-validation table: tool_name → input_schema for
243    /// every STRICT pinned Definition. Loaded once per drive/verify (the
244    /// scheduler's `validate_args` closure must never touch the store), and
245    /// a pure function of the manifest — so live and replay agree.
246    fn load_arg_schemas(
247        &self,
248        manifest: &RunManifest,
249    ) -> Result<BTreeMap<String, Value>, RunError> {
250        let mut schemas = BTreeMap::new();
251        for p in &manifest.pinned {
252            if p.executor != "host" || p.tool_hash.is_empty() {
253                continue;
254            }
255            let h = Hash::from_hex(&p.tool_hash).map_err(err_run)?;
256            let def = self
257                .facade
258                .with_store(|m| m.get(&h))
259                .map_err(err_run)?
260                .to_tool()
261                .map_err(err_run)?;
262            if def.strict == Some(true) {
263                if let Some(schema) = def.input_schema {
264                    schemas.insert(p.tool_name.clone(), schema);
265                }
266            }
267        }
268        Ok(schemas)
269    }
270
271    /// Start a new run of the Workflow at `plan_hash` and drive it until it
272    /// finishes or parks on a Client ask.
273    pub fn start(
274        &self,
275        plan_hash: &Hash,
276        run_id: &str,
277        input: Value,
278        opts: &RunOptions,
279    ) -> Result<RunSession, RunError> {
280        self.check_run_verb(areev_core::authz::Verb::RunExecute)?;
281        areev_core::types::validate_run_id(run_id).map_err(err_run)?;
282        // A run id is an identity: starting twice under one id would
283        // interleave two journals (§5.4 forks are the sanctioned reuse).
284        if self.load_manifest(run_id).is_ok() {
285            return Err(RunError::Tainted {
286                why: format!("run '{run_id}' already exists — resume it or fork"),
287            });
288        }
289        let plan = self.load_plan(plan_hash)?;
290        let manifest = self.facade.with_store(|m| {
291            RunManifest::resolve(
292                m,
293                &self.ns,
294                run_id,
295                plan_hash,
296                &plan,
297                &self.principal,
298                opts.budgets,
299                opts.ask_ttl_sec,
300                input.clone(),
301                self.llm.is_some(),
302                opts.llm_max_tokens,
303            )
304        })?;
305        self.facade
306            .with_store(|m| manifest.persist(m))
307            .map_err(err_run)?;
308
309        let st = SchedulerState::new(run_id, &plan);
310        let events = vec![
311            EventIn::ClockReading { unix_ms: self.clock.now_ms() },
312            EventIn::Start { input },
313        ];
314        self.drive(&plan, plan_hash, &manifest, st, JournalView::default(), events, opts)
315    }
316
317    /// Resume a run from its latest checkpoint: settle expired asks, adopt
318    /// journaled responses, re-deliver dangling intents (§5.3).
319    pub fn resume(&self, run_id: &str, opts: &RunOptions) -> Result<RunSession, RunError> {
320        self.check_run_verb(areev_core::authz::Verb::RunExecute)?;
321        let manifest = self.load_manifest(run_id)?;
322        let plan_hash = Hash::from_hex(&manifest.plan_hash)
323            .map_err(|_| RunError::ManifestMismatch { why: "bad plan hash".into() })?;
324        let plan = self.load_plan(&plan_hash)?;
325        let view = self
326            .facade
327            .with_store(|m| journal::load(m, &self.ns, run_id))
328            .map_err(err_run)?;
329        let Some(last) = view.checkpoints.last() else {
330            // A crash inside the FIRST superstep: the manifest exists but no
331            // checkpoint does. The manifest carries the input for exactly
332            // this — rebuild from it (dangling intents are adopted by the
333            // normal §5.3 path below) rather than bricking the run id.
334            let st = SchedulerState::new(run_id, &plan);
335            let events = vec![
336                EventIn::ClockReading { unix_ms: self.clock.now_ms() },
337                EventIn::Start { input: manifest.input.clone() },
338            ];
339            return self.drive(&plan, &plan_hash, &manifest, st, view, events, opts);
340        };
341        let st: SchedulerState = serde_json::from_value(last.scheduler.clone())
342            .map_err(|e| RunError::ManifestMismatch { why: format!("checkpoint state: {e}") })?;
343        if st.run_id != run_id {
344            return Err(RunError::ManifestMismatch {
345                why: format!("checkpoint belongs to run '{}'", st.run_id),
346            });
347        }
348        if st.is_terminal() {
349            return Ok(RunSession::Finished {
350                outcome: st.outcome().cloned().expect("terminal"),
351                run_id: run_id.to_string(),
352            });
353        }
354
355        let now = self.clock.now_ms();
356        let mut events = vec![EventIn::ClockReading { unix_ms: now }];
357
358        // Settled-while-parked responses (a respond() wrote the result) and
359        // expired asks (settled here as Timeout — never re-dispatched,
360        // never silently eternal).
361        let executors = manifest.executors();
362        for (id, pending) in st.pending_asks.clone() {
363            if let Some(entry) = view.entries.get(&pending.key) {
364                if let Some((_, outcome)) = &entry.result {
365                    events.push(EventIn::ResponseSettled {
366                        tool_call_id: id.clone(),
367                        outcome: outcome.clone(),
368                    });
369                    continue;
370                }
371            }
372            if pending
373                .ask
374                .expires_at_sec
375                .is_some_and(|exp| (now / 1000) as i64 > exp)
376            {
377                let detail = "client ask expired".to_string();
378                let outcome = EffectOutcome::Failed {
379                    cause: FailCause::Timeout,
380                    journal_bytes: detail.len() as u64,
381                    detail,
382                };
383                let executor = executors
384                    .get(pending.node_idx)
385                    .cloned()
386                    .ok_or_else(|| RunError::ManifestMismatch {
387                        why: "ask node outside manifest".into(),
388                    })?;
389                let intent = view
390                    .entries
391                    .get(&pending.key)
392                    .map(|e| e.intent)
393                    .ok_or_else(|| RunError::Tainted {
394                        why: "pending ask with no journaled intent".into(),
395                    })?;
396                self.facade
397                    .with_store(|m| {
398                        journal::write_result(
399                            m,
400                            &self.ns,
401                            run_id,
402                            &plan_hash,
403                            &intent,
404                            &pending.key,
405                            &executor,
406                            &outcome,
407                            st.superstep,
408                            now,
409                            &self.principal,
410                        )
411                    })
412                    .map_err(err_run)?;
413                events.push(EventIn::ResponseSettled { tool_call_id: id, outcome });
414            }
415        }
416
417        self.drive(&plan, &plan_hash, &manifest, st, view, events, opts)
418    }
419
420    /// §5.4 time-travel fork: a NEW run descending from one of `base`'s
421    /// checkpoints. The fork's manifest records `(base_run,
422    /// base_checkpoint)`, an `mg:fork_of` Fact indexes the relationship,
423    /// and the SEED checkpoint `derived_from`s the base checkpoint — the
424    /// verifiability chain. The base run is never touched.
425    ///
426    /// - **Same plan** (`new_plan = None`): the base checkpoint's scheduler
427    ///   state carries over under the new run id; the fork re-executes
428    ///   everything past the checkpoint under its own journal keys.
429    ///   A `BudgetExhausted` terminal re-opens (that is how a run continues
430    ///   under raised budgets — budgets are the fork's own knobs).
431    /// - **New plan** (in-flight migration): full §6.1 validation re-runs;
432    ///   the graph restarts at its entry with the base checkpoint's CONTEXT
433    ///   as the input.
434    ///
435    /// v1 refuses forking a checkpoint that parked mid-superstep (pending
436    /// asks address journal keys of the parent) — pick a closed superstep.
437    ///
438    /// Returns the seed checkpoint hash; `resume` continues the fork.
439    pub fn fork(
440        &self,
441        base_run_id: &str,
442        at_superstep: Option<u64>,
443        new_run_id: &str,
444        new_plan: Option<&Hash>,
445        opts: &RunOptions,
446    ) -> Result<Hash, RunError> {
447        self.check_run_verb(areev_core::authz::Verb::RunExecute)?;
448        areev_core::types::validate_run_id(new_run_id).map_err(err_run)?;
449        if self.load_manifest(new_run_id).is_ok() {
450            return Err(RunError::Tainted {
451                why: format!("run '{new_run_id}' already exists — fork ids must be fresh"),
452            });
453        }
454        let base_manifest = self.load_manifest(base_run_id)?;
455        let view = self
456            .facade
457            .with_store(|m| journal::load(m, &self.ns, base_run_id))
458            .map_err(err_run)?;
459        let base_ckpt = match at_superstep {
460            Some(ss) => {
461                // A terminal checkpoint shares its superstep with that
462                // superstep's close — `--at N` means the CLOSED state, so
463                // prefer the last Idle checkpoint there (the terminal one
464                // still wins when it is all there is, e.g. BudgetExhausted).
465                let matching: Vec<_> =
466                    view.checkpoints.iter().filter(|c| c.superstep == ss).collect();
467                matching
468                    .iter()
469                    .rev()
470                    .find(|c| c.scheduler.get("phase") == Some(&json!("Idle")))
471                    .or(matching.last())
472                    .copied()
473                    .ok_or_else(|| RunError::ManifestMismatch {
474                        why: format!(
475                            "run '{base_run_id}' has no checkpoint at superstep {ss}"
476                        ),
477                    })?
478            }
479            None => view.checkpoints.last().ok_or_else(|| RunError::ManifestMismatch {
480                why: format!("run '{base_run_id}' has no checkpoint to fork from"),
481            })?,
482        };
483        let mut st: SchedulerState = serde_json::from_value(base_ckpt.scheduler.clone())
484            .map_err(|e| RunError::ManifestMismatch { why: format!("checkpoint state: {e}") })?;
485        if !st.pending_asks.is_empty()
486            || matches!(st.phase, areev_run_core::Phase::Open { .. })
487        {
488            return Err(RunError::Tainted {
489                why: format!(
490                    "checkpoint at superstep {} parked mid-superstep — v1 forks \
491                     only from closed supersteps (respond or pick another)",
492                    base_ckpt.superstep
493                ),
494            });
495        }
496        match st.outcome() {
497            Some(RunOutcome::BudgetExhausted { .. }) => {
498                // The sanctioned continue-under-raised-budgets path.
499                st.phase = areev_run_core::Phase::Idle;
500                st.exhausted = None;
501            }
502            Some(other) => {
503                return Err(RunError::Tainted {
504                    why: format!(
505                        "base checkpoint is terminal ({other:?}) — time-travel \
506                         forks pick an earlier superstep (--at N)"
507                    ),
508                });
509            }
510            None => {}
511        }
512
513        let fork_base = crate::manifest::ForkBase {
514            base_run: base_run_id.to_string(),
515            base_checkpoint: base_ckpt.hash.to_hex(),
516            base_superstep: base_ckpt.superstep,
517        };
518        let mut manifest = match new_plan {
519            Some(h) => {
520                // In-flight migration: validate the new plan in full, then
521                // restart its graph with the inherited context as input.
522                let plan = self.load_plan(h)?;
523                let context = st.context.clone();
524                let manifest = self.facade.with_store(|m| {
525                    RunManifest::resolve(
526                        m,
527                        &self.ns,
528                        new_run_id,
529                        h,
530                        &plan,
531                        &self.principal,
532                        opts.budgets,
533                        opts.ask_ttl_sec,
534                        context.clone(),
535                        self.llm.is_some(),
536                        opts.llm_max_tokens,
537                    )
538                })?;
539                let mut fresh = SchedulerState::new(new_run_id, &plan);
540                // The Start bootstrap, applied here so the seed checkpoint
541                // is immediately resumable (§ Start: entry activates).
542                fresh.context = context;
543                fresh.clock_ms = st.clock_ms;
544                fresh.node_state[0] = areev_run_core::NodeState::Ready;
545                st = fresh;
546                manifest
547            }
548            None => {
549                // Resolution freeze: the fork inherits the base's pins
550                // verbatim — never re-resolves. Budgets/TTL are the fork's
551                // own knobs.
552                st.run_id = new_run_id.to_string();
553                RunManifest {
554                    run_id: new_run_id.to_string(),
555                    // The FORKER triggers this run — separation of duties
556                    // must compare against them, not the base run's starter.
557                    principal: self.principal.clone(),
558                    budgets: opts.budgets,
559                    ask_ttl_sec: opts.ask_ttl_sec,
560                    ..base_manifest
561                }
562            }
563        };
564        manifest.fork_of = Some(fork_base.clone());
565        self.facade
566            .with_store(|m| manifest.persist(m))
567            .map_err(err_run)?;
568
569        // The fork index Fact: `runs_touching`-style joins report forks for
570        // inherited-prefix grains through this.
571        let now = self.clock.now_ms();
572        let mut fact = Fact::new(
573            &format!("run:{new_run_id}"),
574            "mg:fork_of",
575            &format!("{}@{}", fork_base.base_run, fork_base.base_checkpoint),
576        )
577        .namespace(HARNESS_NS)
578        .created_at(now as i64);
579        fact.common.author_did = Some(self.principal.clone());
580        fact.common.extra_fields.insert("run_id".into(), json!(new_run_id));
581        fact.common
582            .extra_fields
583            .insert("base_run_id".into(), json!(fork_base.base_run));
584        self.facade.with_store(|m| m.add(&fact)).map_err(err_run)?;
585
586        // The seed checkpoint, derived_from the BASE checkpoint (§5.4's
587        // hash chain) — what `resume` continues from.
588        let state_json = serde_json::to_value(&st).unwrap_or(Value::Null);
589        let record = DecisionRecord {
590            superstep: st.superstep,
591            clock_open_ms: st.clock_ms,
592            clock_close_ms: st.clock_ms,
593            ..Default::default()
594        };
595        let base_hash = base_ckpt.hash;
596        self.facade
597            .with_store(|m| {
598                journal::write_checkpoint(
599                    m,
600                    &self.ns,
601                    new_run_id,
602                    &state_json,
603                    &record,
604                    st.clock_ms,
605                    Some(&base_hash),
606                    &self.principal,
607                )
608            })
609            .map_err(err_run)
610    }
611
612    /// Answer a Client ask (§6.6's validation order). Responding and
613    /// resuming are separate acts: this writes the journaled result and
614    /// returns; `resume` spends the compute.
615    pub fn respond(
616        &self,
617        run_id: &str,
618        tool_call_id: &str,
619        result: Value,
620        is_error: bool,
621        responder: &str,
622    ) -> Result<(), RunError> {
623        self.check_responder(responder)?;
624        let manifest = self.load_manifest(run_id)?;
625        let plan_hash = Hash::from_hex(&manifest.plan_hash)
626            .map_err(|_| RunError::ManifestMismatch { why: "bad plan hash".into() })?;
627        let view = self
628            .facade
629            .with_store(|m| journal::load(m, &self.ns, run_id))
630            .map_err(err_run)?;
631        let Some(last) = view.checkpoints.last() else {
632            return Err(RunError::ManifestMismatch { why: "run has no checkpoint".into() });
633        };
634        let st: SchedulerState = serde_json::from_value(last.scheduler.clone())
635            .map_err(|e| RunError::ManifestMismatch { why: format!("checkpoint state: {e}") })?;
636
637        // Fresh clock reading — respond is its own effect boundary (§6.6):
638        // there is no scheduler clock while parked. The reading lands on
639        // the result grain, so replay sees it.
640        let now = self.clock.now_ms();
641
642        let journal_rejection = |why: &str| {
643            // The losing/invalid response is journaled BEFORE the caller is
644            // told no — "two officers approved conflicting outcomes seconds
645            // apart" is audit evidence, not noise (§6.6).
646            let mut obs = Observation::new(responder, "human")
647                .subject(&format!("run:{run_id}"))
648                .object(&format!("rejected response to ask {tool_call_id}: {why}"))
649                .namespace(HARNESS_NS)
650                .created_at(now as i64);
651            obs.common
652                .extra_fields
653                .insert("run_id".into(), json!(run_id));
654            let _ = self.facade.with_store(|m| m.add(&obs));
655        };
656
657        let Some(pending) = st.pending_asks.get(tool_call_id) else {
658            journal_rejection("no such pending ask (settled, expired, or never asked)");
659            return Err(RunError::UnknownAsk { tool_call_id: tool_call_id.to_string() });
660        };
661        if view
662            .entries
663            .get(&pending.key)
664            .is_some_and(|e| e.result.is_some())
665        {
666            journal_rejection("already settled — first commit won");
667            return Err(RunError::UnknownAsk { tool_call_id: tool_call_id.to_string() });
668        }
669        if pending
670            .ask
671            .expires_at_sec
672            .is_some_and(|exp| (now / 1000) as i64 > exp)
673        {
674            journal_rejection("expired");
675            return Err(RunError::UnknownAsk { tool_call_id: tool_call_id.to_string() });
676        }
677        // Approval separation of duties (§6.6, D5): structurally refused,
678        // not conventioned — mirrors the loop's self-approval block.
679        if pending.ask.approval && responder == manifest.principal {
680            return Err(RunError::Unauthorized {
681                what: format!(
682                    "approval ask {tool_call_id} requires a responder other than \
683                     the triggering principal '{}'",
684                    manifest.principal
685                ),
686            });
687        }
688
689        let outcome = if is_error {
690            let detail = result.to_string();
691            EffectOutcome::Failed {
692                cause: FailCause::UserAborted,
693                journal_bytes: detail.len() as u64,
694                detail,
695            }
696        } else {
697            EffectOutcome::Completed {
698                journal_bytes: result.to_string().len() as u64,
699                result,
700                input_tokens: 0,
701                output_tokens: 0,
702                usd_micros: 0,
703            }
704        };
705        let executor = manifest
706            .executors()
707            .get(pending.node_idx)
708            .cloned()
709            .ok_or_else(|| RunError::ManifestMismatch { why: "ask node outside manifest".into() })?;
710        let intent = view
711            .entries
712            .get(&pending.key)
713            .map(|e| e.intent)
714            .ok_or_else(|| RunError::UnknownAsk { tool_call_id: tool_call_id.to_string() })?;
715        self.facade
716            .with_store(|m| {
717                journal::write_result(
718                    m,
719                    &self.ns,
720                    run_id,
721                    &plan_hash,
722                    &intent,
723                    &pending.key,
724                    &executor,
725                    &outcome,
726                    st.superstep,
727                    now,
728                    responder,
729                )
730            })
731            .map_err(err_run)?;
732        Ok(())
733    }
734
735    /// Write the cancel marker (the kill-switch primitive). The drive loop
736    /// polls it every iteration; `resume` after cancel drains.
737    pub fn cancel(&self, run_id: &str, principal: &str, reason: &str) -> Result<(), RunError> {
738        // Deliberately the LOW bar (D5): the brake must never be blocked by
739        // missing privilege — RunCancel, not RunExecute.
740        self.check_run_verb(areev_core::authz::Verb::RunCancel)?;
741        let now = self.clock.now_ms();
742        let mut f = Fact::new(&format!("run:{run_id}"), "mg:cancel", reason)
743            .namespace(HARNESS_NS)
744            .created_at(now as i64);
745        f.common.author_did = Some(principal.to_string());
746        f.common.extra_fields.insert("run_id".into(), json!(run_id));
747        self.facade.with_store(|m| m.add(&f)).map_err(err_run)?;
748        Ok(())
749    }
750
751    fn cancel_marker(&self, run_id: &str) -> Option<(String, String)> {
752        // A cancel on ANY ancestor stops the whole subtree: child run ids
753        // are `{parent}~{16-hex}` by construction, so walking suffix-strips
754        // covers arbitrarily nested subgraphs (the <5-minute kill-switch
755        // clause must not be unbounded under nesting).
756        let mut candidate = run_id.to_string();
757        loop {
758            let hit = self
759                .facade
760                .with_store(|m| m.latest(HARNESS_NS, &format!("run:{candidate}"), "mg:cancel"))
761                .ok()
762                .flatten()
763                .map(|g| {
764                    (
765                        g.get_str("author_did").unwrap_or("unknown").to_string(),
766                        g.get_str("object").unwrap_or("").to_string(),
767                    )
768                });
769            if hit.is_some() {
770                return hit;
771            }
772            match candidate.rfind('~') {
773                Some(pos) if candidate.len() - pos == 17 => candidate.truncate(pos),
774                _ => return None,
775            }
776        }
777    }
778
779    /// Execute a subgraph node as a CHILD RUN: own run id (deterministic —
780    /// derived from the journal key, so replays and permutations agree),
781    /// own journal, linked by content on the parent's effect grains. The
782    /// child's final context becomes the node's result, merging into the
783    /// parent state through the reducers like any other node result.
784    ///
785    /// v1 limitation, stated: a child that PARKS on a Client ask fails the
786    /// parent node with a clear message — HITL nodes belong in the parent
787    /// graph until ask-bubbling ships. A terminally failed child is never
788    /// retried (resuming a terminal run returns the same outcome forever).
789    fn run_subgraph_effect(
790        &self,
791        parent_run_id: &str,
792        key: &JournalKey,
793        workflow_hash: &str,
794        input: &Value,
795        opts: &RunOptions,
796    ) -> EffectOutcome {
797        let child_run_id = format!("{parent_run_id}~{}", &key.tool_call_id()[..16]);
798        let fail = |cause: FailCause, detail: String| EffectOutcome::Failed {
799            journal_bytes: detail.len() as u64,
800            cause,
801            detail,
802        };
803        let child_session = if self
804            .facade
805            .with_store(|m| RunManifest::load(m, &child_run_id))
806            .is_ok()
807        {
808            self.resume(&child_run_id, opts)
809        } else {
810            let Ok(h) = Hash::from_hex(workflow_hash) else {
811                return fail(FailCause::Unknown, "bad subgraph plan hash".into());
812            };
813            self.start(&h, &child_run_id, input.clone(), opts)
814        };
815        match child_session {
816            Ok(RunSession::Finished { outcome: RunOutcome::Completed, .. }) => {
817                // The child's final context is the node's contribution.
818                let context = self
819                    .facade
820                    .with_store(|m| journal::load(m, &self.ns, &child_run_id))
821                    .ok()
822                    .and_then(|v| v.checkpoints.last().cloned())
823                    .and_then(|c| c.scheduler.get("context").cloned())
824                    .unwrap_or(Value::Null);
825                EffectOutcome::Completed {
826                    journal_bytes: context.to_string().len() as u64,
827                    result: context,
828                    input_tokens: 0,
829                    output_tokens: 0,
830                    usd_micros: 0,
831                }
832            }
833            Ok(RunSession::Finished { outcome, .. }) => fail(
834                FailCause::Unknown,
835                format!("subgraph run '{child_run_id}' finished {outcome:?}"),
836            ),
837            Ok(RunSession::Parked { .. }) => fail(
838                FailCause::Unknown,
839                format!(
840                    "subgraph run '{child_run_id}' parked on a Client ask — v1 \
841                     does not bubble asks through subgraphs; move HITL nodes \
842                     into the parent graph"
843                ),
844            ),
845            Err(e) => fail(FailCause::Unknown, format!("subgraph: {e}")),
846        }
847    }
848
849    /// The shared drive loop. One clock reading per iteration; journal hits
850    /// replay without executing; everything else goes to the pool.
851    #[allow(clippy::too_many_arguments)]
852    fn drive(
853        &self,
854        plan: &PlanGraph,
855        plan_hash: &Hash,
856        manifest: &RunManifest,
857        st: SchedulerState,
858        view: JournalView,
859        initial_events: Vec<EventIn>,
860        opts: &RunOptions,
861    ) -> Result<RunSession, RunError> {
862        let executors = manifest.executors();
863        let arg_schemas = self.load_arg_schemas(manifest)?;
864        let validate_args = make_validate_args(&arg_schemas);
865        let reduce = crate::reducers::make_reduce(&manifest.reducers);
866        let env = StepEnv {
867            plan,
868            executors: &executors,
869            budgets: manifest.budgets.to_budgets(),
870            reduce: &reduce,
871            eval_cond: &builtin_eval,
872            ask_ttl_sec: manifest.ask_ttl_sec,
873            validate_args: &validate_args,
874            llm_reserve_tokens: manifest.llm_reserve_tokens(),
875            max_effects_per_attempt: 16,
876        };
877        let run_id = st.run_id.clone();
878        // §6.10: the event bus wraps the observer; `emit` never blocks.
879        // Arc'd because LLM jobs carry a TokenChunk sink into the pool.
880        let bus = self
881            .observer
882            .as_ref()
883            .map(|o| {
884                Arc::new(crate::stream::EventBus::new(
885                    Arc::clone(o),
886                    crate::stream::EVENT_BUFFER,
887                ))
888            });
889        let emit = |ev: crate::stream::RunEvent| {
890            if let Some(b) = &bus {
891                b.emit(ev);
892            }
893        };
894        emit(if view.checkpoints.is_empty() {
895            crate::stream::RunEvent::RunStarted { run_id: run_id.clone() }
896        } else {
897            crate::stream::RunEvent::RunResumed { run_id: run_id.clone() }
898        });
899        let pool = Pool::new(
900            Arc::clone(&self.executor),
901            self.llm.clone(),
902            opts.workers.max(1),
903        );
904        let mut intents: BTreeMap<JournalKey, Hash> =
905            view.entries.iter().map(|(k, e)| (k.clone(), e.intent)).collect();
906        let mut results: BTreeMap<JournalKey, EffectOutcome> = view
907            .entries
908            .iter()
909            .filter_map(|(k, e)| e.result.as_ref().map(|(_, o)| (k.clone(), o.clone())))
910            .collect();
911        let mut prev_ckpt: Option<Hash> = view.checkpoints.last().map(|c| c.hash);
912        let mut in_flight: usize = 0;
913        let mut st = st;
914        let mut events = initial_events;
915        let mut intents_written: u32 = 0;
916        let mut results_seen: u32 = 0;
917
918        // Crash-window danglings (§5.3 step 4): fail fast when the host
919        // cannot honor idempotency keys; otherwise the re-emitted intents
920        // below adopt these and re-dispatch under the same key.
921        if opts.on_dangling == OnDangling::Fail {
922            if let Some(last) = view.checkpoints.last() {
923                if let Some(e) = view
924                    .dangling(last.superstep)
925                    .into_iter()
926                    .find(|e| !st.pending_asks.values().any(|p| p.key == e.key))
927                {
928                    return Err(RunError::DanglingIntent {
929                        key: e.key.tool_call_id(),
930                    });
931                }
932            }
933        }
934
935        loop {
936            // The kill switch: polled every iteration (a µs point read).
937            if st.cancel.is_none() {
938                if let Some((by, reason)) = self.cancel_marker(&run_id) {
939                    // `step()`'s contract: any call that may CLOSE a
940                    // superstep carries a fresh ClockReading (doc comment on
941                    // `step()`). A freshly-seen cancel closes the run's
942                    // FINAL superstep, but without a reading taken HERE
943                    // `st.clock_ms` stays whatever an earlier, already-
944                    // completed wave last set it to — which can predate the
945                    // cancel Fact's own `created_at`, journaling a
946                    // clock_close_ms that appears to close before the
947                    // operator asked to cancel. `cancel_marker` only
948                    // returns once its read has observed the write `cancel`
949                    // performed, so a reading taken now is monotone at or
950                    // after it. `verify()`'s replay already assumes this
951                    // shape at both its own cancel_peek sites (a
952                    // ClockReading immediately preceding CancelSeen in the
953                    // same batch) — this is the live counterpart.
954                    events.push(EventIn::ClockReading { unix_ms: self.clock.now_ms() });
955                    events.push(EventIn::CancelSeen { principal: by, reason });
956                }
957            }
958
959            let out = step(&env, st, &events);
960            st = out.state;
961            events = Vec::new();
962
963            let mut parked_envelope: Option<Value> = None;
964            let mut finished: Option<RunOutcome> = None;
965            // This iteration's dispatch wave, in COMMAND ORDER: every
966            // Dispatch lands here — journal-answered replays as
967            // Some(outcome), fresh pool submissions as None — and is fed
968            // back to step() as one batch at the wave boundary below.
969            let mut wave: Vec<(JournalKey, Option<EffectOutcome>)> = Vec::new();
970
971            for cmd in out.commands {
972                match cmd {
973                    Command::WriteIntent { key, executor, input, superstep, clock_ms } => {
974                        if let Some(existing) = intents.get(&key).copied() {
975                            // Adopted: a crash re-opened this superstep. A
976                            // REDELIVERY Observation is recorded only when
977                            // the effect will actually redeliver (no result
978                            // journaled) — a journal-answered replay is not
979                            // a redelivery and must not fabricate one.
980                            if results.contains_key(&key) {
981                                continue;
982                            }
983                            let mut obs = Observation::new(&self.principal, "system")
984                                .subject(&format!("run:{run_id}"))
985                                .object(&format!(
986                                    "redelivery of intent {} for {}",
987                                    existing.to_hex(),
988                                    key.tool_call_id()
989                                ))
990                                .namespace(HARNESS_NS)
991                                .created_at(clock_ms as i64);
992                            obs.common
993                                .extra_fields
994                                .insert("run_id".into(), json!(run_id));
995                            self.facade
996                                .with_store(|m| m.add_if_novel(&obs))
997                                .map_err(err_run)?;
998                            continue;
999                        }
1000                        let h = self
1001                            .facade
1002                            .with_store(|m| {
1003                                journal::write_intent(
1004                                    m,
1005                                    &self.ns,
1006                                    &run_id,
1007                                    plan_hash,
1008                                    &key,
1009                                    &executor,
1010                                    &input,
1011                                    superstep,
1012                                    clock_ms,
1013                                    &self.principal,
1014                                )
1015                            })
1016                            .map_err(err_run)?;
1017                        emit(crate::stream::RunEvent::NodeDispatched {
1018                            superstep,
1019                            node: key.node.clone(),
1020                            task_path: key.task_path.clone(),
1021                            attempt: key.attempt,
1022                            effect_seq: key.effect_seq,
1023                        });
1024                        intents.insert(key, h);
1025                        intents_written += 1;
1026                        if opts.inject_crash == Some(CrashPoint::AfterIntent(intents_written)) {
1027                            return Err(RunError::Storage {
1028                                detail: "injected crash after intent write".into(),
1029                            });
1030                        }
1031                    }
1032                    Command::Dispatch { key, executor, input } => {
1033                        if let Some(outcome) = results.get(&key) {
1034                            // Replay: the journal answers; nothing executes.
1035                            // Resolved WITH the wave (not immediately), so a
1036                            // mixed replay+fresh superstep feeds one batch in
1037                            // dispatch order — the cadence verify replays.
1038                            wave.push((key, Some(outcome.clone())));
1039                            continue;
1040                        }
1041                        // Subgraphs run INLINE on the driver thread — the
1042                        // child needs the store, which pool workers never
1043                        // touch. Parallel subgraph siblings therefore
1044                        // serialize (documented v1 bound).
1045                        if let areev_run_core::NodeExecutor::Subgraph { workflow_hash } =
1046                            &executor
1047                        {
1048                            let outcome = self.run_subgraph_effect(
1049                                &run_id,
1050                                &key,
1051                                workflow_hash,
1052                                &input,
1053                                opts,
1054                            );
1055                            let intent = intents.get(&key).copied().ok_or_else(|| {
1056                                RunError::Storage {
1057                                    detail: "subgraph dispatch without intent".into(),
1058                                }
1059                            })?;
1060                            let now = self.clock.now_ms();
1061                            self.facade
1062                                .with_store(|m| {
1063                                    journal::write_result(
1064                                        m,
1065                                        &self.ns,
1066                                        &run_id,
1067                                        plan_hash,
1068                                        &intent,
1069                                        &key,
1070                                        &executor,
1071                                        &outcome,
1072                                        st.superstep,
1073                                        now,
1074                                        &self.principal,
1075                                    )
1076                                })
1077                                .map_err(err_run)?;
1078                            emit(crate::stream::RunEvent::EffectSettled {
1079                                superstep: st.superstep,
1080                                node: key.node.clone(),
1081                                task_path: key.task_path.clone(),
1082                                attempt: key.attempt,
1083                                effect_seq: key.effect_seq,
1084                                ok: matches!(outcome, EffectOutcome::Completed { .. }),
1085                            });
1086                            results.insert(key.clone(), outcome.clone());
1087                            wave.push((key, Some(outcome)));
1088                            continue;
1089                        }
1090                        // LLM effects: prepare the request ON THIS THREAD
1091                        // (transcript translation + pinned Definitions from
1092                        // the store), then hand the pure job to the pool.
1093                        let llm_prepared = if key.kind == areev_run_core::EffectKind::Llm {
1094                            let areev_run_core::NodeExecutor::Abstract { tools } = &executor
1095                            else {
1096                                return Err(RunError::Storage {
1097                                    detail: "llm effect on a non-abstract node".into(),
1098                                });
1099                            };
1100                            let mut defs = Vec::with_capacity(tools.len());
1101                            for t in tools {
1102                                let h = Hash::from_hex(&t.tool_hash).map_err(err_run)?;
1103                                let def = self
1104                                    .facade
1105                                    .with_store(|m| m.get(&h))
1106                                    .map_err(err_run)?
1107                                    .to_tool()
1108                                    .map_err(err_run)?;
1109                                defs.push(def);
1110                            }
1111                            // §6.10 TokenChunk sink: only when observed —
1112                            // otherwise the plain call (no stream overhead).
1113                            let on_token = bus.as_ref().map(|b| {
1114                                let b = Arc::clone(b);
1115                                let node = key.node.clone();
1116                                let task_path = key.task_path.clone();
1117                                let attempt = key.attempt;
1118                                Box::new(move |t: &str| {
1119                                    b.emit(crate::stream::RunEvent::TokenChunk {
1120                                        node: node.clone(),
1121                                        task_path: task_path.clone(),
1122                                        attempt,
1123                                        text: t.to_string(),
1124                                    });
1125                                }) as crate::executor::TokenSink
1126                            });
1127                            Some(crate::executor::PreparedLlm {
1128                                messages: seam_messages(&input),
1129                                tools: defs,
1130                                max_tokens: manifest.llm_max_tokens.unwrap_or(1024),
1131                                on_token,
1132                            })
1133                        } else {
1134                            None
1135                        };
1136                        let idem = idempotency_key(&key, &input);
1137                        wave.push((key.clone(), None));
1138                        pool.submit(DispatchJob {
1139                            key,
1140                            executor,
1141                            input,
1142                            idempotency_key: idem,
1143                            llm: llm_prepared,
1144                        });
1145                        in_flight += 1;
1146                    }
1147                    Command::EmitEnvelope { asks } => {
1148                        for ask in &asks {
1149                            emit(crate::stream::RunEvent::AskRaised {
1150                                superstep: st.superstep,
1151                                node: ask.node.clone(),
1152                                tool_call_id: ask.tool_call_id.clone(),
1153                            });
1154                        }
1155                        parked_envelope = Some(json!({
1156                            "kind": "requires_action",
1157                            "run_id": run_id,
1158                            "checkpoint": prev_ckpt.map(|h| h.to_hex()),
1159                            "asks": asks,
1160                        }));
1161                    }
1162                    Command::WriteCheckpoint { decision_record, state_json, .. } => {
1163                        let clock = decision_record
1164                            .clock_close_ms
1165                            .max(decision_record.clock_open_ms);
1166                        let superstep = decision_record.superstep;
1167                        let h = self
1168                            .facade
1169                            .with_store(|m| {
1170                                journal::write_checkpoint(
1171                                    m,
1172                                    &self.ns,
1173                                    &run_id,
1174                                    &state_json,
1175                                    &decision_record,
1176                                    clock,
1177                                    prev_ckpt.as_ref(),
1178                                    &self.principal,
1179                                )
1180                            })
1181                            .map_err(err_run)?;
1182                        emit(crate::stream::RunEvent::CheckpointWritten {
1183                            superstep,
1184                            hash: h.to_hex(),
1185                        });
1186                        prev_ckpt = Some(h);
1187                    }
1188                    Command::Finish { outcome } => {
1189                        finished = Some(outcome);
1190                    }
1191                }
1192            }
1193
1194            if let Some(outcome) = finished {
1195                // Terminal checkpoint: the Finished state itself.
1196                let state_json = serde_json::to_value(&st).unwrap_or(Value::Null);
1197                let record = DecisionRecord {
1198                    superstep: st.superstep,
1199                    clock_open_ms: st.clock_ms,
1200                    clock_close_ms: st.clock_ms,
1201                    ..Default::default()
1202                };
1203                self.facade
1204                    .with_store(|m| {
1205                        journal::write_checkpoint(
1206                            m,
1207                            &self.ns,
1208                            &run_id,
1209                            &state_json,
1210                            &record,
1211                            st.clock_ms,
1212                            prev_ckpt.as_ref(),
1213                            &self.principal,
1214                        )
1215                    })
1216                    .map_err(err_run)?;
1217                // The run-outcome record (§8 Wave 4): one compact
1218                // Observation per terminal run — outcome + the spent
1219                // figures — the loop's run_outcome analyzer's food and the
1220                // cost-attribution source. Journaled AFTER the terminal
1221                // checkpoint; replay never sees it (it is not a journal
1222                // entry), so verify is unaffected.
1223                let outcome_label = match &outcome {
1224                    RunOutcome::Completed => "completed".to_string(),
1225                    RunOutcome::Stalled { .. } => "stalled".to_string(),
1226                    RunOutcome::Failed { .. } => "failed".to_string(),
1227                    RunOutcome::Canceled { .. } => "canceled".to_string(),
1228                    RunOutcome::BudgetExhausted { axis } => {
1229                        format!("budget_exhausted:{}", axis.as_str())
1230                    }
1231                };
1232                let mut obs = Observation::new(&self.principal, "system")
1233                    .subject(&format!("run:{run_id}"))
1234                    .object(&outcome_label)
1235                    .namespace(HARNESS_NS)
1236                    .created_at(st.clock_ms as i64);
1237                let ex = &mut obs.common.extra_fields;
1238                ex.insert("run_id".into(), json!(run_id));
1239                ex.insert("observation_kind".into(), json!("run_outcome"));
1240                ex.insert("plan_hash".into(), json!(plan_hash.to_hex()));
1241                ex.insert("spent_input_tokens".into(), json!(st.spent.input_tokens));
1242                ex.insert("spent_output_tokens".into(), json!(st.spent.output_tokens));
1243                ex.insert("spent_usd_micros".into(), json!(st.spent.usd_micros));
1244                ex.insert("spent_wall_ms".into(), json!(st.spent.wall_ms));
1245                ex.insert("spent_supersteps".into(), json!(st.spent.supersteps));
1246                if let Some(detail) = match &outcome {
1247                    RunOutcome::Failed { node, detail } => {
1248                        Some(format!("{node}: {detail}"))
1249                    }
1250                    RunOutcome::Stalled { node } => Some(node.clone()),
1251                    _ => None,
1252                } {
1253                    ex.insert("outcome_detail".into(), json!(detail));
1254                }
1255                self.facade.with_store(|m| m.add(&obs)).map_err(err_run)?;
1256                emit(crate::stream::RunEvent::RunFinished {
1257                    run_id: run_id.clone(),
1258                    outcome: format!("{outcome:?}"),
1259                    dropped_events: bus.as_ref().map(|b| b.dropped()).unwrap_or(0),
1260                });
1261                return Ok(RunSession::Finished { outcome, run_id });
1262            }
1263
1264            if !wave.is_empty() {
1265                // The wave boundary: verify replays every superstep as ONE
1266                // close reading followed by its resolutions in dispatch
1267                // order, so the live driver must feed exactly that.
1268                // Trickling completions in arrival batches — each with its
1269                // own clock reading — makes scheduler state depend on
1270                // thread timing: an unjournaled decision that diverges
1271                // replay (and shifts every later scripted-clock value).
1272                let mut completed: BTreeMap<JournalKey, DispatchDone> = BTreeMap::new();
1273                while in_flight > 0 {
1274                    let done = pool
1275                        .done_rx
1276                        .recv()
1277                        .map_err(|_| RunError::Storage { detail: "executor pool died".into() })?;
1278                    in_flight -= 1;
1279                    results_seen += 1;
1280                    if opts.inject_crash == Some(CrashPoint::BeforeResult(results_seen)) {
1281                        // The effect FIRED (the executor ran); its result
1282                        // dies with this "process". The §5.3 redelivery
1283                        // contract owns what happens next.
1284                        return Err(RunError::Storage {
1285                            detail: "injected crash before result write".into(),
1286                        });
1287                    }
1288                    completed.insert(done.key.clone(), done);
1289                }
1290                let now = self.clock.now_ms();
1291                events.push(EventIn::ClockReading { unix_ms: now });
1292                for (key, replayed) in wave {
1293                    let outcome = match replayed {
1294                        Some(outcome) => outcome,
1295                        None => {
1296                            let done = completed.remove(&key).ok_or_else(|| {
1297                                RunError::Storage {
1298                                    detail: "completion missing for a dispatched effect".into(),
1299                                }
1300                            })?;
1301                            let intent = intents.get(&key).copied().ok_or_else(|| {
1302                                RunError::Storage { detail: "completion without intent".into() }
1303                            })?;
1304                            // Result durability precedes bookkeeping. The
1305                            // executor is the one the job DISPATCHED under
1306                            // (a flow tool runs as Host inside an Abstract
1307                            // node) — the result grain re-states what ran,
1308                            // matching its intent.
1309                            self.facade
1310                                .with_store(|m| {
1311                                    journal::write_result(
1312                                        m,
1313                                        &self.ns,
1314                                        &run_id,
1315                                        plan_hash,
1316                                        &intent,
1317                                        &key,
1318                                        &done.executor,
1319                                        &done.outcome,
1320                                        st.superstep,
1321                                        now,
1322                                        &self.principal,
1323                                    )
1324                                })
1325                                .map_err(err_run)?;
1326                            emit(crate::stream::RunEvent::EffectSettled {
1327                                superstep: st.superstep,
1328                                node: key.node.clone(),
1329                                task_path: key.task_path.clone(),
1330                                attempt: key.attempt,
1331                                effect_seq: key.effect_seq,
1332                                ok: matches!(done.outcome, EffectOutcome::Completed { .. }),
1333                            });
1334                            results.insert(key.clone(), done.outcome.clone());
1335                            done.outcome
1336                        }
1337                    };
1338                    events.push(EventIn::EffectResolved { key, outcome });
1339                }
1340                continue;
1341            }
1342
1343            if let Some(envelope) = parked_envelope {
1344                // in_flight is 0 here: every dispatch drains with its wave.
1345                return Ok(RunSession::Parked { envelope, run_id });
1346            }
1347
1348            // Parked with the envelope announced in an earlier session.
1349            if !st.pending_asks.is_empty() {
1350                return Ok(RunSession::Parked {
1351                    envelope: json!({
1352                        "kind": "requires_action",
1353                        "run_id": run_id,
1354                        "checkpoint": prev_ckpt.map(|h| h.to_hex()),
1355                        "asks": st
1356                            .pending_asks
1357                            .values()
1358                            .map(|p| p.ask.clone())
1359                            .collect::<Vec<_>>(),
1360                    }),
1361                    run_id,
1362                });
1363            }
1364            return Err(RunError::Storage {
1365                detail: "driver made no progress with no work in flight".into(),
1366            });
1367        }
1368    }
1369
1370    /// Shadow evaluation (§8 Wave 4): journal-consistent replay of the given
1371    /// runs, writing nothing and dispatching nothing — the verify path holds
1372    /// no executor, no pool, and no model, so "zero effect dispatches" is a
1373    /// property of the code shape, and the report states it explicitly for
1374    /// the §7.4 pre-apply gate. A run that fails to load is reported
1375    /// inconsistent, never skipped silently.
1376    pub fn shadow_eval(&self, run_ids: &[String]) -> crate::ShadowReport {
1377        let mut report = crate::ShadowReport::default();
1378        for run_id in run_ids {
1379            let row = match self.verify(run_id) {
1380                Ok(v) => {
1381                    let effects = self
1382                        .facade
1383                        .with_store(|m| journal::load(m, &self.ns, run_id))
1384                        .map(|view| {
1385                            view.entries.values().filter(|e| e.result.is_some()).count() as u64
1386                        })
1387                        .unwrap_or(0);
1388                    crate::ShadowRun {
1389                        run_id: run_id.clone(),
1390                        consistent: v.verified,
1391                        effects_replayed: effects,
1392                        steps: v.steps,
1393                    }
1394                }
1395                Err(e) => crate::ShadowRun {
1396                    run_id: run_id.clone(),
1397                    consistent: false,
1398                    effects_replayed: 0,
1399                    steps: vec![crate::VerifyStep {
1400                        superstep: 0,
1401                        verdict: format!("shadow load failed: {e}"),
1402                        ok: false,
1403                    }],
1404                },
1405            };
1406            report.runs.push(row);
1407        }
1408        report.all_consistent =
1409            !report.runs.is_empty() && report.runs.iter().all(|r| r.consistent);
1410        report.effect_dispatches = 0;
1411        report
1412    }
1413
1414    /// The `run_id`s of the most recent runs in this namespace, newest
1415    /// first — manifest link Facts in `agent:harness` are the census.
1416    pub fn recent_runs(&self, limit: usize) -> Result<Vec<String>, RunError> {
1417        let mut out: Vec<(i64, String)> = self
1418            .facade
1419            .with_store(|m| {
1420                m.recent(
1421                    HARNESS_NS,
1422                    Some(areev_core::types::GrainType::Fact),
1423                    limit.saturating_mul(8).max(64),
1424                )
1425            })
1426            .map_err(err_run)?
1427            .into_iter()
1428            .filter(|g| g.get_str("relation") == Some("mg:harness"))
1429            .filter_map(|g| {
1430                let id = g.get_str("run_id")?.to_string();
1431                Some((g.get_i64("created_at").unwrap_or(0), id))
1432            })
1433            .collect();
1434        out.sort_by_key(|(at, _)| std::cmp::Reverse(*at));
1435        out.dedup_by(|a, b| a.1 == b.1);
1436        Ok(out.into_iter().map(|(_, id)| id).take(limit).collect())
1437    }
1438
1439    /// §5.5 gate 1 / verify tier 1: **journal-consistent replay**. Re-drives
1440    /// the run from the manifest's input with the clock scripted from
1441    /// journaled readings and every effect answered from the journal —
1442    /// writing NOTHING — and byte-compares every checkpoint the replay
1443    /// commands against the stored chain. A missing journaled result stops
1444    /// verification at that step (reported, not asserted); a divergent
1445    /// checkpoint is the `RUN-E009` condition, reported with both hashes.
1446    pub fn verify(&self, run_id: &str) -> Result<VerifyReport, RunError> {
1447        let manifest = self.load_manifest(run_id)?;
1448        let plan_hash = Hash::from_hex(&manifest.plan_hash)
1449            .map_err(|_| RunError::ManifestMismatch { why: "bad plan hash".into() })?;
1450        let plan = self.load_plan(&plan_hash)?;
1451        let view = self
1452            .facade
1453            .with_store(|m| journal::load(m, &self.ns, run_id))
1454            .map_err(err_run)?;
1455        let executors = manifest.executors();
1456        let arg_schemas = self.load_arg_schemas(&manifest)?;
1457        let validate_args = make_validate_args(&arg_schemas);
1458        let reduce = crate::reducers::make_reduce(&manifest.reducers);
1459        let env = StepEnv {
1460            plan: &plan,
1461            executors: &executors,
1462            budgets: manifest.budgets.to_budgets(),
1463            reduce: &reduce,
1464            eval_cond: &builtin_eval,
1465            ask_ttl_sec: manifest.ask_ttl_sec,
1466            validate_args: &validate_args,
1467            llm_reserve_tokens: manifest.llm_reserve_tokens(),
1468            max_effects_per_attempt: 16,
1469        };
1470
1471        let mut report = VerifyReport::default();
1472        if view.checkpoints.is_empty() {
1473            report.steps.push(VerifyStep {
1474                superstep: 0,
1475                verdict: "no checkpoints — nothing to verify".into(),
1476                ok: false,
1477            });
1478            return Ok(report);
1479        }
1480
1481        // A fork replays its TAIL: the seed checkpoint is trusted through
1482        // the §5.4 base-checkpoint hash chain (it `derived_from`s the base),
1483        // and replay verifies everything the fork itself executed.
1484        let (mut st, mut ckpt_idx, mut events);
1485        if manifest.fork_of.is_some() {
1486            st = serde_json::from_value::<SchedulerState>(view.checkpoints[0].scheduler.clone())
1487                .map_err(|e| RunError::ManifestMismatch {
1488                    why: format!("fork seed checkpoint: {e}"),
1489                })?;
1490            ckpt_idx = 1;
1491            if view.checkpoints.len() == 1 {
1492                report.steps.push(VerifyStep {
1493                    superstep: st.superstep,
1494                    verdict: format!(
1495                        "fork seed checkpoint {} chains to base {} — the fork has \
1496                         not executed yet",
1497                        view.checkpoints[0].hash.to_hex(),
1498                        manifest.fork_of.as_ref().map(|f| f.base_checkpoint.as_str()).unwrap_or("?"),
1499                    ),
1500                    ok: true,
1501                });
1502                report.verified = true;
1503                return Ok(report);
1504            }
1505            events = vec![EventIn::ClockReading {
1506                unix_ms: view.checkpoints[1].decisions.clock_open_ms,
1507            }];
1508        } else {
1509            st = SchedulerState::new(run_id, &plan);
1510            ckpt_idx = 0;
1511            // The first reading is the first superstep's journaled open.
1512            events = vec![
1513                EventIn::ClockReading {
1514                    unix_ms: view.checkpoints[0].decisions.clock_open_ms,
1515                },
1516                EventIn::Start { input: manifest.input.clone() },
1517            ];
1518        }
1519        // Cancel is an OUT-OF-BAND marker (drive polls the Fact; the
1520        // journal has no CancelSeen entry) — replay it from where it IS
1521        // journaled: the checkpoint state. WHERE inside the superstep the
1522        // live driver saw it is read off the journal's own evidence: the
1523        // driver polls at wave boundaries, so a checkpoint that carries
1524        // both this superstep's resolutions and the cancel means the
1525        // marker rode the SAME event batch as the resolutions; a step
1526        // whose dispatches the journal never answered, while the coming
1527        // checkpoint carries the cancel, means the live driver canceled
1528        // BEFORE dispatching — that boundary is rewound and re-fed with
1529        // CancelSeen up front. Feeding it blindly at the superstep's open
1530        // (the old rule) replayed a phase the live driver can only
1531        // produce when the marker predates the run.
1532        let cancel_peek = |ckpt_idx: usize, st: &SchedulerState| -> Option<EventIn> {
1533            if st.cancel.is_some() {
1534                return None;
1535            }
1536            let c = view.checkpoints.get(ckpt_idx)?.scheduler.get("cancel")?.as_array()?;
1537            Some(EventIn::CancelSeen {
1538                principal: c.first()?.as_str()?.to_string(),
1539                reason: c.get(1)?.as_str()?.to_string(),
1540            })
1541        };
1542        let mut guard = 0;
1543        'replay: loop {
1544            guard += 1;
1545            if guard > 100_000 {
1546                report.steps.push(VerifyStep {
1547                    superstep: st.superstep,
1548                    verdict: "replay did not terminate".into(),
1549                    ok: false,
1550                });
1551                return Ok(report);
1552            }
1553            let pre_state = st.clone();
1554            let pre_events = events.clone();
1555            let pre_ckpt_idx = ckpt_idx;
1556            let pre_report_len = report.steps.len();
1557            let out = step(&env, st, &events);
1558            st = out.state;
1559            events = Vec::new();
1560            let mut resolved: Vec<EventIn> = Vec::new();
1561            let mut parked = false;
1562            let mut unanswered: Option<JournalKey> = None;
1563            for cmd in out.commands {
1564                match cmd {
1565                    Command::WriteIntent { .. } => {}
1566                    Command::Dispatch { key, .. } => {
1567                        match view.entries.get(&key).and_then(|e| {
1568                            e.result.as_ref().map(|(_, o)| (o.clone(), e.result_clock_ms))
1569                        }) {
1570                            Some((outcome, _clock)) => {
1571                                resolved.push(EventIn::EffectResolved { key, outcome });
1572                            }
1573                            None => {
1574                                unanswered = Some(key);
1575                                break;
1576                            }
1577                        }
1578                    }
1579                    Command::EmitEnvelope { .. } => {
1580                        parked = true;
1581                    }
1582                    Command::WriteCheckpoint { superstep, state_json, .. } => {
1583                        let Some(stored) = view.checkpoints.get(ckpt_idx) else {
1584                            report.steps.push(VerifyStep {
1585                                superstep,
1586                                verdict: "replay produced a checkpoint the journal lacks"
1587                                    .into(),
1588                                ok: false,
1589                            });
1590                            break 'replay;
1591                        };
1592                        ckpt_idx += 1;
1593                        let ok = stored.superstep == superstep
1594                            && stored.scheduler == state_json;
1595                        report.steps.push(VerifyStep {
1596                            superstep,
1597                            verdict: if ok {
1598                                format!(
1599                                    "checkpoint {} journal-consistent",
1600                                    stored.hash.to_hex()
1601                                )
1602                            } else {
1603                                format!(
1604                                    "checkpoint {} DIVERGES from replay (RUN-E009): {}",
1605                                    stored.hash.to_hex(),
1606                                    diff_fields(&stored.scheduler, &state_json)
1607                                )
1608                            },
1609                            ok,
1610                        });
1611                        if !ok {
1612                            break 'replay;
1613                        }
1614                    }
1615                    Command::Finish { .. } => {}
1616                }
1617            }
1618            if let Some(key) = unanswered {
1619                if let Some(cancel_ev) = cancel_peek(pre_ckpt_idx, &pre_state) {
1620                    // The live driver canceled at this boundary BEFORE
1621                    // dispatching (the journal never answered these
1622                    // effects, and the coming checkpoint carries the
1623                    // cancel): rewind and re-feed the same boundary with
1624                    // CancelSeen — the batch the live driver actually saw.
1625                    st = pre_state;
1626                    ckpt_idx = pre_ckpt_idx;
1627                    report.steps.truncate(pre_report_len);
1628                    events = pre_events;
1629                    events.push(cancel_ev);
1630                    continue;
1631                }
1632                report.steps.push(VerifyStep {
1633                    superstep: st.superstep,
1634                    verdict: format!(
1635                        "effect {} has no journaled result — not \
1636                         verifiable past this point",
1637                        key.tool_call_id()
1638                    ),
1639                    ok: false,
1640                });
1641                break 'replay;
1642            }
1643            if st.is_terminal() {
1644                // The terminal checkpoint (written by drive on Finish).
1645                if let Some(stored) = view.checkpoints.get(ckpt_idx) {
1646                    let replay_json = serde_json::to_value(&st).unwrap_or(Value::Null);
1647                    let ok = stored.scheduler == replay_json;
1648                    report.steps.push(VerifyStep {
1649                        superstep: st.superstep,
1650                        verdict: if ok {
1651                            format!(
1652                                "terminal checkpoint {} journal-consistent",
1653                                stored.hash.to_hex()
1654                            )
1655                        } else {
1656                            format!(
1657                                "terminal checkpoint {} DIVERGES from replay (RUN-E009)",
1658                                stored.hash.to_hex()
1659                            )
1660                        },
1661                        ok,
1662                    });
1663                }
1664                break 'replay;
1665            }
1666            if !resolved.is_empty() {
1667                // The close-time reading: this superstep's journaled close.
1668                let close = view
1669                    .checkpoints
1670                    .get(ckpt_idx)
1671                    .map(|c| c.decisions.clock_close_ms)
1672                    .unwrap_or(st.clock_ms);
1673                events.push(EventIn::ClockReading { unix_ms: close });
1674                events.extend(resolved);
1675                // The checkpoint these resolutions close may also carry
1676                // the cancel: the live driver polls the marker at wave
1677                // boundaries, so it saw it WITH this batch — CancelSeen
1678                // rides the same feed, never a boundary of its own.
1679                if let Some(cancel_ev) = cancel_peek(ckpt_idx, &st) {
1680                    events.push(cancel_ev);
1681                }
1682                continue;
1683            }
1684            if parked {
1685                // Settle parked asks from the journal. The reading fed here
1686                // is the NEXT stored checkpoint's journaled close — the
1687                // scheduler accrues park-elapsed at close from that reading
1688                // (never from response-apply time, which is not journaled),
1689                // so this is exactly what makes the replayed state byte-equal.
1690                let mut settled_any = false;
1691                let close = view
1692                    .checkpoints
1693                    .get(ckpt_idx)
1694                    .map(|c| c.decisions.clock_close_ms)
1695                    .unwrap_or(st.clock_ms);
1696                for (id, pending) in st.pending_asks.clone() {
1697                    if let Some(entry) = view.entries.get(&pending.key) {
1698                        if let Some((_, outcome)) = &entry.result {
1699                            if !settled_any {
1700                                events.push(EventIn::ClockReading { unix_ms: close });
1701                            }
1702                            events.push(EventIn::ResponseSettled {
1703                                tool_call_id: id,
1704                                outcome: outcome.clone(),
1705                            });
1706                            settled_any = true;
1707                        }
1708                    }
1709                }
1710                if settled_any {
1711                    continue;
1712                }
1713                // A cancel that arrived while parked: the next checkpoint
1714                // carries it — feed it (with its journaled close) so the
1715                // drain and terminal checkpoints verify too, instead of
1716                // breaking here and silently skipping them.
1717                let cancel_next = view.checkpoints.get(ckpt_idx).and_then(|c| {
1718                    c.scheduler.get("cancel").and_then(|v| v.as_array()).and_then(|a| {
1719                        Some((
1720                            a.first()?.as_str()?.to_string(),
1721                            a.get(1)?.as_str()?.to_string(),
1722                        ))
1723                    })
1724                });
1725                if st.cancel.is_none() {
1726                    if let Some((by, reason)) = cancel_next {
1727                        events.push(EventIn::ClockReading { unix_ms: close });
1728                        events.push(EventIn::CancelSeen { principal: by, reason });
1729                        continue;
1730                    }
1731                }
1732                report.steps.push(VerifyStep {
1733                    superstep: st.superstep,
1734                    verdict: if ckpt_idx < view.checkpoints.len() {
1735                        format!(
1736                            "run is parked awaiting a response — verified up to                              the park; {} trailing checkpoint(s) NOT verified",
1737                            view.checkpoints.len() - ckpt_idx
1738                        )
1739                    } else {
1740                        "run is parked awaiting a response — verified up to the park".into()
1741                    },
1742                    ok: ckpt_idx >= view.checkpoints.len(),
1743                });
1744                break 'replay;
1745            }
1746            // No dispatches, not parked, not terminal: feed the next open
1747            // reading (a between-supersteps boundary).
1748            if let Some(next) = view.checkpoints.get(ckpt_idx) {
1749                events.push(EventIn::ClockReading {
1750                    unix_ms: next.decisions.clock_open_ms,
1751                });
1752                continue;
1753            }
1754            break 'replay;
1755        }
1756        report.verified = !report.steps.is_empty() && report.steps.iter().all(|s| s.ok);
1757        Ok(report)
1758    }
1759}