1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum OnDangling {
30 #[default]
31 Redispatch,
32 Fail,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum CrashPoint {
41 AfterIntent(u32),
44 BeforeResult(u32),
48}
49
50#[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 pub llm_max_tokens: Option<u32>,
60 pub inject_crash: Option<CrashPoint>,
62}
63
64pub struct Runner {
66 pub facade: Arc<AreevFacade>,
67 pub clock: Arc<dyn Clock>,
68 pub executor: Arc<dyn HostToolExecutor>,
69 pub llm: Option<Arc<dyn areev_llm::ToolCallLlm>>,
72 pub observer: Option<Arc<dyn crate::stream::RunObserver>>,
75 pub ns: String,
76 pub principal: String,
77}
78
79fn 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
144fn 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
164fn 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 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 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 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 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 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 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 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 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 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 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 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 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 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 st.run_id = new_run_id.to_string();
553 RunManifest {
554 run_id: new_run_id.to_string(),
555 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 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 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 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 let now = self.clock.now_ms();
641
642 let journal_rejection = |why: &str| {
643 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 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 pub fn cancel(&self, run_id: &str, principal: &str, reason: &str) -> Result<(), RunError> {
738 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 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 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 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 #[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 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 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 if st.cancel.is_none() {
938 if let Some((by, reason)) = self.cancel_marker(&run_id) {
939 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 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 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 wave.push((key, Some(outcome.clone())));
1039 continue;
1040 }
1041 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 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 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 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 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 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 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 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 return Ok(RunSession::Parked { envelope, run_id });
1346 }
1347
1348 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 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 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 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 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 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 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 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 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 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 if let Some(cancel_ev) = cancel_peek(ckpt_idx, &st) {
1680 events.push(cancel_ev);
1681 }
1682 continue;
1683 }
1684 if parked {
1685 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 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 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}