Skip to main content

deepstrike_core/runtime/kernel/
runtime.rs

1use super::*;
2use std::collections::{HashMap, HashSet, VecDeque};
3use std::ops::{Deref, DerefMut};
4
5pub(super) const EVENT_REPLAY_WINDOW_CAPACITY: usize = 256;
6const COMPLETED_EFFECT_REPLAY_WINDOW_CAPACITY: usize = 256;
7const SNAPSHOT_INPUT_LIMIT: usize = 10_000;
8const MAX_INPUT_BYTES: usize = 16 * 1024 * 1024;
9const SNAPSHOT_JOURNAL_BYTES_LIMIT: usize = 64 * 1024 * 1024;
10
11#[derive(Clone)]
12struct RecordedTransition {
13    fingerprint: Vec<u8>,
14    step: KernelStep,
15}
16
17#[derive(serde::Serialize)]
18struct KernelSnapshotRefV2<'a> {
19    snapshot_version: u32,
20    abi_version: u32,
21    initial_policy: KernelSnapshotPolicy,
22    lifecycle: KernelLifecycle,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    operation_id: Option<&'a str>,
25    next_step_seq: u64,
26    snapshot_input_limit: usize,
27    max_input_bytes: usize,
28    snapshot_journal_bytes_limit: usize,
29    accepted_input_bytes: usize,
30    accepted_inputs: &'a [KernelInput],
31    #[serde(skip_serializing_if = "Option::is_none")]
32    last_step: Option<&'a KernelStep>,
33}
34
35#[derive(Clone)]
36struct AcceptedCancellation {
37    reason: CancellationReason,
38    pending_call_ids: Vec<String>,
39    step: KernelStep,
40}
41
42struct ReplayWindow {
43    entries: HashMap<String, RecordedTransition>,
44    order: VecDeque<String>,
45    capacity: usize,
46}
47
48impl ReplayWindow {
49    fn new(capacity: usize) -> Self {
50        Self {
51            entries: HashMap::with_capacity(capacity),
52            order: VecDeque::with_capacity(capacity),
53            capacity,
54        }
55    }
56
57    fn get(&self, identity: &str) -> Option<&RecordedTransition> {
58        self.entries.get(identity)
59    }
60
61    fn insert(&mut self, identity: String, transition: RecordedTransition) {
62        if self.entries.len() == self.capacity {
63            if let Some(expired_identity) = self.order.pop_front() {
64                self.entries.remove(&expired_identity);
65            }
66        }
67        self.order.push_back(identity.clone());
68        self.entries.insert(identity, transition);
69    }
70
71    fn set_capacity(&mut self, capacity: usize) {
72        self.capacity = capacity;
73        while self.entries.len() > capacity {
74            if let Some(expired_identity) = self.order.pop_front() {
75                self.entries.remove(&expired_identity);
76            }
77        }
78        self.entries.shrink_to(capacity);
79        self.order.shrink_to(capacity);
80    }
81
82    #[cfg(test)]
83    fn len(&self) -> usize {
84        self.entries.len()
85    }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89enum PendingEffectKind {
90    Provider,
91    Tool,
92    Milestone,
93    Approval,
94    WorkflowSpawn,
95    Preempt,
96    MemoryPersist,
97    MemoryQuery,
98    LargeResultSpool,
99    PageOutArchive,
100}
101
102#[derive(Clone, Copy)]
103enum LifecycleTransition {
104    Stay,
105    Configure,
106    Start,
107    Resume,
108}
109
110#[derive(Clone, Copy, PartialEq, Eq)]
111enum StepMode {
112    Live,
113    SnapshotReplay,
114}
115
116struct StepIdentity {
117    operation_id: String,
118    input_event_id: String,
119    step_seq: u64,
120}
121
122impl StepIdentity {
123    fn empty(&self, observations: Vec<KernelObservation>) -> KernelStep {
124        KernelStep::empty(
125            self.operation_id.clone(),
126            self.input_event_id.clone(),
127            self.step_seq,
128            observations,
129        )
130    }
131
132    fn single(self, action: LoopAction, observations: Vec<KernelObservation>) -> KernelStep {
133        KernelStep::single(
134            self.operation_id,
135            self.input_event_id,
136            self.step_seq,
137            action,
138            observations,
139        )
140    }
141}
142
143#[doc(hidden)]
144pub struct KernelRuntimeState {
145    sm: LoopStateMachine,
146    initial_policy: SchedulerBudget,
147    lifecycle: KernelLifecycle,
148    operation_id: Option<String>,
149    next_step_seq: u64,
150    recorded_events: ReplayWindow,
151    pending_effects: HashMap<String, PendingEffectKind>,
152    completed_effects: ReplayWindow,
153    memory_records: crate::mm::memory::MemoryRecordStore,
154    pending_memory_write: Option<crate::mm::memory::MemoryRecord>,
155    pending_memory_store: Option<crate::mm::memory::MemoryRecordStore>,
156    pending_memory_query: Option<(crate::mm::memory::MemoryQuery, usize)>,
157    budget_usage_reported: bool,
158    accepted_cancellation: Option<AcceptedCancellation>,
159    accepted_inputs: Vec<KernelInput>,
160    accepted_input_count: usize,
161    accepted_input_bytes: usize,
162    snapshot_input_limit: usize,
163    max_input_bytes: usize,
164    snapshot_journal_bytes_limit: usize,
165    snapshot_overflowed: bool,
166    last_step: Option<KernelStep>,
167}
168
169struct PreparedCandidate {
170    token: String,
171    base_generation: u64,
172    state: Box<KernelRuntimeState>,
173    step: KernelStep,
174    accepted_inputs_before: usize,
175}
176
177/// Pure kernel runtime wrapper. SDKs should migrate toward feeding
178/// `KernelInput` values here instead of directly driving `LoopStateMachine`.
179pub struct KernelRuntime {
180    state: Option<Box<KernelRuntimeState>>,
181    prepared: Option<PreparedCandidate>,
182    generation: u64,
183    poisoned: bool,
184    next_prepare_token: u64,
185}
186
187impl Deref for KernelRuntime {
188    type Target = KernelRuntimeState;
189
190    fn deref(&self) -> &Self::Target {
191        self.state
192            .as_deref()
193            .expect("kernel runtime state is staged; commit or abort the prepared transition")
194    }
195}
196
197impl DerefMut for KernelRuntime {
198    fn deref_mut(&mut self) -> &mut Self::Target {
199        self.state
200            .as_deref_mut()
201            .expect("kernel runtime state is staged; commit or abort the prepared transition")
202    }
203}
204
205impl KernelRuntime {
206    pub fn new(policy: SchedulerBudget) -> Self {
207        Self {
208            state: Some(Box::new(KernelRuntimeState {
209                sm: LoopStateMachine::new(policy.clone()),
210                initial_policy: policy,
211                lifecycle: KernelLifecycle::Created,
212                operation_id: None,
213                next_step_seq: 1,
214                recorded_events: ReplayWindow::new(EVENT_REPLAY_WINDOW_CAPACITY),
215                pending_effects: HashMap::new(),
216                completed_effects: ReplayWindow::new(COMPLETED_EFFECT_REPLAY_WINDOW_CAPACITY),
217                memory_records: crate::mm::memory::MemoryRecordStore::default(),
218                pending_memory_write: None,
219                pending_memory_store: None,
220                pending_memory_query: None,
221                budget_usage_reported: false,
222                accepted_cancellation: None,
223                accepted_inputs: Vec::new(),
224                accepted_input_count: 0,
225                accepted_input_bytes: 0,
226                snapshot_input_limit: SNAPSHOT_INPUT_LIMIT,
227                max_input_bytes: MAX_INPUT_BYTES,
228                snapshot_journal_bytes_limit: SNAPSHOT_JOURNAL_BYTES_LIMIT,
229                snapshot_overflowed: false,
230                last_step: None,
231            })),
232            prepared: None,
233            generation: 0,
234            poisoned: false,
235            next_prepare_token: 1,
236        }
237    }
238
239    /// Capture a portable ABI-v2 checkpoint without exposing private scheduler representation.
240    pub fn snapshot(&self) -> Result<KernelSnapshot, KernelFault> {
241        self.ensure_snapshot_available()?;
242        Ok(KernelSnapshot {
243            snapshot_version: KERNEL_SNAPSHOT_VERSION,
244            abi_version: KERNEL_ABI_VERSION,
245            initial_policy: KernelSnapshotPolicy::from(&self.initial_policy),
246            lifecycle: self.lifecycle,
247            operation_id: self.operation_id.clone(),
248            next_step_seq: self.next_step_seq,
249            snapshot_input_limit: self.snapshot_input_limit,
250            max_input_bytes: self.max_input_bytes,
251            snapshot_journal_bytes_limit: self.snapshot_journal_bytes_limit,
252            accepted_input_bytes: self.accepted_input_bytes,
253            accepted_inputs: self.accepted_inputs.clone(),
254            last_step: self.last_step.clone(),
255        })
256    }
257
258    pub fn snapshot_json(&self) -> Result<String, KernelFault> {
259        self.ensure_snapshot_available()?;
260        let snapshot = KernelSnapshotRefV2 {
261            snapshot_version: KERNEL_SNAPSHOT_VERSION,
262            abi_version: KERNEL_ABI_VERSION,
263            initial_policy: KernelSnapshotPolicy::from(&self.initial_policy),
264            lifecycle: self.lifecycle,
265            operation_id: self.operation_id.as_deref(),
266            next_step_seq: self.next_step_seq,
267            snapshot_input_limit: self.snapshot_input_limit,
268            max_input_bytes: self.max_input_bytes,
269            snapshot_journal_bytes_limit: self.snapshot_journal_bytes_limit,
270            accepted_input_bytes: self.accepted_input_bytes,
271            accepted_inputs: &self.accepted_inputs,
272            last_step: self.last_step.as_ref(),
273        };
274        serde_json::to_string(&snapshot).map_err(|error| {
275            snapshot_fault(
276                self.operation_id.clone(),
277                format!("failed to encode kernel snapshot: {error}"),
278            )
279        })
280    }
281
282    /// Restore by deterministically replaying accepted public ABI transactions, then fail closed
283    /// if any checkpoint metadata differs from the rebuilt runtime.
284    pub fn restore_snapshot(snapshot: KernelSnapshot) -> Result<Self, KernelFault> {
285        if snapshot.snapshot_version != KERNEL_SNAPSHOT_VERSION
286            || snapshot.abi_version != KERNEL_ABI_VERSION
287            || !(1..=100_000).contains(&snapshot.snapshot_input_limit)
288            || !(256..=64 * 1024 * 1024).contains(&snapshot.max_input_bytes)
289            || !(256..=1024 * 1024 * 1024usize).contains(&snapshot.snapshot_journal_bytes_limit)
290            || snapshot.accepted_inputs.len() > snapshot.snapshot_input_limit
291            || snapshot.accepted_input_bytes > snapshot.snapshot_journal_bytes_limit
292        {
293            return Err(snapshot_fault(
294                snapshot.operation_id,
295                "incompatible kernel snapshot version or bounds".to_string(),
296            ));
297        }
298
299        let initial_policy = SchedulerBudget::try_from(&snapshot.initial_policy)
300            .map_err(|message| snapshot_fault(snapshot.operation_id.clone(), message))?;
301        if KernelSnapshotPolicy::from(&initial_policy) != snapshot.initial_policy {
302            return Err(snapshot_fault(
303                snapshot.operation_id,
304                "kernel snapshot policy is not canonically encoded".to_string(),
305            ));
306        }
307        let mut runtime = Self::new(initial_policy);
308        runtime.snapshot_input_limit = snapshot.snapshot_input_limit;
309        runtime.max_input_bytes = snapshot.max_input_bytes;
310        runtime.snapshot_journal_bytes_limit = snapshot.snapshot_journal_bytes_limit;
311        let mut event_ids = HashSet::with_capacity(snapshot.accepted_inputs.len());
312        for input in snapshot.accepted_inputs.iter().cloned() {
313            if !event_ids.insert(input.event_id.clone()) {
314                return Err(snapshot_fault(
315                    snapshot.operation_id.clone(),
316                    format!(
317                        "kernel snapshot journal repeats accepted event_id {}",
318                        input.event_id
319                    ),
320                ));
321            }
322            let step = runtime.step_internal(input, StepMode::SnapshotReplay);
323            if let Some(fault) = step.faults.first() {
324                return Err(snapshot_fault(
325                    snapshot.operation_id.clone(),
326                    format!(
327                        "snapshot replay rejected an accepted input: {}",
328                        fault.message
329                    ),
330                ));
331            }
332        }
333        let rebuilt_last = serde_json::to_value(&runtime.last_step).ok();
334        let expected_last = serde_json::to_value(&snapshot.last_step).ok();
335        if runtime.lifecycle != snapshot.lifecycle
336            || runtime.operation_id != snapshot.operation_id
337            || runtime.next_step_seq != snapshot.next_step_seq
338            || runtime.snapshot_input_limit != snapshot.snapshot_input_limit
339            || runtime.max_input_bytes != snapshot.max_input_bytes
340            || runtime.snapshot_journal_bytes_limit != snapshot.snapshot_journal_bytes_limit
341            || runtime.accepted_input_count != snapshot.accepted_inputs.len()
342            || runtime.accepted_input_bytes != snapshot.accepted_input_bytes
343            || runtime.snapshot_overflowed
344            || rebuilt_last != expected_last
345        {
346            return Err(snapshot_fault(
347                snapshot.operation_id,
348                "kernel snapshot metadata does not match deterministic replay".to_string(),
349            ));
350        }
351        runtime.accepted_inputs = snapshot.accepted_inputs;
352        runtime.generation = runtime.accepted_input_count as u64;
353        Ok(runtime)
354    }
355
356    pub fn restore_snapshot_json(snapshot_json: &str) -> Result<Self, KernelFault> {
357        let snapshot = serde_json::from_str(snapshot_json).map_err(|error| {
358            snapshot_fault(None, format!("invalid kernel snapshot JSON: {error}"))
359        })?;
360        Self::restore_snapshot(snapshot)
361    }
362
363    #[cfg(test)]
364    pub(super) fn state_machine(&self) -> &LoopStateMachine {
365        &self.sm
366    }
367
368    #[cfg(test)]
369    pub(super) fn clear_test_observations(&mut self) {
370        self.sm.take_observations();
371    }
372
373    #[cfg(test)]
374    pub(super) fn push_test_history(&mut self, message: Message, tokens: u32) {
375        self.sm.ctx.push_history(message, tokens);
376    }
377
378    #[cfg(test)]
379    pub(super) fn accepted_snapshot_input_count(&self) -> usize {
380        self.accepted_inputs.len()
381    }
382
383    pub fn is_terminal(&self) -> bool {
384        self.lifecycle.is_terminal()
385    }
386
387    pub fn diagnostics(&self) -> KernelDiagnostics {
388        KernelDiagnostics {
389            lifecycle: self.lifecycle,
390            next_step_seq: self.next_step_seq,
391            accepted_input_count: self.accepted_input_count,
392            accepted_input_bytes: self.accepted_input_bytes,
393            snapshot_input_limit: self.snapshot_input_limit,
394            snapshot_journal_bytes_limit: self.snapshot_journal_bytes_limit,
395            max_input_bytes: self.max_input_bytes,
396            snapshot_overflowed: self.snapshot_overflowed,
397            recorded_event_count: self.recorded_events.entries.len(),
398            completed_effect_count: self.completed_effects.entries.len(),
399            pending_effect_count: self.pending_effects.len(),
400        }
401    }
402
403    pub fn lifecycle(&self) -> KernelLifecycle {
404        self.lifecycle
405    }
406
407    pub fn turn(&self) -> u32 {
408        self.sm.turn
409    }
410
411    pub fn recovery_content_bytes(&self) -> usize {
412        let tokens = self
413            .sm
414            .ctx
415            .config
416            .recovery_content_tokens(self.sm.ctx.max_tokens);
417        self.sm.ctx.engine.token_budget_to_bytes(tokens)
418    }
419
420    pub fn render(&self) -> RenderedContext {
421        self.sm.ctx.render()
422    }
423
424    pub fn drain_new_messages(&mut self) -> Vec<Message> {
425        self.sm.drain_new_messages()
426    }
427
428    pub fn preserved_refs(&self) -> Vec<String> {
429        self.sm.ctx.partitions.task_state.preserved_refs.clone()
430    }
431
432    pub fn count_tokens(&self, text: &str) -> u32 {
433        self.sm.ctx.engine.count(text)
434    }
435
436    /// L1 (RunGroup): this vehicle's cumulative sub-agent spawns this run, read back by the SDK at run
437    /// end to charge the group ledger (so the next member's cumulative spawn cap is seeded correctly).
438    pub fn local_subagents_spawned(&self) -> u32 {
439        self.sm.local_subagents_spawned()
440    }
441
442    #[cfg(test)]
443    pub(super) fn pending_provider_effect_id(&self) -> String {
444        self.pending_effect_id(PendingEffectKind::Provider)
445    }
446
447    #[cfg(test)]
448    pub(super) fn pending_tool_effect_id(&self) -> String {
449        self.pending_effect_id(PendingEffectKind::Tool)
450    }
451
452    #[cfg(test)]
453    fn pending_effect_id(&self, kind: PendingEffectKind) -> String {
454        let matches = self
455            .pending_effects
456            .iter()
457            .filter(|(_, pending_kind)| **pending_kind == kind)
458            .collect::<Vec<_>>();
459        assert_eq!(
460            matches.len(),
461            1,
462            "test transition must have one {kind:?} effect"
463        );
464        matches[0].0.clone()
465    }
466
467    #[cfg(test)]
468    pub(super) fn recorded_event_count(&self) -> usize {
469        self.recorded_events.len()
470    }
471
472    fn boundary_max_input_bytes(&self) -> usize {
473        self.state
474            .as_deref()
475            .or_else(|| {
476                self.prepared
477                    .as_ref()
478                    .map(|candidate| candidate.state.as_ref())
479            })
480            .map_or(MAX_INPUT_BYTES, |state| state.max_input_bytes)
481    }
482
483    fn boundary_step_seq(&self) -> u64 {
484        self.state
485            .as_deref()
486            .map(|state| state.next_step_seq)
487            .or_else(|| {
488                self.prepared
489                    .as_ref()
490                    .map(|candidate| candidate.step.step_seq)
491            })
492            .unwrap_or(1)
493    }
494
495    /// Stage one transition without publishing it as committed runtime state. A newly accepted
496    /// transition returns an opaque one-shot token; exact event replay and rejected inputs return no
497    /// token because neither requires a new durable transaction.
498    pub fn prepare_step(&mut self, mut input: KernelInput) -> KernelPreparedStep {
499        normalize_input(&mut input);
500        let base_generation = self.generation;
501        let operation_id = input.operation_id.clone();
502        let event_id = input.event_id.clone();
503
504        if self.poisoned {
505            return KernelPreparedStep {
506                status: KernelPreparationStatus::Rejected,
507                base_generation,
508                prepare_token: None,
509                input,
510                step: transaction_fault_step(
511                    operation_id,
512                    event_id,
513                    self.boundary_step_seq(),
514                    KernelFaultCode::TransactionConflict,
515                    "kernel runtime was invalidated by a transaction consistency error".to_string(),
516                ),
517            };
518        }
519
520        if let Some(candidate) = &self.prepared {
521            return KernelPreparedStep {
522                status: KernelPreparationStatus::Rejected,
523                base_generation,
524                prepare_token: None,
525                input,
526                step: transaction_fault_step(
527                    operation_id,
528                    event_id,
529                    candidate.step.step_seq,
530                    KernelFaultCode::TransactionConflict,
531                    "another kernel transition is already prepared".to_string(),
532                ),
533            };
534        }
535
536        if self.snapshot_overflowed {
537            return KernelPreparedStep {
538                status: KernelPreparationStatus::Rejected,
539                base_generation,
540                prepare_token: None,
541                input,
542                step: transaction_fault_step(
543                    operation_id,
544                    event_id,
545                    self.next_step_seq,
546                    KernelFaultCode::ResourceLimitExceeded,
547                    "kernel journal cannot roll back a prepared transition after overflow"
548                        .to_string(),
549                ),
550            };
551        }
552
553        let fingerprint = serde_json::to_vec(&input)
554            .expect("KernelInput serialization must succeed after typed construction");
555        let was_exact_replay = self
556            .recorded_events
557            .get(&event_id)
558            .is_some_and(|recorded| recorded.fingerprint == fingerprint);
559        let accepted_inputs_before = self.accepted_inputs.len();
560        let step = self.step_internal(input.clone(), StepMode::Live);
561        let candidate_state = self
562            .state
563            .take()
564            .expect("step execution must leave a runtime state");
565
566        if !step.faults.is_empty() {
567            self.state = Some(candidate_state);
568            return KernelPreparedStep {
569                status: KernelPreparationStatus::Rejected,
570                base_generation,
571                prepare_token: None,
572                input,
573                step,
574            };
575        }
576
577        if was_exact_replay {
578            self.state = Some(candidate_state);
579            return KernelPreparedStep {
580                status: KernelPreparationStatus::Replayed,
581                base_generation,
582                prepare_token: None,
583                input,
584                step,
585            };
586        }
587
588        let token = format!("kernel-prepare-{}", self.next_prepare_token);
589        self.next_prepare_token = self.next_prepare_token.saturating_add(1);
590        self.prepared = Some(PreparedCandidate {
591            token: token.clone(),
592            base_generation,
593            state: candidate_state,
594            step: step.clone(),
595            accepted_inputs_before,
596        });
597        KernelPreparedStep {
598            status: KernelPreparationStatus::Prepared,
599            base_generation,
600            prepare_token: Some(token),
601            input,
602            step,
603        }
604    }
605
606    /// Publish the candidate selected by `prepare_step`. A token mismatch invalidates this runtime;
607    /// the host must discard it and rebuild from the authoritative committed stream.
608    pub fn commit_prepared(&mut self, token: &str) -> Result<KernelStep, KernelFault> {
609        if self.poisoned {
610            return Err(transaction_conflict_fault(
611                None,
612                "kernel runtime was invalidated by a transaction consistency error".to_string(),
613            ));
614        }
615        let Some(candidate) = self.prepared.as_ref() else {
616            return Err(transaction_conflict_fault(
617                None,
618                "no kernel transition is prepared".to_string(),
619            ));
620        };
621        if candidate.token != token {
622            let operation_id = candidate.state.operation_id.clone();
623            self.poisoned = true;
624            return Err(transaction_conflict_fault(
625                operation_id,
626                "prepare token does not match the staged transition".to_string(),
627            ));
628        }
629        if candidate.base_generation != self.generation {
630            let operation_id = candidate.state.operation_id.clone();
631            self.poisoned = true;
632            return Err(transaction_conflict_fault(
633                operation_id,
634                "prepared transition no longer matches the runtime generation".to_string(),
635            ));
636        }
637
638        let candidate = self
639            .prepared
640            .take()
641            .expect("prepared candidate was validated above");
642        let step = candidate.step.clone();
643        self.state = Some(candidate.state);
644        self.generation = self.generation.saturating_add(1);
645        Ok(step)
646    }
647
648    /// Abort a staged transition after durable append failure. Rollback is intentionally paid only
649    /// on this failure path: the committed prefix is deterministically rebuilt from its journal,
650    /// while the normal prepare/commit path moves the candidate state without cloning history.
651    pub fn abort_prepared(&mut self, token: &str) -> Result<(), KernelFault> {
652        if self.poisoned {
653            return Err(transaction_conflict_fault(
654                None,
655                "kernel runtime was invalidated by a transaction consistency error".to_string(),
656            ));
657        }
658        let Some(candidate) = self.prepared.as_ref() else {
659            return Err(transaction_conflict_fault(
660                None,
661                "no kernel transition is prepared".to_string(),
662            ));
663        };
664        if candidate.token != token {
665            let operation_id = candidate.state.operation_id.clone();
666            self.poisoned = true;
667            return Err(transaction_conflict_fault(
668                operation_id,
669                "prepare token does not match the staged transition".to_string(),
670            ));
671        }
672
673        let candidate = self
674            .prepared
675            .take()
676            .expect("prepared candidate was validated above");
677        let mut committed_inputs = candidate.state.accepted_inputs.clone();
678        committed_inputs.truncate(candidate.accepted_inputs_before);
679        match Self::rebuild_accepted_inputs(
680            candidate.state.initial_policy.clone(),
681            committed_inputs,
682        ) {
683            Ok(mut restored) => {
684                self.state = restored.state.take();
685                Ok(())
686            }
687            Err(fault) => {
688                self.prepared = Some(candidate);
689                Err(fault)
690            }
691        }
692    }
693
694    fn rebuild_accepted_inputs(
695        initial_policy: SchedulerBudget,
696        accepted_inputs: Vec<KernelInput>,
697    ) -> Result<Self, KernelFault> {
698        let mut runtime = Self::new(initial_policy);
699        for input in accepted_inputs.iter().cloned() {
700            let step = runtime.step_internal(input, StepMode::SnapshotReplay);
701            if let Some(fault) = step.faults.first() {
702                return Err(snapshot_fault(
703                    runtime.operation_id.clone(),
704                    format!(
705                        "committed transaction replay rejected an accepted input: {}",
706                        fault.message
707                    ),
708                ));
709            }
710        }
711        runtime.accepted_inputs = accepted_inputs;
712        runtime.generation = runtime.accepted_input_count as u64;
713        Ok(runtime)
714    }
715
716    /// Decode and execute one wire input. The version is probed before decoding
717    /// the v2 envelope so a v1 payload receives a structured kernel fault rather
718    /// than a host-language deserialization error.
719    pub fn step_json(&mut self, input_json: &str) -> Result<KernelStep, serde_json::Error> {
720        let max_input_bytes = self.boundary_max_input_bytes();
721        if input_json.len() > max_input_bytes {
722            return Ok(transaction_fault_step(
723                String::new(),
724                String::new(),
725                self.boundary_step_seq(),
726                KernelFaultCode::ResourceLimitExceeded,
727                format!(
728                    "kernel input is {} bytes; configured maximum is {} bytes",
729                    input_json.len(),
730                    max_input_bytes
731                ),
732            ));
733        }
734        let value: serde_json::Value = serde_json::from_str(input_json)?;
735        if value.get("version").and_then(serde_json::Value::as_u64)
736            != Some(KERNEL_ABI_VERSION as u64)
737        {
738            let operation_id = value
739                .get("operation_id")
740                .and_then(serde_json::Value::as_str)
741                .unwrap_or_default()
742                .to_string();
743            let event_id = value
744                .get("event_id")
745                .and_then(serde_json::Value::as_str)
746                .unwrap_or_default()
747                .to_string();
748            let received_version = value
749                .get("version")
750                .and_then(serde_json::Value::as_u64)
751                .map_or_else(|| "missing".to_string(), |version| version.to_string());
752            return Ok(transaction_fault_step(
753                operation_id,
754                event_id,
755                self.boundary_step_seq(),
756                KernelFaultCode::VersionMismatch,
757                format!(
758                    "kernel ABI version mismatch: input v{received_version}, kernel v{KERNEL_ABI_VERSION}"
759                ),
760            ));
761        }
762
763        serde_json::from_value(value).map(|input| self.step(input))
764    }
765
766    /// Decode and stage one wire input for a host-controlled durable commit boundary.
767    pub fn prepare_step_json(
768        &mut self,
769        input_json: &str,
770    ) -> Result<KernelPreparedStep, serde_json::Error> {
771        let value: serde_json::Value = serde_json::from_str(input_json)?;
772        let input: KernelInput = serde_json::from_value(value)?;
773        let max_input_bytes = self.boundary_max_input_bytes();
774        if input_json.len() > max_input_bytes {
775            return Ok(KernelPreparedStep {
776                status: KernelPreparationStatus::Rejected,
777                base_generation: self.generation,
778                prepare_token: None,
779                step: transaction_fault_step(
780                    input.operation_id.clone(),
781                    input.event_id.clone(),
782                    self.boundary_step_seq(),
783                    KernelFaultCode::ResourceLimitExceeded,
784                    format!(
785                        "kernel input is {} bytes; configured maximum is {} bytes",
786                        input_json.len(),
787                        max_input_bytes
788                    ),
789                ),
790                input,
791            });
792        }
793        Ok(self.prepare_step(input))
794    }
795
796    pub fn step(&mut self, input: KernelInput) -> KernelStep {
797        if self.poisoned {
798            return transaction_fault_step(
799                input.operation_id,
800                input.event_id,
801                self.boundary_step_seq(),
802                KernelFaultCode::TransactionConflict,
803                "kernel runtime was invalidated by a transaction consistency error".to_string(),
804            );
805        }
806        if let Some(candidate) = &self.prepared {
807            return transaction_fault_step(
808                input.operation_id,
809                input.event_id,
810                candidate.step.step_seq,
811                KernelFaultCode::TransactionConflict,
812                "another kernel transition is already prepared".to_string(),
813            );
814        }
815        let accepted_input_count = self.accepted_input_count;
816        let step = self.step_internal(input, StepMode::Live);
817        if self.accepted_input_count > accepted_input_count {
818            self.generation = self.generation.saturating_add(1);
819        }
820        step
821    }
822
823    fn step_internal(&mut self, mut input: KernelInput, mode: StepMode) -> KernelStep {
824        normalize_input(&mut input);
825        let operation_id = input.operation_id.clone();
826        let event_id = input.event_id.clone();
827
828        if input.version != KERNEL_ABI_VERSION {
829            return self.fault_step(
830                operation_id,
831                event_id,
832                KernelFaultCode::VersionMismatch,
833                format!(
834                    "kernel ABI version mismatch: input v{}, kernel v{}",
835                    input.version, KERNEL_ABI_VERSION
836                ),
837                None,
838            );
839        }
840
841        if operation_id.is_empty() || event_id.is_empty() {
842            return self.fault_step(
843                operation_id,
844                event_id,
845                KernelFaultCode::InvalidConfig,
846                "operation_id and event_id must be non-empty".to_string(),
847                None,
848            );
849        }
850
851        let fingerprint = serde_json::to_vec(&input)
852            .expect("KernelInput serialization must succeed after typed construction");
853        let input_bytes = fingerprint.len();
854        if mode == StepMode::Live && input_bytes > self.max_input_bytes {
855            return self.fault_step(
856                operation_id,
857                event_id,
858                KernelFaultCode::ResourceLimitExceeded,
859                format!(
860                    "kernel input is {input_bytes} bytes; configured maximum is {} bytes",
861                    self.max_input_bytes
862                ),
863                None,
864            );
865        }
866        if let Some(recorded) = self.recorded_events.get(&event_id) {
867            if recorded.fingerprint == fingerprint {
868                return recorded.step.clone();
869            }
870            return self.fault_step(
871                operation_id,
872                event_id,
873                KernelFaultCode::DuplicateEventConflict,
874                "event_id was already accepted with a different payload".to_string(),
875                None,
876            );
877        }
878
879        if let Some(bound_operation_id) = &self.operation_id {
880            if bound_operation_id != &operation_id {
881                return self.fault_step(
882                    operation_id,
883                    event_id,
884                    KernelFaultCode::OperationMismatch,
885                    format!("input operation does not match bound operation {bound_operation_id}"),
886                    None,
887                );
888            }
889        }
890
891        if let KernelInputEvent::CancelOperation {
892            operation_id: cancelled_operation_id,
893            reason,
894            pending_call_ids,
895        } = &input.event
896        {
897            if cancelled_operation_id != &operation_id {
898                return self.fault_step(
899                    operation_id,
900                    event_id,
901                    KernelFaultCode::OperationMismatch,
902                    "cancel_operation identity does not match the input envelope".to_string(),
903                    None,
904                );
905            }
906            if cancelled_operation_id.is_empty()
907                || pending_call_ids.iter().any(|call_id| call_id.is_empty())
908            {
909                return self.fault_step(
910                    operation_id,
911                    event_id,
912                    KernelFaultCode::InvalidConfig,
913                    "cancel_operation requires non-empty operation and pending call identities"
914                        .to_string(),
915                    None,
916                );
917            }
918            if let Some(accepted) = &self.accepted_cancellation {
919                if accepted.reason == *reason && accepted.pending_call_ids == *pending_call_ids {
920                    let step = accepted.step.clone();
921                    self.recorded_events.insert(
922                        event_id,
923                        RecordedTransition {
924                            fingerprint,
925                            step: step.clone(),
926                        },
927                    );
928                    self.record_accepted_input(
929                        (mode == StepMode::Live).then_some(input),
930                        input_bytes,
931                    );
932                    self.last_step = Some(step.clone());
933                    return step;
934                }
935                return self.fault_step(
936                    operation_id,
937                    event_id,
938                    KernelFaultCode::DuplicateEventConflict,
939                    "cancel_operation conflicts with the committed cancellation".to_string(),
940                    None,
941                );
942            }
943        }
944
945        let result_fingerprint = result_effect(&input.event).map(|(effect_id, _)| {
946            (
947                effect_id.to_string(),
948                serde_json::to_vec(&input.event)
949                    .expect("KernelInputEvent serialization must succeed after typed construction"),
950            )
951        });
952        if let Some((effect_id, result_fingerprint)) = &result_fingerprint {
953            if let Some(completed) = self.completed_effects.get(effect_id) {
954                if completed.fingerprint == *result_fingerprint {
955                    let step = completed.step.clone();
956                    self.recorded_events.insert(
957                        event_id,
958                        RecordedTransition {
959                            fingerprint,
960                            step: step.clone(),
961                        },
962                    );
963                    self.record_accepted_input(
964                        (mode == StepMode::Live).then_some(input),
965                        input_bytes,
966                    );
967                    self.last_step = Some(step.clone());
968                    return step;
969                }
970                return self.fault_step(
971                    operation_id,
972                    event_id,
973                    KernelFaultCode::UnexpectedEffectResult,
974                    format!("effect result conflicts with the completed result: {effect_id}"),
975                    Some(effect_id.clone()),
976                );
977            }
978        }
979
980        let lifecycle_transition = match self.lifecycle_transition(&input.event) {
981            Ok(transition) => transition,
982            Err(message) => {
983                return self.fault_step(
984                    operation_id,
985                    event_id,
986                    KernelFaultCode::InvalidLifecycle,
987                    message,
988                    None,
989                );
990            }
991        };
992        if let KernelInputEvent::ConfigureRun { config } = &input.event {
993            if let Err(message) = validate_run_config(config, self.sm.ctx.max_tokens) {
994                return self.fault_step(
995                    operation_id,
996                    event_id,
997                    KernelFaultCode::InvalidConfig,
998                    message,
999                    None,
1000                );
1001            }
1002            if config
1003                .reliability
1004                .as_ref()
1005                .and_then(|reliability| reliability.snapshot_input_limit)
1006                .is_some_and(|limit| limit <= self.accepted_input_count)
1007            {
1008                return self.fault_step(
1009                    operation_id,
1010                    event_id,
1011                    KernelFaultCode::InvalidConfig,
1012                    "snapshot_input_limit must leave room for the configure transaction"
1013                        .to_string(),
1014                    None,
1015                );
1016            }
1017            if config
1018                .reliability
1019                .as_ref()
1020                .and_then(|reliability| reliability.snapshot_journal_bytes_limit)
1021                .is_some_and(|limit| limit < self.accepted_input_bytes.saturating_add(input_bytes))
1022            {
1023                return self.fault_step(
1024                    operation_id,
1025                    event_id,
1026                    KernelFaultCode::InvalidConfig,
1027                    "snapshot_journal_bytes_limit must leave room for the configure transaction"
1028                        .to_string(),
1029                    None,
1030                );
1031            }
1032            if config
1033                .reliability
1034                .as_ref()
1035                .and_then(|reliability| reliability.max_input_bytes)
1036                .is_some_and(|limit| limit < input_bytes)
1037            {
1038                return self.fault_step(
1039                    operation_id,
1040                    event_id,
1041                    KernelFaultCode::InvalidConfig,
1042                    "max_input_bytes must admit the configure transaction".to_string(),
1043                    None,
1044                );
1045            }
1046        }
1047        if let KernelInputEvent::DeliverSignal {
1048            delivery_id,
1049            attempt,
1050            ..
1051        } = &input.event
1052        {
1053            if delivery_id.is_empty() || *attempt == 0 {
1054                return self.fault_step(
1055                    operation_id,
1056                    event_id,
1057                    KernelFaultCode::InvalidConfig,
1058                    "deliver_signal requires a non-empty delivery_id and attempt >= 1".to_string(),
1059                    None,
1060                );
1061            }
1062        }
1063        if let KernelInputEvent::SetSignalPolicy { policy } = &input.event {
1064            if let Err(message) = validate_signal_policy(policy) {
1065                return self.fault_step(
1066                    operation_id,
1067                    event_id,
1068                    KernelFaultCode::InvalidConfig,
1069                    message,
1070                    None,
1071                );
1072            }
1073        }
1074        if let KernelInputEvent::QueryMemory { query } = &input.event {
1075            if let Err(message) = query.validate() {
1076                return self.fault_step(
1077                    operation_id,
1078                    event_id,
1079                    KernelFaultCode::InvalidConfig,
1080                    message,
1081                    None,
1082                );
1083            }
1084        }
1085        if let KernelInputEvent::MemoryQueryResult {
1086            effect_id,
1087            hits,
1088            error,
1089        } = &input.event
1090        {
1091            if error.is_none() {
1092                let Some((query, requested_k)) = self.pending_memory_query.as_ref() else {
1093                    return self.fault_step(
1094                        operation_id,
1095                        event_id,
1096                        KernelFaultCode::UnexpectedEffectResult,
1097                        "memory query result has no pending query".to_string(),
1098                        Some(effect_id.clone()),
1099                    );
1100                };
1101                if let Err(message) = query.validate_hits(hits, *requested_k) {
1102                    return self.fault_step(
1103                        operation_id,
1104                        event_id,
1105                        KernelFaultCode::UnexpectedEffectResult,
1106                        message,
1107                        Some(effect_id.clone()),
1108                    );
1109                }
1110            }
1111        }
1112        if let KernelInputEvent::WorkflowSpawnResult {
1113            effect_id,
1114            started_agent_ids,
1115            failures,
1116            error,
1117        } = &input.event
1118        {
1119            if let Err(message) = self.sm.validate_workflow_spawn_result(
1120                started_agent_ids,
1121                failures,
1122                error.as_deref(),
1123            ) {
1124                return self.fault_step(
1125                    operation_id,
1126                    event_id,
1127                    KernelFaultCode::UnexpectedEffectResult,
1128                    message,
1129                    Some(effect_id.clone()),
1130                );
1131            }
1132        }
1133        if let KernelInputEvent::LargeResultSpoolResult {
1134            effect_id,
1135            spool_ref,
1136            error,
1137        } = &input.event
1138        {
1139            if error.is_none() && spool_ref.as_deref().map_or(true, str::is_empty) {
1140                return self.fault_step(
1141                    operation_id,
1142                    event_id,
1143                    KernelFaultCode::UnexpectedEffectResult,
1144                    "successful spool result requires a non-empty spool_ref".to_string(),
1145                    Some(effect_id.clone()),
1146                );
1147            }
1148        }
1149
1150        if let Some((effect_id, expected_kind)) = result_effect(&input.event) {
1151            match self.pending_effects.get(effect_id) {
1152                Some(actual_kind) if actual_kind == &expected_kind => {
1153                    self.pending_effects.remove(effect_id);
1154                }
1155                _ => {
1156                    return self.fault_step(
1157                        operation_id,
1158                        event_id,
1159                        KernelFaultCode::UnexpectedEffectResult,
1160                        format!("effect result does not match a pending effect: {effect_id}"),
1161                        Some(effect_id.to_string()),
1162                    );
1163                }
1164            }
1165        }
1166
1167        if self.operation_id.is_none() {
1168            self.operation_id = Some(operation_id.clone());
1169        }
1170
1171        if input.observed_at_ms > 0 {
1172            self.sm.set_observed_time(input.observed_at_ms);
1173        }
1174
1175        let accepted_input = (mode == StepMode::Live).then(|| input.clone());
1176        let step_seq = self.allocate_step_seq();
1177        let cancellation_identity = match &input.event {
1178            KernelInputEvent::CancelOperation {
1179                reason,
1180                pending_call_ids,
1181                ..
1182            } => Some((*reason, pending_call_ids.clone())),
1183            _ => None,
1184        };
1185        let mut step = self.dispatch(
1186            StepIdentity {
1187                operation_id: operation_id.clone(),
1188                input_event_id: event_id.clone(),
1189                step_seq,
1190            },
1191            input.event,
1192        );
1193        let reservation_id = self
1194            .sm
1195            .budget_grant()
1196            .map(|grant| grant.reservation_id.clone());
1197        for observation in &mut step.observations {
1198            if let KernelObservation::BudgetExceeded {
1199                operation_id: observed_operation_id,
1200                reservation_id: observed_reservation_id,
1201                ..
1202            } = observation
1203            {
1204                *observed_operation_id = operation_id.clone();
1205                if observed_reservation_id.is_none() {
1206                    *observed_reservation_id = reservation_id.clone();
1207                }
1208            }
1209        }
1210        let is_terminal = step
1211            .actions
1212            .iter()
1213            .any(|action| matches!(action.effect, KernelEffect::Done { .. }));
1214        if is_terminal && !self.budget_usage_reported {
1215            if let Some(reservation_id) = reservation_id {
1216                let (tokens, subagents, rounds) = self.sm.local_budget_usage();
1217                step.observations
1218                    .push(KernelObservation::BudgetUsageReported {
1219                        operation_id: operation_id.clone(),
1220                        reservation_id,
1221                        tokens,
1222                        subagents,
1223                        rounds,
1224                    });
1225                self.budget_usage_reported = true;
1226            }
1227        }
1228        self.advance_lifecycle(lifecycle_transition, &step);
1229        if is_terminal {
1230            self.pending_effects.clear();
1231        } else {
1232            for action in &step.actions {
1233                if let Some(kind) = pending_effect_kind(&action.effect) {
1234                    self.pending_effects
1235                        .retain(|_, pending_kind| *pending_kind != kind);
1236                    self.pending_effects.insert(action.effect_id.clone(), kind);
1237                }
1238            }
1239        }
1240        if let Some((effect_id, result_fingerprint)) = result_fingerprint {
1241            self.completed_effects.insert(
1242                effect_id,
1243                RecordedTransition {
1244                    fingerprint: result_fingerprint,
1245                    step: step.clone(),
1246                },
1247            );
1248        }
1249        if let Some((reason, pending_call_ids)) = cancellation_identity {
1250            self.accepted_cancellation = Some(AcceptedCancellation {
1251                reason,
1252                pending_call_ids,
1253                step: step.clone(),
1254            });
1255        }
1256        self.recorded_events.insert(
1257            event_id,
1258            RecordedTransition {
1259                fingerprint,
1260                step: step.clone(),
1261            },
1262        );
1263        self.record_accepted_input(accepted_input, input_bytes);
1264        self.last_step = Some(step.clone());
1265        step
1266    }
1267
1268    fn ensure_snapshot_available(&self) -> Result<(), KernelFault> {
1269        if self.poisoned {
1270            return Err(transaction_conflict_fault(
1271                None,
1272                "kernel runtime was invalidated by a transaction consistency error".to_string(),
1273            ));
1274        }
1275        if let Some(candidate) = &self.prepared {
1276            return Err(transaction_conflict_fault(
1277                candidate.state.operation_id.clone(),
1278                "cannot snapshot an uncommitted prepared transition".to_string(),
1279            ));
1280        }
1281        if self.snapshot_overflowed
1282            || self.accepted_input_count > self.snapshot_input_limit
1283            || self.accepted_input_bytes > self.snapshot_journal_bytes_limit
1284        {
1285            return Err(snapshot_fault(
1286                self.operation_id.clone(),
1287                format!(
1288                    "snapshot input journal exceeded configured limits: {} inputs / {} bytes",
1289                    self.snapshot_input_limit, self.snapshot_journal_bytes_limit
1290                ),
1291            ));
1292        }
1293        Ok(())
1294    }
1295
1296    fn record_accepted_input(&mut self, input: Option<KernelInput>, input_bytes: usize) {
1297        if self.accepted_input_count < self.snapshot_input_limit
1298            && self.accepted_input_bytes.saturating_add(input_bytes)
1299                <= self.snapshot_journal_bytes_limit
1300        {
1301            self.accepted_input_count += 1;
1302            self.accepted_input_bytes += input_bytes;
1303            if let Some(input) = input {
1304                self.accepted_inputs.push(input);
1305            }
1306        } else {
1307            self.snapshot_overflowed = true;
1308        }
1309    }
1310
1311    fn dispatch(&mut self, identity: StepIdentity, event: KernelInputEvent) -> KernelStep {
1312        let action = match event {
1313            KernelInputEvent::SetTools { tools } => {
1314                self.sm.tools = tools;
1315                return identity.empty(self.sm.take_observations());
1316            }
1317            KernelInputEvent::SetAvailableSkills { skills } => {
1318                self.sm.ctx.set_available_skills(skills);
1319                return identity.empty(self.sm.take_observations());
1320            }
1321            KernelInputEvent::SkillActivated { name, lease_turns } => {
1322                // B1: record the activation (B2 reads it in emit_call_llm to narrow tools).
1323                // The returned `changed` flag is the epoch boundary for D's cache re-anchor.
1324                // K3: a lease converts to an absolute expiry turn here (the manager is turn-blind).
1325                let expires_at_turn = lease_turns.map(|n| self.sm.turn.saturating_add(n));
1326                self.sm.ctx.activate_skill_leased(name, expires_at_turn);
1327                return identity.empty(self.sm.take_observations());
1328            }
1329            KernelInputEvent::SkillDeactivated { name } => {
1330                self.sm.ctx.deactivate_skill(&name);
1331                return identity.empty(self.sm.take_observations());
1332            }
1333            KernelInputEvent::SetStableCoreTools { tool_ids } => {
1334                self.sm
1335                    .ctx
1336                    .set_stable_core_tools(tool_ids.into_iter().map(Into::into));
1337                return identity.empty(self.sm.take_observations());
1338            }
1339            KernelInputEvent::SetMemoryEnabled { enabled } => {
1340                self.sm.ctx.set_memory_enabled(enabled);
1341                return identity.empty(self.sm.take_observations());
1342            }
1343            KernelInputEvent::SetKnowledgeEnabled { enabled } => {
1344                self.sm.ctx.set_knowledge_enabled(enabled);
1345                return identity.empty(self.sm.take_observations());
1346            }
1347            KernelInputEvent::SetPlanToolEnabled { enabled } => {
1348                self.sm.ctx.set_plan_tool_enabled(enabled);
1349                return identity.empty(self.sm.take_observations());
1350            }
1351            KernelInputEvent::SetTokenizer { .. } => {
1352                // Local BPE tokenisers are no longer used — accuracy comes from
1353                // observed_input_tokens reported by the provider API (P0-1 Step 2).
1354                // char_approx is always used for pre-flight truncation estimates.
1355                self.sm.ctx.engine = ContextTokenEngine::char_approx();
1356                return identity.empty(self.sm.take_observations());
1357            }
1358            KernelInputEvent::AddSystemMessage { content, tokens } => {
1359                self.sm
1360                    .ctx
1361                    .partitions
1362                    .system
1363                    .push(Message::system(content), tokens.max(1));
1364                return identity.empty(self.sm.take_observations());
1365            }
1366            KernelInputEvent::AddKnowledgeMessage {
1367                content,
1368                tokens,
1369                key,
1370                pinned,
1371            } => {
1372                // P1-B2 cache contract: the knowledge partition renders into the cached system[1]
1373                // block. Appending here is the right home for *stable* reference material (skill
1374                // defs, durable artifacts) — it's append-only, so the existing prefix stays
1375                // byte-stable, and a fresh append costs only a one-time system[1] re-cache. Do NOT
1376                // route *per-turn* retrievals (a memory/knowledge lookup that changes every turn)
1377                // through here: each would rewrite the cached block and invalidate it plus the
1378                // history cache every turn. Volatile per-turn context belongs on the signal/tail
1379                // path (`push_signal` → state_turn), which is uncached *and* high-attention (P1-F).
1380                //
1381                // K1: a `key` gives the entry identity — a same-key push stages a boundary-deferred
1382                // upsert instead of appending a duplicate (the cache contract above is why the swap
1383                // waits for the next compaction/renewal boundary).
1384                self.sm.ctx.push_knowledge_entry(
1385                    key.map(compact_str::CompactString::from),
1386                    Message::system(content),
1387                    tokens.max(1),
1388                    pinned,
1389                );
1390                return identity.empty(self.sm.take_observations());
1391            }
1392            KernelInputEvent::RemoveKnowledge { key } => {
1393                self.sm.ctx.remove_knowledge(&key);
1394                return identity.empty(self.sm.take_observations());
1395            }
1396            KernelInputEvent::AddHistoryMessage { message, tokens } => {
1397                let tokens = tokens.unwrap_or_else(|| self.sm.ctx.engine.count_message(&message));
1398                self.sm.ctx.push_history(message, tokens.max(1));
1399                return identity.empty(self.sm.take_observations());
1400            }
1401            KernelInputEvent::PreloadHistory { messages } => {
1402                self.sm.preload_history(messages);
1403                return identity.empty(self.sm.take_observations());
1404            }
1405            KernelInputEvent::MountCapability { capability } => {
1406                self.sm.mount_capability(capability, None, None);
1407                return identity.empty(self.sm.take_observations());
1408            }
1409            KernelInputEvent::UnmountCapability {
1410                capability_kind,
1411                id,
1412            } => {
1413                self.sm.unmount_capability(capability_kind, &id);
1414                return identity.empty(self.sm.take_observations());
1415            }
1416            KernelInputEvent::LoadMilestoneContract { contract } => {
1417                self.sm.load_milestone_contract(contract);
1418                return identity.empty(self.sm.take_observations());
1419            }
1420            KernelInputEvent::LoadGovernancePolicy {
1421                default_action,
1422                rules,
1423                vetoed_tools,
1424                rate_limits,
1425                constraints,
1426            } => {
1427                self.sm.set_governance(build_governance_pipeline(
1428                    default_action,
1429                    rules,
1430                    vetoed_tools,
1431                    rate_limits,
1432                    constraints,
1433                ));
1434                return identity.empty(self.sm.take_observations());
1435            }
1436            KernelInputEvent::ConfigureRun { config } => {
1437                // K2: apply a bundle of run-setup config in one event (tools / governance / attention /
1438                // quota / scheduler / toggles), replacing the ~10 separate `Set*` / `Load*` events the
1439                // SDK used to fire one-by-one. Each field is optional; an absent field is left untouched.
1440                // The individual events remain for runtime mutation (skill mount, mid-run budget change).
1441                // Each branch delegates to exactly the method its granular event uses, so the two paths
1442                // can never diverge.
1443                let RunConfig {
1444                    tools,
1445                    available_skills,
1446                    stable_core_tools,
1447                    memory_enabled,
1448                    knowledge_enabled,
1449                    plan_tool_enabled,
1450                    tokenizer,
1451                    governance,
1452                    signal_policy,
1453                    prompt_budget,
1454                    context_policy,
1455                    scheduler_policy,
1456                    resource_quota,
1457                    budget_grant,
1458                    repeat_fuse,
1459                    criteria_gate,
1460                    knowledge_budget_ratio,
1461                    entropy_watch,
1462                    reliability,
1463                } = config;
1464                if let Some(tools) = tools {
1465                    self.sm.tools = tools;
1466                }
1467                if let Some(skills) = available_skills {
1468                    self.sm.ctx.set_available_skills(skills);
1469                }
1470                if let Some(ids) = stable_core_tools {
1471                    self.sm
1472                        .ctx
1473                        .set_stable_core_tools(ids.into_iter().map(Into::into));
1474                }
1475                if let Some(enabled) = memory_enabled {
1476                    self.sm.ctx.set_memory_enabled(enabled);
1477                }
1478                if let Some(enabled) = knowledge_enabled {
1479                    self.sm.ctx.set_knowledge_enabled(enabled);
1480                }
1481                if let Some(enabled) = plan_tool_enabled {
1482                    self.sm.ctx.set_plan_tool_enabled(enabled);
1483                }
1484                if tokenizer.is_some() {
1485                    self.sm.ctx.engine = ContextTokenEngine::char_approx();
1486                }
1487                if let Some(g) = governance {
1488                    self.sm.set_governance(build_governance_pipeline(
1489                        g.default_action,
1490                        g.rules,
1491                        g.vetoed_tools,
1492                        g.rate_limits,
1493                        g.constraints,
1494                    ));
1495                }
1496                if let Some(policy) = signal_policy {
1497                    self.sm.set_signal_policy(
1498                        policy.queue_max as usize,
1499                        policy.ttl_ms,
1500                        policy.deadline_escalation.unwrap_or(false),
1501                    );
1502                }
1503                if let Some(prompt_budget) = prompt_budget {
1504                    self.sm.ctx.set_prompt_budget(prompt_budget);
1505                }
1506                if let Some(context_policy) = context_policy {
1507                    self.sm.ctx.apply_context_policy(&context_policy);
1508                }
1509                if let Some(policy) = scheduler_policy {
1510                    self.sm.set_scheduler_policy(policy);
1511                }
1512                if let Some(quota) = resource_quota {
1513                    self.sm.set_resource_quota(quota);
1514                }
1515                if let Some(grant) = budget_grant {
1516                    self.sm.set_budget_grant(grant);
1517                }
1518                if let Some(fuse) = repeat_fuse {
1519                    self.sm.set_repeat_fuse(fuse);
1520                }
1521                if let Some(enabled) = criteria_gate {
1522                    self.sm.set_criteria_gate(enabled);
1523                }
1524                if let Some(ratio) = knowledge_budget_ratio {
1525                    self.sm.ctx.config.knowledge_budget_ratio = ratio;
1526                }
1527                if let Some(watch) = entropy_watch {
1528                    self.sm.set_entropy_watch(watch);
1529                }
1530                if let Some(reliability) = reliability {
1531                    if let Some(capacity) = reliability.event_replay_capacity {
1532                        self.recorded_events.set_capacity(capacity);
1533                    }
1534                    if let Some(capacity) = reliability.completed_effect_replay_capacity {
1535                        self.completed_effects.set_capacity(capacity);
1536                    }
1537                    if let Some(limit) = reliability.snapshot_input_limit {
1538                        self.snapshot_input_limit = limit;
1539                    }
1540                    if let Some(limit) = reliability.max_input_bytes {
1541                        self.max_input_bytes = limit;
1542                    }
1543                    if let Some(limit) = reliability.snapshot_journal_bytes_limit {
1544                        self.snapshot_journal_bytes_limit = limit;
1545                    }
1546                    self.sm.set_reliability_config(&reliability);
1547                }
1548                return identity.empty(self.sm.take_observations());
1549            }
1550            KernelInputEvent::SetSignalPolicy { policy } => {
1551                self.sm.set_signal_policy(
1552                    policy.queue_max as usize,
1553                    policy.ttl_ms,
1554                    policy.deadline_escalation.unwrap_or(false),
1555                );
1556                return identity.empty(self.sm.take_observations());
1557            }
1558            KernelInputEvent::PageIn { entries } => {
1559                self.sm.apply_page_in(&entries);
1560                return identity.empty(self.sm.take_observations());
1561            }
1562            KernelInputEvent::ForceCompact => {
1563                self.sm.force_compact();
1564                self.sm
1565                    .externalize_pending_host_effect(LoopAction::AwaitingResume)
1566            }
1567            KernelInputEvent::UpdateTask { update } => {
1568                self.sm.ctx.update_task(update);
1569                return identity.empty(self.sm.take_observations());
1570            }
1571            KernelInputEvent::StartRun { task, run_spec } => {
1572                self.sm.run_spec = run_spec;
1573                self.sm.start(task)
1574            }
1575            KernelInputEvent::CapabilityCommand { command } => {
1576                self.sm.execute_capability_command(command);
1577                return identity.empty(self.sm.take_observations());
1578            }
1579            KernelInputEvent::Resume => self.sm.resume_after_preload(),
1580            KernelInputEvent::ApprovalResult {
1581                effect_id: _,
1582                approved_calls,
1583                denied_calls,
1584                error,
1585            } => match error {
1586                Some(error) => self.sm.retry_approval(error),
1587                None => self.sm.resolve_approval(approved_calls, denied_calls),
1588            },
1589            KernelInputEvent::WorkflowSpawnResult {
1590                effect_id: _,
1591                started_agent_ids,
1592                failures,
1593                error,
1594            } => match error {
1595                Some(error) => self.sm.retry_workflow_spawn(error),
1596                None => self.sm.resolve_workflow_spawn(started_agent_ids, failures),
1597            },
1598            KernelInputEvent::PreemptResult {
1599                effect_id: _,
1600                error,
1601            } => match error {
1602                Some(error) => self.sm.retry_preempt(error),
1603                None => self.sm.resolve_preempt(),
1604            },
1605            KernelInputEvent::MemoryPersistResult {
1606                effect_id: _,
1607                error,
1608            } => {
1609                let memory = self
1610                    .pending_memory_write
1611                    .take()
1612                    .expect("validated memory result requires pending write");
1613                let staged_store = self
1614                    .pending_memory_store
1615                    .take()
1616                    .expect("validated memory result requires staged scoped upsert");
1617                let turn = self.sm.turn;
1618                match error {
1619                    Some(error) => {
1620                        self.sm
1621                            .observations
1622                            .push(KernelObservation::MemoryWriteFailed {
1623                                turn,
1624                                record_id: memory.record_id,
1625                                error,
1626                            })
1627                    }
1628                    None => {
1629                        self.memory_records = staged_store;
1630                        self.sm.observations.push(KernelObservation::MemoryWritten {
1631                            turn,
1632                            record_id: memory.record_id,
1633                            scope: memory.scope,
1634                            memory_kind: memory.kind,
1635                            name: memory.name,
1636                            size_bytes: memory.content.len() as u32,
1637                        });
1638                    }
1639                }
1640                return identity.empty(self.sm.take_observations());
1641            }
1642            KernelInputEvent::MemoryQueryResult {
1643                effect_id: _,
1644                hits,
1645                error,
1646            } => {
1647                let (query, requested_k) = self
1648                    .pending_memory_query
1649                    .take()
1650                    .expect("validated memory result requires pending query");
1651                let turn = self.sm.turn;
1652                let continuation = match error {
1653                    Some(error) => {
1654                        let continuation = self.sm.resume_after_preload();
1655                        self.sm
1656                            .observations
1657                            .push(KernelObservation::MemoryQueryFailed {
1658                                turn,
1659                                scope: query.scope,
1660                                query: query.query,
1661                                error,
1662                            });
1663                        continuation
1664                    }
1665                    None => {
1666                        // M3/M4: journal recall lifecycle derived from the routed hits. Each hit
1667                        // carries the host store's current recall_count, so the incremented count is
1668                        // computed statelessly here (the durable ledger lives host-side) and mirrored
1669                        // back via observation. Recall time is the current turn — deterministic and
1670                        // replay-safe; the kernel owns no wall clock. Promotion is edge-triggered on
1671                        // the threshold crossing so it never nags.
1672                        let promotion_threshold = self
1673                            .sm
1674                            .memory_policy()
1675                            .and_then(|policy| policy.promotion_recall_threshold);
1676                        let mut recalls = Vec::new();
1677                        let mut promotions = Vec::new();
1678                        for hit in &hits {
1679                            let trust = match hit.record.provenance.trust {
1680                                crate::mm::memory::MemoryTrustLevel::Untrusted => "untrusted",
1681                                crate::mm::memory::MemoryTrustLevel::UserAsserted => {
1682                                    "user_asserted"
1683                                }
1684                                crate::mm::memory::MemoryTrustLevel::HostVerified => {
1685                                    "host_verified"
1686                                }
1687                            };
1688                            let content = format!(
1689                                "[MEMORY record_id={} trust={} score={:.3}] {}",
1690                                hit.record.record_id, trust, hit.score, hit.record.content
1691                            );
1692                            let tokens = self.sm.ctx.engine.count(&content).max(1);
1693                            self.sm.ctx.push_history(
1694                                crate::types::message::Message::user(content),
1695                                tokens,
1696                            );
1697
1698                            let record_id = hit.record.record_id.clone();
1699                            let before = hit.record.recall_count;
1700                            let after = before.saturating_add(1);
1701                            recalls.push(crate::mm::memory::MemoryRecallLifecycle {
1702                                record_id: record_id.clone(),
1703                                recall_count: after,
1704                                last_recalled_at: turn as u64,
1705                            });
1706                            if let Some(threshold) = promotion_threshold {
1707                                if before < threshold && after >= threshold {
1708                                    promotions.push((record_id, after));
1709                                }
1710                            }
1711                        }
1712                        let continuation = self.sm.resume_after_preload();
1713                        self.sm.observations.push(KernelObservation::MemoryQueried {
1714                            turn,
1715                            scope: query.scope.clone(),
1716                            query: query.query,
1717                            requested_k,
1718                            requires_async_response: false,
1719                        });
1720                        if !recalls.is_empty() {
1721                            self.sm.observations.push(KernelObservation::MemoryRecalled {
1722                                turn,
1723                                scope: query.scope,
1724                                recalls,
1725                            });
1726                        }
1727                        for (record_id, recall_count) in promotions {
1728                            self.sm
1729                                .observations
1730                                .push(KernelObservation::PromotionSuggested {
1731                                    turn,
1732                                    record_id,
1733                                    recall_count,
1734                                });
1735                        }
1736                        continuation
1737                    }
1738                };
1739                // Recall is a deferred host effect in the reasoning path. Re-render after
1740                // committing the hits so the provider continuation observes the recalled
1741                // history instead of leaving the loop parked on an empty step.
1742                return identity.single(continuation, self.sm.take_observations());
1743            }
1744            KernelInputEvent::LargeResultSpoolResult {
1745                effect_id: _,
1746                spool_ref,
1747                error,
1748            } => self.sm.resolve_large_result_spool(spool_ref, error),
1749            KernelInputEvent::PageOutArchiveResult {
1750                effect_id: _,
1751                archive_ref,
1752                error,
1753            } => self.sm.resolve_page_out_archive(archive_ref, error),
1754            KernelInputEvent::SetSchedulerBudget { max_wall_ms } => {
1755                self.sm.set_wall_budget(max_wall_ms);
1756                return identity.empty(self.sm.take_observations());
1757            }
1758            KernelInputEvent::SetResourceQuota { quota } => {
1759                self.sm.set_resource_quota(quota);
1760                return identity.empty(self.sm.take_observations());
1761            }
1762            KernelInputEvent::SetCriteriaGate { enabled } => {
1763                self.sm.set_criteria_gate(enabled);
1764                return identity.empty(self.sm.take_observations());
1765            }
1766            KernelInputEvent::SetKnowledgeBudget { ratio } => {
1767                self.sm.ctx.config.knowledge_budget_ratio = ratio;
1768                return identity.empty(self.sm.take_observations());
1769            }
1770            KernelInputEvent::SetRepeatFuse {
1771                enabled,
1772                deny_after,
1773                terminate_after,
1774            } => {
1775                let mut cfg = self.sm.repeat_fuse_config();
1776                if let Some(e) = enabled {
1777                    cfg.enabled = e;
1778                }
1779                if let Some(d) = deny_after {
1780                    cfg.deny_after = d;
1781                }
1782                if let Some(t) = terminate_after {
1783                    cfg.terminate_after = t;
1784                }
1785                self.sm.set_repeat_fuse(cfg);
1786                return identity.empty(self.sm.take_observations());
1787            }
1788            KernelInputEvent::SetEntropyWatch {
1789                enabled,
1790                threshold,
1791                hysteresis,
1792                cooldown_turns,
1793                notify_model,
1794            } => {
1795                let mut cfg = self.sm.entropy_watch_config();
1796                if let Some(e) = enabled {
1797                    cfg.enabled = e;
1798                }
1799                if let Some(t) = threshold {
1800                    cfg.threshold = t;
1801                }
1802                if let Some(h) = hysteresis {
1803                    cfg.hysteresis = h;
1804                }
1805                if let Some(c) = cooldown_turns {
1806                    cfg.cooldown_turns = c;
1807                }
1808                if let Some(n) = notify_model {
1809                    cfg.notify_model = n;
1810                }
1811                self.sm.set_entropy_watch(cfg);
1812                return identity.empty(self.sm.take_observations());
1813            }
1814            KernelInputEvent::ProviderResult {
1815                effect_id: _,
1816                message,
1817                observed_input_tokens,
1818                observed_output_tokens: _,
1819                now_ms,
1820                stop_reason,
1821            } => {
1822                if let Some(tokens) = observed_input_tokens {
1823                    self.sm.ctx.set_observed_prompt_tokens(tokens);
1824                }
1825                // Feed the clock before the governance gate fires inside `feed`, so the
1826                // rate limiter sees a real timestamp (no-op when no policy is loaded).
1827                if let Some(ms) = now_ms {
1828                    self.sm.set_observed_time(ms);
1829                }
1830                // Stash stop_reason so `feed` can detect an output-cap truncation and drive recovery.
1831                self.sm.set_pending_stop_reason(stop_reason);
1832                self.sm.feed(LoopEvent::LLMResponse { message })
1833            }
1834            KernelInputEvent::ToolResults {
1835                effect_id: _,
1836                results,
1837            } => self.sm.feed(LoopEvent::ToolResults { results }),
1838            KernelInputEvent::ProviderError {
1839                effect_id: _,
1840                message,
1841            } => {
1842                // Reactive recovery is a kernel decision: classify + bounded compact-and-retry,
1843                // returning the next action (retry or honest terminal) through the common tail.
1844                self.sm.recover_from_provider_error(&message)
1845            }
1846            KernelInputEvent::DeliverSignal {
1847                delivery_id,
1848                attempt,
1849                signal,
1850            } => match self.sm.signal_event(
1851                identity.operation_id.clone(),
1852                delivery_id,
1853                attempt,
1854                signal,
1855            ) {
1856                Some(action) => action,
1857                // Non-actionable disposition (queued / observed / ignored / dropped):
1858                // no provider call this step, just the correlated delivery disposition.
1859                None => {
1860                    return identity.empty(self.sm.take_observations());
1861                }
1862            },
1863            KernelInputEvent::MilestoneResult {
1864                effect_id: _,
1865                result,
1866            } => self.sm.feed(LoopEvent::MilestoneResult { result }),
1867            KernelInputEvent::SpawnSubAgent {
1868                spec,
1869                parent_session_id,
1870            } => self.sm.spawn_sub_agent(spec, &parent_session_id),
1871            KernelInputEvent::LoadWorkflow {
1872                spec,
1873                parent_session_id,
1874                resumed_submissions,
1875                resumed_submission_bases,
1876                resumed_outcomes,
1877            } => {
1878                if resumed_outcomes.is_empty() && resumed_submissions.is_empty() {
1879                    self.sm.load_workflow(spec, &parent_session_id)
1880                } else {
1881                    self.sm.load_workflow_resumed(
1882                        spec,
1883                        &parent_session_id,
1884                        &resumed_submissions,
1885                        &resumed_submission_bases,
1886                        &resumed_outcomes,
1887                    )
1888                }
1889            }
1890            KernelInputEvent::SubAgentCompleted { result } => {
1891                self.sm.feed(LoopEvent::SubAgentCompleted { result })
1892            }
1893            KernelInputEvent::SubmitWorkflowNodes {
1894                nodes,
1895                submitter_agent_id,
1896            } => self
1897                .sm
1898                .submit_workflow_nodes(nodes, submitter_agent_id.as_deref()),
1899            KernelInputEvent::SubmitWorkflow {
1900                spec,
1901                parent_session_id,
1902                submitter_agent_id,
1903            } => self
1904                .sm
1905                .submit_workflow(spec, &parent_session_id, submitter_agent_id.as_deref()),
1906            KernelInputEvent::SetMemoryPolicy {
1907                memory_path,
1908                stale_warning_days,
1909                retrieval_top_k,
1910                validation_enabled,
1911                max_content_bytes,
1912                max_name_length,
1913                promotion_recall_threshold,
1914            } => {
1915                // Phase 7: install the memory policy. The kernel enforces validation_enabled +
1916                // retrieval_top_k + size/name overrides at the WriteMemory/QueryMemory traps and
1917                // (M4) promotion_recall_threshold on recall; memory_path / stale_warning_days are
1918                // carried for the SDK's recall I/O. Capacity eviction is host-side (the host store
1919                // owns the full cross-session record set).
1920                self.sm.set_memory_policy(crate::mm::memory::MemoryPolicy {
1921                    memory_path,
1922                    stale_warning_days,
1923                    retrieval_top_k,
1924                    validation_enabled,
1925                    max_content_bytes,
1926                    max_name_length,
1927                    promotion_recall_threshold,
1928                });
1929                return identity.empty(self.sm.take_observations());
1930            }
1931            KernelInputEvent::WriteMemory { memory } => {
1932                // Phase 7: Validate memory write request.
1933                // Kernel validates; SDK performs I/O.
1934                use crate::mm::memory::validate_memory_write;
1935                let turn = self.sm.turn;
1936                // M2: route the write through the syscall trap so the resource quota (write-rate
1937                // limit) applies. A rate-limited / denied write surfaces as a validation failure
1938                // (the write does not happen) and short-circuits before validation.
1939                let disposition = self
1940                    .sm
1941                    .gate_syscall(&crate::syscall::Syscall::WriteMemory(memory.clone()));
1942                if !disposition.is_allowed() {
1943                    let error = match disposition {
1944                        crate::syscall::Disposition::RateLimited { retry_after_ms } => {
1945                            format!("memory write rate limited; retry after {retry_after_ms}ms")
1946                        }
1947                        crate::syscall::Disposition::Deny { reason, .. } => {
1948                            format!("memory write denied: {reason}")
1949                        }
1950                        _ => "memory write not permitted".to_string(),
1951                    };
1952                    self.sm
1953                        .observations
1954                        .push(KernelObservation::MemoryValidationFailed {
1955                            turn,
1956                            record_id: memory.record_id.clone(),
1957                            error,
1958                        });
1959                    return identity.empty(self.sm.take_observations());
1960                }
1961                // Validate honoring any installed memory policy: a policy with validation disabled
1962                // admits the write outright; a policy with size/name overrides validates against
1963                // those; no policy uses the default rules (pre-policy behavior).
1964                let validation_result = match self.sm.memory_policy() {
1965                    Some(p) if !p.validation_enabled => Ok(()),
1966                    Some(p) => p.validation().validate(&memory),
1967                    None => validate_memory_write(&memory),
1968                };
1969                match validation_result {
1970                    Ok(()) => {
1971                        let key = memory.key();
1972                        let mut staged_store = self.memory_records.clone();
1973                        if let Err(error) = staged_store.upsert(memory) {
1974                            self.sm
1975                                .observations
1976                                .push(KernelObservation::MemoryValidationFailed {
1977                                    turn,
1978                                    record_id: error.record_id().to_string(),
1979                                    error: format!("record id conflicts with another scoped key"),
1980                                });
1981                            return identity.empty(self.sm.take_observations());
1982                        }
1983                        let canonical = staged_store
1984                            .get(&key.scope, key.kind, &key.name)
1985                            .expect("scoped upsert must retain its canonical record")
1986                            .clone();
1987                        self.pending_memory_write = Some(canonical.clone());
1988                        self.pending_memory_store = Some(staged_store);
1989                        LoopAction::PersistMemory { memory: canonical }
1990                    }
1991                    Err(err) => {
1992                        // Emit validation error observation
1993                        use crate::mm::memory::MemoryValidationError;
1994                        let error_msg = match err {
1995                            MemoryValidationError::MissingRequiredField { field } => {
1996                                format!("Missing required field: {}", field)
1997                            }
1998                            MemoryValidationError::ContentTooLarge { size, limit } => {
1999                                format!("Content too large: {} bytes (limit: {})", size, limit)
2000                            }
2001                            MemoryValidationError::ForbiddenPattern { pattern, reason } => {
2002                                format!("Forbidden pattern '{}': {}", pattern, reason)
2003                            }
2004                            MemoryValidationError::InvalidKind { kind } => {
2005                                format!("Invalid kind: {}", kind)
2006                            }
2007                            MemoryValidationError::NameTooLong { length, limit } => {
2008                                format!("Name too long: {} chars (limit: {})", length, limit)
2009                            }
2010                        };
2011                        self.sm
2012                            .observations
2013                            .push(KernelObservation::MemoryValidationFailed {
2014                                turn,
2015                                record_id: memory.record_id.clone(),
2016                                error: error_msg,
2017                            });
2018                        return identity.empty(self.sm.take_observations());
2019                    }
2020                }
2021            }
2022            KernelInputEvent::QueryMemory { query } => {
2023                // Phase 7: Query memory for context.
2024                // Kernel emits observation; SDK responds asynchronously.
2025                // An installed policy caps retrieval breadth: requested_k = min(query.top_k, policy).
2026                let requested_k = match self.sm.memory_policy() {
2027                    Some(p) => p.clamp_top_k(query.top_k),
2028                    None => query.top_k,
2029                };
2030                self.pending_memory_query = Some((query.clone(), requested_k));
2031                LoopAction::QueryMemory { query, requested_k }
2032            }
2033            KernelInputEvent::CompleteRun => self.sm.feed(LoopEvent::Complete),
2034            KernelInputEvent::CancelOperation {
2035                operation_id,
2036                reason,
2037                pending_call_ids,
2038            } => {
2039                self.pending_memory_write = None;
2040                self.pending_memory_store = None;
2041                self.pending_memory_query = None;
2042                self.sm
2043                    .cancel_operation(operation_id, reason, pending_call_ids)
2044            }
2045        };
2046        let action = self.sm.externalize_pending_host_effect(action);
2047        if matches!(action, LoopAction::AwaitingResume) {
2048            return identity.empty(self.sm.take_observations());
2049        }
2050        identity.single(action, self.sm.take_observations())
2051    }
2052
2053    fn lifecycle_transition(
2054        &self,
2055        event: &KernelInputEvent,
2056    ) -> Result<LifecycleTransition, String> {
2057        if self.lifecycle.is_terminal() && !matches!(event, KernelInputEvent::DeliverSignal { .. })
2058        {
2059            return Err(format!(
2060                "kernel is terminal in lifecycle {:?}",
2061                self.lifecycle
2062            ));
2063        }
2064
2065        match event {
2066            KernelInputEvent::ConfigureRun { .. } => match self.lifecycle {
2067                KernelLifecycle::Created | KernelLifecycle::Configured => {
2068                    Ok(LifecycleTransition::Configure)
2069                }
2070                _ => Err(format!(
2071                    "configure_run is not valid in lifecycle {:?}",
2072                    self.lifecycle
2073                )),
2074            },
2075            KernelInputEvent::SetSignalPolicy { .. } => match self.lifecycle {
2076                KernelLifecycle::Created | KernelLifecycle::Configured => {
2077                    Ok(LifecycleTransition::Configure)
2078                }
2079                _ => Err(format!(
2080                    "set_signal_policy is not valid in lifecycle {:?}",
2081                    self.lifecycle
2082                )),
2083            },
2084            KernelInputEvent::StartRun { .. } => match self.lifecycle {
2085                KernelLifecycle::Created | KernelLifecycle::Configured => {
2086                    Ok(LifecycleTransition::Start)
2087                }
2088                _ => Err(format!(
2089                    "start_run is not valid in lifecycle {:?}",
2090                    self.lifecycle
2091                )),
2092            },
2093            KernelInputEvent::Resume => match self.lifecycle {
2094                KernelLifecycle::Configured => Ok(LifecycleTransition::Resume),
2095                _ => Err(format!(
2096                    "resume is not valid in lifecycle {:?}",
2097                    self.lifecycle
2098                )),
2099            },
2100            KernelInputEvent::ApprovalResult { .. }
2101            | KernelInputEvent::WorkflowSpawnResult { .. }
2102            | KernelInputEvent::PreemptResult { .. } => match self.lifecycle {
2103                KernelLifecycle::Suspended => Ok(LifecycleTransition::Resume),
2104                _ => Err(format!(
2105                    "effect result is not valid in lifecycle {:?}",
2106                    self.lifecycle
2107                )),
2108            },
2109            KernelInputEvent::MemoryPersistResult { .. }
2110            | KernelInputEvent::LargeResultSpoolResult { .. }
2111            | KernelInputEvent::PageOutArchiveResult { .. } => match self.lifecycle {
2112                KernelLifecycle::Configured | KernelLifecycle::Running => {
2113                    Ok(LifecycleTransition::Stay)
2114                }
2115                _ => Err(format!(
2116                    "memory effect result is not valid in lifecycle {:?}",
2117                    self.lifecycle
2118                )),
2119            },
2120            KernelInputEvent::MemoryQueryResult { .. } => match self.lifecycle {
2121                KernelLifecycle::Configured | KernelLifecycle::Running => {
2122                    Ok(LifecycleTransition::Resume)
2123                }
2124                _ => Err(format!(
2125                    "memory query result is not valid in lifecycle {:?}",
2126                    self.lifecycle
2127                )),
2128            },
2129            KernelInputEvent::ProviderResult { .. }
2130            | KernelInputEvent::ProviderError { .. }
2131            | KernelInputEvent::ToolResults { .. }
2132            | KernelInputEvent::MilestoneResult { .. }
2133            | KernelInputEvent::LoadWorkflow { .. }
2134            | KernelInputEvent::SpawnSubAgent { .. } => match self.lifecycle {
2135                KernelLifecycle::Running => Ok(LifecycleTransition::Stay),
2136                _ => Err(format!(
2137                    "execution input is not valid in lifecycle {:?}",
2138                    self.lifecycle
2139                )),
2140            },
2141            KernelInputEvent::SubmitWorkflow { .. }
2142            | KernelInputEvent::SubmitWorkflowNodes { .. }
2143            | KernelInputEvent::CompleteRun
2144            | KernelInputEvent::CancelOperation { .. } => match self.lifecycle {
2145                KernelLifecycle::Running | KernelLifecycle::Suspended => {
2146                    Ok(LifecycleTransition::Stay)
2147                }
2148                _ => Err(format!(
2149                    "execution input is not valid in lifecycle {:?}",
2150                    self.lifecycle
2151                )),
2152            },
2153            KernelInputEvent::DeliverSignal { .. } => match self.lifecycle {
2154                KernelLifecycle::Running
2155                | KernelLifecycle::Suspended
2156                | KernelLifecycle::Completed
2157                | KernelLifecycle::Failed
2158                | KernelLifecycle::Cancelled => Ok(LifecycleTransition::Stay),
2159                _ => Err(format!(
2160                    "deliver_signal is not valid in lifecycle {:?}",
2161                    self.lifecycle
2162                )),
2163            },
2164            KernelInputEvent::SubAgentCompleted { .. } => match self.lifecycle {
2165                KernelLifecycle::Running | KernelLifecycle::Suspended => {
2166                    Ok(LifecycleTransition::Resume)
2167                }
2168                _ => Err(format!(
2169                    "sub_agent_completed is not valid in lifecycle {:?}",
2170                    self.lifecycle
2171                )),
2172            },
2173            _ => match self.lifecycle {
2174                KernelLifecycle::Suspended => Err(
2175                    "only resume, sub_agent_completed, or cancellation is valid while suspended"
2176                        .to_string(),
2177                ),
2178                KernelLifecycle::Created => Ok(LifecycleTransition::Configure),
2179                _ => Ok(LifecycleTransition::Stay),
2180            },
2181        }
2182    }
2183
2184    fn advance_lifecycle(&mut self, transition: LifecycleTransition, step: &KernelStep) {
2185        if let Some(result) = step.actions.iter().find_map(|action| match &action.effect {
2186            KernelEffect::Done { result } => Some(result),
2187            _ => None,
2188        }) {
2189            self.lifecycle = match result.termination {
2190                crate::types::result::TerminationReason::Completed => KernelLifecycle::Completed,
2191                crate::types::result::TerminationReason::UserAbort => KernelLifecycle::Cancelled,
2192                _ => KernelLifecycle::Failed,
2193            };
2194            return;
2195        }
2196
2197        if self.sm.is_suspended() {
2198            self.lifecycle = KernelLifecycle::Suspended;
2199            return;
2200        }
2201
2202        self.lifecycle = match transition {
2203            LifecycleTransition::Configure if self.lifecycle == KernelLifecycle::Created => {
2204                KernelLifecycle::Configured
2205            }
2206            LifecycleTransition::Start | LifecycleTransition::Resume => KernelLifecycle::Running,
2207            _ => self.lifecycle,
2208        };
2209    }
2210
2211    fn allocate_step_seq(&mut self) -> u64 {
2212        let step_seq = self.next_step_seq;
2213        self.next_step_seq = self.next_step_seq.saturating_add(1);
2214        step_seq
2215    }
2216
2217    fn fault_step(
2218        &mut self,
2219        operation_id: String,
2220        event_id: String,
2221        code: KernelFaultCode,
2222        message: String,
2223        effect_id: Option<String>,
2224    ) -> KernelStep {
2225        // Rejected transactions do not consume a committed step identity. This keeps effect IDs
2226        // deterministic across retries and lets snapshots rebuild from accepted inputs only.
2227        let step_seq = self.next_step_seq;
2228        KernelStep::fault(
2229            operation_id.clone(),
2230            event_id.clone(),
2231            step_seq,
2232            KernelFault {
2233                code,
2234                message,
2235                operation_id: Some(operation_id),
2236                event_id: Some(event_id),
2237                effect_id,
2238            },
2239        )
2240    }
2241}
2242
2243fn normalize_input(input: &mut KernelInput) {
2244    if let KernelInputEvent::CancelOperation {
2245        pending_call_ids, ..
2246    } = &mut input.event
2247    {
2248        pending_call_ids.sort();
2249        pending_call_ids.dedup();
2250    }
2251}
2252
2253fn transaction_fault_step(
2254    operation_id: String,
2255    event_id: String,
2256    step_seq: u64,
2257    code: KernelFaultCode,
2258    message: String,
2259) -> KernelStep {
2260    KernelStep::fault(
2261        operation_id.clone(),
2262        event_id.clone(),
2263        step_seq,
2264        KernelFault {
2265            code,
2266            message,
2267            operation_id: Some(operation_id),
2268            event_id: Some(event_id),
2269            effect_id: None,
2270        },
2271    )
2272}
2273
2274fn transaction_conflict_fault(operation_id: Option<String>, message: String) -> KernelFault {
2275    KernelFault {
2276        code: KernelFaultCode::TransactionConflict,
2277        message,
2278        operation_id,
2279        event_id: None,
2280        effect_id: None,
2281    }
2282}
2283
2284fn snapshot_fault(operation_id: Option<String>, message: String) -> KernelFault {
2285    KernelFault {
2286        code: KernelFaultCode::SnapshotIncompatible,
2287        message,
2288        operation_id,
2289        event_id: None,
2290        effect_id: None,
2291    }
2292}
2293
2294fn result_effect(event: &KernelInputEvent) -> Option<(&str, PendingEffectKind)> {
2295    match event {
2296        KernelInputEvent::ProviderResult { effect_id, .. }
2297        | KernelInputEvent::ProviderError { effect_id, .. } => {
2298            Some((effect_id, PendingEffectKind::Provider))
2299        }
2300        KernelInputEvent::ToolResults { effect_id, .. } => {
2301            Some((effect_id, PendingEffectKind::Tool))
2302        }
2303        KernelInputEvent::MilestoneResult { effect_id, .. } => {
2304            Some((effect_id, PendingEffectKind::Milestone))
2305        }
2306        KernelInputEvent::ApprovalResult { effect_id, .. } => {
2307            Some((effect_id, PendingEffectKind::Approval))
2308        }
2309        KernelInputEvent::WorkflowSpawnResult { effect_id, .. } => {
2310            Some((effect_id, PendingEffectKind::WorkflowSpawn))
2311        }
2312        KernelInputEvent::PreemptResult { effect_id, .. } => {
2313            Some((effect_id, PendingEffectKind::Preempt))
2314        }
2315        KernelInputEvent::MemoryPersistResult { effect_id, .. } => {
2316            Some((effect_id, PendingEffectKind::MemoryPersist))
2317        }
2318        KernelInputEvent::MemoryQueryResult { effect_id, .. } => {
2319            Some((effect_id, PendingEffectKind::MemoryQuery))
2320        }
2321        KernelInputEvent::LargeResultSpoolResult { effect_id, .. } => {
2322            Some((effect_id, PendingEffectKind::LargeResultSpool))
2323        }
2324        KernelInputEvent::PageOutArchiveResult { effect_id, .. } => {
2325            Some((effect_id, PendingEffectKind::PageOutArchive))
2326        }
2327        _ => None,
2328    }
2329}
2330
2331fn pending_effect_kind(effect: &KernelEffect) -> Option<PendingEffectKind> {
2332    match effect {
2333        KernelEffect::CallProvider { .. } => Some(PendingEffectKind::Provider),
2334        KernelEffect::ExecuteTool { .. } => Some(PendingEffectKind::Tool),
2335        KernelEffect::EvaluateMilestone { .. } => Some(PendingEffectKind::Milestone),
2336        KernelEffect::RequestApproval { .. } => Some(PendingEffectKind::Approval),
2337        KernelEffect::SpawnWorkflow { .. } => Some(PendingEffectKind::WorkflowSpawn),
2338        KernelEffect::PreemptSubAgents { .. } => Some(PendingEffectKind::Preempt),
2339        KernelEffect::PersistMemory { .. } => Some(PendingEffectKind::MemoryPersist),
2340        KernelEffect::QueryMemory { .. } => Some(PendingEffectKind::MemoryQuery),
2341        KernelEffect::SpoolLargeResult { .. } => Some(PendingEffectKind::LargeResultSpool),
2342        KernelEffect::ArchivePageOut { .. } => Some(PendingEffectKind::PageOutArchive),
2343        KernelEffect::Done { .. } => None,
2344    }
2345}
2346
2347fn validate_run_config(config: &RunConfig, max_tokens: u32) -> Result<(), String> {
2348    if config
2349        .budget_grant
2350        .as_ref()
2351        .is_some_and(|grant| grant.reservation_id.is_empty())
2352    {
2353        return Err("budget_grant reservation_id must be non-empty".to_string());
2354    }
2355    if let Some(reliability) = &config.reliability {
2356        for (name, capacity) in [
2357            ("event_replay_capacity", reliability.event_replay_capacity),
2358            (
2359                "completed_effect_replay_capacity",
2360                reliability.completed_effect_replay_capacity,
2361            ),
2362        ] {
2363            if let Some(capacity) = capacity {
2364                if !(1..=65_536).contains(&capacity) {
2365                    return Err(format!("{name} must be between 1 and 65536"));
2366                }
2367            }
2368        }
2369        if reliability
2370            .snapshot_input_limit
2371            .is_some_and(|value| !(1..=100_000).contains(&value))
2372        {
2373            return Err("snapshot_input_limit must be between 1 and 100000".to_string());
2374        }
2375        if reliability
2376            .max_input_bytes
2377            .is_some_and(|value| !(256..=64 * 1024 * 1024).contains(&value))
2378        {
2379            return Err("max_input_bytes must be between 256 and 67108864".to_string());
2380        }
2381        if reliability
2382            .snapshot_journal_bytes_limit
2383            .is_some_and(|value| !(256..=1024 * 1024 * 1024).contains(&value))
2384        {
2385            return Err(
2386                "snapshot_journal_bytes_limit must be between 256 and 1073741824".to_string(),
2387            );
2388        }
2389        if reliability
2390            .provider_recovery_attempts
2391            .is_some_and(|value| value > 16)
2392        {
2393            return Err("provider_recovery_attempts must be at most 16".to_string());
2394        }
2395        if reliability
2396            .output_recovery_attempts
2397            .is_some_and(|value| value > 16)
2398        {
2399            return Err("output_recovery_attempts must be at most 16".to_string());
2400        }
2401        if reliability
2402            .host_effect_retry_attempts
2403            .is_some_and(|value| value > 16)
2404        {
2405            return Err("host_effect_retry_attempts must be at most 16".to_string());
2406        }
2407        let threshold = reliability.spool_threshold_bytes.unwrap_or(50 * 1024);
2408        let preview = reliability.spool_preview_bytes.unwrap_or(2 * 1024);
2409        if threshold == 0 {
2410            return Err("spool_threshold_bytes must be greater than zero".to_string());
2411        }
2412        if preview == 0 || preview > threshold {
2413            return Err(
2414                "spool_preview_bytes must be greater than zero and no larger than spool_threshold_bytes"
2415                    .to_string(),
2416            );
2417        }
2418    }
2419    if let Some(policy) = &config.signal_policy {
2420        validate_signal_policy(policy)?;
2421    }
2422    if config
2423        .prompt_budget
2424        .is_some_and(|budget| budget.reserved_tokens() >= max_tokens)
2425    {
2426        return Err(
2427            "prompt_budget reserves must leave at least one token for provider input".to_string(),
2428        );
2429    }
2430    if let Some(policy) = &config.context_policy {
2431        policy.validate()?;
2432    }
2433    if let Some(policy) = config.scheduler_policy {
2434        policy.validate()?;
2435    }
2436    if let Some(ratio) = config.knowledge_budget_ratio {
2437        if !ratio.is_finite() || !(0.0..=1.0).contains(&ratio) {
2438            return Err("knowledge_budget_ratio must be finite and between 0 and 1".to_string());
2439        }
2440    }
2441    if let Some(fuse) = config.repeat_fuse {
2442        if fuse.enabled
2443            && fuse.deny_after > 0
2444            && fuse.terminate_after > 0
2445            && fuse.deny_after >= fuse.terminate_after
2446        {
2447            return Err("repeat_fuse terminate_after must be greater than deny_after".to_string());
2448        }
2449    }
2450    if let Some(watch) = config.entropy_watch {
2451        if !watch.threshold.is_finite() || !(0.0..=1.0).contains(&watch.threshold) {
2452            return Err("entropy_watch threshold must be finite and between 0 and 1".to_string());
2453        }
2454        if !watch.hysteresis.is_finite()
2455            || watch.hysteresis < 0.0
2456            || watch.hysteresis > watch.threshold
2457        {
2458            return Err(
2459                "entropy_watch hysteresis must be finite and no greater than threshold".to_string(),
2460            );
2461        }
2462    }
2463    if let Some(quota) = &config.resource_quota {
2464        if matches!(quota.memory_writes_per_window, Some((_, 0))) {
2465            return Err("memory write quota window must be greater than zero".to_string());
2466        }
2467    }
2468    if let Some(governance) = &config.governance {
2469        if governance
2470            .rate_limits
2471            .iter()
2472            .any(|limit| limit.window_ms == 0)
2473        {
2474            return Err("governance rate-limit windows must be greater than zero".to_string());
2475        }
2476        for constraint in &governance.constraints {
2477            if let ConstraintSpec::Range {
2478                min: Some(min),
2479                max: Some(max),
2480                ..
2481            } = constraint
2482            {
2483                if !min.is_finite() || !max.is_finite() || min > max {
2484                    return Err(
2485                        "governance range constraints require finite min <= max".to_string()
2486                    );
2487                }
2488            }
2489        }
2490    }
2491    Ok(())
2492}
2493
2494fn validate_signal_policy(policy: &SignalPolicyConfig) -> Result<(), String> {
2495    if policy.version != SIGNAL_POLICY_VERSION {
2496        return Err(format!(
2497            "signal_policy version must be {SIGNAL_POLICY_VERSION}"
2498        ));
2499    }
2500    if policy.queue_max == 0 {
2501        return Err("signal_policy.queue_max must be greater than zero".to_string());
2502    }
2503    if matches!(policy.ttl_ms, Some(0)) {
2504        return Err("signal_policy.ttl_ms must be greater than zero when present".to_string());
2505    }
2506    Ok(())
2507}