Skip to main content

deepstrike_core/runtime/kernel/wire/
driver.rs

1//! The canonical operation driver — the plan function of [`KernelTransaction`] (spec §5.4, §6, §7.4,
2//! §10.1, §10.2).
3//!
4//! This is the layer the migration's semantic half turns on. Everything above it is contract
5//! (envelope → record → transaction); everything below it is the kernel's existing semantic
6//! machinery (the scheduler's [`LoopStateMachine`], the P1 syscall gate, the P2 task table, the P3
7//! context VM). The driver is the **only** place the two meet, and §5.4 fixes exactly how: every
8//! wire input reduces to a P1/P2/P3 primitive, and none of them is allowed to grow a parallel
9//! business state machine here.
10//!
11//! Three properties define it:
12//!
13//! 1. **No protocol adapter.** The driver reads [`NormalizedPayload`] directly and reduces it to
14//!    scheduler/context primitives such as `LoopStateMachine::start`, `load_workflow`,
15//!    `resolve_workflow_spawn`, and `feed`. The canonical envelope is the only wire contract.
16//! 2. **`RootKind` is immutable and `ExecutionFocus` moves only on a committed transition**
17//!    (§6.1.5/6.1.6, §7.4). Both live in [`CanonicalOperationDriver`], and `plan` never writes
18//!    them — it *stages* the next value, and [`CanonicalOperationDriver::note_committed`] is what
19//!    installs it. There is no input, host command or otherwise, that sets a focus directly.
20//! 3. **A root start is one atomic input.** `ConfigureOperation` builds the engine;
21//!    `StartOperation` seeds the initial context, enters the root, and publishes the first effect
22//!    — a provider call for an agent root, a task spawn for a workflow root — in the *same*
23//!    planned step. The historical 12+ separate accepted inputs before a first provider call
24//!    (§3.3 item 20) have no equivalent path here.
25//! 4. **One resolution entry, one decision per failure** (§7.9). Every pending effect — provider,
26//!    tools, approval, spawn, preempt, memory, page-out, milestone — is answered through
27//!    `ResolveEffect` and nothing else, and a `Failed` outcome buys exactly one policy decision:
28//!    abandon, switch recovery ladder, or commit a terminal. The kernel never re-emits the same
29//!    intent (DEC-5), so the historical unbounded `retry_approval` / `retry_workflow_spawn` /
30//!    `retry_preempt` round trips are not expressible on this path. `ContextOverflow` is not a
31//!    failure at all: it is the one *semantic* provider outcome, and it feeds the compaction ladder.
32//!
33//! ### What `plan` may mutate
34//!
35//! [`KernelTransaction::prepare`] guarantees that a non-`Prepared` outcome leaves the *transaction*
36//! byte-for-byte unchanged, and it can reject after the planner has already run (an unsupported
37//! effect kind, a duplicate effect identity, the tail hard limit). The driver answers that in two
38//! layers:
39//!
40//! * every refusal the driver itself owns — root authority, focus depth, an unreducible input — is
41//!   decided **before** the semantic engine is touched, so it is a genuine zero-mutation rejection;
42//! * the engine advance that a successful plan performs is guarded by a staging slot. A second
43//!   `plan` without an intervening `note_committed` means the previous plan was discarded while the
44//!   engine had already moved, and the driver fails closed with a poison fault that names the only
45//!   legal recovery — rebuild from the journal (§8.3).
46
47use std::collections::{BTreeMap, BTreeSet};
48
49use serde::Serialize;
50
51use super::checkpoint::{
52    AuthoredMemoryQueryState, AuthoredMemoryWriteState, ChildProcessState, ContextVmStateV1,
53    EntropyState, EntropyTurnState, HandleState, InlineMessageBody, KnowledgeSlotState,
54    LogicalCompressionEntry, LogicalKernelState, LogicalPlanStep, LogicalStateProjection,
55    LogicalTaskState, LogicalToolCall, MessagePartition, MilestoneState, PartitionTokenState,
56    PendingPayloadLoadState, PendingProviderCallState, QueuedSignalState, ReferencedMessageBody,
57    SchedulerStateV1, SkillLeaseState, StoredMessageBody, StoredMessageState,
58    StructuredMessageBody, SyscallStateV1, TaskAttemptState, TaskControlState, WorkflowGraphState,
59    WorkflowNodeState,
60};
61use super::command::{
62    ApplyCapabilityPatchCommand, ApplyKnowledgeMutationCommand, ApplyPolicyPatchCommand,
63    ApplySkillActivationCommand, CancelCommand, CancellationReason, HostCommand, LivePolicyState,
64    SeedKnowledgeCommand, TaskUpdate as WireTaskUpdate, UpdateDeadlineCommand, UpdateTaskCommand,
65};
66use super::config::ResolvedOperationConfig;
67use super::effect::{
68    ApprovalRequest as WireApprovalRequest, ArchivePageOutEffect, CallProviderEffect,
69    CanonicalMemoryQuery, CanonicalMemoryWrite, EffectKind, EffectKindTag, EffectOutcome,
70    EffectSuccess, EvaluateMilestoneEffect, ExecuteToolsEffect, HostEffectFailure, KernelEffect,
71    LaunchToken, LoadPayloadEffect, PageOutPayload, PayloadRef, PersistMemoryEffect,
72    PreemptTasksEffect, ProviderCompleted, ProviderMessage, ProviderOutcome, QueryMemoryEffect,
73    RenderedContext as WireRenderedContext, RequestApprovalEffect, SpawnTasksEffect,
74    TaskAttemptRef, TaskLaunch, ToolCall as WireToolCall, ToolResultDisposition,
75    ToolResultPayload as WireToolResultPayload, ToolSchema as WireToolSchema,
76    WorkflowBudget as WireWorkflowBudget,
77};
78use super::envelope::{OperationLifecycle, ResolveEffect};
79use super::event::{
80    ChildCompleted, ChildStatus, DeliverSignal, ExternalEvent, LogicalSignal, SignalSourceKind,
81    SignalTarget, SignalUrgency,
82};
83use super::fault::{KernelFault, KernelFaultCode};
84use super::record::NormalizedPayload;
85use super::root::{
86    AgentIsolation as WireIsolation, AgentRole as WireRole, ExecutionFocus, InitialContext,
87    LogicalAgentSpec, LogicalContextInheritance as WireContextInheritance, LogicalTask,
88    MessageRole, RootEntry, RootKind, WorkflowNode as WireNode, WorkflowSpec as WireSpec,
89};
90use super::scalar::{
91    AttemptId, EffectId, MemoryBindingId, NodeId, OperationId, TaskId, WireU64, WorkflowId,
92};
93use super::syscall::{
94    ChildAttemptCausation, MemoryKind as WireMemoryKind, ProviderToolCausation, SyscallCausation,
95    SyscallRequest,
96};
97use super::terminal::{
98    AgentTerminal, CancelledTerminal, EffectsDisposition, FailedTerminal, KernelFailure,
99    KernelFailureCode, KernelTerminal, LoopResult as WireLoopResult, StepDisposition,
100    TerminalDisposition, TerminationReason as WireTermination, UsageReport, WorkflowOutcome,
101    WorkflowStatus, WorkflowTerminal,
102};
103use super::transaction::{PlanContext, TransitionStep};
104
105use crate::context::manager::READ_RESULT_TOOL_NAME;
106use crate::context::task_state::{CompressionEntry, PlanStep, TaskState};
107use crate::mm::handle::{Handle, HandleKind, Residency};
108use crate::orchestration::task_graph::TaskStatus;
109use crate::orchestration::workflow::run::{WorkflowNodeStatus, WorkflowRuntimeNodeState};
110use crate::orchestration::workflow::{
111    WorkflowNode as CoreWorkflowNode, WorkflowSpec as CoreWorkflowSpec,
112};
113use crate::runtime::kernel::{KernelObservation, WorkflowSpawnFailure};
114use crate::scheduler::policy::SchedulerBudget;
115use crate::scheduler::state_machine::{
116    AdjudicatedTurn, AnsweredCall, IdleContinuation, LoopAction, LoopEvent, LoopStateMachine,
117};
118use crate::scheduler::tcb::{BudgetLedger, ProcInfo, TaskLifecycle, Tcb, WaitReason};
119use crate::signals::queue::QueuedSignalRuntimeState;
120use crate::signals::router::SignalRouterRuntimeState;
121use crate::syscall::{Disposition, Syscall as CoreSyscall};
122use crate::types::agent::{
123    AgentCapabilityFilter, AgentIdentity, AgentIsolation, AgentRole, AgentRunSpec,
124    ContextInheritance, LoopRoundSpec,
125};
126use crate::types::message::{Content, ContentPart, Message, Role, ToolErrorKind, ToolResult};
127use crate::types::result::{
128    LoopResult, PaceAction as CorePaceAction, SubAgentResult, TerminationReason,
129};
130use crate::types::signal::{RuntimeSignal, SignalSource, SignalType, Urgency};
131use crate::types::task::{RuntimeTask, TaskLane};
132
133// ---------------------------------------------------------------------------------------------
134// the planned step
135// ---------------------------------------------------------------------------------------------
136
137/// One planned transition, as the canonical driver produces it.
138///
139/// The record freezes only this value's **digest** (§22.12), so its shape is what a rebuild has to
140/// reproduce bit-for-bit. Three fields, each load-bearing:
141///
142/// * `root_kind` — the operation's immutable root class *after* this step. Present from the root
143///   start onward and never different from the value the start committed;
144/// * `focus` — the execution focus after this step. Because it is inside the digest, a focus that
145///   moved differently on a replay is a `RecordCorrupted` rebuild failure rather than a silent
146///   divergence;
147/// * `observations` — facts produced by this exact transition, published only after commit;
148/// * `disposition` — effects **or** a terminal, never both (§7.12).
149#[derive(Debug, Clone, Serialize)]
150pub struct PlannedStep {
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub root_kind: Option<RootKind>,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub focus: Option<ExecutionFocus>,
155    #[serde(skip_serializing_if = "Vec::is_empty")]
156    pub observations: Vec<KernelObservation>,
157    pub disposition: StepDisposition,
158}
159
160impl PartialEq for PlannedStep {
161    fn eq(&self, other: &Self) -> bool {
162        self.root_kind == other.root_kind
163            && self.focus == other.focus
164            && self.disposition == other.disposition
165            && serde_json::to_vec(&self.observations).ok()
166                == serde_json::to_vec(&other.observations).ok()
167    }
168}
169
170impl PlannedStep {
171    fn quiet(root_kind: Option<RootKind>, focus: Option<ExecutionFocus>) -> Self {
172        Self {
173            root_kind,
174            focus,
175            observations: Vec::new(),
176            disposition: StepDisposition::Effects(EffectsDisposition::default()),
177        }
178    }
179}
180
181impl TransitionStep for PlannedStep {
182    fn disposition(&self) -> &StepDisposition {
183        &self.disposition
184    }
185}
186
187// ---------------------------------------------------------------------------------------------
188// the driver
189// ---------------------------------------------------------------------------------------------
190
191/// The internal id of the root agent task. The kernel's task table has used it since M1d; the
192/// canonical `TaskId` is the same string so an `AgentTurn` focus names a row that exists.
193pub const ROOT_TASK_ID: &str = "root";
194
195/// The session slot the canonical path fills on the legacy `AgentIdentity` that `AgentRunSpec`
196/// still declares (§22.6 · Task 11): nothing. Host session identity is not a kernel fact, the
197/// canonical wire has no field for one, and the field itself is deleted with the legacy input enum
198/// in Task 23 — until then the canonical path must be the proof that it is never populated.
199const NO_HOST_SESSION: &str = "";
200
201#[derive(Debug, Clone, PartialEq)]
202struct StagedFocus {
203    step_seq: WireU64,
204    root_kind: Option<RootKind>,
205    focus: Option<ExecutionFocus>,
206}
207
208/// What the kernel published on one provider call, kept so a `ProviderTool` causation can be
209/// *derived* rather than believed (§7.6).
210///
211/// Two facts, both kernel-owned:
212///
213/// * `task_id` — the task whose turn issued the call. This is the caller a syscall inherits; no
214///   host names it;
215/// * `exposed_tools` — exactly the surface that turn advertised. A tool call naming anything else
216///   has no causation to derive from, so it is refused rather than adjudicated.
217#[derive(Debug, Clone, PartialEq)]
218struct PendingProviderCall {
219    task_id: TaskId,
220    exposed_tools: BTreeSet<String>,
221}
222
223/// A syscall the driver refused *after* its caller was established — a malformed request, a gate
224/// denial, a quarantined caller, a memory binding the operation does not hold.
225///
226/// Deliberately not a [`KernelFault`]. §7.7's GAP-4 and the §7.6 fixture both require that a
227/// refused request leaves an **audit fact** and no derived action, while the transition it arrived
228/// on still commits: a child's execution is not undone because the parent denied one of the
229/// requests it attached, and a model's bad argument is not a host protocol violation.
230#[derive(Debug, Clone, PartialEq)]
231struct SyscallRejection {
232    operation: &'static str,
233    /// The derived caller. Present on every rejection raised after causation succeeded, which is
234    /// all of them — an audit fact that cannot name who asked is not an audit fact.
235    subject: Option<String>,
236    reason: String,
237}
238
239impl SyscallRejection {
240    fn new(operation: &'static str, reason: impl Into<String>) -> Self {
241        Self {
242            operation,
243            subject: None,
244            reason: reason.into(),
245        }
246    }
247
248    fn by(mut self, caller: &TaskId) -> Self {
249        self.subject = Some(caller.as_str().to_string());
250        self
251    }
252}
253
254/// The two ways a syscall can fail, kept apart on purpose (§7.13 vs §7.7 GAP-4).
255#[derive(Debug, Clone)]
256enum SyscallRefusal {
257    /// **Who** could not be established, or the transition itself is inadmissible. Zero mutation,
258    /// and the whole input is refused.
259    Fault(KernelFault),
260    /// **What** was asked is refused. The caller was established, so the answer is an audit fact
261    /// the model reads on its next turn; the transition still commits.
262    Rejected(SyscallRejection),
263}
264
265/// Stable wire label of a handle kind, for the §12.1 context-VM projection.
266///
267/// Written out rather than derived from the internal enum's serde rename: the checkpoint's
268/// vocabulary is a contract, and a rename inside `mm::handle` must break this match arm instead of
269/// silently changing what a stored checkpoint means.
270fn handle_kind_label(kind: &HandleKind) -> &'static str {
271    match kind {
272        HandleKind::ToolResult => "tool_result",
273        HandleKind::MemoryPage => "memory_page",
274        HandleKind::KnowledgeEntry => "knowledge_entry",
275        HandleKind::SubAgentJoin => "sub_agent_join",
276    }
277}
278
279fn role_label(role: Role) -> &'static str {
280    match role {
281        Role::System => "system",
282        Role::User => "user",
283        Role::Assistant => "assistant",
284        Role::Tool => "tool",
285    }
286}
287
288fn role_from_label(label: &str) -> Option<Role> {
289    match label {
290        "system" => Some(Role::System),
291        "user" => Some(Role::User),
292        "assistant" => Some(Role::Assistant),
293        "tool" => Some(Role::Tool),
294        _ => None,
295    }
296}
297
298/// Reduce a stored message to `(text, tool_call_id, is_error)`, or report that it does not reduce.
299///
300/// `None` means "this body is multimodal": it carries an image or audio part, and flattening it to
301/// the text parts beside it would drop content a restore could never get back. Those travel as
302/// [`StoredMessageBody::Structured`] instead.
303#[allow(clippy::type_complexity)]
304fn message_body_parts(message: &Message) -> Option<(String, Option<String>, bool)> {
305    match &message.content {
306        Content::Text(text) => Some((text.clone(), None, false)),
307        Content::Parts(parts) => {
308            let mut text = String::new();
309            let mut tool_call_id = None;
310            let mut is_error = false;
311            for part in parts {
312                match part {
313                    ContentPart::Text { text: chunk } => text.push_str(chunk),
314                    ContentPart::ToolResult {
315                        call_id,
316                        output,
317                        is_error: failed,
318                    } => {
319                        if tool_call_id.is_some() {
320                            // Two results in one message have two call ids; the pair projection
321                            // holds one. Carry the whole content instead of picking a winner.
322                            return None;
323                        }
324                        tool_call_id = Some(call_id.to_string());
325                        text.push_str(output);
326                        is_error = *failed;
327                    }
328                    ContentPart::Image { .. } | ContentPart::Audio { .. } => return None,
329                }
330            }
331            Some((text, tool_call_id, is_error))
332        }
333    }
334}
335
336/// Rebuild the stored content a [`StoredMessageBody`] describes.
337///
338/// The exact inverse of [`message_body_parts`]: a body with a `tool_call_id` was a single-part tool
339/// result and goes back as one, everything else was flat text.
340fn message_content(text: String, tool_call_id: Option<&str>, is_error: bool) -> Content {
341    match tool_call_id {
342        Some(call_id) => Content::Parts(vec![ContentPart::ToolResult {
343            call_id: call_id.into(),
344            output: text,
345            is_error,
346        }]),
347        None => Content::Text(text),
348    }
349}
350
351fn workflow_kind_label(state: &WorkflowRuntimeNodeState) -> &'static str {
352    match state.node.kind {
353        crate::orchestration::workflow::NodeKind::Spawn => "spawn",
354        crate::orchestration::workflow::NodeKind::Loop { .. } => "loop",
355        crate::orchestration::workflow::NodeKind::Classify { .. } => "classify",
356        crate::orchestration::workflow::NodeKind::Tournament { .. } => "tournament",
357        crate::orchestration::workflow::NodeKind::Reduce { .. } => "reduce",
358    }
359}
360
361fn workflow_status_label(status: TaskStatus) -> &'static str {
362    match status {
363        TaskStatus::Pending => "pending",
364        TaskStatus::Ready => "ready",
365        TaskStatus::Running => "running",
366        TaskStatus::Completed => "completed",
367        TaskStatus::CompletedPartial => "completed_partial",
368        TaskStatus::Failed => "failed",
369        TaskStatus::SkippedUpstreamFailed => "skipped_upstream_failed",
370    }
371}
372
373fn restore_workflow_status(label: &str) -> Result<TaskStatus, KernelFault> {
374    match label {
375        "pending" => Ok(TaskStatus::Pending),
376        "ready" => Ok(TaskStatus::Ready),
377        "running" => Ok(TaskStatus::Running),
378        "completed" => Ok(TaskStatus::Completed),
379        "completed_partial" => Ok(TaskStatus::CompletedPartial),
380        "failed" => Ok(TaskStatus::Failed),
381        "skipped_upstream_failed" => Ok(TaskStatus::SkippedUpstreamFailed),
382        other => Err(KernelFault::new(
383            KernelFaultCode::CheckpointIncompatible,
384            format!("workflow checkpoint carries unknown node status {other:?}"),
385        )),
386    }
387}
388
389fn agent_role_label(role: AgentRole) -> &'static str {
390    match role {
391        AgentRole::Explore => "explore",
392        AgentRole::Plan => "plan",
393        AgentRole::Implement => "implement",
394        AgentRole::Verify => "verify",
395        AgentRole::Custom => "custom",
396    }
397}
398
399fn restore_agent_role(label: &str) -> Result<AgentRole, KernelFault> {
400    match label {
401        "explore" => Ok(AgentRole::Explore),
402        "plan" => Ok(AgentRole::Plan),
403        "implement" => Ok(AgentRole::Implement),
404        "verify" => Ok(AgentRole::Verify),
405        "custom" => Ok(AgentRole::Custom),
406        other => Err(KernelFault::new(
407            KernelFaultCode::CheckpointIncompatible,
408            format!("child process carries unknown role {other:?}"),
409        )),
410    }
411}
412
413fn agent_isolation_label(isolation: AgentIsolation) -> &'static str {
414    match isolation {
415        AgentIsolation::Shared => "shared",
416        AgentIsolation::ReadOnly => "read_only",
417        AgentIsolation::Worktree => "worktree",
418        AgentIsolation::Remote => "remote",
419    }
420}
421
422fn restore_agent_isolation(label: &str) -> Result<AgentIsolation, KernelFault> {
423    match label {
424        "shared" => Ok(AgentIsolation::Shared),
425        "read_only" => Ok(AgentIsolation::ReadOnly),
426        "worktree" => Ok(AgentIsolation::Worktree),
427        "remote" => Ok(AgentIsolation::Remote),
428        other => Err(KernelFault::new(
429            KernelFaultCode::CheckpointIncompatible,
430            format!("child process carries unknown isolation {other:?}"),
431        )),
432    }
433}
434
435fn context_inheritance_label(inheritance: ContextInheritance) -> &'static str {
436    match inheritance {
437        ContextInheritance::None => "none",
438        ContextInheritance::SystemOnly => "system_only",
439        ContextInheritance::Full => "full",
440    }
441}
442
443fn restore_context_inheritance(label: &str) -> Result<ContextInheritance, KernelFault> {
444    match label {
445        "none" => Ok(ContextInheritance::None),
446        "system_only" => Ok(ContextInheritance::SystemOnly),
447        "full" => Ok(ContextInheritance::Full),
448        other => Err(KernelFault::new(
449            KernelFaultCode::CheckpointIncompatible,
450            format!("child process carries unknown context inheritance {other:?}"),
451        )),
452    }
453}
454
455fn queued_signal_state(queued: &QueuedSignalRuntimeState) -> QueuedSignalState {
456    let signal = &queued.signal;
457    QueuedSignalState {
458        signal_id: super::scalar::SignalId::new(signal.id.as_str())
459            .expect("a canonical runtime signal keeps its branded id"),
460        source: signal_source_label(signal.source).to_string(),
461        signal_type: signal_type_label(signal.signal_type).to_string(),
462        urgency: urgency_label(signal.urgency).to_string(),
463        summary: signal.summary.to_string(),
464        payload: super::scalar::BoundedJson::new(signal.payload.clone())
465            .expect("a canonical signal payload remains bounded"),
466        dedupe_key: signal.dedupe_key.as_ref().map(ToString::to_string),
467        deadline_ms: signal.deadline_ms.map(WireU64::new),
468        coalesce_key: signal.coalesce_key.as_ref().map(ToString::to_string),
469        coalesced_count: signal.coalesced_count,
470        recipient: signal.recipient.as_ref().map(ToString::to_string),
471        timestamp_ms: WireU64::new(signal.timestamp_ms),
472        deadline_escalated: queued.deadline_escalated,
473        dedupe_keys: queued.dedupe_keys.iter().map(ToString::to_string).collect(),
474    }
475}
476
477fn restore_queued_signal(
478    queued: &QueuedSignalState,
479) -> Result<QueuedSignalRuntimeState, KernelFault> {
480    if queued.coalesced_count == 0 {
481        return Err(KernelFault::new(
482            KernelFaultCode::CheckpointIncompatible,
483            format!(
484                "queued signal {} carries a zero coalesced count",
485                queued.signal_id
486            ),
487        ));
488    }
489    Ok(QueuedSignalRuntimeState {
490        signal: RuntimeSignal {
491            id: queued.signal_id.as_str().into(),
492            source: restore_signal_source(&queued.source)?,
493            signal_type: restore_signal_type(&queued.signal_type)?,
494            urgency: restore_urgency(&queued.urgency)?,
495            summary: queued.summary.as_str().into(),
496            payload: queued.payload.get().clone(),
497            dedupe_key: queued.dedupe_key.as_deref().map(Into::into),
498            deadline_ms: queued.deadline_ms.map(WireU64::get),
499            coalesce_key: queued.coalesce_key.as_deref().map(Into::into),
500            coalesced_count: queued.coalesced_count,
501            recipient: queued.recipient.as_deref().map(Into::into),
502            timestamp_ms: queued.timestamp_ms.get(),
503        },
504        deadline_escalated: queued.deadline_escalated,
505        dedupe_keys: queued
506            .dedupe_keys
507            .iter()
508            .map(|key| key.as_str().into())
509            .collect(),
510    })
511}
512
513fn signal_source_label(source: SignalSource) -> &'static str {
514    match source {
515        SignalSource::Cron => "cron",
516        SignalSource::Gateway => "gateway",
517        SignalSource::Heartbeat => "heartbeat",
518        SignalSource::Custom => "custom",
519    }
520}
521
522fn restore_signal_source(label: &str) -> Result<SignalSource, KernelFault> {
523    match label {
524        "cron" => Ok(SignalSource::Cron),
525        "gateway" => Ok(SignalSource::Gateway),
526        "heartbeat" => Ok(SignalSource::Heartbeat),
527        "custom" => Ok(SignalSource::Custom),
528        other => Err(KernelFault::new(
529            KernelFaultCode::CheckpointIncompatible,
530            format!("queued signal carries unknown source {other:?}"),
531        )),
532    }
533}
534
535fn signal_type_label(signal_type: SignalType) -> &'static str {
536    match signal_type {
537        SignalType::Event => "event",
538        SignalType::Job => "job",
539        SignalType::Alert => "alert",
540    }
541}
542
543fn restore_signal_type(label: &str) -> Result<SignalType, KernelFault> {
544    match label {
545        "event" => Ok(SignalType::Event),
546        "job" => Ok(SignalType::Job),
547        "alert" => Ok(SignalType::Alert),
548        other => Err(KernelFault::new(
549            KernelFaultCode::CheckpointIncompatible,
550            format!("queued signal carries unknown type {other:?}"),
551        )),
552    }
553}
554
555fn urgency_label(urgency: Urgency) -> &'static str {
556    match urgency {
557        Urgency::Low => "low",
558        Urgency::Normal => "normal",
559        Urgency::High => "high",
560        Urgency::Critical => "critical",
561    }
562}
563
564fn restore_urgency(label: &str) -> Result<Urgency, KernelFault> {
565    match label {
566        "low" => Ok(Urgency::Low),
567        "normal" => Ok(Urgency::Normal),
568        "high" => Ok(Urgency::High),
569        "critical" => Ok(Urgency::Critical),
570        other => Err(KernelFault::new(
571            KernelFaultCode::CheckpointIncompatible,
572            format!("queued signal carries unknown urgency {other:?}"),
573        )),
574    }
575}
576
577/// §12.2 · put the scheduler partition back on the engine.
578fn restore_scheduler(
579    engine: &mut LoopStateMachine,
580    config: &ResolvedOperationConfig,
581    state: &SchedulerStateV1,
582) -> Result<(), KernelFault> {
583    engine.turn = state.turn;
584    engine.restore_budget_usage(state.total_tokens.get(), state.rounds_completed);
585    engine.restore_started_at_ms(state.started_at_ms.map(WireU64::get));
586    engine.set_wall_budget(state.wall_budget_ms.map(WireU64::get));
587    engine
588        .restore_entropy_checkpoint_state(crate::scheduler::entropy::EntropyTrackerRuntimeState {
589            window: state
590                .entropy
591                .window
592                .iter()
593                .map(|entry| crate::scheduler::entropy::EntropyTurnRuntimeState {
594                    errored_results: entry.errored_results,
595                    total_results: entry.total_results,
596                    rollbacks: entry.rollbacks,
597                })
598                .collect(),
599            rollbacks_pending: state.entropy.rollbacks_pending,
600            disarmed: state.entropy.disarmed,
601            last_alert_turn: state.entropy.last_alert_turn,
602        })
603        .map_err(|error| {
604            KernelFault::new(
605                KernelFaultCode::CheckpointIncompatible,
606                format!("entropy checkpoint could not be rebuilt: {error}"),
607            )
608        })?;
609
610    let limits = SchedulerBudget {
611        max_tokens: config.execution_policy.max_context_tokens,
612        max_turns: config.execution_policy.max_turns,
613        max_total_tokens: config.execution_policy.max_total_tokens.get(),
614        max_wall_ms: state.wall_budget_ms.map(WireU64::get),
615    };
616    let table = engine.task_table_mut();
617    for task in &state.tasks {
618        let mut tcb = Tcb::root(task.task_id.as_str(), limits.clone());
619        tcb.parent = task
620            .parent_task_id
621            .as_ref()
622            .map(|parent| parent.as_str().into());
623        tcb.state = restore_task_lifecycle(task)?;
624        tcb.wait = restore_task_wait(task)?;
625        tcb.caps = task.capability_ids.iter().map(|cap| cap.into()).collect();
626        tcb.proc = task
627            .process
628            .as_ref()
629            .map(|process| {
630                let result = process
631                    .join_result
632                    .as_ref()
633                    .map(|value| {
634                        serde_json::from_value(value.get().clone()).map_err(|error| {
635                            KernelFault::new(
636                                KernelFaultCode::CheckpointIncompatible,
637                                format!(
638                                    "task {} carries an invalid child join result: {error}",
639                                    task.task_id
640                                ),
641                            )
642                        })
643                    })
644                    .transpose()?;
645                if result.as_ref().is_some_and(|result: &SubAgentResult| {
646                    result.agent_id.as_str() != task.task_id.as_str()
647                }) {
648                    return Err(KernelFault::new(
649                        KernelFaultCode::CheckpointIncompatible,
650                        format!(
651                            "task {} carries a join result for another child",
652                            task.task_id
653                        ),
654                    ));
655                }
656                Ok(ProcInfo {
657                    role: restore_agent_role(&process.role)?,
658                    isolation: restore_agent_isolation(&process.isolation)?,
659                    context_inheritance: restore_context_inheritance(&process.context_inheritance)?,
660                    result,
661                })
662            })
663            .transpose()?;
664        tcb.budget = BudgetLedger {
665            limits: limits.clone(),
666            turns: task.turns_used,
667            total_tokens: task.tokens_used.get(),
668            started_at_ms: state.started_at_ms.map(WireU64::get),
669        };
670        table.insert(tcb);
671    }
672
673    let queued = state
674        .queued_signals
675        .iter()
676        .map(restore_queued_signal)
677        .collect::<Result<Vec<_>, _>>()?;
678    engine
679        .restore_signal_checkpoint_state(SignalRouterRuntimeState {
680            queued,
681            seen_order: state
682                .signal_dedupe_keys
683                .iter()
684                .map(|key| key.as_str().into())
685                .collect(),
686        })
687        .map_err(|error| {
688            KernelFault::new(
689                KernelFaultCode::CheckpointIncompatible,
690                format!("signal checkpoint could not be rebuilt: {error}"),
691            )
692        })?;
693
694    if let Some(workflow) = &state.workflow {
695        let wire_spec = WireSpec {
696            name: String::new(),
697            nodes: workflow
698                .nodes
699                .iter()
700                .map(|node| WireNode {
701                    node_id: node.node_id.clone(),
702                    task: node.task.clone(),
703                    depends_on: node.depends_on.clone(),
704                    run_spec: node.run_spec.clone(),
705                })
706                .collect(),
707        };
708        let core_spec = build_core_spec(&wire_spec).map_err(|fault| {
709            KernelFault::new(KernelFaultCode::CheckpointIncompatible, fault.message)
710        })?;
711        let runtime_states: Result<Vec<_>, KernelFault> = workflow
712            .nodes
713            .iter()
714            .enumerate()
715            .zip(core_spec.nodes.iter())
716            .map(|((index, node), core)| {
717                if node.kind != "spawn" {
718                    return Err(KernelFault::new(
719                        KernelFaultCode::CheckpointIncompatible,
720                        format!(
721                            "workflow node {} carries unsupported checkpoint kind {:?}",
722                            node.node_id, node.kind
723                        ),
724                    ));
725                }
726                let result = engine
727                    .task_table()
728                    .get(&crate::orchestration::workflow::node_agent_id(index))
729                    .and_then(|task| task.proc.as_ref())
730                    .and_then(|process| process.result.as_ref())
731                    .map(|result| result.result.clone());
732                Ok(WorkflowRuntimeNodeState {
733                    node: core.clone(),
734                    status: restore_workflow_status(&node.status)?,
735                    result,
736                    active_agent_id: node.active_agent_id.clone(),
737                    iterations_completed: node.iterations_completed as usize,
738                })
739            })
740            .collect();
741        let run = crate::orchestration::workflow::WorkflowRun::restore_from_checkpoint(
742            &core_spec,
743            &runtime_states?,
744        )
745        .map_err(|error| {
746            KernelFault::new(
747                KernelFaultCode::CheckpointIncompatible,
748                format!("workflow checkpoint could not be rebuilt: {error}"),
749            )
750        })?;
751        engine.restore_checkpoint_workflow(run);
752    }
753    Ok(())
754}
755
756fn restore_task_lifecycle(task: &TaskControlState) -> Result<TaskLifecycle, KernelFault> {
757    let lifecycle = match task.lifecycle.as_str() {
758        "pending_launch" => TaskLifecycle::PendingLaunch,
759        "starting" => TaskLifecycle::Starting,
760        "ready" => TaskLifecycle::Ready,
761        "running" => TaskLifecycle::Running,
762        "suspended" => TaskLifecycle::Suspended,
763        "done" => {
764            let label = task.termination.as_deref().ok_or_else(|| {
765                incompatible(format!(
766                    "task {} is done but the checkpoint does not say why; a finished task without \
767                     its termination reason is not restorable",
768                    task.task_id
769                ))
770            })?;
771            TaskLifecycle::Done(termination_from_label(label).ok_or_else(|| {
772                incompatible(format!(
773                    "task {} names termination reason {label:?}, which this kernel does not know",
774                    task.task_id
775                ))
776            })?)
777        }
778        other => {
779            return Err(incompatible(format!(
780                "task {} names lifecycle {other:?}, which this kernel does not know",
781                task.task_id
782            )));
783        }
784    };
785    Ok(lifecycle)
786}
787
788fn restore_task_wait(task: &TaskControlState) -> Result<Option<WaitReason>, KernelFault> {
789    match task.wait.as_deref() {
790        None => Ok(None),
791        Some("approval") => Ok(Some(WaitReason::Approval)),
792        Some("sub_agent_join") => Ok(Some(WaitReason::SubAgentJoin(
793            task.waiting_on
794                .iter()
795                .map(|child| child.as_str().into())
796                .collect(),
797        ))),
798        Some(other) => Err(incompatible(format!(
799            "task {} waits on {other:?}, which this kernel does not know",
800            task.task_id
801        ))),
802    }
803}
804
805fn termination_from_label(label: &str) -> Option<TerminationReason> {
806    Some(match label {
807        "completed" => TerminationReason::Completed,
808        "max_turns" => TerminationReason::MaxTurns,
809        "token_budget" => TerminationReason::TokenBudget,
810        "timeout" => TerminationReason::Timeout,
811        "user_abort" => TerminationReason::UserAbort,
812        "error" => TerminationReason::Error,
813        "milestone_exceeded" => TerminationReason::MilestoneExceeded,
814        "context_overflow" => TerminationReason::ContextOverflow,
815        "no_progress" => TerminationReason::NoProgress,
816        _ => return None,
817    })
818}
819
820/// §12.2 · put the context-VM partition back on the engine.
821///
822/// Order matters: the messages are pushed first so the partition token counters land on the
823/// checkpoint's own numbers, then the handle table is repopulated **by id** so a handle addresses
824/// the same body it addressed before, then the allocator is moved past all of them.
825fn restore_context_vm(
826    engine: &mut LoopStateMachine,
827    state: &ContextVmStateV1,
828) -> Result<(), KernelFault> {
829    let ctx = &mut engine.ctx;
830    for entry in &state.messages {
831        let message = restore_message(&entry.role, &entry.body, &entry.tool_calls)?;
832        match entry.partition {
833            MessagePartition::System => ctx.partitions.system.push(message, entry.tokens),
834            MessagePartition::History => ctx.partitions.history.push(message, entry.tokens),
835        }
836    }
837    for slot in &state.knowledge {
838        let message = restore_message(&slot.role, &slot.body, &[])?;
839        ctx.partitions.knowledge.push_entry(
840            slot.key.as_deref().map(Into::into),
841            message,
842            slot.tokens,
843            slot.pinned,
844        );
845        // The boundary-eviction mark is bookkeeping the push path does not take, so it is set
846        // straight onto the entry it belongs to — a slot that was marked for removal must still be
847        // marked after a restore, or the next sweep keeps something the run had already dropped.
848        if slot.evict_at_boundary
849            && let Some(entry) = ctx.partitions.knowledge.entries.last_mut()
850        {
851            entry.evict_at_boundary = true;
852        }
853    }
854    ctx.partitions.signals = state.signals.clone();
855    ctx.partitions.task_state = restore_task_state(&state.task_state);
856    ctx.last_activity_ms = state.last_activity_ms.get();
857    ctx.last_compact_ms = state.last_compact_ms.map(WireU64::get);
858    ctx.active_skills = state
859        .active_skills
860        .iter()
861        .map(|lease| (lease.skill.as_str().into(), lease.lease_until_turn))
862        .collect();
863
864    for handle in &state.handles {
865        ctx.handles.insert(Handle {
866            id: handle.handle_id,
867            kind: restore_handle_kind(&handle.kind)?,
868            residency: restore_residency(handle)?,
869            tokens: handle.tokens,
870            source: handle.source.as_deref().map(Into::into),
871        });
872    }
873    ctx.restore_next_handle_id(state.next_handle_id);
874    if !ctx.restore_frozen_history_len(state.frozen_history_len as usize) {
875        return Err(incompatible(format!(
876            "the checkpoint freezes {} history messages but restores only {}",
877            state.frozen_history_len,
878            ctx.partitions.history.messages.len()
879        )));
880    }
881    Ok(())
882}
883
884fn restore_message(
885    role: &str,
886    body: &StoredMessageBody,
887    tool_calls: &[LogicalToolCall],
888) -> Result<Message, KernelFault> {
889    let role = role_from_label(role)
890        .ok_or_else(|| incompatible(format!("the checkpoint carries message role {role:?}")))?;
891    let content = match body {
892        StoredMessageBody::Inline(inline) => message_content(
893            inline.text.clone(),
894            inline.tool_call_id.as_deref(),
895            inline.is_error,
896        ),
897        StoredMessageBody::Reference(reference) => message_content(
898            reference.preview.clone(),
899            reference.tool_call_id.as_deref(),
900            reference.is_error,
901        ),
902        StoredMessageBody::Structured(structured) => serde_json::from_str(&structured.content_json)
903            .map_err(|error| {
904                incompatible(format!(
905                    "the checkpoint carries a structured message body that does not decode: {error}"
906                ))
907            })?,
908    };
909    Ok(Message {
910        role,
911        content,
912        tool_calls: tool_calls
913            .iter()
914            .map(|call| {
915                Ok(crate::types::message::ToolCall {
916                    id: call.call_id.as_str().into(),
917                    name: call.name.as_str().into(),
918                    arguments: serde_json::from_str(&call.arguments).map_err(|error| {
919                        incompatible(format!(
920                            "tool call {} carries arguments that do not decode: {error}",
921                            call.call_id
922                        ))
923                    })?,
924                })
925            })
926            .collect::<Result<Vec<_>, KernelFault>>()?,
927        token_count: None,
928    })
929}
930
931fn restore_handle_kind(label: &str) -> Result<HandleKind, KernelFault> {
932    Ok(match label {
933        "tool_result" => HandleKind::ToolResult,
934        "memory_page" => HandleKind::MemoryPage,
935        "knowledge_entry" => HandleKind::KnowledgeEntry,
936        "sub_agent_join" => HandleKind::SubAgentJoin,
937        other => {
938            return Err(incompatible(format!(
939                "the checkpoint carries handle kind {other:?}, which this kernel does not know"
940            )));
941        }
942    })
943}
944
945fn restore_residency(handle: &HandleState) -> Result<Residency, KernelFault> {
946    let missing = |what: &str| {
947        incompatible(format!(
948            "handle {} is {} but carries no {what}",
949            handle.handle_id, handle.residency
950        ))
951    };
952    Ok(match handle.residency.as_str() {
953        "resident" => Residency::Resident,
954        "collapsed" => Residency::Collapsed,
955        "external" => Residency::External {
956            payload_ref: handle
957                .payload_ref
958                .clone()
959                .ok_or_else(|| missing("locator"))?,
960            digest: handle.digest.clone().ok_or_else(|| missing("digest"))?,
961            original_size: handle
962                .original_size
963                .ok_or_else(|| missing("original size"))?
964                .get(),
965        },
966        "paged_out" => Residency::PagedOut {
967            payload_ref: handle
968                .payload_ref
969                .clone()
970                .ok_or_else(|| missing("locator"))?,
971            digest: handle.digest.clone().ok_or_else(|| missing("digest"))?,
972        },
973        other => {
974            return Err(incompatible(format!(
975                "the checkpoint carries residency {other:?}, which this kernel does not know"
976            )));
977        }
978    })
979}
980
981fn incompatible(message: String) -> KernelFault {
982    KernelFault::new(KernelFaultCode::CheckpointIncompatible, message)
983}
984
985fn project_task_state(state: &TaskState) -> LogicalTaskState {
986    LogicalTaskState {
987        goal: state.goal.clone(),
988        criteria: state.criteria.clone(),
989        plan: state
990            .plan
991            .iter()
992            .map(|step| LogicalPlanStep {
993                label: step.label.clone(),
994                done: step.done,
995            })
996            .collect(),
997        current_step: state.current_step.map(|index| index as u32),
998        progress: state.progress.clone(),
999        scratchpad: state.scratchpad.clone(),
1000        blocked_on: state.blocked_on.clone(),
1001        directives: state.directives.clone(),
1002        preserved_refs: state.preserved_refs.clone(),
1003        recent_actions: state.recent_actions.clone(),
1004        compression_log: state
1005            .compression_log
1006            .iter()
1007            .map(|entry| LogicalCompressionEntry {
1008                action: entry.action.clone(),
1009                summary: entry.summary.clone(),
1010            })
1011            .collect(),
1012        compression_log_dropped: WireU64::new(state.compression_log_dropped),
1013    }
1014}
1015
1016fn restore_task_state(state: &LogicalTaskState) -> TaskState {
1017    TaskState {
1018        goal: state.goal.clone(),
1019        criteria: state.criteria.clone(),
1020        plan: state
1021            .plan
1022            .iter()
1023            .map(|step| PlanStep {
1024                label: step.label.clone(),
1025                done: step.done,
1026            })
1027            .collect(),
1028        current_step: state.current_step.map(|index| index as usize),
1029        progress: state.progress.clone(),
1030        scratchpad: state.scratchpad.clone(),
1031        blocked_on: state.blocked_on.clone(),
1032        directives: state.directives.clone(),
1033        preserved_refs: state.preserved_refs.clone(),
1034        recent_actions: state.recent_actions.clone(),
1035        compression_log: state
1036            .compression_log
1037            .iter()
1038            .map(|entry| CompressionEntry {
1039                action: entry.action.clone(),
1040                summary: entry.summary.clone(),
1041            })
1042            .collect(),
1043        compression_log_dropped: state.compression_log_dropped.get(),
1044    }
1045}
1046
1047fn authority(message: &str) -> SyscallRefusal {
1048    SyscallRefusal::Fault(KernelFault::new(
1049        KernelFaultCode::InvalidAuthority,
1050        message.to_string(),
1051    ))
1052}
1053
1054fn denial_reason(disposition: &Disposition, fallback: &str) -> String {
1055    match disposition {
1056        Disposition::Deny { stage, reason } => format!("{stage}: {reason}"),
1057        Disposition::RateLimited { retry_after_ms } => {
1058            format!("rate limited; retry after {retry_after_ms}ms")
1059        }
1060        Disposition::Gate { reason, .. } => format!("awaiting approval: {reason}"),
1061        Disposition::Defer { slot } => format!("deferred at slot {slot}"),
1062        Disposition::Allow => fallback.to_string(),
1063    }
1064}
1065
1066/// What the kernel authored for a pending `PersistMemory` effect (§22.13).
1067#[derive(Debug, Clone, PartialEq)]
1068struct AuthoredMemoryWrite {
1069    binding_id: MemoryBindingId,
1070    name: String,
1071    kind: WireMemoryKind,
1072    size_bytes: u32,
1073}
1074
1075/// What the kernel authored for a pending `QueryMemory` effect.
1076#[derive(Debug, Clone, PartialEq)]
1077struct AuthoredMemoryQuery {
1078    binding_id: MemoryBindingId,
1079    text: String,
1080    requested_k: u32,
1081}
1082
1083/// What the kernel published for a pending `LoadPayload` effect (§7.10 rule 4).
1084#[derive(Debug, Clone, PartialEq)]
1085struct PendingPayloadLoad {
1086    /// The wire address of the handle — the tool `call_id` for an external result, the kernel-minted
1087    /// archive id for a paged-out one.
1088    handle_id: String,
1089    /// The digest the body has to reproduce. The kernel never saw the body, so this is the *only*
1090    /// thing that makes a restored payload the one that left.
1091    digest: String,
1092    /// Present only for [`Residency::External`], whose declared size the kernel admitted and can
1093    /// therefore hold the host to. A page-out archive is checked by digest alone.
1094    original_size: Option<u64>,
1095}
1096
1097/// What one admitted syscall produced.
1098#[derive(Debug, Default)]
1099struct SyscallOutcome {
1100    effects: Vec<KernelEffect>,
1101    /// Set only by `SubmitWorkflow`'s bootstrap arm — the one syscall that moves the focus.
1102    focus: Option<ExecutionFocus>,
1103    /// The DAG grew and still owes a spawn round. Honoured on the provider-tool path; on the
1104    /// child-completion path the completion's own drive produces the batch (§7.7 ordering).
1105    needs_workflow_round: bool,
1106}
1107
1108/// Reduces the five canonical input classes onto the kernel's existing semantic mechanisms.
1109///
1110/// Use it as the plan function of [`KernelTransaction::prepare`](super::transaction::KernelTransaction::prepare):
1111///
1112/// ```ignore
1113/// let preparation = tx.prepare(&envelope, |ctx| driver.plan(ctx));
1114/// // ... host CAS-appends the record ...
1115/// let committed = tx.commit(&token, &head)?;
1116/// driver.note_committed(committed.step_seq)?;
1117/// ```
1118pub struct CanonicalOperationDriver {
1119    engine: Option<LoopStateMachine>,
1120    root_kind: Option<RootKind>,
1121    focus: Option<ExecutionFocus>,
1122    workflow_id: Option<WorkflowId>,
1123    /// Wire node identity by internal DAG index — the DAG the engine runs is index-addressed, the
1124    /// wire is not, and the mapping is what keeps a `SpawnTasks` effect nameable by the host.
1125    node_ids: Vec<NodeId>,
1126    /// Canonical source DAG in the same index order as `node_ids`. Checkpoint projection pairs it
1127    /// with the semantic node statuses without serializing the graph's private indexes.
1128    workflow_nodes: Vec<WireNode>,
1129    /// The attempt the kernel minted for each **live** task. §10.4: a host may not create or
1130    /// rewrite child identity through a resolution or a completion, so a completion that names an
1131    /// attempt this kernel never issued is refused rather than folded in. A completed attempt is
1132    /// removed here, which is what makes a second completion — and the `parent_requests` riding on
1133    /// it — a stale causation rather than a second helping of authority.
1134    attempts: BTreeMap<String, AttemptId>,
1135    /// §7.6 · the provider calls this operation is waiting on, by effect id. At most one is live
1136    /// (DEC-3); the map keys it so a resolution names its own call rather than "the current one".
1137    provider_calls: BTreeMap<EffectId, PendingProviderCall>,
1138    /// §22.13 · the memory records **this kernel authored** for its pending `PersistMemory`
1139    /// effects. The resolution reports these, never what the host echoes back: a receipt may carry
1140    /// its store's own locator and digest and nothing else, so no host reply can restate a name,
1141    /// kind, size, trust or provenance the kernel derived.
1142    pending_memory_writes: BTreeMap<EffectId, AuthoredMemoryWrite>,
1143    /// The same for `QueryMemory`: the query the kernel authored and the width it clamped.
1144    pending_memory_queries: BTreeMap<EffectId, AuthoredMemoryQuery>,
1145    /// §7.10 · the handle each pending `LoadPayload` addresses, with the digest the loaded body has
1146    /// to reproduce. Kept here rather than re-read from the handle table at resolution time, so a
1147    /// residency that moved in between cannot silently change what a page-in is verified against.
1148    pending_payload_loads: BTreeMap<EffectId, PendingPayloadLoad>,
1149    /// Tool call ids that already produced a syscall. A causation is spent once — replaying the
1150    /// same provider result with a fresh input id must not buy a second workflow append.
1151    consumed_calls: BTreeSet<String>,
1152    /// §13.2 / DEC-6 · the live-mutable half of the configuration, plus the revision two concurrent
1153    /// writers race on. Seeded from the genesis record's resolved configuration; only
1154    /// `HostCommand::ApplyPolicyPatch` moves it. Boot-only axes are read from `context.config`
1155    /// instead, which is why the transaction never needs a second copy of this.
1156    policy: Option<LivePolicyState>,
1157    /// §7.3 · the verification contract whose cascade is installed on the engine, if any.
1158    ///
1159    /// The engine's own `LoopAction::EvaluateMilestone` names only a phase, because internally
1160    /// there is one cascade and a phase id is enough. The wire needs the pair: `phase_id` is
1161    /// unique only within its contract, so `(contract_id, phase_id)` is the host's complete lookup
1162    /// key. Threading it here rather than through the semantic engine keeps the legacy internal
1163    /// action untouched (Task 23 owns that) while the canonical projection is already total.
1164    loaded_contract_id: Option<String>,
1165    staged: Option<StagedFocus>,
1166    poison: Option<KernelFault>,
1167}
1168
1169impl Default for CanonicalOperationDriver {
1170    fn default() -> Self {
1171        Self::new()
1172    }
1173}
1174
1175impl CanonicalOperationDriver {
1176    pub fn new() -> Self {
1177        Self {
1178            engine: None,
1179            root_kind: None,
1180            focus: None,
1181            workflow_id: None,
1182            node_ids: Vec::new(),
1183            workflow_nodes: Vec::new(),
1184            attempts: BTreeMap::new(),
1185            provider_calls: BTreeMap::new(),
1186            pending_memory_writes: BTreeMap::new(),
1187            pending_memory_queries: BTreeMap::new(),
1188            pending_payload_loads: BTreeMap::new(),
1189            consumed_calls: BTreeSet::new(),
1190            policy: None,
1191            loaded_contract_id: None,
1192            staged: None,
1193            poison: None,
1194        }
1195    }
1196
1197    // ----- observers -----
1198
1199    /// The operation's root class. `None` until the root start commits; immutable afterwards.
1200    pub fn root_kind(&self) -> Option<RootKind> {
1201        self.root_kind
1202    }
1203
1204    /// Where control currently is. Moves only on a committed transition (§7.4).
1205    pub fn focus(&self) -> Option<&ExecutionFocus> {
1206        self.focus.as_ref()
1207    }
1208
1209    pub fn workflow_id(&self) -> Option<&WorkflowId> {
1210        self.workflow_id.as_ref()
1211    }
1212
1213    /// Return the kernel-issued live attempt for `task_id`.
1214    ///
1215    /// Bindings use this read-only projection when a legacy host completion carries only a task
1216    /// identity. The value comes from checkpointed kernel state; hosts must never synthesize it.
1217    pub fn attempt_id(&self, task_id: &str) -> Option<&AttemptId> {
1218        self.attempts.get(task_id)
1219    }
1220
1221    pub fn poison(&self) -> Option<&KernelFault> {
1222        self.poison.as_ref()
1223    }
1224
1225    /// Read-only access to the semantic engine, for tests and host projections.
1226    pub fn engine(&self) -> Option<&LoopStateMachine> {
1227        self.engine.as_ref()
1228    }
1229
1230    /// Where the driver's own fold says the operation is. The transaction stays the authority on
1231    /// lifecycle; this exists so a host projection never needs a second copy of the rule.
1232    pub fn lifecycle(&self) -> OperationLifecycle {
1233        match (self.engine.is_some(), self.root_kind) {
1234            (false, _) => OperationLifecycle::Created,
1235            (true, None) => OperationLifecycle::Configured,
1236            (true, Some(_)) => OperationLifecycle::Running,
1237        }
1238    }
1239
1240    // ----- §12.1 · the logical-state projection -----
1241
1242    /// Project the three driver-owned partitions of §12.1, plus the two transition fields the
1243    /// driver rather than the transaction owns.
1244    ///
1245    /// Explicitly a **projection**, not a serialisation: every value below is read through a named
1246    /// accessor and written into a versioned DTO field. That is the whole point of §12.1 — adding a
1247    /// field to [`LoopStateMachine`] must not change the checkpoint format, and a checkpoint field
1248    /// must not silently vanish because an internal one was renamed. It is also why the internal
1249    /// enums travel as their `label()` plus their carried data: `TaskLifecycle::Done(reason)` and
1250    /// `Residency::External { .. }` are semantic-kernel shapes, and mirroring them would make the
1251    /// checkpoint a checkpoint of a private layout.
1252    pub fn project_logical_state(&self) -> LogicalStateProjection {
1253        LogicalStateProjection {
1254            root_kind: self.root_kind,
1255            focus: self.focus.clone(),
1256            syscall: self.project_syscall_state(),
1257            scheduler: self.project_scheduler_state(),
1258            context_vm: self.project_context_vm_state(),
1259        }
1260    }
1261
1262    fn project_syscall_state(&self) -> SyscallStateV1 {
1263        SyscallStateV1 {
1264            policy_revision: self.policy.as_ref().map(LivePolicyState::revision),
1265            live_config: self.policy.as_ref().map(|policy| policy.config().clone()),
1266            provider_calls: self
1267                .provider_calls
1268                .iter()
1269                .map(|(effect_id, call)| PendingProviderCallState {
1270                    effect_id: effect_id.clone(),
1271                    task_id: call.task_id.clone(),
1272                    exposed_tools: call.exposed_tools.iter().cloned().collect(),
1273                })
1274                .collect(),
1275            consumed_call_ids: self.consumed_calls.iter().cloned().collect(),
1276            authored_memory_writes: self
1277                .pending_memory_writes
1278                .iter()
1279                .map(|(effect_id, write)| AuthoredMemoryWriteState {
1280                    effect_id: effect_id.clone(),
1281                    binding_id: write.binding_id.clone(),
1282                    name: write.name.clone(),
1283                    kind: write.kind,
1284                    size_bytes: write.size_bytes,
1285                })
1286                .collect(),
1287            authored_memory_queries: self
1288                .pending_memory_queries
1289                .iter()
1290                .map(|(effect_id, query)| AuthoredMemoryQueryState {
1291                    effect_id: effect_id.clone(),
1292                    binding_id: query.binding_id.clone(),
1293                    text: query.text.clone(),
1294                    requested_k: query.requested_k,
1295                })
1296                .collect(),
1297            memory_write_window_ms: self
1298                .engine
1299                .as_ref()
1300                .map(|engine| {
1301                    engine
1302                        .memory_write_window()
1303                        .iter()
1304                        .copied()
1305                        .map(WireU64::new)
1306                        .collect()
1307                })
1308                .unwrap_or_default(),
1309        }
1310    }
1311
1312    fn project_scheduler_state(&self) -> SchedulerStateV1 {
1313        let Some(engine) = self.engine.as_ref() else {
1314            return SchedulerStateV1::default();
1315        };
1316        let (total_tokens, subagents_spawned, rounds_completed) = engine.local_budget_usage();
1317        let signal_state = engine.signal_checkpoint_state();
1318        let entropy_state = engine.entropy_checkpoint_state();
1319        let workflow = engine.workflow_checkpoint_nodes().map(|runtime_nodes| {
1320            let workflow_id = self
1321                .workflow_id
1322                .as_ref()
1323                .expect("an active canonical workflow has a logical identity");
1324            assert_eq!(
1325                runtime_nodes.len(),
1326                self.workflow_nodes.len(),
1327                "the semantic workflow and its canonical source DAG stay index-aligned"
1328            );
1329            WorkflowGraphState {
1330                workflow_id: workflow_id.clone(),
1331                nodes: runtime_nodes
1332                    .into_iter()
1333                    .zip(self.workflow_nodes.iter())
1334                    .map(|(runtime, wire)| WorkflowNodeState {
1335                        node_id: wire.node_id.clone(),
1336                        task: wire.task.clone(),
1337                        depends_on: wire.depends_on.clone(),
1338                        run_spec: wire.run_spec.clone(),
1339                        kind: workflow_kind_label(&runtime).to_string(),
1340                        status: workflow_status_label(runtime.status).to_string(),
1341                        active_agent_id: runtime.active_agent_id,
1342                        iterations_completed: runtime.iterations_completed as u32,
1343                    })
1344                    .collect(),
1345            }
1346        });
1347        SchedulerStateV1 {
1348            turn: engine.turn,
1349            total_tokens: WireU64::new(total_tokens),
1350            rounds_completed,
1351            subagents_spawned,
1352            started_at_ms: engine.started_at_ms().map(WireU64::new),
1353            wall_budget_ms: engine.wall_budget().map(WireU64::new),
1354            tasks: engine
1355                .task_table()
1356                .all()
1357                .iter()
1358                .map(|tcb| TaskControlState {
1359                    task_id: TaskId::new(tcb.id.as_str())
1360                        .expect("an internal task id is always a legal branded ref"),
1361                    parent_task_id: tcb
1362                        .parent
1363                        .as_ref()
1364                        .and_then(|parent| TaskId::new(parent.as_str()).ok()),
1365                    lifecycle: tcb.state.label().to_string(),
1366                    termination: match tcb.state {
1367                        TaskLifecycle::Done(reason) => Some(reason.label().to_string()),
1368                        _ => None,
1369                    },
1370                    wait: tcb.wait.as_ref().map(|wait| wait.label().to_string()),
1371                    waiting_on: match &tcb.wait {
1372                        Some(WaitReason::SubAgentJoin(children)) => children
1373                            .iter()
1374                            .filter_map(|child| TaskId::new(child.as_str()).ok())
1375                            .collect(),
1376                        _ => Vec::new(),
1377                    },
1378                    capability_ids: tcb.caps.iter().map(|cap| cap.to_string()).collect(),
1379                    process: tcb.proc.as_ref().map(|process| ChildProcessState {
1380                        role: agent_role_label(process.role).to_string(),
1381                        isolation: agent_isolation_label(process.isolation).to_string(),
1382                        context_inheritance: context_inheritance_label(process.context_inheritance)
1383                            .to_string(),
1384                        join_result: process.result.as_ref().map(|result| {
1385                            super::scalar::BoundedJson::new(
1386                                serde_json::to_value(result)
1387                                    .expect("a child join result is serializable"),
1388                            )
1389                            .expect("a child join result is bounded")
1390                        }),
1391                    }),
1392                    tokens_used: WireU64::new(tcb.budget.total_tokens),
1393                    turns_used: tcb.budget.turns,
1394                })
1395                .collect(),
1396            attempts: self
1397                .attempts
1398                .iter()
1399                .filter_map(|(task_id, attempt_id)| {
1400                    Some(TaskAttemptState {
1401                        task_id: TaskId::new(task_id.as_str()).ok()?,
1402                        attempt_id: attempt_id.clone(),
1403                    })
1404                })
1405                .collect(),
1406            workflow,
1407            queued_signals: signal_state
1408                .queued
1409                .into_iter()
1410                .map(|queued| queued_signal_state(&queued))
1411                .collect(),
1412            signal_dedupe_keys: signal_state
1413                .seen_order
1414                .into_iter()
1415                .map(|key| key.to_string())
1416                .collect(),
1417            milestone: self
1418                .loaded_contract_id
1419                .as_ref()
1420                .map(|contract_id| MilestoneState {
1421                    contract_id: contract_id.clone(),
1422                    phase_id: engine.current_milestone_phase_id().map(str::to_string),
1423                    complete: engine.is_milestone_complete(),
1424                    blocked_count: engine.milestone_blocked_count(),
1425                }),
1426            entropy: EntropyState {
1427                window: entropy_state
1428                    .window
1429                    .into_iter()
1430                    .map(|entry| EntropyTurnState {
1431                        errored_results: entry.errored_results,
1432                        total_results: entry.total_results,
1433                        rollbacks: entry.rollbacks,
1434                    })
1435                    .collect(),
1436                rollbacks_pending: entropy_state.rollbacks_pending,
1437                disarmed: entropy_state.disarmed,
1438                last_alert_turn: entropy_state.last_alert_turn,
1439            },
1440        }
1441    }
1442
1443    fn project_context_vm_state(&self) -> ContextVmStateV1 {
1444        let Some(engine) = self.engine.as_ref() else {
1445            return ContextVmStateV1::default();
1446        };
1447        let ctx = &engine.ctx;
1448        ContextVmStateV1 {
1449            handles: ctx
1450                .handles
1451                .all()
1452                .iter()
1453                .map(|handle| {
1454                    let (payload_ref, digest, original_size) = match &handle.residency {
1455                        Residency::External {
1456                            payload_ref,
1457                            digest,
1458                            original_size,
1459                        } => (
1460                            Some(payload_ref.clone()),
1461                            Some(digest.clone()),
1462                            Some(WireU64::new(*original_size)),
1463                        ),
1464                        Residency::PagedOut {
1465                            payload_ref,
1466                            digest,
1467                        } => (Some(payload_ref.clone()), Some(digest.clone()), None),
1468                        Residency::Resident | Residency::Collapsed => (None, None, None),
1469                    };
1470                    HandleState {
1471                        handle_id: handle.id,
1472                        kind: handle_kind_label(&handle.kind).to_string(),
1473                        residency: handle.residency.label().to_string(),
1474                        payload_ref,
1475                        digest,
1476                        original_size,
1477                        tokens: handle.tokens,
1478                        source: handle.source.as_ref().map(|source| source.to_string()),
1479                    }
1480                })
1481                .collect(),
1482            next_handle_id: ctx.next_handle_id(),
1483            pending_payload_loads: self
1484                .pending_payload_loads
1485                .iter()
1486                .map(|(effect_id, load)| PendingPayloadLoadState {
1487                    effect_id: effect_id.clone(),
1488                    handle_id: load.handle_id.clone(),
1489                    digest: load.digest.clone(),
1490                    original_size: load.original_size.map(WireU64::new),
1491                })
1492                .collect(),
1493            active_skills: ctx
1494                .active_skills
1495                .iter()
1496                .map(|(skill, lease)| SkillLeaseState {
1497                    skill: skill.to_string(),
1498                    lease_until_turn: *lease,
1499                })
1500                .collect(),
1501            knowledge: ctx
1502                .partitions
1503                .knowledge
1504                .entries
1505                .iter()
1506                .map(|entry| KnowledgeSlotState {
1507                    key: entry.key.as_ref().map(|key| key.to_string()),
1508                    role: role_label(entry.message.role).to_string(),
1509                    body: self.project_body(&entry.message),
1510                    tokens: entry.tokens,
1511                    pinned: entry.pinned,
1512                    evict_at_boundary: entry.evict_at_boundary,
1513                })
1514                .collect(),
1515            signals: ctx.partitions.signals.clone(),
1516            messages: ctx
1517                .partitions
1518                .system
1519                .messages
1520                .iter()
1521                .map(|message| self.project_message(MessagePartition::System, message))
1522                .chain(
1523                    ctx.partitions
1524                        .history
1525                        .messages
1526                        .iter()
1527                        .map(|message| self.project_message(MessagePartition::History, message)),
1528                )
1529                .collect(),
1530            task_state: project_task_state(&ctx.partitions.task_state),
1531            partition_tokens: PartitionTokenState {
1532                system: ctx.partitions.system.token_count,
1533                knowledge: ctx.partitions.knowledge.token_count,
1534                history: ctx.partitions.history.token_count,
1535            },
1536            history_len: ctx.partitions.history.messages.len() as u32,
1537            frozen_history_len: ctx.frozen_history_len() as u32,
1538            last_activity_ms: WireU64::new(ctx.last_activity_ms),
1539            last_compact_ms: ctx.last_compact_ms.map(WireU64::new),
1540        }
1541    }
1542
1543    /// §5q-2 · one stored message, projected.
1544    fn project_message(
1545        &self,
1546        partition: MessagePartition,
1547        message: &Message,
1548    ) -> StoredMessageState {
1549        StoredMessageState {
1550            partition,
1551            role: role_label(message.role).to_string(),
1552            body: self.project_body(message),
1553            tool_calls: message
1554                .tool_calls
1555                .iter()
1556                .map(|call| LogicalToolCall {
1557                    call_id: call.id.to_string(),
1558                    name: call.name.to_string(),
1559                    arguments: call.arguments.to_string(),
1560                })
1561                .collect(),
1562            tokens: message.token_count.unwrap_or(0),
1563        }
1564    }
1565
1566    /// §7.10 · inline or by reference, decided by the message's own residency.
1567    ///
1568    /// The rule is not "is this body big" but "where does this body live": a result whose handle
1569    /// says `External` was never resident in the first place (only its preview is in the message),
1570    /// and one that says `PagedOut` left under pressure and lives with the host now. Either way the
1571    /// checkpoint carries the reference and the digest that verifies a page-in — putting the bytes
1572    /// back would re-create exactly the round trip §7.10 exists to delete.
1573    fn project_body(&self, message: &Message) -> StoredMessageBody {
1574        let Some((text, tool_call_id, is_error)) = message_body_parts(message) else {
1575            return StoredMessageBody::Structured(StructuredMessageBody {
1576                content_json: serde_json::to_string(&message.content)
1577                    .unwrap_or_else(|_| "null".to_string()),
1578            });
1579        };
1580        let Some(call_id) = tool_call_id.as_deref() else {
1581            return StoredMessageBody::Inline(InlineMessageBody {
1582                text,
1583                tool_call_id,
1584                is_error,
1585            });
1586        };
1587        let referenced = self.engine.as_ref().and_then(|engine| {
1588            let handle = engine
1589                .ctx
1590                .handles
1591                .all()
1592                .iter()
1593                .find(|handle| handle.source.as_deref() == Some(call_id))?;
1594            let digest = handle.residency.digest()?;
1595            Some((
1596                handle.id,
1597                digest.to_string(),
1598                matches!(handle.residency, Residency::PagedOut { .. }),
1599            ))
1600        });
1601        match referenced {
1602            Some((handle_id, digest, is_paged_out)) => {
1603                StoredMessageBody::Reference(ReferencedMessageBody {
1604                    handle_id,
1605                    digest,
1606                    preview: if is_paged_out {
1607                        crate::context::renderer::collapse_preview(&text, call_id)
1608                    } else {
1609                        truncate_on_char_boundary(&text, self.preview_bytes())
1610                    },
1611                    tool_call_id,
1612                    is_error,
1613                })
1614            }
1615            None => StoredMessageBody::Inline(InlineMessageBody {
1616                text,
1617                tool_call_id,
1618                is_error,
1619            }),
1620        }
1621    }
1622
1623    fn preview_bytes(&self) -> usize {
1624        self.policy
1625            .as_ref()
1626            .map(|policy| policy.config().payload_policy.preview_bytes as usize)
1627            .unwrap_or(2 * 1024)
1628    }
1629
1630    // ----- §12.2 · the logical-state restore -----
1631
1632    /// Rebuild a driver from a checkpoint's logical state (§12.2 line 3).
1633    ///
1634    /// The exact inverse of [`Self::project_logical_state`], and deliberately nothing more: every
1635    /// value written here is a value the projection reads back, so "did the restore work" is not a
1636    /// judgement call — [`super::restore::restore_operation`] re-projects immediately afterwards and
1637    /// compares the digest. A field this function forgets therefore fails the restore rather than
1638    /// producing a runtime that is quietly one field short of the one that crashed.
1639    ///
1640    /// Task 16b makes every scheduler branch invertible here: workflow source nodes rebuild their
1641    /// private graph indexes, queued signals rebuild priority and dedupe state, and child process
1642    /// identity is restored without re-running permission defaults. Unknown labels and inconsistent
1643    /// relationships still fail closed as `CheckpointIncompatible`.
1644    pub fn restore_logical_state(
1645        genesis_config: &ResolvedOperationConfig,
1646        state: &LogicalKernelState,
1647    ) -> Result<Self, KernelFault> {
1648        let mut driver = Self::new();
1649        let live_config = state
1650            .syscall
1651            .live_config
1652            .clone()
1653            .unwrap_or_else(|| genesis_config.clone());
1654
1655        // The engine is built from the configuration and then *moved* onto the checkpointed facts;
1656        // it is never deserialised. Boot-only axes come from the genesis configuration the record
1657        // froze, live-mutable ones from the patched configuration the checkpoint carries.
1658        let mut engine = build_engine(genesis_config);
1659        install_live_policies(&mut engine, &live_config);
1660        engine.set_root_workflow(state.transition.root_kind == Some(RootKind::Workflow));
1661        driver.policy = Some(LivePolicyState::restore(
1662            state.syscall.policy_revision.unwrap_or(WireU64::ZERO),
1663            live_config.clone(),
1664        ));
1665
1666        restore_scheduler(&mut engine, &live_config, &state.scheduler)?;
1667        if let Some(preempt) =
1668            state
1669                .transition
1670                .pending_effects
1671                .iter()
1672                .find_map(|effect| match &effect.effect {
1673                    EffectKind::PreemptTasks(preempt) => Some(preempt),
1674                    _ => None,
1675                })
1676        {
1677            engine.restore_pending_preempt(
1678                preempt
1679                    .attempts
1680                    .iter()
1681                    .map(|attempt| attempt.task_id.as_str().to_string())
1682                    .collect(),
1683                preempt.reason.clone(),
1684            );
1685        }
1686        restore_context_vm(&mut engine, &state.context_vm)?;
1687        engine.restore_memory_write_window(
1688            state
1689                .syscall
1690                .memory_write_window_ms
1691                .iter()
1692                .map(|at| at.get())
1693                .collect(),
1694        );
1695
1696        if let Some(milestone) = &state.scheduler.milestone {
1697            let contract = live_config
1698                .verification_contract(&milestone.contract_id)
1699                .ok_or_else(|| {
1700                    KernelFault::new(
1701                        KernelFaultCode::CheckpointIncompatible,
1702                        format!(
1703                            "the checkpoint runs verification contract {:?}, which this \
1704                             operation's configuration no longer declares",
1705                            milestone.contract_id
1706                        ),
1707                    )
1708                })?;
1709            engine.load_milestone_contract(core_milestone_contract(contract, &live_config));
1710            if !engine
1711                .restore_milestone_cursor(milestone.phase_id.as_deref(), milestone.blocked_count)
1712            {
1713                return Err(KernelFault::new(
1714                    KernelFaultCode::CheckpointIncompatible,
1715                    format!(
1716                        "the checkpoint sits on milestone phase {:?} of contract {:?}, which that \
1717                         contract does not declare",
1718                        milestone.phase_id, milestone.contract_id
1719                    ),
1720                ));
1721            }
1722            driver.loaded_contract_id = Some(milestone.contract_id.clone());
1723        }
1724
1725        driver.engine = Some(engine);
1726        driver.root_kind = state.transition.root_kind;
1727        driver.focus = state.transition.focus.clone();
1728        if let Some(workflow) = &state.scheduler.workflow {
1729            driver.workflow_id = Some(workflow.workflow_id.clone());
1730            driver.node_ids = workflow
1731                .nodes
1732                .iter()
1733                .map(|node| node.node_id.clone())
1734                .collect();
1735            driver.workflow_nodes = workflow
1736                .nodes
1737                .iter()
1738                .map(|node| WireNode {
1739                    node_id: node.node_id.clone(),
1740                    task: node.task.clone(),
1741                    depends_on: node.depends_on.clone(),
1742                    run_spec: node.run_spec.clone(),
1743                })
1744                .collect();
1745        }
1746        driver.attempts = state
1747            .scheduler
1748            .attempts
1749            .iter()
1750            .map(|attempt| {
1751                (
1752                    attempt.task_id.as_str().to_string(),
1753                    attempt.attempt_id.clone(),
1754                )
1755            })
1756            .collect();
1757        driver.provider_calls = state
1758            .syscall
1759            .provider_calls
1760            .iter()
1761            .map(|call| {
1762                (
1763                    call.effect_id.clone(),
1764                    PendingProviderCall {
1765                        task_id: call.task_id.clone(),
1766                        exposed_tools: call.exposed_tools.iter().cloned().collect(),
1767                    },
1768                )
1769            })
1770            .collect();
1771        driver.consumed_calls = state.syscall.consumed_call_ids.iter().cloned().collect();
1772        driver.pending_memory_writes = state
1773            .syscall
1774            .authored_memory_writes
1775            .iter()
1776            .map(|write| {
1777                (
1778                    write.effect_id.clone(),
1779                    AuthoredMemoryWrite {
1780                        binding_id: write.binding_id.clone(),
1781                        name: write.name.clone(),
1782                        kind: write.kind,
1783                        size_bytes: write.size_bytes,
1784                    },
1785                )
1786            })
1787            .collect();
1788        driver.pending_memory_queries = state
1789            .syscall
1790            .authored_memory_queries
1791            .iter()
1792            .map(|query| {
1793                (
1794                    query.effect_id.clone(),
1795                    AuthoredMemoryQuery {
1796                        binding_id: query.binding_id.clone(),
1797                        text: query.text.clone(),
1798                        requested_k: query.requested_k,
1799                    },
1800                )
1801            })
1802            .collect();
1803        driver.pending_payload_loads = state
1804            .context_vm
1805            .pending_payload_loads
1806            .iter()
1807            .map(|load| {
1808                (
1809                    load.effect_id.clone(),
1810                    PendingPayloadLoad {
1811                        handle_id: load.handle_id.clone(),
1812                        digest: load.digest.clone(),
1813                        original_size: load.original_size.map(WireU64::get),
1814                    },
1815                )
1816            })
1817            .collect();
1818        Ok(driver)
1819    }
1820
1821    // ----- the plan function -----
1822
1823    /// Plan one input.
1824    ///
1825    /// Pass this to [`KernelTransaction::prepare`](super::transaction::KernelTransaction::prepare).
1826    /// The focus/root-kind fold does **not** advance here — call [`Self::note_committed`] once the
1827    /// host's append and the transaction's commit have both succeeded.
1828    pub fn plan(&mut self, context: &PlanContext<'_>) -> Result<PlannedStep, KernelFault> {
1829        if let Some(fault) = &self.poison {
1830            return Err(fault.clone());
1831        }
1832        if let Some(staged) = &self.staged {
1833            let staged_seq = staged.step_seq;
1834            return Err(self.poison_with(KernelFault::new(
1835                KernelFaultCode::TransactionConflict,
1836                format!(
1837                    "the driver still holds the plan of step {staged_seq}; its transition never \
1838                     committed while the semantic kernel already advanced under it, so this \
1839                     runtime no longer describes the journal — rebuild from the records"
1840                ),
1841            )));
1842        }
1843        let mut step = self.plan_inner(context)?;
1844        step.observations = self
1845            .engine
1846            .as_mut()
1847            .map(LoopStateMachine::take_observations)
1848            .unwrap_or_default();
1849        self.staged = Some(StagedFocus {
1850            step_seq: context.step_seq,
1851            root_kind: step.root_kind,
1852            focus: step.focus.clone(),
1853        });
1854        Ok(step)
1855    }
1856
1857    /// Install the staged fold after the transaction committed the record (§7.4: a focus moves only
1858    /// on a committed transition).
1859    pub fn note_committed(&mut self, step_seq: WireU64) -> Result<(), KernelFault> {
1860        if let Some(fault) = &self.poison {
1861            return Err(fault.clone());
1862        }
1863        let Some(staged) = self.staged.take() else {
1864            return Err(self.poison_with(KernelFault::new(
1865                KernelFaultCode::TransactionConflict,
1866                format!("step {step_seq} committed, but the driver planned no such step"),
1867            )));
1868        };
1869        if staged.step_seq != step_seq {
1870            let planned = staged.step_seq;
1871            return Err(self.poison_with(KernelFault::new(
1872                KernelFaultCode::TransactionConflict,
1873                format!("step {step_seq} committed, but the driver planned step {planned}"),
1874            )));
1875        }
1876        if let Some(kind) = staged.root_kind {
1877            self.root_kind = Some(kind);
1878        }
1879        self.focus = staged.focus;
1880        Ok(())
1881    }
1882
1883    /// Plan **and** fold in one call — the shape
1884    /// [`rebuild_from_records`](super::transaction::KernelTransaction::rebuild_from_records) needs,
1885    /// where every record it replays is by definition already durable.
1886    pub fn fold(&mut self, context: &PlanContext<'_>) -> Result<PlannedStep, KernelFault> {
1887        let step = self.plan(context)?;
1888        self.note_committed(context.step_seq)?;
1889        Ok(step)
1890    }
1891
1892    // ----- §10.2 · the agent-authored workflow seam -----
1893
1894    /// Enter a workflow the agent asked for, inside an agent root (§10.2).
1895    ///
1896    /// This is the P1 reduction point Task 10 wires its `SyscallRequest::SubmitWorkflow` gate to;
1897    /// the authority rules it enforces are already the final ones:
1898    ///
1899    /// * the root kind stays `Agent` — a syscall never re-roots an operation;
1900    /// * the focus moves to `WorkflowController { parent_task_id: Some(agent task) }`;
1901    /// * depth is at most 1. Asking for a workflow while the focus already *is* a
1902    ///   `WorkflowController` is an `InvalidAuthority` fault with zero mutation — workflows do not
1903    ///   stack (§15.4).
1904    pub fn begin_nested_workflow(
1905        &mut self,
1906        context: &PlanContext<'_>,
1907        spec: &WireSpec,
1908    ) -> Result<PlannedStep, KernelFault> {
1909        if let Some(fault) = &self.poison {
1910            return Err(fault.clone());
1911        }
1912        let mut index = 0;
1913        let outcome = self
1914            .enter_nested_workflow(context, spec, &mut index)
1915            .map_err(|refusal| match refusal {
1916                SyscallRefusal::Fault(fault) => fault,
1917                // A direct caller has no observation channel, so the gate's denial becomes the
1918                // transition's refusal. Through the P1 path the same denial is an audit fact.
1919                SyscallRefusal::Rejected(rejected) => {
1920                    KernelFault::new(KernelFaultCode::ResourceLimitExceeded, rejected.reason)
1921                }
1922            })?;
1923        let step = PlannedStep {
1924            root_kind: Some(RootKind::Agent),
1925            focus: outcome.focus,
1926            observations: self
1927                .engine
1928                .as_mut()
1929                .map(LoopStateMachine::take_observations)
1930                .unwrap_or_default(),
1931            disposition: StepDisposition::Effects(EffectsDisposition {
1932                effects: outcome.effects,
1933            }),
1934        };
1935        self.staged = Some(StagedFocus {
1936            step_seq: context.step_seq,
1937            root_kind: step.root_kind,
1938            focus: step.focus.clone(),
1939        });
1940        Ok(step)
1941    }
1942
1943    /// The body of [`Self::begin_nested_workflow`], without the staging — so a syscall batch that
1944    /// also carries other requests composes it instead of racing it for the staging slot.
1945    fn enter_nested_workflow(
1946        &mut self,
1947        context: &PlanContext<'_>,
1948        spec: &WireSpec,
1949        effect_index: &mut u32,
1950    ) -> Result<SyscallOutcome, SyscallRefusal> {
1951        let staged = self.staged.as_ref().map(|staged| staged.focus.clone());
1952        let focus = staged.as_ref().unwrap_or(&self.focus);
1953        let root_kind = self.root_kind;
1954
1955        let parent_task_id = match (root_kind, focus) {
1956            (Some(RootKind::Agent), Some(ExecutionFocus::AgentTurn(turn))) => turn.task_id.clone(),
1957            (Some(RootKind::Agent), Some(ExecutionFocus::WorkflowController(_))) => {
1958                return Err(authority(
1959                    "a workflow is already the execution focus; workflows do not stack, so a \
1960                     second start request is refused with no spawn effect (§7.4 focus depth ≤ 1)",
1961                ));
1962            }
1963            (Some(RootKind::Workflow), _) => {
1964                return Err(authority(
1965                    "this operation's root is a workflow; its focus never moves, and a nested \
1966                     workflow start is not a transition it admits (§7.4)",
1967                ));
1968            }
1969            _ => {
1970                return Err(SyscallRefusal::Fault(KernelFault::new(
1971                    KernelFaultCode::InvalidLifecycle,
1972                    "no root has started, so there is no agent turn to suspend".to_string(),
1973                )));
1974            }
1975        };
1976
1977        for node in &spec.nodes {
1978            self.require_known_contract(context.config, node.run_spec.as_ref())
1979                .map_err(|fault| {
1980                    SyscallRefusal::Rejected(SyscallRejection::new("start_workflow", fault.message))
1981                })?;
1982        }
1983        let core_spec = build_core_spec(spec).map_err(SyscallRefusal::Fault)?;
1984        let node_ids = wire_node_ids(spec);
1985        let workflow_id = mint_workflow_id(&context.input.operation_id, context.step_seq);
1986        self.require_effect_support(context.config, EffectKindTag::SpawnTasks)
1987            .map_err(SyscallRefusal::Fault)?;
1988
1989        // §10.2 · the resource gate runs before the DAG is installed, so a denial commits with no
1990        // spawn effect at all rather than with a workflow the run cannot afford.
1991        let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
1992        let disposition = engine.gate_syscall(&CoreSyscall::LoadWorkflow {
1993            node_count: spec.nodes.len(),
1994        });
1995        if !disposition.is_allowed() {
1996            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
1997                "start_workflow",
1998                denial_reason(&disposition, "workflow authoring denied"),
1999            )));
2000        }
2001
2002        // ----- past this line the semantic engine advances -----
2003        engine.set_root_workflow(false);
2004        let action = engine.load_workflow(core_spec);
2005        self.node_ids = node_ids;
2006        self.workflow_nodes = spec.nodes.clone();
2007        self.workflow_id = Some(workflow_id.clone());
2008        let disposition = self
2009            .disposition_for_at(context, action, RootKind::Agent, effect_index)
2010            .map_err(SyscallRefusal::Fault)?;
2011        let StepDisposition::Effects(effects) = disposition else {
2012            return Err(SyscallRefusal::Fault(KernelFault::new(
2013                KernelFaultCode::InvalidLifecycle,
2014                "entering a nested workflow cannot terminate the operation".to_string(),
2015            )));
2016        };
2017        Ok(SyscallOutcome {
2018            effects: effects.effects,
2019            focus: Some(ExecutionFocus::workflow_controller(
2020                workflow_id,
2021                Some(parent_task_id),
2022            )),
2023            needs_workflow_round: false,
2024        })
2025    }
2026
2027    // ----- §7.6 · P1 syscalls and caller causation -----
2028
2029    /// Derive the caller of every syscall a provider result carries.
2030    ///
2031    /// This is the whole of "the host does not declare a caller". The kernel already knows which
2032    /// provider effect it is resolving and which surface that call advertised; a `ProviderTool`
2033    /// causation is that knowledge written down. Three refusals, all before anything moves:
2034    ///
2035    /// * the effect is not one this driver published as a provider call — there is no turn to
2036    ///   attribute the request to;
2037    /// * the tool name was never exposed on that turn — a result may not invent a surface, and a
2038    ///   forged `start_workflow` in a run that never offered one dies here;
2039    /// * the call id already produced a syscall — a causation is spent once, so re-delivering the
2040    ///   same result under a fresh input id buys nothing.
2041    fn derive_provider_syscalls(
2042        &self,
2043        effect_id: &EffectId,
2044        calls: &[WireToolCall],
2045    ) -> Result<Vec<(SyscallCausation, WireToolCall)>, KernelFault> {
2046        let Some(pending) = self.provider_calls.get(effect_id) else {
2047            return Err(KernelFault::new(
2048                KernelFaultCode::InvalidAuthority,
2049                format!(
2050                    "effect {effect_id} is not a provider call this kernel published, so a tool \
2051                     call inside its result has no caller to derive (§7.6)"
2052                ),
2053            ));
2054        };
2055        let mut seen: BTreeSet<&str> = BTreeSet::new();
2056        let mut derived = Vec::with_capacity(calls.len());
2057        for call in calls {
2058            if !pending.exposed_tools.contains(&call.name) {
2059                return Err(KernelFault::new(
2060                    KernelFaultCode::InvalidAuthority,
2061                    format!(
2062                        "the turn behind effect {effect_id} exposed no tool named {:?}; a caller is \
2063                         derived from the surface the kernel published, never from the name a \
2064                         result carries (§7.6)",
2065                        call.name
2066                    ),
2067                ));
2068            }
2069            if self.consumed_calls.contains(call.call_id.as_str())
2070                || !seen.insert(call.call_id.as_str())
2071            {
2072                return Err(KernelFault::new(
2073                    KernelFaultCode::InvalidAuthority,
2074                    format!(
2075                        "call {} already produced a syscall; a causation is consumed once (§7.6)",
2076                        call.call_id
2077                    ),
2078                ));
2079            }
2080            derived.push((
2081                SyscallCausation::ProviderTool(ProviderToolCausation {
2082                    provider_effect_id: effect_id.clone(),
2083                    call_id: call.call_id.clone(),
2084                    task_id: pending.task_id.clone(),
2085                }),
2086                call.clone(),
2087            ));
2088        }
2089        Ok(derived)
2090    }
2091
2092    /// §7.9 + §7.6 · one completed provider turn, whole.
2093    ///
2094    /// The batch a model returns can mix two populations the kernel must not confuse: **P1
2095    /// syscalls**, which the kernel adjudicates itself, and **host tool calls**, which it
2096    /// dispatches. Both halves happen here, in this order and for this reason:
2097    ///
2098    /// 1. the syscalls are adjudicated *before* the turn is fed, because they change what the next
2099    ///    rendered context contains — an activated skill, an edited plan, a grown DAG;
2100    /// 2. the assistant message is fed **whole**, tool calls included, so the model reads back the
2101    ///    turn it actually emitted; each syscall is closed with the kernel's own answer so every
2102    ///    call still has a result (the trained convention);
2103    /// 3. the continuation is the engine's, not this function's — [`LoopStateMachine::feed`] stays
2104    ///    the single place that decides what a provider turn means. All this reduction contributes
2105    ///    is *what the kernel already has outstanding*, which is the §5k question: a pure
2106    ///    control-plane batch publishes no effect, so without an answer the operation would stall.
2107    fn plan_provider_completed(
2108        &mut self,
2109        context: &PlanContext<'_>,
2110        effect_id: &EffectId,
2111        completed: &ProviderCompleted,
2112    ) -> Result<PlannedStep, KernelFault> {
2113        let root_kind = self.require_root_kind()?;
2114        self.require_pending_provider_call(effect_id)?;
2115        let syscalls: Vec<WireToolCall> = completed
2116            .message
2117            .tool_calls
2118            .iter()
2119            .filter(|call| is_syscall_tool(&call.name))
2120            .cloned()
2121            .collect();
2122        let derived = self.derive_provider_syscalls(effect_id, &syscalls)?;
2123        let message = core_provider_message(&completed.message)?;
2124
2125        // ----- past this line the semantic engine advances -----
2126        let engine = self.engine_mut()?;
2127        if let Some(tokens) = completed.observed_input_tokens {
2128            engine.ctx.set_observed_prompt_tokens(tokens);
2129        }
2130        // §22.8 · the typed stop reason answers the one question the loop asks, so no vendor text
2131        // is classified anywhere on this path.
2132        engine.set_output_truncated(matches!(
2133            completed.stop_reason,
2134            Some(super::effect::ProviderStopReason::MaxTokens)
2135        ));
2136
2137        let mut index = 0u32;
2138        let mut effects = Vec::new();
2139        let mut syscall_focus: Option<ExecutionFocus> = None;
2140        let mut answered: Vec<AnsweredCall> = Vec::with_capacity(derived.len());
2141        let mut needs_round = false;
2142        for (causation, call) in &derived {
2143            let outcome = match decode_syscall(call) {
2144                Ok(request) => self.apply_syscall(context, causation, &request, &mut index),
2145                Err(rejection) => Err(SyscallRefusal::Rejected(rejection)),
2146            };
2147            match outcome {
2148                Ok(outcome) => {
2149                    answered.push(AnsweredCall {
2150                        call_id: call.call_id.as_str().into(),
2151                        output: syscall_ack(&call.name).to_string(),
2152                        is_error: false,
2153                    });
2154                    effects.extend(outcome.effects);
2155                    if let Some(next) = outcome.focus {
2156                        syscall_focus = Some(next);
2157                    }
2158                    needs_round |= outcome.needs_workflow_round;
2159                }
2160                Err(SyscallRefusal::Fault(fault)) => return Err(fault),
2161                Err(SyscallRefusal::Rejected(rejection)) => {
2162                    answered.push(AnsweredCall {
2163                        call_id: call.call_id.as_str().into(),
2164                        output: rejection.reason.clone(),
2165                        is_error: true,
2166                    });
2167                    self.note_rejection(rejection.by(&causation_task(causation)))
2168                }
2169            }
2170        }
2171        // §10.3 · a batch that grew the DAG owes it a spawn round, and the author waits for the
2172        // work it just authored rather than taking another turn.
2173        let mut awaits_kernel_work = !effects.is_empty();
2174        if needs_round {
2175            awaits_kernel_work = true;
2176            let action = self.engine_mut()?.drive_workflow_round();
2177            self.extend_with_action(context, action, root_kind, &mut index, &mut effects)?;
2178        }
2179
2180        let engine = self.engine_mut()?;
2181        engine.stage_adjudicated_turn(AdjudicatedTurn {
2182            answered_calls: answered,
2183            idle_continuation: if awaits_kernel_work {
2184                IdleContinuation::Await
2185            } else {
2186                IdleContinuation::CallProvider
2187            },
2188        });
2189        // The syscalls' own observations happened before `feed`, which clears the buffer at the
2190        // head of every event — carry them across, exactly as the child-completion path does.
2191        let syscall_observations = engine.take_observations();
2192        let action = engine.feed(LoopEvent::LLMResponse { message });
2193        let mut step = self.continue_after_at(context, action, root_kind, &mut index)?;
2194        if let Some(engine) = self.engine.as_mut() {
2195            engine.observations.splice(0..0, syscall_observations);
2196        }
2197        if syscall_focus.is_some() {
2198            step.focus = syscall_focus;
2199        }
2200        if !effects.is_empty() {
2201            match &mut step.disposition {
2202                StepDisposition::Effects(published) => {
2203                    let mut merged = effects;
2204                    merged.append(&mut published.effects);
2205                    published.effects = merged;
2206                }
2207                StepDisposition::Terminal(_) => {
2208                    return Err(KernelFault::new(
2209                        KernelFaultCode::InvalidLifecycle,
2210                        "a provider turn that terminates the operation cannot also publish the \
2211                         effects its syscalls asked for (§7.12)"
2212                            .to_string(),
2213                    ));
2214                }
2215            }
2216        }
2217
2218        // The causation ledger is settled last, so a transition that ends in a fault does not leave
2219        // this driver claiming a spent call id and a resolved provider surface that the journal has
2220        // no record of. (The engine advance above is guarded the same way every other plan is — by
2221        // the staging slot, which fails closed on a plan that never commits.)
2222        self.provider_calls.remove(effect_id);
2223        for (_, call) in &derived {
2224            self.consumed_calls
2225                .insert(call.call_id.as_str().to_string());
2226        }
2227        Ok(step)
2228    }
2229
2230    /// Apply one already-attributed request. Every arm reads its caller from `causation` and
2231    /// nothing else — there is no parameter through which a host could name a different one.
2232    fn apply_syscall(
2233        &mut self,
2234        context: &PlanContext<'_>,
2235        causation: &SyscallCausation,
2236        request: &SyscallRequest,
2237        effect_index: &mut u32,
2238    ) -> Result<SyscallOutcome, SyscallRefusal> {
2239        let caller = causation_task(causation);
2240        let quarantined = self
2241            .engine
2242            .as_ref()
2243            .is_some_and(|engine| engine.task_quarantined(caller.as_str()));
2244        if quarantined && let Some(family) = privileged_family(request) {
2245            // §7.6 · a quarantined task read untrusted content. Letting it grow the DAG, mutate its
2246            // capability surface or reach memory would make the untrusted content the author of the
2247            // escalation. Fails closed on the whole family rather than trusting per-request
2248            // coercion to be exhaustive.
2249            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2250                family,
2251                format!(
2252                    "quarantine: task {caller} is quarantined and may not widen its authority \
2253                     through a {family} syscall"
2254                ),
2255            )));
2256        }
2257
2258        match request {
2259            SyscallRequest::SubmitWorkflow(submit) => {
2260                if submit.spec.nodes.is_empty() {
2261                    return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2262                        "start_workflow",
2263                        "an authored workflow with no nodes has nothing to spawn",
2264                    )));
2265                }
2266                if self
2267                    .engine
2268                    .as_ref()
2269                    .is_some_and(LoopStateMachine::workflow_active)
2270                {
2271                    // §10.2 flatten: the caller is already inside a DAG, so its spec grows that DAG
2272                    // rather than stacking a second one. One root lifecycle, one quota.
2273                    self.append_nodes(
2274                        context.config,
2275                        &submit.spec.nodes,
2276                        &caller,
2277                        CoreSyscall::LoadWorkflow { node_count: 0 },
2278                        "start_workflow",
2279                    )
2280                } else {
2281                    self.enter_nested_workflow(context, &submit.spec, effect_index)
2282                }
2283            }
2284            SyscallRequest::AppendWorkflowNodes(append) => {
2285                if append.nodes.is_empty() {
2286                    return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2287                        "submit_workflow_nodes",
2288                        "an empty submission appends nothing",
2289                    )));
2290                }
2291                if !self
2292                    .engine
2293                    .as_ref()
2294                    .is_some_and(LoopStateMachine::workflow_active)
2295                {
2296                    return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2297                        "submit_workflow_nodes",
2298                        "no workflow is in flight, so there is no graph to append to",
2299                    )));
2300                }
2301                self.append_nodes(
2302                    context.config,
2303                    &append.nodes,
2304                    &caller,
2305                    CoreSyscall::SubmitNodes { count: 0 },
2306                    "submit_workflow_nodes",
2307                )
2308            }
2309            SyscallRequest::ActivateSkill(activate) => {
2310                let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2311                if !engine.ctx.skill_available(&activate.name) {
2312                    return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2313                        "skill",
2314                        format!(
2315                            "this operation declares no skill named {:?}; activation is a \
2316                             capability mutation and is refused rather than invented",
2317                            activate.name
2318                        ),
2319                    )));
2320                }
2321                let expires_at_turn = activate
2322                    .lease_turns
2323                    .map(|turns| engine.turn.saturating_add(turns));
2324                engine
2325                    .ctx
2326                    .activate_skill_leased(activate.name.as_str(), expires_at_turn);
2327                Ok(SyscallOutcome::default())
2328            }
2329            SyscallRequest::UpdateTask(update) => {
2330                let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2331                engine.ctx.update_task(core_task_update(&update.update));
2332                Ok(SyscallOutcome::default())
2333            }
2334            SyscallRequest::RequestMemoryWrite(write) => {
2335                self.plan_memory_write(context, causation, &write.proposal, effect_index)
2336            }
2337            SyscallRequest::RequestMemoryQuery(query) => {
2338                self.plan_memory_query(context, causation, &query.query, effect_index)
2339            }
2340            SyscallRequest::PageIn(page_in) => {
2341                self.plan_page_in(context, &page_in.handle_id, effect_index)
2342            }
2343        }
2344    }
2345
2346    /// §10.3 · gate + trust-aware append, with the spawn round deferred to the caller.
2347    fn append_nodes(
2348        &mut self,
2349        config: &ResolvedOperationConfig,
2350        nodes: &[WireNode],
2351        caller: &TaskId,
2352        syscall: CoreSyscall,
2353        label: &'static str,
2354    ) -> Result<SyscallOutcome, SyscallRefusal> {
2355        // §7.3 · an authored node may not name a contract the operation never declared. Same rule
2356        // as a root spec, and checked on the same side of the gate as identity/acyclicity: a batch
2357        // with a dangling reference is malformed and must not spend quota.
2358        for node in nodes {
2359            self.require_known_contract(config, node.run_spec.as_ref())
2360                .map_err(|fault| {
2361                    SyscallRefusal::Rejected(SyscallRejection::new(label, fault.message))
2362                })?;
2363        }
2364        // Batch-relative identity and acyclicity are checked before the gate sees a count, so a
2365        // malformed batch never spends quota.
2366        let core_nodes = build_core_spec(&WireSpec {
2367            name: String::new(),
2368            nodes: nodes.to_vec(),
2369        })
2370        .map_err(|fault| SyscallRefusal::Rejected(SyscallRejection::new(label, fault.message)))?
2371        .nodes;
2372
2373        let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2374        let admitted = engine.append_workflow_nodes(
2375            core_nodes,
2376            // §7.6 · the submitter is the derived caller, never an optional host field. The
2377            // historical `submitter_agent_id: Option<String>` erred open — omitting it skipped the
2378            // quarantine coercion entirely — and there is no shape here that can omit it.
2379            Some(caller.as_str()),
2380            // `append_workflow_nodes` refills the count from the batch, so the two entry points
2381            // cannot disagree about what they meter.
2382            syscall,
2383            label,
2384        );
2385        if !admitted {
2386            // `append_workflow_nodes` already pushed the rejection observation and the model-facing
2387            // note; re-recording it here would double the audit fact.
2388            return Ok(SyscallOutcome::default());
2389        }
2390        // §10.3 · deterministic node identity is a kernel fact. An appended batch lands at the end
2391        // of the index-addressed DAG, so mirroring its wire ids here is what lets a later spawn
2392        // effect and the workflow terminal still name the node the caller declared — rather than
2393        // falling back to the internal `wf-nodeN` id.
2394        self.node_ids
2395            .extend(nodes.iter().map(|node| node.node_id.clone()));
2396        self.workflow_nodes.extend_from_slice(nodes);
2397        Ok(SyscallOutcome {
2398            effects: Vec::new(),
2399            focus: None,
2400            needs_workflow_round: true,
2401        })
2402    }
2403
2404    fn plan_memory_write(
2405        &mut self,
2406        context: &PlanContext<'_>,
2407        causation: &SyscallCausation,
2408        proposal: &super::syscall::MemoryWriteProposal,
2409        effect_index: &mut u32,
2410    ) -> Result<SyscallOutcome, SyscallRefusal> {
2411        let Some(binding) = context.config.memory_access.clone() else {
2412            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2413                "write_memory",
2414                "this operation holds no memory binding, so it can author no memory record",
2415            )));
2416        };
2417        if !binding.capabilities.write {
2418            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2419                "write_memory",
2420                "this operation's memory binding is read-only",
2421            )));
2422        }
2423        self.require_effect_support(context.config, EffectKindTag::PersistMemory)
2424            .map_err(SyscallRefusal::Fault)?;
2425        let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2426        let disposition = engine.gate_memory_write_proposal();
2427        if !disposition.is_allowed() {
2428            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2429                "write_memory",
2430                denial_reason(&disposition, "memory write denied"),
2431            )));
2432        }
2433        // §22.13 · the proposal contributed name/kind/content/evidence and nothing else. Tenant,
2434        // author, trust, timestamp and provenance are authored here, from the operation's binding,
2435        // the envelope's accepted time and the derived causation.
2436        let authored = AuthoredMemoryWrite {
2437            binding_id: binding.binding_id.clone(),
2438            name: proposal.name.clone(),
2439            kind: proposal.kind,
2440            size_bytes: proposal.content.len() as u32,
2441        };
2442        let effect = EffectKind::PersistMemory(PersistMemoryEffect {
2443            binding,
2444            memory: CanonicalMemoryWrite {
2445                name: proposal.name.clone(),
2446                kind: proposal.kind,
2447                content: proposal.content.clone(),
2448                description: proposal.description.clone(),
2449                evidence_refs: proposal.evidence_refs.clone(),
2450                accepted_at_ms: context.input.observed_at_ms,
2451                causation: causation.clone(),
2452            },
2453        });
2454        let published = self.mint_effect(context, effect, effect_index);
2455        self.pending_memory_writes
2456            .insert(published.effect_id.clone(), authored);
2457        Ok(SyscallOutcome {
2458            effects: vec![published],
2459            focus: None,
2460            needs_workflow_round: false,
2461        })
2462    }
2463
2464    fn plan_memory_query(
2465        &mut self,
2466        context: &PlanContext<'_>,
2467        causation: &SyscallCausation,
2468        proposal: &super::syscall::MemoryQueryProposal,
2469        effect_index: &mut u32,
2470    ) -> Result<SyscallOutcome, SyscallRefusal> {
2471        let Some(binding) = context.config.memory_access.clone() else {
2472            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2473                "query_memory",
2474                "this operation holds no memory binding, so it can read no memory record",
2475            )));
2476        };
2477        if !binding.capabilities.read {
2478            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2479                "query_memory",
2480                "this operation's memory binding is write-only",
2481            )));
2482        }
2483        self.require_effect_support(context.config, EffectKindTag::QueryMemory)
2484            .map_err(SyscallRefusal::Fault)?;
2485        // The retrieval width is the operation's policy, clamped — the host does not re-decide it
2486        // and a model cannot widen it by asking for more.
2487        let ceiling = context.config.memory_policy.retrieval_top_k;
2488        let requested_k = proposal.limit.unwrap_or(ceiling).clamp(1, ceiling);
2489        let authored = AuthoredMemoryQuery {
2490            binding_id: binding.binding_id.clone(),
2491            text: proposal.text.clone(),
2492            requested_k,
2493        };
2494        let effect = EffectKind::QueryMemory(QueryMemoryEffect {
2495            binding,
2496            query: CanonicalMemoryQuery {
2497                text: proposal.text.clone(),
2498                kinds: proposal.kinds.clone(),
2499                accepted_at_ms: context.input.observed_at_ms,
2500                causation: causation.clone(),
2501            },
2502            requested_k,
2503        });
2504        let published = self.mint_effect(context, effect, effect_index);
2505        self.pending_memory_queries
2506            .insert(published.effect_id.clone(), authored);
2507        Ok(SyscallOutcome {
2508            effects: vec![published],
2509            focus: None,
2510            needs_workflow_round: false,
2511        })
2512    }
2513
2514    /// §7.6 / §7.10 rule 4 · `read_result` reduces to exactly one thing: a `LoadPayload` effect for
2515    /// a body the caller already holds an address for.
2516    ///
2517    /// Two refusals, both *rejections* rather than faults — the caller was established, so what is
2518    /// refused is the address it named, and the transition that carried it still commits with an
2519    /// audit fact the model reads on its next turn:
2520    ///
2521    /// - an address that is not in this operation's handle table. A page-in reaches only what the
2522    ///   caller already holds, which is what stops `read_result` from becoming a general read
2523    ///   primitive — the historical SDK answered it by scanning a spool directory and then the
2524    ///   session log, so any path-shaped string was a readable address.
2525    /// - an address whose body core still holds. `Resident` and `Collapsed` are not paged out at
2526    ///   all. Neither yields a locator the kernel could hand back,
2527    ///   and fabricating one is exactly the confusion the closed union removes.
2528    fn plan_page_in(
2529        &mut self,
2530        context: &PlanContext<'_>,
2531        handle_id: &super::scalar::HandleId,
2532        effect_index: &mut u32,
2533    ) -> Result<SyscallOutcome, SyscallRefusal> {
2534        let engine = self.engine_mut().map_err(SyscallRefusal::Fault)?;
2535        let Some(residency) = engine.ctx.payload_residency(handle_id.as_str()).cloned() else {
2536            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2537                READ_RESULT_TOOL_NAME,
2538                format!(
2539                    "handle {handle_id} is not reachable in this operation's handle table; a \
2540                     page-in addresses only what the caller already holds"
2541                ),
2542            )));
2543        };
2544        let (Some(payload_ref), Some(digest)) = (residency.payload_ref(), residency.digest())
2545        else {
2546            return Err(SyscallRefusal::Rejected(SyscallRejection::new(
2547                READ_RESULT_TOOL_NAME,
2548                format!(
2549                    "handle {handle_id} is {} — its body is held by this kernel, not by the \
2550                     payload store, so there is nothing to page in",
2551                    residency.label()
2552                ),
2553            )));
2554        };
2555        let payload_ref = PayloadRef::new(payload_ref).map_err(|error| {
2556            SyscallRefusal::Fault(KernelFault::new(
2557                KernelFaultCode::MalformedEnvelope,
2558                format!(
2559                    "handle {handle_id} records an unusable payload locator: {}",
2560                    error.message
2561                ),
2562            ))
2563        })?;
2564        self.require_effect_support(context.config, EffectKindTag::LoadPayload)
2565            .map_err(SyscallRefusal::Fault)?;
2566        let effect = EffectKind::LoadPayload(LoadPayloadEffect {
2567            handle_id: handle_id.clone(),
2568            payload_ref,
2569        });
2570        let published = self.mint_effect(context, effect, effect_index);
2571        self.pending_payload_loads.insert(
2572            published.effect_id.clone(),
2573            PendingPayloadLoad {
2574                handle_id: handle_id.as_str().to_string(),
2575                digest: digest.to_string(),
2576                original_size: match &residency {
2577                    Residency::External { original_size, .. } => Some(*original_size),
2578                    _ => None,
2579                },
2580            },
2581        );
2582        Ok(SyscallOutcome {
2583            effects: vec![published],
2584            focus: None,
2585            needs_workflow_round: false,
2586        })
2587    }
2588
2589    /// Record a refused request as an audit fact the model reads on its next turn (§7.6, §7.7).
2590    fn note_rejection(&mut self, rejection: SyscallRejection) {
2591        let Some(engine) = self.engine.as_mut() else {
2592            return;
2593        };
2594        let note = crate::scheduler::rollback::build_control_rejection_note(
2595            rejection.operation,
2596            &rejection.reason,
2597            engine.ctx.config.verbose_control_notes,
2598        );
2599        engine.ctx.push_signal(note);
2600        let turn = engine.turn;
2601        engine
2602            .observations
2603            .push(KernelObservation::ControlRequestRejected {
2604                turn,
2605                operation: rejection.operation.to_string(),
2606                subject: rejection.subject,
2607                reason: rejection.reason,
2608            });
2609    }
2610
2611    fn mint_effect(
2612        &self,
2613        context: &PlanContext<'_>,
2614        effect: EffectKind,
2615        effect_index: &mut u32,
2616    ) -> KernelEffect {
2617        let effect_id =
2618            mint_effect_id(&context.input.operation_id, context.step_seq, *effect_index);
2619        *effect_index += 1;
2620        KernelEffect {
2621            effect_id,
2622            causation_input_id: context.input.input_id.clone(),
2623            effect,
2624        }
2625    }
2626
2627    /// Fold one engine action's effects into an accumulating step.
2628    fn extend_with_action(
2629        &mut self,
2630        context: &PlanContext<'_>,
2631        action: LoopAction,
2632        root_kind: RootKind,
2633        effect_index: &mut u32,
2634        effects: &mut Vec<KernelEffect>,
2635    ) -> Result<(), KernelFault> {
2636        match self.disposition_for_at(context, action, root_kind, effect_index)? {
2637            StepDisposition::Effects(published) => {
2638                effects.extend(published.effects);
2639                Ok(())
2640            }
2641            StepDisposition::Terminal(_) => Err(KernelFault::new(
2642                KernelFaultCode::InvalidLifecycle,
2643                "a syscall batch cannot terminate the operation; §7.12 admits effects or a \
2644                 terminal, never both in one step"
2645                    .to_string(),
2646            )),
2647        }
2648    }
2649
2650    // ----- internals -----
2651
2652    fn plan_inner(&mut self, context: &PlanContext<'_>) -> Result<PlannedStep, KernelFault> {
2653        // Every transition reads the observations *its own* semantic call produced. `start`/`feed`
2654        // clear the buffer themselves; `load_workflow` and `resolve_workflow_spawn` do not, so the
2655        // driver clears it here rather than letting a stale `WorkflowCompleted` from an earlier
2656        // step decide a later one's disposition.
2657        if let Some(engine) = self.engine.as_mut() {
2658            engine.take_observations();
2659            // §11.2 · the envelope's accepted time is this operation's only clock, and it is fed
2660            // once, here, before any semantic call. Every clock-dependent decision the step makes
2661            // (signal TTL and deadline escalation, rate-limit windows, the wall-time budget axis)
2662            // therefore reads a fact the journal already holds, so a replay decides identically.
2663            engine.observe_accepted_time(context.input.observed_at_ms.get());
2664        }
2665        match &context.input.input {
2666            NormalizedPayload::ConfigureOperation(configure) => {
2667                self.plan_configure(&configure.config)
2668            }
2669            NormalizedPayload::StartOperation(start) => {
2670                self.plan_start(context, &start.entry, &start.initial_context)
2671            }
2672            NormalizedPayload::ResolveEffect(resolve) => self.plan_resolve_effect(context, resolve),
2673            NormalizedPayload::DeliverExternalEvent(event) => {
2674                self.plan_external_event(context, &event.event)
2675            }
2676            NormalizedPayload::HostControl(control) => {
2677                self.plan_host_control(context, &control.command)
2678            }
2679        }
2680    }
2681
2682    /// §6.1.2 · genesis. The engine is built from the **resolved** configuration the record froze,
2683    /// never from this binary's defaults, so a rebuild on a newer kernel plans the same step.
2684    fn plan_configure(
2685        &mut self,
2686        config: &ResolvedOperationConfig,
2687    ) -> Result<PlannedStep, KernelFault> {
2688        self.engine = Some(build_engine(config));
2689        // §13.2 · the live-mutable half starts at revision 0, holding exactly what the genesis
2690        // record froze. A patch rebases onto this, never onto a compile-time default.
2691        self.policy = Some(LivePolicyState::new(config.clone()));
2692        Ok(PlannedStep::quiet(None, None))
2693    }
2694
2695    /// §7.4 · the one atomic root start.
2696    ///
2697    /// Both arms are ordered the same way and for the same reason: every refusal this transition
2698    /// can raise is decided while nothing has moved, and only then does the semantic engine
2699    /// advance. A rejected root start therefore leaves an operation that is still `Configured` and
2700    /// still free to choose a root.
2701    fn plan_start(
2702        &mut self,
2703        context: &PlanContext<'_>,
2704        entry: &RootEntry,
2705        initial: &InitialContext,
2706    ) -> Result<PlannedStep, KernelFault> {
2707        if self.root_kind.is_some() || self.staged.is_some() {
2708            return Err(KernelFault::new(
2709                KernelFaultCode::InvalidLifecycle,
2710                "this operation already has a root; a root entry is chosen once and is immutable \
2711                 (§6.1.3–6.1.5)"
2712                    .to_string(),
2713            ));
2714        }
2715
2716        match entry {
2717            RootEntry::Agent(agent) => {
2718                self.require_effect_support(context.config, EffectKindTag::CallProvider)?;
2719                self.require_known_contract(context.config, agent.run_spec.as_ref())?;
2720                let task = runtime_task(&agent.task);
2721                let run_spec = agent.run_spec.as_ref().map(agent_run_spec);
2722
2723                // ----- past this line the semantic engine advances -----
2724                // The cascade is installed before `start`, which is the engine's own precondition:
2725                // a contract loaded afterwards would leave phase 0 already behind the run.
2726                self.load_verification_contract(context.config, agent.run_spec.as_ref())?;
2727                let engine = self.engine_mut()?;
2728                seed_initial_context(engine, initial);
2729                engine.run_spec = run_spec;
2730                let action = engine.start(task);
2731                let disposition = self.disposition_for(context, action, RootKind::Agent)?;
2732                if !publishes(&disposition, EffectKindTag::CallProvider) {
2733                    return Err(KernelFault::new(
2734                        KernelFaultCode::InvalidLifecycle,
2735                        "an agent root's first committed step must publish a provider call (§7.4)"
2736                            .to_string(),
2737                    ));
2738                }
2739                Ok(PlannedStep {
2740                    root_kind: Some(RootKind::Agent),
2741                    focus: Some(ExecutionFocus::agent_turn(root_task_id())),
2742                    observations: Vec::new(),
2743                    disposition,
2744                })
2745            }
2746            RootEntry::Workflow(workflow) => {
2747                self.require_effect_support(context.config, EffectKindTag::SpawnTasks)?;
2748                for node in &workflow.spec.nodes {
2749                    self.require_known_contract(context.config, node.run_spec.as_ref())?;
2750                }
2751                if workflow.spec.nodes.is_empty() {
2752                    return Err(KernelFault::new(
2753                        KernelFaultCode::InvalidConfig,
2754                        "a workflow root with no nodes has no first task to spawn; a root entry \
2755                         must be able to publish its first effect (§10.1)"
2756                            .to_string(),
2757                    ));
2758                }
2759                let core_spec = build_core_spec(&workflow.spec)?;
2760                let node_ids = wire_node_ids(&workflow.spec);
2761                let workflow_id = mint_workflow_id(&context.input.operation_id, context.step_seq);
2762
2763                // ----- past this line the semantic engine advances -----
2764                let engine = self.engine_mut()?;
2765                seed_initial_context(engine, initial);
2766                // §6.1.7 — this DAG *is* the root, so its completion is the operation's terminal
2767                // rather than one more turn of a parent agent loop.
2768                engine.set_root_workflow(true);
2769                let action = engine.load_workflow(core_spec);
2770                self.node_ids = node_ids;
2771                self.workflow_nodes = workflow.spec.nodes.clone();
2772                self.workflow_id = Some(workflow_id.clone());
2773                let disposition = self.disposition_for(context, action, RootKind::Workflow)?;
2774                if !publishes(&disposition, EffectKindTag::SpawnTasks) {
2775                    return Err(KernelFault::new(
2776                        KernelFaultCode::InvalidLifecycle,
2777                        "a workflow root's first committed step must publish a task spawn, never a \
2778                         provider call (§10.1)"
2779                            .to_string(),
2780                    ));
2781                }
2782                Ok(PlannedStep {
2783                    root_kind: Some(RootKind::Workflow),
2784                    focus: Some(ExecutionFocus::workflow_controller(workflow_id, None)),
2785                    observations: Vec::new(),
2786                    disposition,
2787                })
2788            }
2789        }
2790    }
2791
2792    /// §7.9 · effect resolution — the single entry every pending effect is answered through.
2793    ///
2794    /// Two arms and no third, mirroring [`EffectOutcome`]. The transaction has already decided
2795    /// *admissibility* (still pending, kind matches, not a conflicting duplicate — §15.3), so what
2796    /// is left here is purely semantic: which internal mechanism the outcome feeds.
2797    fn plan_resolve_effect(
2798        &mut self,
2799        context: &PlanContext<'_>,
2800        resolve: &ResolveEffect,
2801    ) -> Result<PlannedStep, KernelFault> {
2802        match &resolve.outcome {
2803            EffectOutcome::Succeeded(success) => {
2804                self.plan_effect_success(context, &resolve.effect_id, &success.result)
2805            }
2806            EffectOutcome::Failed(failed) => {
2807                self.plan_effect_failure(context, &resolve.effect_id, &failed.failure)
2808            }
2809        }
2810    }
2811
2812    /// The success half: each variant reduces onto the mechanism that already owns that fact.
2813    fn plan_effect_success(
2814        &mut self,
2815        context: &PlanContext<'_>,
2816        effect_id: &EffectId,
2817        success: &EffectSuccess,
2818    ) -> Result<PlannedStep, KernelFault> {
2819        match success {
2820            EffectSuccess::Provider(provider) => match &provider.outcome {
2821                ProviderOutcome::Completed(completed) => {
2822                    self.plan_provider_completed(context, effect_id, completed)
2823                }
2824                // §7.9 · a *semantic* outcome, not a transport failure: the kernel compacts and
2825                // re-emits a provider call, and no vendor text is read to decide that (§22.8).
2826                ProviderOutcome::ContextOverflow(overflow) => {
2827                    let root_kind = self.require_root_kind()?;
2828                    self.require_pending_provider_call(effect_id)?;
2829                    let engine = self.engine_mut()?;
2830                    if let Some(tokens) = overflow.observed_input_tokens {
2831                        engine.ctx.set_observed_prompt_tokens(tokens);
2832                    }
2833                    let action = engine.recover_from_context_overflow();
2834                    self.provider_calls.remove(effect_id);
2835                    self.continue_after(context, action, root_kind)
2836                }
2837            },
2838            EffectSuccess::Tools(tools) => {
2839                let root_kind = self.require_root_kind()?;
2840                // Zero-mutation discipline: the whole batch is adjudicated against the payload
2841                // policy before any of it reaches the engine, so a batch with one illegal result
2842                // does not half-land.
2843                for payload in &tools.results {
2844                    check_payload_policy(payload, &context.config.payload_policy)?;
2845                }
2846                let mut results: Vec<ToolResult> =
2847                    tools.results.iter().map(core_tool_result).collect();
2848                results.extend(self.close_out_fatal_batch(&tools.results)?);
2849                let mut action = self.engine_mut()?.feed(LoopEvent::ToolResults { results });
2850                self.record_external_payloads(&tools.results)?;
2851                self.engine_mut()?.refresh_call_llm_action(&mut action);
2852                self.continue_after(context, action, root_kind)
2853            }
2854            EffectSuccess::Approval(approval) => {
2855                let root_kind = self.require_root_kind()?;
2856                let approved = approval
2857                    .approved_call_ids
2858                    .iter()
2859                    .map(|id| id.as_str().to_string())
2860                    .collect();
2861                let denied = approval
2862                    .denied_call_ids
2863                    .iter()
2864                    .map(|id| id.as_str().to_string())
2865                    .collect();
2866                let action = self.engine_mut()?.resolve_approval(approved, denied);
2867                self.continue_after(context, action, root_kind)
2868            }
2869            EffectSuccess::TasksSpawned(spawned) => {
2870                let mut started: Vec<String> = Vec::new();
2871                let mut failures: Vec<WorkflowSpawnFailure> = Vec::new();
2872                for attempt in &spawned.attempts {
2873                    let agent_id = attempt.task_id.as_str().to_string();
2874                    match &attempt.outcome {
2875                        super::effect::TaskLaunchStatus::Started(_) => started.push(agent_id),
2876                        super::effect::TaskLaunchStatus::Failed(failed) => {
2877                            // §10.4 · a failed launch terminates the attempt. Dropping it here is
2878                            // what makes a later completion naming it a stale causation rather than
2879                            // a resurrection of a task that never started.
2880                            self.attempts.remove(&agent_id);
2881                            failures.push(WorkflowSpawnFailure {
2882                                agent_id,
2883                                error: failed.failure.message.clone(),
2884                            });
2885                        }
2886                    }
2887                }
2888                let root_kind = self.require_root_kind()?;
2889                let action = self.engine_mut()?.resolve_workflow_spawn(started, failures);
2890                self.continue_after(context, action, root_kind)
2891            }
2892            EffectSuccess::TasksPreempted(preempted) => {
2893                let root_kind = self.require_root_kind()?;
2894                // §10.4 · every attempt named here is spent, whichever way it went: a preempted
2895                // child is gone, and one that had already finished is finished. Either way a later
2896                // completion naming it is a stale causation.
2897                for attempt in &preempted.attempts {
2898                    self.attempts.remove(attempt.task_id.as_str());
2899                }
2900                let action = self.engine_mut()?.resolve_preempt();
2901                self.continue_after(context, action, root_kind)
2902            }
2903            EffectSuccess::MemoryPersisted(persisted) => {
2904                self.commit_memory_write(effect_id, Some(&persisted.receipt), None)
2905            }
2906            EffectSuccess::MemoryQueried(queried) => {
2907                let root_kind = self.require_root_kind()?;
2908                let Some(query) = self.pending_memory_queries.remove(effect_id) else {
2909                    return Err(unowned_resolution(effect_id, "memory query"));
2910                };
2911                let turn = self.engine_mut()?.turn;
2912                let mut recalled = Vec::with_capacity(queried.recalls.len());
2913                for recall in &queried.recalls {
2914                    // §22.13 · the host answers with records, never with authority: the recall is
2915                    // rendered into history as content the model reads, and nothing about it
2916                    // rewrites this operation's binding, trust or provenance.
2917                    let content = format!(
2918                        "[MEMORY record_ref={} kind={}] {}",
2919                        recall.record_ref,
2920                        wire_memory_kind_label(recall.kind),
2921                        recall.content
2922                    );
2923                    let engine = self.engine_mut()?;
2924                    let tokens = engine.ctx.engine.count(&content).max(1);
2925                    engine.ctx.push_history(Message::user(content), tokens);
2926                    recalled.push(recall.record_ref.as_str().to_string());
2927                }
2928                // The recalls are in history now, so the turn that asked for them resumes with a
2929                // rendered context that contains them. The observation is pushed *after* the
2930                // resume, which clears the buffer at its head: a fact recorded before it would be
2931                // erased by the very continuation it describes.
2932                let engine = self.engine_mut()?;
2933                let action = engine.resume_after_preload();
2934                engine.observations.push(KernelObservation::MemoryQueried {
2935                    turn,
2936                    scope: binding_scope(&query.binding_id),
2937                    query: query.text.clone(),
2938                    requested_k: query.requested_k as usize,
2939                    requires_async_response: false,
2940                });
2941                let mut step = self.continue_after(context, action, root_kind)?;
2942                step.focus = self.focus.clone();
2943                Ok(step)
2944            }
2945            EffectSuccess::PageOutArchived(archived) => {
2946                let root_kind = self.require_root_kind()?;
2947                self.verify_page_out_receipt(context, effect_id, &archived.receipt)?;
2948                let receipt = &archived.receipt;
2949                let action = self
2950                    .engine_mut()?
2951                    .commit_page_out_archive(Some(receipt.payload_ref.as_str().to_string()));
2952                // B19 · the archived body is `PagedOut`, not `External`: it *was* resident and left
2953                // under pressure. Both states page in through the same `LoadPayload` effect, and
2954                // keeping them apart is what lets a restore say which of the two happened.
2955                let engine = self.engine_mut()?;
2956                let turn = engine.turn;
2957                let previous = engine.ctx.set_payload_residency(
2958                    receipt.handle_id.as_str(),
2959                    HandleKind::MemoryPage,
2960                    0,
2961                    Residency::PagedOut {
2962                        payload_ref: receipt.payload_ref.as_str().to_string(),
2963                        digest: receipt.digest.as_str().to_string(),
2964                    },
2965                );
2966                engine
2967                    .observations
2968                    .push(KernelObservation::PayloadResidencyChanged {
2969                        turn,
2970                        handle_id: receipt.handle_id.as_str().to_string(),
2971                        from: previous.map(|residency| residency.label().to_string()),
2972                        to: "paged_out".to_string(),
2973                        payload_ref: Some(receipt.payload_ref.as_str().to_string()),
2974                        original_size: receipt.original_size.get(),
2975                    });
2976                self.continue_after(context, action, root_kind)
2977            }
2978            EffectSuccess::MilestoneEvaluated(evaluated) => {
2979                let root_kind = self.require_root_kind()?;
2980                let action = self.engine_mut()?.feed(LoopEvent::MilestoneResult {
2981                    result: core_milestone_result(&evaluated.result),
2982                });
2983                self.continue_after(context, action, root_kind)
2984            }
2985            EffectSuccess::PayloadLoaded(loaded) => {
2986                self.commit_payload_load(context, effect_id, loaded)
2987            }
2988        }
2989    }
2990
2991    /// §7.10 · what a `fatal` disposition means on the batch that carried it.
2992    ///
2993    /// `fatal` is the host saying "the executor stopped here", so the calls this batch dispatched
2994    /// but never answered are not pending — they will never be answered at all. Left alone they
2995    /// would be orphan `tool_call`s: a provider replay with an assistant turn whose calls have no
2996    /// matching results, which is malformed on every vendor wire and is the exact failure mode the
2997    /// pairing repair in the SDKs exists to paper over.
2998    ///
2999    /// So the kernel closes them out, in the same feed, with the same shape the `ExecuteTools`
3000    /// *failure* arm uses for a batch that never ran: a committed, model-visible error result that
3001    /// says the call did not take effect. Same reasoning as v0.2.42's model-facing surface batch —
3002    /// the model adapts to a failure it can see and cannot adapt to an attempt that was erased.
3003    ///
3004    /// Returns an empty vector for an ordinary batch: no fatal result, nothing to close out.
3005    ///
3006    /// The fatality scan is **total over residency** (§7.10 rule 9): a call that failed fatally
3007    /// and spilled a large body is still a call that stopped the executor, so it stops the batch
3008    /// exactly as an inline one does. Reading only the inline arm would make the close-out depend
3009    /// on how big the failure's output happened to be.
3010    fn close_out_fatal_batch(
3011        &mut self,
3012        submitted: &[WireToolResultPayload],
3013    ) -> Result<Vec<ToolResult>, KernelFault> {
3014        let fatal = submitted
3015            .iter()
3016            .any(|payload| payload.disposition().is_fatal());
3017        if !fatal {
3018            return Ok(Vec::new());
3019        }
3020        let answered: BTreeSet<&str> = submitted
3021            .iter()
3022            .map(|payload| payload.call_id().as_str())
3023            .collect();
3024        Ok(self
3025            .engine_mut()?
3026            .dispatched_tool_calls()
3027            .iter()
3028            .filter(|call| !answered.contains(call.id.as_str()))
3029            .map(|call| ToolResult {
3030                call_id: call.id.clone(),
3031                output: Content::Text(
3032                    "not executed: an earlier call in this batch failed fatally and the executor \
3033                     stopped. This call did not take effect — re-issue it only if the failure it \
3034                     followed does not make it pointless."
3035                        .to_string(),
3036                ),
3037                is_error: true,
3038                is_fatal: false,
3039                error_kind: Some(ToolErrorKind::Fatal),
3040                token_count: None,
3041            })
3042            .collect())
3043    }
3044
3045    /// The failure half (§7.9 · DEC-5).
3046    ///
3047    /// One policy decision per effect kind, taken **once**. The kernel never re-emits the same
3048    /// intent: a host that wants another attempt asks again with a new causation and keeps its own
3049    /// idempotency on the effect id / launch token. The kind comes from the effect the kernel
3050    /// itself published — `HostEffectFailure` is deliberately kind-agnostic (§7.9), so it is not,
3051    /// and must not be, the thing that selects the decision.
3052    fn plan_effect_failure(
3053        &mut self,
3054        context: &PlanContext<'_>,
3055        effect_id: &EffectId,
3056        failure: &HostEffectFailure,
3057    ) -> Result<PlannedStep, KernelFault> {
3058        let Some(pending) = context.resolving else {
3059            return Err(unowned_resolution(effect_id, "effect"));
3060        };
3061        let tag = pending.tag();
3062        match tag {
3063            // The loop cannot advance without a provider turn, and asking again would be the
3064            // redispatch DEC-5 deletes. Terminal.
3065            EffectKindTag::CallProvider => {
3066                self.provider_calls.remove(effect_id);
3067                self.host_effect_terminal(tag, failure)
3068            }
3069            // An unverifiable phase must not advance — that is the whole point of the gate — and a
3070            // second evaluation is the same intent. Terminal.
3071            EffectKindTag::EvaluateMilestone => self.host_effect_terminal(tag, failure),
3072            // The batch did not run. Answer every dispatched call with a visible error result and
3073            // let the model adapt: that is a *different* next request, not a retry of this one.
3074            EffectKindTag::ExecuteTools => {
3075                let root_kind = self.require_root_kind()?;
3076                let engine = self.engine_mut()?;
3077                let results = engine
3078                    .dispatched_tool_calls()
3079                    .iter()
3080                    .map(|call| ToolResult {
3081                        call_id: call.id.clone(),
3082                        output: Content::Text(format!(
3083                            "not executed: the executor could not run this batch ({}). The call \
3084                             did not take effect — try a different approach or a smaller step.",
3085                            failure.kind.as_str()
3086                        )),
3087                        is_error: true,
3088                        is_fatal: false,
3089                        error_kind: Some(ToolErrorKind::Fatal),
3090                        token_count: None,
3091                    })
3092                    .collect();
3093                let action = engine.feed(LoopEvent::ToolResults { results });
3094                self.continue_after(context, action, root_kind)
3095            }
3096            // Fail closed: no approval was obtained, so nothing gated is approved. `resolve_approval`
3097            // with an empty verdict denies exactly the gated calls and resumes the rest.
3098            EffectKindTag::RequestApproval => {
3099                let root_kind = self.require_root_kind()?;
3100                let engine = self.engine_mut()?;
3101                let turn = engine.turn;
3102                // Pushed after the resolution, which clears the buffer at its head: a fact
3103                // recorded before it would be erased by the continuation it describes.
3104                let action = engine.resolve_approval(Vec::new(), Vec::new());
3105                engine
3106                    .observations
3107                    .push(KernelObservation::ApprovalResolutionFailed {
3108                        turn,
3109                        error: host_failure_text(failure),
3110                    });
3111                self.continue_after(context, action, root_kind)
3112            }
3113            // No task in the batch started. Charge the failure against exactly the batch the kernel
3114            // published and let the DAG's own dependency policy decide what that starves.
3115            EffectKindTag::SpawnTasks => {
3116                let root_kind = self.require_root_kind()?;
3117                let error = host_failure_text(failure);
3118                let engine = self.engine_mut()?;
3119                let failures: Vec<WorkflowSpawnFailure> = engine
3120                    .pending_spawn_agent_ids()
3121                    .into_iter()
3122                    .map(|agent_id| WorkflowSpawnFailure {
3123                        agent_id,
3124                        error: error.clone(),
3125                    })
3126                    .collect();
3127                for failed in &failures {
3128                    self.attempts.remove(&failed.agent_id);
3129                }
3130                let action = self
3131                    .engine_mut()?
3132                    .resolve_workflow_spawn(Vec::new(), failures);
3133                self.continue_after(context, action, root_kind)
3134            }
3135            // The children were not stopped. Record it and resume: re-issuing the preemption is the
3136            // unbounded `retry_preempt` loop DEC-5 deletes.
3137            EffectKindTag::PreemptTasks => {
3138                // for the refusal, not for the value: a resolution with no root is not a resolution
3139                self.require_root_kind()?;
3140                let error = host_failure_text(failure);
3141                let engine = self.engine_mut()?;
3142                let turn = engine.turn;
3143                let agent_ids: Vec<String> = match &pending.effect {
3144                    EffectKind::PreemptTasks(preempt) => preempt
3145                        .attempts
3146                        .iter()
3147                        .map(|attempt| attempt.task_id.as_str().to_string())
3148                        .collect(),
3149                    _ => Vec::new(),
3150                };
3151                engine
3152                    .observations
3153                    .push(KernelObservation::AgentPreemptFailed {
3154                        turn,
3155                        agent_ids,
3156                        reason: match &pending.effect {
3157                            EffectKind::PreemptTasks(preempt) => preempt.reason.clone(),
3158                            _ => String::new(),
3159                        },
3160                        error,
3161                    });
3162                Ok(self.quiet_step())
3163            }
3164            EffectKindTag::PersistMemory => {
3165                self.commit_memory_write(effect_id, None, Some(host_failure_text(failure)))
3166            }
3167            // The recall did not happen. The turn that asked for it resumes without it — a memory
3168            // search that found nothing and a memory store that was unreachable are the same shape
3169            // to the model, and neither is worth stalling the run for.
3170            EffectKindTag::QueryMemory => {
3171                let root_kind = self.require_root_kind()?;
3172                let Some(query) = self.pending_memory_queries.remove(effect_id) else {
3173                    return Err(unowned_resolution(effect_id, "memory query"));
3174                };
3175                let error = host_failure_text(failure);
3176                let engine = self.engine_mut()?;
3177                let turn = engine.turn;
3178                let action = engine.resume_after_preload();
3179                engine
3180                    .observations
3181                    .push(KernelObservation::MemoryQueryFailed {
3182                        turn,
3183                        scope: binding_scope(&query.binding_id),
3184                        query: query.text,
3185                        error,
3186                    });
3187                let mut step = self.continue_after(context, action, root_kind)?;
3188                step.focus = self.focus.clone();
3189                Ok(step)
3190            }
3191            // Abandon the archive: its compaction already happened in this kernel, so the run stays
3192            // live and degraded rather than dying on a best-effort durability effect.
3193            EffectKindTag::ArchivePageOut => {
3194                let root_kind = self.require_root_kind()?;
3195                let action = self
3196                    .engine_mut()?
3197                    .abandon_page_out_archive(host_failure_text(failure));
3198                self.continue_after(context, action, root_kind)
3199            }
3200            // DEC-5 · abandon the read. A body the host cannot produce leaves the operation exactly
3201            // where it was — the preview is still in context and the handle still names the
3202            // reference — so the model is told the read failed and takes its next turn, rather than
3203            // the kernel re-issuing the same load or killing a live run over one page-in.
3204            EffectKindTag::LoadPayload => {
3205                let root_kind = self.require_root_kind()?;
3206                let handle_id = self
3207                    .pending_payload_loads
3208                    .remove(effect_id)
3209                    .map(|pending| pending.handle_id)
3210                    .unwrap_or_default();
3211                let error = host_failure_text(failure);
3212                let engine = self.engine_mut()?;
3213                let turn = engine.turn;
3214                let action = engine.resume_after_preload();
3215                engine
3216                    .observations
3217                    .push(KernelObservation::PayloadLoadFailed {
3218                        turn,
3219                        handle_id,
3220                        error,
3221                    });
3222                let mut step = self.continue_after(context, action, root_kind)?;
3223                step.focus = self.focus.clone();
3224                Ok(step)
3225            }
3226        }
3227    }
3228
3229    /// The two effect kinds whose absence makes the operation unsound rather than degraded.
3230    fn host_effect_terminal(
3231        &mut self,
3232        tag: EffectKindTag,
3233        failure: &HostEffectFailure,
3234    ) -> Result<PlannedStep, KernelFault> {
3235        let root_kind = self.require_root_kind()?;
3236        let usage = self.usage_report();
3237        // The engine stops too: the operation is over, and a later input must not find a loop that
3238        // still believes it is running.
3239        if let Some(engine) = self.engine.as_mut() {
3240            engine.close_for_host_effect_failure();
3241        }
3242        Ok(PlannedStep {
3243            root_kind: Some(root_kind),
3244            focus: self.focus.clone(),
3245            observations: Vec::new(),
3246            disposition: StepDisposition::Terminal(TerminalDisposition {
3247                terminal: KernelTerminal::Failed(FailedTerminal {
3248                    failure: KernelFailure {
3249                        code: KernelFailureCode::HostEffectFailed,
3250                        message: format!(
3251                            "the host could not execute this operation's {tag} effect ({}){}",
3252                            failure.kind.as_str(),
3253                            if failure.message.is_empty() {
3254                                String::new()
3255                            } else {
3256                                format!(": {}", failure.message)
3257                            }
3258                        ),
3259                    },
3260                    usage,
3261                }),
3262            }),
3263        })
3264    }
3265
3266    /// A transition that changes kernel state but publishes nothing.
3267    ///
3268    /// Reads the folded root kind rather than taking one: a control command is admissible while the
3269    /// operation is still only `Configured`, so the value it reports may legitimately be `None`,
3270    /// and every other caller already proved a root exists through `require_root_kind`.
3271    fn quiet_step(&self) -> PlannedStep {
3272        PlannedStep {
3273            root_kind: self.root_kind,
3274            focus: self.focus.clone(),
3275            observations: Vec::new(),
3276            disposition: StepDisposition::Effects(EffectsDisposition::default()),
3277        }
3278    }
3279
3280    /// §22.13 · the memory write resolution. The record the kernel authored is the record; the
3281    /// host receipt contributes only its own opaque locator and digest, and never a name, kind,
3282    /// size, trust or provenance the kernel did not derive.
3283    fn commit_memory_write(
3284        &mut self,
3285        effect_id: &EffectId,
3286        receipt: Option<&super::effect::MemoryPersistReceipt>,
3287        failure: Option<String>,
3288    ) -> Result<PlannedStep, KernelFault> {
3289        // for the refusal, not for the value: a resolution with no root is not a resolution
3290        self.require_root_kind()?;
3291        let Some(authored) = self.pending_memory_writes.remove(effect_id) else {
3292            return Err(unowned_resolution(effect_id, "memory write"));
3293        };
3294        let engine = self.engine_mut()?;
3295        let turn = engine.turn;
3296        match (receipt, failure) {
3297            (Some(receipt), _) => {
3298                engine.observations.push(KernelObservation::MemoryWritten {
3299                    turn,
3300                    record_id: receipt.record_ref.as_str().to_string(),
3301                    scope: binding_scope(&authored.binding_id),
3302                    memory_kind: core_memory_kind(authored.kind),
3303                    name: authored.name,
3304                    size_bytes: authored.size_bytes,
3305                });
3306            }
3307            (None, Some(error)) => {
3308                engine
3309                    .observations
3310                    .push(KernelObservation::MemoryWriteFailed {
3311                        turn,
3312                        // No record exists to name, so the audit fact names the *intent* — the
3313                        // kernel-authored key — instead of a host id it never received.
3314                        record_id: authored.name,
3315                        error,
3316                    });
3317            }
3318            (None, None) => unreachable!("a memory resolution is either a receipt or a failure"),
3319        }
3320        Ok(self.quiet_step())
3321    }
3322
3323    /// §7.10 rule 4 · a body the host paged back in.
3324    ///
3325    /// Three things are checked before a byte enters context, and all three are the same question
3326    /// asked from different sides: *is this the body that left?* The effect must be one this kernel
3327    /// published, the outcome must name the handle that effect addressed, and the content must
3328    /// reproduce the digest the residency recorded. The kernel never saw the body, so the digest is
3329    /// the only evidence there is — which is why a mismatch is an
3330    /// [`UnexpectedEffectOutcome`](KernelFaultCode::UnexpectedEffectOutcome) with zero mutation and
3331    /// not a degraded read.
3332    fn commit_payload_load(
3333        &mut self,
3334        context: &PlanContext<'_>,
3335        effect_id: &EffectId,
3336        loaded: &super::effect::PayloadLoadedSuccess,
3337    ) -> Result<PlannedStep, KernelFault> {
3338        let root_kind = self.require_root_kind()?;
3339        let Some(pending) = self.pending_payload_loads.get(effect_id).cloned() else {
3340            return Err(unowned_resolution(effect_id, "payload load"));
3341        };
3342        let mismatch = |what: &str| {
3343            Err(KernelFault::new(
3344                KernelFaultCode::UnexpectedEffectOutcome,
3345                format!(
3346                    "the payload loaded for effect {effect_id} is not the body the kernel paged \
3347                     out: {what}"
3348                ),
3349            ))
3350        };
3351        if loaded.handle_id.as_str() != pending.handle_id {
3352            return mismatch(&format!(
3353                "it names handle {}, but the effect addressed {}",
3354                loaded.handle_id, pending.handle_id
3355            ));
3356        }
3357        let content = loaded.payload.content.as_str();
3358        if loaded.payload.original_size.get() != content.len() as u64 {
3359            return mismatch(&format!(
3360                "it declares {} bytes and carries {}",
3361                loaded.payload.original_size,
3362                content.len()
3363            ));
3364        }
3365        if let Some(original_size) = pending.original_size
3366            && loaded.payload.original_size.get() != original_size
3367        {
3368            return mismatch(&format!(
3369                "it carries {} bytes and the handle records {original_size}",
3370                loaded.payload.original_size
3371            ));
3372        }
3373        let digest = super::record::canonical_digest(content.as_bytes());
3374        if digest.as_str() != pending.digest {
3375            return mismatch(&format!(
3376                "its content digests to {digest}, and the handle records {}",
3377                pending.digest
3378            ));
3379        }
3380
3381        // ----- past this line the semantic engine advances -----
3382        self.pending_payload_loads.remove(effect_id);
3383        let engine = self.engine_mut()?;
3384        let turn = engine.turn;
3385        // The body enters as its own unit of history, exactly as a memory recall does: the preview
3386        // that stands in for it is left untouched, so nothing that was already rendered is
3387        // rewritten and the model reads the page-in as the answer to the read it asked for.
3388        let body = format!("[PAYLOAD handle_id={}]\n{content}", pending.handle_id);
3389        let tokens = engine.ctx.engine.count(&body).max(1);
3390        engine.ctx.push_history(Message::user(body), tokens);
3391        let previous = engine.ctx.set_payload_residency(
3392            &pending.handle_id,
3393            HandleKind::ToolResult,
3394            tokens,
3395            Residency::Resident,
3396        );
3397        let action = engine.resume_after_preload();
3398        engine
3399            .observations
3400            .push(KernelObservation::PayloadResidencyChanged {
3401                turn,
3402                handle_id: pending.handle_id.clone(),
3403                from: previous.map(|residency| residency.label().to_string()),
3404                to: "resident".to_string(),
3405                payload_ref: None,
3406                original_size: loaded.payload.original_size.get(),
3407            });
3408        let mut step = self.continue_after(context, action, root_kind)?;
3409        step.focus = self.focus.clone();
3410        Ok(step)
3411    }
3412
3413    /// §7.10 rule 3 / §25.9 · move each external result's P3 handle onto the reference the host
3414    /// supplied, and record the transfer as a fact.
3415    ///
3416    /// Runs **after** the engine accepted the batch, because the handle this moves is minted by the
3417    /// engine as the result enters history — there is nothing to address before that. What lands in
3418    /// context is the preview; what the handle now says is where the body actually is.
3419    fn record_external_payloads(
3420        &mut self,
3421        payloads: &[WireToolResultPayload],
3422    ) -> Result<(), KernelFault> {
3423        for payload in payloads {
3424            let WireToolResultPayload::External(external) = payload else {
3425                continue;
3426            };
3427            let engine = self.engine_mut()?;
3428            let turn = engine.turn;
3429            let previous = engine.ctx.set_payload_residency(
3430                external.call_id.as_str(),
3431                HandleKind::ToolResult,
3432                // The body was never resident: only the preview is, and it is the anchored
3433                // message's own weight, not this handle's.
3434                0,
3435                Residency::External {
3436                    payload_ref: external.payload_ref.as_str().to_string(),
3437                    digest: external.digest.as_str().to_string(),
3438                    original_size: external.original_size.get(),
3439                },
3440            );
3441            engine
3442                .observations
3443                .push(KernelObservation::PayloadResidencyChanged {
3444                    turn,
3445                    handle_id: external.call_id.as_str().to_string(),
3446                    from: previous.map(|residency| residency.label().to_string()),
3447                    to: "external".to_string(),
3448                    payload_ref: Some(external.payload_ref.as_str().to_string()),
3449                    original_size: external.original_size.get(),
3450                });
3451        }
3452        Ok(())
3453    }
3454
3455    /// The page-out receipt must describe the body the kernel handed over. A host that answers with
3456    /// a different handle or digest has archived something else, and accepting it would make a
3457    /// later page-in restore content this operation never evicted.
3458    fn verify_page_out_receipt(
3459        &self,
3460        context: &PlanContext<'_>,
3461        effect_id: &EffectId,
3462        receipt: &super::effect::ArchiveReceipt,
3463    ) -> Result<(), KernelFault> {
3464        let Some(KernelEffect {
3465            effect: EffectKind::ArchivePageOut(published),
3466            ..
3467        }) = context.resolving
3468        else {
3469            return Err(unowned_resolution(effect_id, "page-out archive"));
3470        };
3471        if receipt.handle_id != published.handle_id
3472            || receipt.digest != published.payload.digest
3473            || receipt.original_size != published.payload.original_size
3474        {
3475            return Err(KernelFault::new(
3476                KernelFaultCode::UnexpectedEffectOutcome,
3477                format!(
3478                    "the archive receipt for effect {effect_id} names handle {} / digest {}, but \
3479                     the kernel published handle {} / digest {}",
3480                    receipt.handle_id,
3481                    receipt.digest,
3482                    published.handle_id,
3483                    published.payload.digest
3484                ),
3485            ));
3486        }
3487        Ok(())
3488    }
3489
3490    /// Build the page-out effect for one compaction's archived body.
3491    fn page_out_effect(
3492        &self,
3493        context: &PlanContext<'_>,
3494        summary: Option<&str>,
3495        archived: &[Message],
3496        effect_index: u32,
3497    ) -> Result<ArchivePageOutEffect, KernelFault> {
3498        let content = serde_json::to_string(archived).map_err(|error| {
3499            KernelFault::new(
3500                KernelFaultCode::MalformedEnvelope,
3501                format!("archived history is not serialisable: {error}"),
3502            )
3503        })?;
3504        let preview_bytes = context.config.payload_policy.preview_bytes as usize;
3505        let preview = summary
3506            .map(str::to_string)
3507            .unwrap_or_else(|| truncate_on_char_boundary(&content, preview_bytes));
3508        let handle_id = super::scalar::HandleId::new(format!(
3509            "{}:step:{}:page-out:{effect_index}",
3510            context.input.operation_id, context.step_seq
3511        ))
3512        .map_err(malformed)?;
3513        Ok(ArchivePageOutEffect {
3514            handle_id,
3515            payload: PageOutPayload {
3516                digest: super::record::canonical_digest(content.as_bytes()),
3517                original_size: WireU64::new(content.len() as u64),
3518                content,
3519                preview,
3520            },
3521        })
3522    }
3523
3524    fn require_pending_provider_call(
3525        &self,
3526        effect_id: &EffectId,
3527    ) -> Result<&PendingProviderCall, KernelFault> {
3528        self.provider_calls.get(effect_id).ok_or_else(|| {
3529            KernelFault::new(
3530                KernelFaultCode::InvalidAuthority,
3531                format!(
3532                    "effect {effect_id} is not a provider call this kernel published, so its \
3533                     result has no turn to continue (§7.6)"
3534                ),
3535            )
3536        })
3537    }
3538
3539    /// §7.7 · host-observed facts. Two arms and no third: a signal the host observed, and a child
3540    /// attempt that finished.
3541    fn plan_external_event(
3542        &mut self,
3543        context: &PlanContext<'_>,
3544        event: &ExternalEvent,
3545    ) -> Result<PlannedStep, KernelFault> {
3546        match event {
3547            ExternalEvent::DeliverSignal(delivery) => self.plan_signal(context, delivery),
3548            ExternalEvent::ChildCompleted(completed) => {
3549                self.plan_child_completed(context, completed)
3550            }
3551        }
3552    }
3553
3554    /// §7.7 · one signal delivery.
3555    ///
3556    /// Everything decidable is decided before the router is touched, so a refused delivery is a
3557    /// genuine zero-mutation rejection:
3558    ///
3559    /// * a delivery needs a root to interrupt — a signal to an operation that has not started has
3560    ///   no attention to compete for;
3561    /// * `attempt` is 1-based: attempt 0 is a host that did not count its own redelivery, and the
3562    ///   delivery/attempt pair is the only thing that makes "one signal delivered three times"
3563    ///   distinguishable from "three signals" (§14.1);
3564    /// * a task target must name a task this kernel issued and that is still live. `SignalTarget`
3565    ///   is the kernel's own address space — a host session, process or thread is not a target and
3566    ///   has no representation on the wire (§5.2).
3567    ///
3568    /// Admission itself (TTL sweep, dedupe, deadline escalation, queue displacement) belongs to the
3569    /// router and runs on the **envelope's accepted time**, already fed in `plan_inner`. The
3570    /// signal's own `source_timestamp_ms` is audit metadata and never a clock (§11.2).
3571    ///
3572    /// The disposition is a *fact*: queueing, dropping, expiring and displacing produce
3573    /// observations and no effect. The one host action a signal can cause is preempting running
3574    /// children, and that is published as a `PreemptTasks` effect through the ordinary
3575    /// action-to-disposition path — committed only when its resolution comes back (§7.8).
3576    fn plan_signal(
3577        &mut self,
3578        context: &PlanContext<'_>,
3579        delivery: &DeliverSignal,
3580    ) -> Result<PlannedStep, KernelFault> {
3581        let root_kind = self.require_root_kind()?;
3582        if delivery.attempt == 0 {
3583            return Err(KernelFault::new(
3584                KernelFaultCode::MalformedEnvelope,
3585                format!(
3586                    "delivery {} carries attempt 0; attempts are 1-based, and a delivery that \
3587                     cannot say which attempt it is cannot be told apart from a redelivery (§7.7)",
3588                    delivery.delivery_id
3589                ),
3590            ));
3591        }
3592        if let SignalTarget::Task(target) = &delivery.signal.target {
3593            let live = self
3594                .engine
3595                .as_ref()
3596                .and_then(|engine| engine.task_lifecycle(target.task_id.as_str()))
3597                .is_some_and(|lifecycle| !lifecycle.is_terminal());
3598            if !live {
3599                return Err(KernelFault::new(
3600                    KernelFaultCode::InvalidAuthority,
3601                    format!(
3602                        "signal {} targets task {}, which this operation has no live attempt for; \
3603                         a signal addresses the operation or one of its own logical tasks (§7.7)",
3604                        delivery.signal.signal_id, target.task_id
3605                    ),
3606                ));
3607            }
3608        }
3609        // DEC-8 · the one effect a delivery can publish is the preemption of running children, and
3610        // that is reachable only for a critical signal while this kernel holds live attempts.
3611        // Checked here, before the router moves, so a host that cannot stop its children refuses
3612        // the delivery instead of planning an effect it could never execute.
3613        //
3614        // The urgency read is the **effective** one: a signal carrying `escalate_after_ms: 0`
3615        // becomes critical at admission when the operation enabled deadline escalation, so reading
3616        // the bare wire value would let it reach the router and only then discover the host has no
3617        // preemption path. A deadline that comes due *later* escalates inside the queue, and that
3618        // path is covered by the effect-minting `require_effect_support` — a fault rather than a
3619        // zero-mutation refusal, which is the correct asymmetry: at admission nothing has moved.
3620        let escalation_enabled = context.config.signal_policy.deadline_escalation;
3621        if delivery.signal.effective_urgency(escalation_enabled) == SignalUrgency::Critical
3622            && !self.attempts.is_empty()
3623        {
3624            self.require_effect_support(context.config, EffectKindTag::PreemptTasks)?;
3625        }
3626
3627        // ----- past this line the semantic engine advances -----
3628        // DEC-3 · at most one pending effect per kind, so a signal may only force a fresh provider
3629        // request when this operation is not already waiting on one.
3630        let may_issue_request = self.provider_calls.is_empty();
3631        let signal = runtime_signal(&delivery.signal, context.input.observed_at_ms);
3632        let engine = self.engine_mut()?;
3633        let action = engine.signal_event(
3634            context.input.operation_id.as_str().to_string(),
3635            delivery.delivery_id.as_str().to_string(),
3636            delivery.attempt,
3637            signal,
3638            may_issue_request,
3639        );
3640        match action {
3641            Some(action) => self.continue_after(context, action, root_kind),
3642            // Queued / observed / ignored / dropped: the disposition observation is the whole of
3643            // what happened, and the host is asked for nothing.
3644            None => Ok(self.quiet_step()),
3645        }
3646    }
3647
3648    /// §7.7 · a child completion is the event that drains a workflow DAG.
3649    fn plan_child_completed(
3650        &mut self,
3651        context: &PlanContext<'_>,
3652        completed: &ChildCompleted,
3653    ) -> Result<PlannedStep, KernelFault> {
3654        let root_kind = self.require_root_kind()?;
3655        match self.attempts.get(completed.task_id.as_str()) {
3656            Some(minted) if minted == &completed.attempt_id => {}
3657            issued => {
3658                return Err(KernelFault::new(
3659                    KernelFaultCode::InvalidAuthority,
3660                    format!(
3661                        "task {} has attempt {} in this kernel, but the completion names {}; a \
3662                         host does not mint or rewrite child identity (§10.4)",
3663                        completed.task_id,
3664                        issued.map_or("none", AttemptId::as_str),
3665                        completed.attempt_id,
3666                    ),
3667                ));
3668            }
3669        }
3670
3671        // ----- past this line the semantic engine advances -----
3672        // §10.4 · the attempt is spent. A second completion naming it — and any `parent_requests`
3673        // riding on that second completion — is a stale causation, refused by the check above.
3674        self.attempts.remove(completed.task_id.as_str());
3675
3676        // §7.7 · the requests enter P1 first, with `ChildAttempt` causation, and the completion is
3677        // fed afterwards so its own drive produces the single next ready batch.
3678        //
3679        // GAP-4: this loop can neither fail the transition nor skip the completion. Each request is
3680        // adjudicated on its own; a refused one leaves a structured rejection observation and
3681        // changes neither its siblings nor the fact that the child ran.
3682        //
3683        // No arm can move the focus here, which is why only `effects` is collected: the sole
3684        // focus-moving syscall is `SubmitWorkflow`'s *bootstrap*, and a child attempt only exists
3685        // while its DAG is in flight — so that request always takes the flatten arm instead.
3686        let mut index = 0u32;
3687        let mut effects = Vec::new();
3688        for (seq, request) in completed.parent_requests.iter().enumerate() {
3689            let causation = SyscallCausation::ChildAttempt(ChildAttemptCausation {
3690                task_id: completed.task_id.clone(),
3691                attempt_id: completed.attempt_id.clone(),
3692                request_seq: seq as u32,
3693            });
3694            match self.apply_syscall(context, &causation, request, &mut index) {
3695                Ok(outcome) => effects.extend(outcome.effects),
3696                Err(SyscallRefusal::Fault(fault)) => self.note_rejection(
3697                    SyscallRejection::new(
3698                        "parent_request",
3699                        format!("request {seq} refused: {}", fault.message),
3700                    )
3701                    .by(&completed.task_id),
3702                ),
3703                Err(SyscallRefusal::Rejected(rejection)) => {
3704                    self.note_rejection(rejection.by(&completed.task_id))
3705                }
3706            }
3707        }
3708
3709        // `feed` clears the observation buffer at the head of every event, and these facts happened
3710        // *before* it. Carrying them across is what keeps the audit trail of a refused parent
3711        // request from being erased by the very completion it rode in on.
3712        let syscall_observations = self
3713            .engine_mut()?
3714            .take_observations()
3715            .into_iter()
3716            .collect::<Vec<_>>();
3717
3718        let result = sub_agent_result(completed);
3719        let engine = self.engine_mut()?;
3720        let action = engine.feed(LoopEvent::SubAgentCompleted { result });
3721        let mut step = self.continue_after_at(context, action, root_kind, &mut index)?;
3722        if let Some(engine) = self.engine.as_mut() {
3723            engine.observations.splice(0..0, syscall_observations);
3724        }
3725        if !effects.is_empty() {
3726            match &mut step.disposition {
3727                StepDisposition::Effects(published) => {
3728                    let mut merged = effects;
3729                    merged.append(&mut published.effects);
3730                    published.effects = merged;
3731                }
3732                StepDisposition::Terminal(_) => {
3733                    return Err(KernelFault::new(
3734                        KernelFaultCode::InvalidLifecycle,
3735                        "a completion that terminates the operation cannot also publish the \
3736                         effects its parent requests asked for (§7.12)"
3737                            .to_string(),
3738                    ));
3739                }
3740            }
3741        }
3742        Ok(step)
3743    }
3744
3745    // ----- §7.5 · the live control plane -----
3746
3747    /// §7.5 · reduce one host command onto the mechanism that already owns that state.
3748    ///
3749    /// Every arm here carries **host authority**, which is what separates this from the P1 syscall
3750    /// path in [`Self::apply_syscall`]: a host command is not gated, not quarantine-coerced and not
3751    /// attributed to a caller, because the host *is* the authority. `UpdateTask` is the pair that
3752    /// makes the split visible — the same `TaskUpdate` payload, two input classes, two authorities,
3753    /// preserving the authority distinction that the retired shared task-update input could not
3754    /// express (§7.5 现状注记).
3755    ///
3756    /// Refusals are faults rather than model-facing rejections: a control command is a host bug, so
3757    /// it is answered to the host and never becomes a note the model reads.
3758    fn plan_host_control(
3759        &mut self,
3760        context: &PlanContext<'_>,
3761        command: &HostCommand,
3762    ) -> Result<PlannedStep, KernelFault> {
3763        match command {
3764            HostCommand::Cancel(cancel) => self.plan_cancel(context, cancel),
3765            HostCommand::ForceCompact(_) => {
3766                let root_kind = self.require_root_kind()?;
3767                self.engine_mut()?.force_compact();
3768                // A compaction that archived history owes a `page_out` effect; `continue_after`
3769                // externalises it through the same one path an in-turn compaction takes.
3770                self.continue_after(context, LoopAction::AwaitingResume, root_kind)
3771            }
3772            HostCommand::UpdateTask(update) => self.plan_host_task_update(update),
3773            HostCommand::ApplyCapabilityPatch(patch) => self.plan_capability_patch(patch),
3774            HostCommand::ApplyKnowledgeMutation(mutation) => self.plan_knowledge_mutation(mutation),
3775            HostCommand::SeedKnowledge(seed) => self.plan_seed_knowledge(seed),
3776            HostCommand::ApplySkillActivation(activation) => self.plan_skill_activation(activation),
3777            HostCommand::ApplyPolicyPatch(patch) => self.plan_policy_patch(patch),
3778            HostCommand::UpdateDeadline(deadline) => self.plan_update_deadline(deadline),
3779        }
3780    }
3781
3782    /// §11.1 · the cancellation ladder, and the only path an operation is cancelled through.
3783    ///
3784    /// The order is downstream-first and it happens **inside one transition**, because §7.12 admits
3785    /// effects or a terminal and never both:
3786    ///
3787    /// 1. every child attempt this kernel issued is settled and its wait torn down, every pending
3788    ///    workflow batch and deferred host effect dropped (`cancel_operation`);
3789    /// 2. the driver's own ledger of live attempts and pending calls is spent with them, so a late
3790    ///    completion or resolution naming one is a stale causation rather than a resurrection;
3791    /// 3. only then is the root terminal minted.
3792    ///
3793    /// The *real* I/O stop is the host's: §11.1 has the host stop provider, tool and child I/O
3794    /// before it submits this command, and the kernel adjudicates the terminal. That is why this
3795    /// step publishes no `PreemptTasks` effect — asking the host to stop what it already stopped
3796    /// would need a second transition, and the operation would not be cancelled until it came back.
3797    ///
3798    /// The reason is the host's, not the loop's: `cancel_operation` terminates the semantic loop
3799    /// with its own internal `UserAbort`, but a `Deadline` or `HostShutdown` cancellation must not
3800    /// be reported as a user abort, so the terminal is built from the command.
3801    fn plan_cancel(
3802        &mut self,
3803        context: &PlanContext<'_>,
3804        cancel: &CancelCommand,
3805    ) -> Result<PlannedStep, KernelFault> {
3806        let root_kind = self.root_kind;
3807        let focus = self.focus.clone();
3808        let reason = cancel.reason;
3809        // §7.5 · the cancel command carries no operation id of its own; the envelope owns it.
3810        let operation_id = context.input.operation_id.as_str().to_string();
3811        let engine = self.engine_mut()?;
3812        let action = engine.cancel_operation(
3813            operation_id,
3814            reason,
3815            cancel
3816                .pending_call_ids
3817                .iter()
3818                .map(|call_id| call_id.as_str().to_string())
3819                .collect(),
3820        );
3821        // Downstream identity is spent in the same transition that settles the tasks it names.
3822        self.attempts.clear();
3823        self.provider_calls.clear();
3824        self.pending_memory_writes.clear();
3825        self.pending_memory_queries.clear();
3826
3827        let LoopAction::Done { result } = action else {
3828            return Err(KernelFault::new(
3829                KernelFaultCode::InvalidLifecycle,
3830                format!(
3831                    "cancelling the operation produced {} instead of a terminal; cancellation is \
3832                     the one control command that always ends the operation (§11.1)",
3833                    loop_action_label(&action)
3834                ),
3835            ));
3836        };
3837        Ok(PlannedStep {
3838            root_kind,
3839            focus,
3840            observations: Vec::new(),
3841            disposition: StepDisposition::Terminal(TerminalDisposition {
3842                terminal: KernelTerminal::Cancelled(CancelledTerminal {
3843                    reason,
3844                    usage: UsageReport {
3845                        input_tokens: WireU64::new(result.total_tokens_used),
3846                        output_tokens: WireU64::ZERO,
3847                        turns: result.turns_used,
3848                        cached_input_tokens: None,
3849                    },
3850                }),
3851            }),
3852        })
3853    }
3854
3855    /// §7.5 · the host's own plan edit. Same payload as the model's `update_task` syscall, applied
3856    /// through the same context mechanism — and deliberately *not* through the P1 gate, because the
3857    /// authority is the host's rather than a derived caller's.
3858    fn plan_host_task_update(
3859        &mut self,
3860        update: &UpdateTaskCommand,
3861    ) -> Result<PlannedStep, KernelFault> {
3862        let engine = self.engine_mut()?;
3863        engine.ctx.update_task(core_task_update(&update.update));
3864        Ok(self.quiet_step())
3865    }
3866
3867    /// §13.2 · mount/unmount in one command so a swap is atomic. Unmounting something absent errs
3868    /// open (it is already not mounted), which is the only shape that makes a retry safe.
3869    fn plan_capability_patch(
3870        &mut self,
3871        patch: &ApplyCapabilityPatchCommand,
3872    ) -> Result<PlannedStep, KernelFault> {
3873        let engine = self.engine_mut()?;
3874        for grant in &patch.patch.mount {
3875            engine.mount_capability(
3876                crate::types::capability::CapabilityDescriptor {
3877                    id: grant.id.as_str().into(),
3878                    kind: core_capability_kind(grant.kind),
3879                    description: grant.description.clone().unwrap_or_default(),
3880                    tool_schema: None,
3881                    skill: None,
3882                    metadata: serde_json::Value::Null,
3883                    lease: None,
3884                    is_pinned: false,
3885                    version: None,
3886                    mounted_by: None,
3887                    mount_reason: None,
3888                },
3889                None,
3890                None,
3891            );
3892        }
3893        for reference in &patch.patch.unmount {
3894            engine.unmount_capability(core_capability_kind(reference.kind), &reference.id);
3895        }
3896        Ok(self.quiet_step())
3897    }
3898
3899    /// §13.2 · keyed knowledge upsert + removal. Both directions are boundary-deferred by the
3900    /// partition itself, so the model never sees system bytes change mid-turn.
3901    fn plan_knowledge_mutation(
3902        &mut self,
3903        mutation: &ApplyKnowledgeMutationCommand,
3904    ) -> Result<PlannedStep, KernelFault> {
3905        let engine = self.engine_mut()?;
3906        seed_knowledge(engine, &mutation.mutation.upsert);
3907        for key in &mutation.mutation.remove {
3908            engine.ctx.remove_knowledge(key);
3909        }
3910        Ok(self.quiet_step())
3911    }
3912
3913    /// DEC-9 · the host seeding the knowledge partition. Same mechanism as the initial context's
3914    /// `knowledge`, and named apart from the P1 `PageIn { handle_id }` on purpose: the two are
3915    /// opposite directions and must never share a name again (§7.5).
3916    fn plan_seed_knowledge(
3917        &mut self,
3918        seed: &SeedKnowledgeCommand,
3919    ) -> Result<PlannedStep, KernelFault> {
3920        let engine = self.engine_mut()?;
3921        seed_knowledge(engine, &seed.entries);
3922        Ok(self.quiet_step())
3923    }
3924
3925    /// §13.2 · skill activation state, validated whole before anything moves.
3926    ///
3927    /// A name outside the operation's declared catalog is refused rather than invented — the same
3928    /// rule the model's `ActivateSkill` syscall obeys — and because the command is atomic, one bad
3929    /// name refuses the whole swap instead of leaving half of it applied.
3930    fn plan_skill_activation(
3931        &mut self,
3932        activation: &ApplySkillActivationCommand,
3933    ) -> Result<PlannedStep, KernelFault> {
3934        let engine = self.engine_mut()?;
3935        for activate in &activation.activate {
3936            if !engine.ctx.skill_available(&activate.name) {
3937                return Err(KernelFault::new(
3938                    KernelFaultCode::InvalidConfig,
3939                    format!(
3940                        "this operation declares no skill named {:?}; activating one is a \
3941                         capability mutation and is refused rather than invented (§13.2)",
3942                        activate.name
3943                    ),
3944                ));
3945            }
3946        }
3947        let turn = engine.turn;
3948        for activate in &activation.activate {
3949            let expires_at_turn = activate.lease_turns.map(|turns| turn.saturating_add(turns));
3950            engine
3951                .ctx
3952                .activate_skill_leased(activate.name.as_str(), expires_at_turn);
3953        }
3954        for name in &activation.deactivate {
3955            engine.ctx.deactivate_skill(name);
3956        }
3957        Ok(self.quiet_step())
3958    }
3959
3960    /// §13.2 / DEC-6 · one revision-guarded policy patch.
3961    ///
3962    /// [`LivePolicyState::apply`] is all-or-nothing: a stale revision, a widened quota or a policy
3963    /// that fails its boot validator leaves both the configuration and the revision exactly as they
3964    /// were, so a refused patch is a zero-mutation rejection and the writer rebases instead of
3965    /// silently overwriting whoever won the race. Only after it succeeds are the changed policies
3966    /// re-installed into the running engine, through the same installers the genesis build uses.
3967    fn plan_policy_patch(
3968        &mut self,
3969        patch: &ApplyPolicyPatchCommand,
3970    ) -> Result<PlannedStep, KernelFault> {
3971        let Some(policy) = self.policy.as_mut() else {
3972            return Err(KernelFault::new(
3973                KernelFaultCode::InvalidLifecycle,
3974                "the operation has no genesis configuration, so it has no policy to patch"
3975                    .to_string(),
3976            ));
3977        };
3978        let revision = policy.apply(patch).map_err(|rejection| {
3979            KernelFault::new(KernelFaultCode::InvalidConfig, rejection.message)
3980        })?;
3981        let config = policy.config().clone();
3982        let engine = self.engine_mut()?;
3983        install_live_policies(engine, &config);
3984        let turn = engine.turn;
3985        engine
3986            .observations
3987            .push(KernelObservation::LivePolicyChanged {
3988                turn,
3989                policy: live_policy_label(&patch.patch).to_string(),
3990                revision: revision.get(),
3991            });
3992        Ok(self.quiet_step())
3993    }
3994
3995    /// §13.2 · the operation's absolute deadline, projected onto the wall-time budget axis the
3996    /// scheduler already owns.
3997    ///
3998    /// The axis is a duration measured from the operation's first accepted time, so an absolute
3999    /// deadline becomes `deadline − start`. A deadline already in the past is not an error: it
4000    /// yields a zero-length budget, and the next scheduling decision terminates on `Deadline` —
4001    /// the same verdict the axis would have reached on its own a moment later.
4002    fn plan_update_deadline(
4003        &mut self,
4004        deadline: &UpdateDeadlineCommand,
4005    ) -> Result<PlannedStep, KernelFault> {
4006        let engine = self.engine_mut()?;
4007        let started_at_ms = engine.started_at_ms();
4008        let budget = match (deadline.deadline_ms, started_at_ms) {
4009            (None, _) => None,
4010            (Some(deadline_ms), Some(started_at_ms)) => {
4011                Some(deadline_ms.get().saturating_sub(started_at_ms))
4012            }
4013            (Some(_), None) => {
4014                return Err(KernelFault::new(
4015                    KernelFaultCode::InvalidLifecycle,
4016                    "this operation has accepted no timed input yet, so an absolute deadline has \
4017                     no start to measure from (§11.2)"
4018                        .to_string(),
4019                ));
4020            }
4021        };
4022        engine.set_wall_budget(budget);
4023        Ok(self.quiet_step())
4024    }
4025
4026    /// The shared tail of every non-start transition: turn the engine's action into a disposition
4027    /// and re-derive the focus from what the engine actually did.
4028    fn continue_after(
4029        &mut self,
4030        context: &PlanContext<'_>,
4031        action: LoopAction,
4032        root_kind: RootKind,
4033    ) -> Result<PlannedStep, KernelFault> {
4034        let mut index = 0;
4035        self.continue_after_at(context, action, root_kind, &mut index)
4036    }
4037
4038    fn continue_after_at(
4039        &mut self,
4040        context: &PlanContext<'_>,
4041        action: LoopAction,
4042        root_kind: RootKind,
4043        effect_index: &mut u32,
4044    ) -> Result<PlannedStep, KernelFault> {
4045        let workflow_finished = self
4046            .engine()
4047            .map(|engine| {
4048                engine
4049                    .observations
4050                    .iter()
4051                    .any(|o| matches!(o, KernelObservation::WorkflowCompleted { .. }))
4052            })
4053            .unwrap_or(false);
4054        let disposition = self.disposition_for_at(context, action, root_kind, effect_index)?;
4055        let focus = if workflow_finished {
4056            // §6.1.7 / §7.4: a nested workflow's completion restores the parent agent turn; a root
4057            // workflow's completion commits the terminal instead and its focus stops moving.
4058            match (root_kind, &self.focus) {
4059                (RootKind::Agent, Some(ExecutionFocus::WorkflowController(controller))) => {
4060                    controller
4061                        .parent_task_id
4062                        .clone()
4063                        .map(ExecutionFocus::agent_turn)
4064                }
4065                (_, current) => current.clone(),
4066            }
4067        } else {
4068            self.focus.clone()
4069        };
4070        Ok(PlannedStep {
4071            root_kind: Some(root_kind),
4072            focus,
4073            observations: Vec::new(),
4074            disposition,
4075        })
4076    }
4077
4078    /// Project one [`LoopAction`] onto §7.12's closed step disposition.
4079    fn disposition_for(
4080        &mut self,
4081        context: &PlanContext<'_>,
4082        action: LoopAction,
4083        root_kind: RootKind,
4084    ) -> Result<StepDisposition, KernelFault> {
4085        let mut index = 0;
4086        self.disposition_for_at(context, action, root_kind, &mut index)
4087    }
4088
4089    /// The same projection, minting effect ids from a caller-owned counter. A step that reduces a
4090    /// syscall batch publishes several effects, and each one's identity has to stay unique.
4091    fn disposition_for_at(
4092        &mut self,
4093        context: &PlanContext<'_>,
4094        action: LoopAction,
4095        root_kind: RootKind,
4096        effect_index: &mut u32,
4097    ) -> Result<StepDisposition, KernelFault> {
4098        let operation_id = &context.input.operation_id;
4099        let causation = context.input.input_id.clone();
4100        let step_seq = context.step_seq;
4101
4102        // A durability effect produced *inside* the transition (a compaction's page-out) is
4103        // published first and holds the continuation. The
4104        // guard inside makes a second call in the same step a no-op, so a step never activates two.
4105        let mut action = self.engine_mut()?.externalize_pending_host_effect(action);
4106        // DEC-8 · an archive the host never declared it can perform is not a reason to refuse the
4107        // transition that produced it: the compaction already happened in-kernel and the archive is
4108        // best effort. It is abandoned through the same one-decision path a host failure takes, so
4109        // the audit fact is identical whether the host said "I cannot" or "I could not".
4110        while matches!(action, LoopAction::ArchivePageOut { .. })
4111            && !context
4112                .config
4113                .host_effect_support
4114                .supports(EffectKindTag::ArchivePageOut)
4115        {
4116            action = self.engine_mut()?.abandon_page_out_archive(
4117                "this operation's host declares no archive_page_out support".to_string(),
4118            );
4119        }
4120
4121        let effect = match action {
4122            LoopAction::AwaitingResume => {
4123                // A root workflow that just drained its DAG terminates here, and publishes nothing.
4124                if root_kind == RootKind::Workflow
4125                    && let Some(terminal) = self.root_workflow_terminal()
4126                {
4127                    return Ok(StepDisposition::Terminal(TerminalDisposition { terminal }));
4128                }
4129                return Ok(StepDisposition::Effects(EffectsDisposition::default()));
4130            }
4131            LoopAction::Done { result } => {
4132                return Ok(StepDisposition::Terminal(TerminalDisposition {
4133                    terminal: agent_terminal(&result),
4134                }));
4135            }
4136            LoopAction::CallLLM {
4137                context: rendered,
4138                tools,
4139            } => EffectKind::CallProvider(CallProviderEffect {
4140                context: rendered_context(&rendered),
4141                tools: tools.iter().map(tool_schema).collect(),
4142            }),
4143            LoopAction::ExecuteTools { calls } => EffectKind::ExecuteTools(ExecuteToolsEffect {
4144                calls: calls.iter().map(wire_tool_call).collect::<Result<_, _>>()?,
4145            }),
4146            LoopAction::RequestApproval { requests } => {
4147                EffectKind::RequestApproval(RequestApprovalEffect {
4148                    requests: requests
4149                        .iter()
4150                        .map(wire_approval_request)
4151                        .collect::<Result<_, _>>()?,
4152                })
4153            }
4154            LoopAction::SpawnWorkflow { nodes, budget } => {
4155                let mut tasks = Vec::with_capacity(nodes.len());
4156                for node in &nodes {
4157                    tasks.push(self.task_launch(operation_id, step_seq, node)?);
4158                }
4159                EffectKind::SpawnTasks(SpawnTasksEffect {
4160                    tasks,
4161                    budget: budget.as_ref().map(workflow_budget),
4162                })
4163            }
4164            LoopAction::PreemptSubAgents { agent_ids, reason } => {
4165                // §10.4 · a preemption names the attempt this kernel issued. A task with no live
4166                // attempt has nothing to preempt, so it is dropped rather than named with a
4167                // fabricated one.
4168                let attempts = agent_ids
4169                    .iter()
4170                    .filter_map(|agent_id| {
4171                        let attempt_id = self.attempts.get(agent_id)?.clone();
4172                        let task_id = TaskId::new(agent_id).ok()?;
4173                        Some(TaskAttemptRef {
4174                            task_id,
4175                            attempt_id,
4176                        })
4177                    })
4178                    .collect();
4179                EffectKind::PreemptTasks(PreemptTasksEffect { attempts, reason })
4180            }
4181            // The engine's action carries the legacy internal trio — criteria, required evidence
4182            // and the verifier — and the canonical projection consumes none of them. All three
4183            // are host-owned by decision (§5.2, adjudication §5m-3/§5p): the host looks the
4184            // verifier and its criteria up from `(contract_id, phase_id)`, which is why the
4185            // request carries that pair and nothing else. The internal fields stay for the legacy
4186            // path until Task 23 deletes it.
4187            LoopAction::EvaluateMilestone {
4188                phase_id,
4189                criteria: _,
4190                required_evidence: _,
4191                verifier: _,
4192            } => EffectKind::EvaluateMilestone(EvaluateMilestoneEffect {
4193                request: super::effect::MilestoneRequest {
4194                    contract_id: self.require_loaded_contract()?,
4195                    phase_id,
4196                },
4197            }),
4198            LoopAction::ArchivePageOut {
4199                summary, archived, ..
4200            } => EffectKind::ArchivePageOut(self.page_out_effect(
4201                context,
4202                summary.as_deref(),
4203                &archived,
4204                *effect_index,
4205            )?),
4206            // The engine never emits these two: they exist only for the legacy `WriteMemory` /
4207            // `QueryMemory` inputs, which the canonical wire replaced with a P1 syscall that mints
4208            // the effect directly (see `plan_memory_write` / `plan_memory_query`).
4209            action @ (LoopAction::PersistMemory { .. } | LoopAction::QueryMemory { .. }) => {
4210                return Err(KernelFault::new(
4211                    KernelFaultCode::InvalidLifecycle,
4212                    format!(
4213                        "the semantic kernel emitted {}, which only the deleted legacy memory \
4214                         inputs can produce; on the canonical wire a memory effect is minted by \
4215                         the P1 syscall that proposed it",
4216                        loop_action_label(&action)
4217                    ),
4218                ));
4219            }
4220        };
4221
4222        let tag = effect.tag();
4223        self.require_effect_support(context.config, tag)?;
4224        let effect_id = mint_effect_id(operation_id, step_seq, *effect_index);
4225        *effect_index += 1;
4226
4227        match &effect {
4228            // §7.6 · remember what this turn advertised and whose turn it was. That pair *is* the
4229            // `ProviderTool` causation a syscall in the result will be attributed to.
4230            EffectKind::CallProvider(call) => {
4231                self.provider_calls.insert(
4232                    effect_id.clone(),
4233                    PendingProviderCall {
4234                        task_id: self.turn_task_id(),
4235                        exposed_tools: call.tools.iter().map(|tool| tool.name.clone()).collect(),
4236                    },
4237                );
4238            }
4239            // §10.4 · the launch is published, so every task it names leaves `PendingLaunch`. Only
4240            // the host's acknowledgement moves them on to `Running`.
4241            EffectKind::SpawnTasks(spawn) => {
4242                let launched: Vec<String> = spawn
4243                    .tasks
4244                    .iter()
4245                    .map(|task| task.task_id.as_str().to_string())
4246                    .collect();
4247                if let Some(engine) = self.engine.as_mut() {
4248                    engine.mark_tasks_starting(&launched);
4249                }
4250            }
4251            _ => {}
4252        }
4253
4254        Ok(StepDisposition::Effects(EffectsDisposition {
4255            effects: vec![KernelEffect {
4256                effect_id,
4257                causation_input_id: causation,
4258                effect,
4259            }],
4260        }))
4261    }
4262
4263    /// The task whose turn is currently issuing provider calls. An agent focus names it directly;
4264    /// a workflow controller's provider call is the parent agent's turn resuming, and before the
4265    /// first focus is folded there is only the root.
4266    fn turn_task_id(&self) -> TaskId {
4267        let staged = self
4268            .staged
4269            .as_ref()
4270            .and_then(|staged| staged.focus.as_ref());
4271        match staged.or(self.focus.as_ref()) {
4272            Some(ExecutionFocus::AgentTurn(turn)) => turn.task_id.clone(),
4273            Some(ExecutionFocus::WorkflowController(controller)) => controller
4274                .parent_task_id
4275                .clone()
4276                .unwrap_or_else(root_task_id),
4277            None => root_task_id(),
4278        }
4279    }
4280
4281    /// Build the workflow terminal from the outcome the engine just published. Reads the
4282    /// observation rather than re-deriving it: the DAG's own `finish()` is the authority on which
4283    /// nodes completed.
4284    fn root_workflow_terminal(&mut self) -> Option<KernelTerminal> {
4285        let workflow_id = self.workflow_id.clone()?;
4286        let engine = self.engine.as_mut()?;
4287        let outcomes = engine
4288            .observations
4289            .iter()
4290            .find_map(|observation| match observation {
4291                KernelObservation::WorkflowCompleted { node_outcomes, .. } => {
4292                    Some(node_outcomes.clone())
4293                }
4294                _ => None,
4295            })?;
4296        let mut completed = Vec::new();
4297        let mut failed = Vec::new();
4298        for outcome in &outcomes {
4299            let node_id = self.node_id_for(&outcome.node_id);
4300            match outcome.status {
4301                WorkflowNodeStatus::Completed | WorkflowNodeStatus::CompletedPartial => {
4302                    completed.push(node_id)
4303                }
4304                WorkflowNodeStatus::Failed | WorkflowNodeStatus::SkippedUpstreamFailed => {
4305                    failed.push(node_id)
4306                }
4307            }
4308        }
4309        let status = if failed.is_empty() {
4310            WorkflowStatus::Completed
4311        } else {
4312            WorkflowStatus::Failed
4313        };
4314        Some(KernelTerminal::Workflow(WorkflowTerminal {
4315            outcome: WorkflowOutcome {
4316                workflow_id,
4317                status,
4318                completed_nodes: completed,
4319                failed_nodes: failed,
4320            },
4321            usage: self.usage_report(),
4322        }))
4323    }
4324
4325    fn usage_report(&self) -> UsageReport {
4326        let Some(engine) = self.engine.as_ref() else {
4327            return UsageReport::default();
4328        };
4329        let (tokens, _, _) = engine.local_budget_usage();
4330        UsageReport {
4331            input_tokens: WireU64::new(tokens),
4332            output_tokens: WireU64::ZERO,
4333            turns: engine.turn,
4334            cached_input_tokens: None,
4335        }
4336    }
4337
4338    /// One child launch, with kernel-minted identity. The task/attempt/launch triple exists as a
4339    /// committed fact before the host is ever asked to start anything (§10.4).
4340    fn task_launch(
4341        &mut self,
4342        operation_id: &OperationId,
4343        step_seq: WireU64,
4344        info: &crate::orchestration::workflow::WorkflowSpawnInfo,
4345    ) -> Result<TaskLaunch, KernelFault> {
4346        let task_id = TaskId::new(&info.agent_id).map_err(malformed)?;
4347        let attempt_id =
4348            AttemptId::new(format!("{}:attempt:1", info.agent_id)).map_err(malformed)?;
4349        let launch_token = LaunchToken::new(format!(
4350            "{operation_id}:step:{step_seq}:launch:{}",
4351            info.agent_id
4352        ))
4353        .map_err(malformed)?;
4354        self.attempts
4355            .insert(info.agent_id.clone(), attempt_id.clone());
4356        let mut metadata = serde_json::Map::new();
4357        if let Some(model_hint) = &info.model_hint {
4358            metadata.insert(
4359                "model_hint".to_string(),
4360                serde_json::Value::String(model_hint.clone()),
4361            );
4362        }
4363        if let Some(output_schema) = &info.output_schema {
4364            metadata.insert("output_schema".to_string(), output_schema.clone());
4365        }
4366        if !info.input_agent_ids.is_empty() {
4367            metadata.insert(
4368                "input_agent_ids".to_string(),
4369                serde_json::Value::Array(
4370                    info.input_agent_ids
4371                        .iter()
4372                        .cloned()
4373                        .map(serde_json::Value::String)
4374                        .collect(),
4375                ),
4376            );
4377            let dependency_outputs = info
4378                .input_agent_ids
4379                .iter()
4380                .filter_map(|agent_id| {
4381                    let output = self
4382                        .engine
4383                        .as_ref()?
4384                        .task_table()
4385                        .get(agent_id)?
4386                        .proc
4387                        .as_ref()?
4388                        .result
4389                        .as_ref()?
4390                        .result
4391                        .final_message
4392                        .as_ref()
4393                        .and_then(message_body_parts)?
4394                        .0;
4395                    Some((agent_id.clone(), serde_json::Value::String(output)))
4396                })
4397                .collect();
4398            metadata.insert(
4399                "dependency_outputs".to_string(),
4400                serde_json::Value::Object(dependency_outputs),
4401            );
4402        }
4403        Ok(TaskLaunch {
4404            task_id,
4405            attempt_id,
4406            launch_token,
4407            node_id: self.node_id_for(&info.agent_id),
4408            spec: LogicalAgentSpec {
4409                goal: info.goal.clone(),
4410                role: parse_wire_role(&info.role),
4411                isolation: parse_wire_isolation(&info.isolation),
4412                context_inheritance: parse_wire_context_inheritance(&info.context_inheritance),
4413                verification_contract_id: None,
4414                capability_filter: Default::default(),
4415                exposure_baseline: None,
4416                loop_round: None,
4417                metadata: super::scalar::BoundedJson::new(serde_json::Value::Object(metadata))
4418                    .map_err(malformed)?,
4419            },
4420        })
4421    }
4422
4423    /// Wire identity of the DAG node an internal agent id belongs to. Falls back to the internal id
4424    /// when the node came from a runtime append rather than the original spec (Task 10 territory).
4425    fn node_id_for(&self, agent_id: &str) -> NodeId {
4426        parse_node_index(agent_id)
4427            .and_then(|index| self.node_ids.get(index).cloned())
4428            .unwrap_or_else(|| {
4429                NodeId::new(agent_id).expect("an internal agent id is a legal branded ref")
4430            })
4431    }
4432
4433    /// §7.3 · a spec's `verification_contract_id` must name a contract this operation declared.
4434    ///
4435    /// A reference that resolves to nothing is a gate the run believes it has and does not: the
4436    /// agent would start with no phase cascade, never publish an `EvaluateMilestone`, and finish
4437    /// having passed a contract that was never evaluated. Refused before the engine moves, so a
4438    /// rejected start leaves the operation free to start again with a spec that resolves.
4439    fn require_known_contract(
4440        &self,
4441        config: &ResolvedOperationConfig,
4442        spec: Option<&LogicalAgentSpec>,
4443    ) -> Result<(), KernelFault> {
4444        let Some(contract_id) = spec.and_then(|spec| spec.verification_contract_id.as_deref())
4445        else {
4446            return Ok(());
4447        };
4448        if config.verification_contract(contract_id).is_some() {
4449            return Ok(());
4450        }
4451        Err(KernelFault::new(
4452            KernelFaultCode::InvalidConfig,
4453            format!(
4454                "the run spec names verification contract {contract_id:?}, which this operation's \
4455                 catalog does not declare; a contract reference that resolves to nothing is a \
4456                 milestone gate the run believes it has (§7.3)"
4457            ),
4458        ))
4459    }
4460
4461    /// Install the phase cascade an agent's contract reference selects.
4462    ///
4463    /// This is `EvaluateMilestone`'s canonical producer (Task 12 SPEC-ISSUE-4): without it the
4464    /// effect existed in the union, had a resolution path and a failure path, and nothing on the
4465    /// wire could ever cause the kernel to emit one.
4466    fn load_verification_contract(
4467        &mut self,
4468        config: &ResolvedOperationConfig,
4469        spec: Option<&LogicalAgentSpec>,
4470    ) -> Result<(), KernelFault> {
4471        let Some(contract) = spec
4472            .and_then(|spec| spec.verification_contract_id.as_deref())
4473            .and_then(|id| config.verification_contract(id))
4474        else {
4475            return Ok(());
4476        };
4477        // A contract means the operation *will* ask for a verdict, so the effect it will publish
4478        // has to be declared now rather than faulting mid-cascade (DEC-8).
4479        self.require_effect_support(config, EffectKindTag::EvaluateMilestone)?;
4480        let contract_id = contract.contract_id.clone();
4481        let cascade = core_milestone_contract(contract, config);
4482        self.engine_mut()?.load_milestone_contract(cascade);
4483        // Remembered so every `EvaluateMilestone` the cascade produces can name the contract its
4484        // phase belongs to — the half of the host's lookup key the semantic engine does not carry.
4485        self.loaded_contract_id = Some(contract_id);
4486        Ok(())
4487    }
4488
4489    /// The contract id every `EvaluateMilestone` this operation publishes belongs to.
4490    ///
4491    /// Fails closed rather than sending an empty id: a cascade can only be running because
4492    /// `load_verification_contract` installed one, so a milestone request with no contract behind
4493    /// it means the engine produced a phase the canonical wire never declared — and a request the
4494    /// host cannot resolve to a verifier is worse than no request at all.
4495    fn require_loaded_contract(&self) -> Result<String, KernelFault> {
4496        self.loaded_contract_id.clone().ok_or_else(|| {
4497            KernelFault::new(
4498                KernelFaultCode::InvalidLifecycle,
4499                "the semantic kernel asked for a milestone verdict, but this operation installed \
4500                 no verification contract; a milestone request names the (contract_id, phase_id) \
4501                 pair the host looks its verifier up by (§7.8)"
4502                    .to_string(),
4503            )
4504        })
4505    }
4506
4507    fn require_effect_support(
4508        &self,
4509        config: &ResolvedOperationConfig,
4510        tag: EffectKindTag,
4511    ) -> Result<(), KernelFault> {
4512        if config.host_effect_support.supports(tag) {
4513            return Ok(());
4514        }
4515        Err(KernelFault::new(
4516            KernelFaultCode::UnsupportedEffect,
4517            format!(
4518                "this operation's host does not declare support for {tag} effects, so the \
4519                 transition that would publish one is refused before anything moves"
4520            ),
4521        ))
4522    }
4523
4524    fn require_root_kind(&self) -> Result<RootKind, KernelFault> {
4525        self.root_kind.ok_or_else(|| {
4526            KernelFault::new(
4527                KernelFaultCode::InvalidLifecycle,
4528                "no root has started, so there is nothing to advance".to_string(),
4529            )
4530        })
4531    }
4532
4533    fn engine_mut(&mut self) -> Result<&mut LoopStateMachine, KernelFault> {
4534        self.engine.as_mut().ok_or_else(|| {
4535            KernelFault::new(
4536                KernelFaultCode::InvalidLifecycle,
4537                "the operation has no genesis configuration, so it has no semantic kernel to drive"
4538                    .to_string(),
4539            )
4540        })
4541    }
4542
4543    fn poison_with(&mut self, fault: KernelFault) -> KernelFault {
4544        self.staged = None;
4545        self.poison.get_or_insert(fault).clone()
4546    }
4547}
4548
4549// ---------------------------------------------------------------------------------------------
4550// wire ⇄ semantic projections
4551// ---------------------------------------------------------------------------------------------
4552
4553fn root_task_id() -> TaskId {
4554    TaskId::new(ROOT_TASK_ID).expect("the root task id is a legal branded ref")
4555}
4556
4557// ---------------------------------------------------------------------------------------------
4558// §7.6 · the model-facing syscall surface
4559// ---------------------------------------------------------------------------------------------
4560
4561/// Tool names that reduce to a P1 syscall instead of to a host tool execution.
4562///
4563/// This list is what deletes §22.10's bypasses 4 and 5. Historically the SDK *removed*
4564/// `submit_workflow_nodes` / `start_workflow` from the tool loop, faked a tool result for the
4565/// model, and re-submitted the request as a separate kernel input with a submitter of its own
4566/// choosing — so the kernel never saw a `ProviderTool` causation at all. Here the names are the
4567/// kernel's, the arguments are decoded by the kernel, and the caller comes from the pending call.
4568///
4569/// SPEC-ISSUE: `SyscallRequest::RequestMemoryWrite` has no entry here because core advertises no
4570/// model-facing memory *write* surface — `memory` is a search tool, and long-term writes are
4571/// extracted host-side today (§22.13's 现状定位). Its only caller channel is therefore a child's
4572/// `parent_requests`. §7.6 lists the request without saying which tool reaches it, so either the
4573/// kernel's meta-tool set gains a write surface or the spec should state that memory writes are a
4574/// child→parent request only.
4575pub const SYSCALL_TOOL_NAMES: &[&str] = &[
4576    "start_workflow",
4577    "submit_workflow_nodes",
4578    "skill",
4579    "update_plan",
4580    crate::context::manager::MEMORY_TOOL_NAME,
4581    crate::context::manager::READ_RESULT_TOOL_NAME,
4582];
4583
4584fn is_syscall_tool(name: &str) -> bool {
4585    SYSCALL_TOOL_NAMES.contains(&name)
4586}
4587
4588/// Decode a recognised meta-tool call into its typed request.
4589///
4590/// A decode failure is a *rejection*, never a fault: the model wrote bad arguments, which is a
4591/// thing to answer with an audit fact rather than a host protocol violation.
4592fn decode_syscall(call: &WireToolCall) -> Result<SyscallRequest, SyscallRejection> {
4593    let arguments = call.arguments.get().clone();
4594    let name: &'static str = SYSCALL_TOOL_NAMES
4595        .iter()
4596        .copied()
4597        .find(|known| *known == call.name.as_str())
4598        .expect("only recognised syscall tools reach the decoder");
4599
4600    fn decode<T: serde::de::DeserializeOwned>(
4601        name: &'static str,
4602        arguments: serde_json::Value,
4603    ) -> Result<T, SyscallRejection> {
4604        serde_json::from_value(arguments)
4605            .map_err(|error| SyscallRejection::new(name, format!("malformed arguments: {error}")))
4606    }
4607
4608    match name {
4609        "start_workflow" => Ok(SyscallRequest::SubmitWorkflow(
4610            super::syscall::SubmitWorkflowRequest {
4611                spec: decode(name, arguments)?,
4612            },
4613        )),
4614        "submit_workflow_nodes" => {
4615            #[derive(serde::Deserialize)]
4616            struct Args {
4617                nodes: Vec<WireNode>,
4618            }
4619            let args: Args = decode(name, arguments)?;
4620            Ok(SyscallRequest::AppendWorkflowNodes(
4621                super::syscall::AppendWorkflowNodesRequest { nodes: args.nodes },
4622            ))
4623        }
4624        "skill" => {
4625            #[derive(serde::Deserialize)]
4626            struct Args {
4627                name: String,
4628                #[serde(default)]
4629                lease_turns: Option<u32>,
4630            }
4631            let args: Args = decode(name, arguments)?;
4632            Ok(SyscallRequest::ActivateSkill(
4633                super::syscall::ActivateSkillRequest {
4634                    name: args.name,
4635                    lease_turns: args.lease_turns,
4636                },
4637            ))
4638        }
4639        "update_plan" => Ok(SyscallRequest::UpdateTask(
4640            super::syscall::UpdateTaskRequest {
4641                update: decode(name, arguments)?,
4642            },
4643        )),
4644        crate::context::manager::MEMORY_TOOL_NAME => {
4645            #[derive(serde::Deserialize)]
4646            struct Args {
4647                #[serde(default)]
4648                query: String,
4649                #[serde(default)]
4650                kinds: Vec<WireMemoryKind>,
4651                #[serde(default)]
4652                top_k: Option<u32>,
4653            }
4654            let args: Args = decode(name, arguments)?;
4655            Ok(SyscallRequest::RequestMemoryQuery(
4656                super::syscall::RequestMemoryQueryRequest {
4657                    query: super::syscall::MemoryQueryProposal {
4658                        text: args.query,
4659                        kinds: args.kinds,
4660                        limit: args.top_k,
4661                    },
4662                },
4663            ))
4664        }
4665        crate::context::manager::READ_RESULT_TOOL_NAME => {
4666            #[derive(serde::Deserialize)]
4667            struct Args {
4668                call_id: String,
4669            }
4670            let args: Args = decode(name, arguments)?;
4671            let handle_id = super::scalar::HandleId::new(args.call_id).map_err(|error| {
4672                SyscallRejection::new(name, format!("malformed handle: {}", error.message))
4673            })?;
4674            Ok(SyscallRequest::PageIn(super::syscall::PageInRequest {
4675                handle_id,
4676            }))
4677        }
4678        other => unreachable!("unrecognised syscall tool {other}"),
4679    }
4680}
4681
4682/// The task a causation names. Both variants carry one, and neither lets a host choose it.
4683fn causation_task(causation: &SyscallCausation) -> TaskId {
4684    match causation {
4685        SyscallCausation::ProviderTool(provider) => provider.task_id.clone(),
4686        SyscallCausation::ChildAttempt(child) => child.task_id.clone(),
4687    }
4688}
4689
4690/// §7.6 · the authority families a quarantined caller may not touch. `None` ⇒ the request widens
4691/// nothing (a plan edit, a page-in of an address the caller already holds).
4692///
4693/// SPEC-ISSUE: §7.6 requires that "a quarantined task must not escalate through workflow append,
4694/// memory scope or capability mutation", but the canonical [`WorkflowNode`](super::root::WorkflowNode)
4695/// carries no trust level — the internal DAG has `NodeTrust::{Trusted,Quarantined}` and the wire
4696/// has no field for it. The refusal below is therefore complete but currently unreachable through
4697/// the contract: no canonical input can declare a node quarantined. Either §7.4's workflow node
4698/// grows a trust field, or §7.6 has to say where quarantine comes from.
4699fn privileged_family(request: &SyscallRequest) -> Option<&'static str> {
4700    match request {
4701        SyscallRequest::SubmitWorkflow(_) | SyscallRequest::AppendWorkflowNodes(_) => {
4702            Some("workflow")
4703        }
4704        SyscallRequest::RequestMemoryWrite(_) | SyscallRequest::RequestMemoryQuery(_) => {
4705            Some("memory")
4706        }
4707        SyscallRequest::ActivateSkill(_) => Some("capability"),
4708        SyscallRequest::UpdateTask(_) | SyscallRequest::PageIn(_) => None,
4709    }
4710}
4711
4712fn core_task_update(update: &WireTaskUpdate) -> crate::context::task_state::TaskUpdate {
4713    crate::context::task_state::TaskUpdate {
4714        plan: update.plan.clone(),
4715        current_step: update.current_step.map(|step| step as usize),
4716        progress: update.progress.clone(),
4717        scratchpad: update.scratchpad.clone(),
4718        blocked_on: update.blocked_on.clone(),
4719        preserved_refs: update.preserved_refs.clone(),
4720        directives: update.directives.clone(),
4721    }
4722}
4723
4724fn mint_effect_id(operation_id: &OperationId, step_seq: WireU64, index: u32) -> EffectId {
4725    EffectId::new(format!("{operation_id}:step:{step_seq}:effect:{index}"))
4726        .expect("an operation-scoped effect id is always a legal branded ref")
4727}
4728
4729fn mint_workflow_id(operation_id: &OperationId, step_seq: WireU64) -> WorkflowId {
4730    WorkflowId::new(format!("{operation_id}:workflow:{step_seq}"))
4731        .expect("an operation-scoped workflow id is always a legal branded ref")
4732}
4733
4734/// `wf-node{N}` / `wf-node{N}-i{k}` → `N`. The internal DAG is index-addressed; the wire is not.
4735fn parse_node_index(agent_id: &str) -> Option<usize> {
4736    let rest = agent_id.strip_prefix("wf-node")?;
4737    let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
4738    digits.parse().ok()
4739}
4740
4741fn wire_node_ids(spec: &WireSpec) -> Vec<NodeId> {
4742    spec.nodes.iter().map(|node| node.node_id.clone()).collect()
4743}
4744
4745/// Wire DAG → the kernel's index-addressed DAG. Node identity is checked here: a duplicate id or a
4746/// dependency on a node the spec does not declare is refused before the engine sees the spec.
4747fn build_core_spec(spec: &WireSpec) -> Result<CoreWorkflowSpec, KernelFault> {
4748    let mut index_of: BTreeMap<&str, usize> = BTreeMap::new();
4749    for (index, node) in spec.nodes.iter().enumerate() {
4750        if index_of.insert(node.node_id.as_str(), index).is_some() {
4751            return Err(KernelFault::new(
4752                KernelFaultCode::InvalidConfig,
4753                format!(
4754                    "workflow node id {:?} appears twice; node identity is unique within a DAG",
4755                    node.node_id
4756                ),
4757            ));
4758        }
4759    }
4760    let mut nodes = Vec::with_capacity(spec.nodes.len());
4761    for node in &spec.nodes {
4762        let role = node
4763            .run_spec
4764            .as_ref()
4765            .and_then(|spec| spec.role)
4766            .map_or(AgentRole::Custom, core_role);
4767        let mut core = CoreWorkflowNode::new(runtime_task(&node.task), role);
4768        if let Some(isolation) = node.run_spec.as_ref().and_then(|spec| spec.isolation) {
4769            core = core.with_isolation(core_isolation(isolation));
4770        }
4771        if let Some(inheritance) = node
4772            .run_spec
4773            .as_ref()
4774            .and_then(|spec| spec.context_inheritance)
4775        {
4776            core.context_inheritance = core_context_inheritance(inheritance);
4777        }
4778        if let Some(metadata) = node
4779            .run_spec
4780            .as_ref()
4781            .map(|spec| spec.metadata.get())
4782            .and_then(serde_json::Value::as_object)
4783        {
4784            if let Some(model_hint) = metadata
4785                .get("model_hint")
4786                .and_then(serde_json::Value::as_str)
4787            {
4788                core = core.with_model_hint(model_hint);
4789            }
4790            if let Some(output_schema) = metadata.get("output_schema") {
4791                core = core.with_output_schema(output_schema.clone());
4792            }
4793        }
4794        let mut depends_on = Vec::with_capacity(node.depends_on.len());
4795        for dependency in &node.depends_on {
4796            let Some(&index) = index_of.get(dependency.as_str()) else {
4797                return Err(KernelFault::new(
4798                    KernelFaultCode::InvalidConfig,
4799                    format!(
4800                        "workflow node {:?} depends on {:?}, which this DAG does not declare",
4801                        node.node_id, dependency
4802                    ),
4803                ));
4804            };
4805            depends_on.push(index);
4806        }
4807        nodes.push(core.with_depends_on(depends_on));
4808    }
4809    let core = CoreWorkflowSpec::new(nodes);
4810    core.validate()
4811        .map_err(|error| KernelFault::new(KernelFaultCode::InvalidConfig, error.to_string()))?;
4812    Ok(core)
4813}
4814
4815fn runtime_task(task: &LogicalTask) -> RuntimeTask {
4816    RuntimeTask {
4817        goal: task.goal.clone(),
4818        criteria: task.criteria.clone(),
4819        metadata: task.metadata.get().clone(),
4820        lane: task.lane.as_ref().map(TaskLane::new).unwrap_or_default(),
4821    }
4822}
4823
4824/// §7.4 · the logical spec carries no host session identity, so the internal identity it builds
4825/// carries none either. (Task 11 removes the field from `AgentIdentity` outright.)
4826fn agent_run_spec(spec: &LogicalAgentSpec) -> AgentRunSpec {
4827    AgentRunSpec {
4828        identity: AgentIdentity::new(ROOT_TASK_ID, NO_HOST_SESSION),
4829        role: spec.role.map_or(AgentRole::Custom, core_role),
4830        isolation: spec
4831            .isolation
4832            .map_or(AgentIsolation::Shared, core_isolation),
4833        goal: spec.goal.clone(),
4834        verification_contract_id: spec.verification_contract_id.as_deref().map(Into::into),
4835        capability_filter: AgentCapabilityFilter {
4836            allowed_kinds: Vec::new(),
4837            allowed_ids: spec
4838                .capability_filter
4839                .allowed_ids
4840                .iter()
4841                .map(|id| id.as_str().into())
4842                .collect(),
4843        },
4844        milestones: None,
4845        metadata: spec.metadata.get().clone(),
4846        loop_round: spec.loop_round.as_ref().map(|round| LoopRoundSpec {
4847            max_rounds: round.max_rounds,
4848            min_sleep_ms: round.min_sleep_ms.map(WireU64::get),
4849            max_sleep_ms: round.max_sleep_ms.map(WireU64::get),
4850            default_action: round.default_action.clone(),
4851        }),
4852        exposure_baseline: spec
4853            .exposure_baseline
4854            .as_ref()
4855            .map(|ids| ids.iter().map(|id| id.as_str().into()).collect()),
4856    }
4857}
4858
4859fn core_role(role: WireRole) -> AgentRole {
4860    match role {
4861        WireRole::Explore => AgentRole::Explore,
4862        WireRole::Plan => AgentRole::Plan,
4863        WireRole::Implement => AgentRole::Implement,
4864        WireRole::Verify => AgentRole::Verify,
4865        WireRole::Custom => AgentRole::Custom,
4866    }
4867}
4868
4869fn core_isolation(isolation: WireIsolation) -> AgentIsolation {
4870    match isolation {
4871        WireIsolation::Shared => AgentIsolation::Shared,
4872        WireIsolation::ReadOnly => AgentIsolation::ReadOnly,
4873        WireIsolation::Worktree => AgentIsolation::Worktree,
4874        WireIsolation::Remote => AgentIsolation::Remote,
4875    }
4876}
4877
4878fn core_context_inheritance(inheritance: WireContextInheritance) -> ContextInheritance {
4879    match inheritance {
4880        WireContextInheritance::None => ContextInheritance::None,
4881        WireContextInheritance::SystemOnly => ContextInheritance::SystemOnly,
4882        WireContextInheritance::Full => ContextInheritance::Full,
4883    }
4884}
4885
4886/// Internal role/isolation labels back onto the wire vocabulary. `None` is the *absent* field, not
4887/// a parse failure: `custom`/`shared` are the wire defaults, so omitting them keeps a launch spec
4888/// minimal instead of restating what the contract already implies.
4889fn parse_wire_role(label: &str) -> Option<WireRole> {
4890    match label {
4891        "explore" => Some(WireRole::Explore),
4892        "plan" => Some(WireRole::Plan),
4893        "implement" => Some(WireRole::Implement),
4894        "verify" => Some(WireRole::Verify),
4895        _ => None,
4896    }
4897}
4898
4899fn parse_wire_isolation(label: &str) -> Option<WireIsolation> {
4900    match label {
4901        "read_only" => Some(WireIsolation::ReadOnly),
4902        "worktree" => Some(WireIsolation::Worktree),
4903        "remote" => Some(WireIsolation::Remote),
4904        _ => None,
4905    }
4906}
4907
4908fn parse_wire_context_inheritance(label: &str) -> Option<WireContextInheritance> {
4909    match label {
4910        "none" => Some(WireContextInheritance::None),
4911        "system_only" => Some(WireContextInheritance::SystemOnly),
4912        "full" => Some(WireContextInheritance::Full),
4913        _ => None,
4914    }
4915}
4916
4917/// §7.4 · seed the P3 context partitions from the one initial context the start carried. This is
4918/// the whole of what used to be a dozen separate accepted inputs.
4919fn seed_initial_context(engine: &mut LoopStateMachine, initial: &InitialContext) {
4920    if !initial.messages.is_empty() {
4921        engine.preload_history(initial.messages.iter().map(logical_message).collect());
4922    }
4923    seed_knowledge(engine, &initial.knowledge);
4924}
4925
4926/// The one place wire knowledge entries enter the P3 knowledge partition.
4927///
4928/// Shared by the initial context (§7.4), `HostCommand::SeedKnowledge` (DEC-9) and the upsert half
4929/// of `HostCommand::ApplyKnowledgeMutation` (§13.2), so the three cannot drift in how a keyed,
4930/// pinned or token-counted entry is stored.
4931fn seed_knowledge(engine: &mut LoopStateMachine, entries: &[super::root::KnowledgeEntry]) {
4932    if entries.is_empty() {
4933        return;
4934    }
4935    let entries: Vec<crate::mm::PageInEntry> = entries
4936        .iter()
4937        .map(|entry| crate::mm::PageInEntry {
4938            content: entry.content.clone(),
4939            tokens: entry.tokens,
4940            source: None,
4941            key: entry.key.clone(),
4942            pinned: entry.pinned,
4943        })
4944        .collect();
4945    engine.apply_page_in(&entries);
4946}
4947
4948/// §7.7 · project one logical signal onto the runtime signal the in-kernel router works with.
4949///
4950/// Three rules are load-bearing:
4951///
4952/// * the **business** signal id travels verbatim. It is what the disposition, expiry and
4953///   displacement audit facts name, so a derived id would report an identity no caller ever wrote;
4954/// * `timestamp_ms` is the **envelope's accepted time**, not the signal's `source_timestamp_ms`.
4955///   The source timestamp is audit metadata and stays out of every admission decision (§11.2);
4956/// * absent optional fields mean "the author did not say", not a default urgency or source that
4957///   would change how the signal is scheduled;
4958/// * `escalate_after_ms` is a **duration** the kernel anchors to that same accepted time. That is
4959///   what closes the old gap where §13.2 admitted `SignalPolicy.deadline_escalation` while §7.7
4960///   carried nothing that could ever come due, leaving the whole escalation axis unreachable
4961///   (Task 14 · adjudication §5n item 1). The kernel still invents no deadline from
4962///   `source_timestamp_ms`, which is not a clock (§11.2).
4963///
4964/// SPEC-ISSUE (task-targeted routing): §7.7 defines the address space (operation or logical task)
4965/// but no per-task attention semantics, and core holds **one** router per operation. A validated
4966/// task target therefore lands in the operation's queue rather than a queue of its own. Either
4967/// §7.7 states that the target is audit-only addressing, or per-task queues need a contract.
4968fn runtime_signal(
4969    signal: &LogicalSignal,
4970    accepted_at_ms: WireU64,
4971) -> crate::types::signal::RuntimeSignal {
4972    use crate::types::signal::{RuntimeSignal, SignalSource, SignalType, Urgency};
4973
4974    let source = match signal.source {
4975        Some(SignalSourceKind::Cron) => SignalSource::Cron,
4976        Some(SignalSourceKind::Gateway) => SignalSource::Gateway,
4977        Some(SignalSourceKind::Heartbeat) => SignalSource::Heartbeat,
4978        Some(SignalSourceKind::Custom) | None => SignalSource::Custom,
4979    };
4980    let urgency = match signal.urgency {
4981        Some(SignalUrgency::Low) => Urgency::Low,
4982        Some(SignalUrgency::High) => Urgency::High,
4983        Some(SignalUrgency::Critical) => Urgency::Critical,
4984        Some(SignalUrgency::Normal) | None => Urgency::Normal,
4985    };
4986    let mut runtime = RuntimeSignal::new(
4987        source,
4988        // `signal_type` is deliberately not on the canonical wire (adjudication §5n item 3):
4989        // urgency already expresses priority, nothing branches on the router's event/job/alert
4990        // distinction, and a second axis that changes no decision is one more thing four hosts
4991        // would have to agree about. Every canonical signal enters as an event.
4992        SignalType::Event,
4993        urgency,
4994        signal_summary(signal),
4995    )
4996    .with_id(signal.signal_id.as_str())
4997    .with_payload(signal.payload.get().clone())
4998    .with_timestamp(accepted_at_ms.get());
4999    if let Some(key) = &signal.dedupe_key {
5000        runtime = runtime.with_dedupe(key.as_str());
5001    }
5002    // §7.7 · `escalate_after_ms` is a duration; the router works in instants. Anchoring it to the
5003    // envelope's accepted time here is the whole point of carrying a duration on the wire: the
5004    // same bytes redelivered produce the same deadline relative to *this* admission, and no host
5005    // clock ever enters the payload (DEC-2).
5006    if let Some(after) = signal.escalate_after_ms {
5007        runtime = runtime.with_deadline(accepted_at_ms.get().saturating_add(after.get()));
5008    }
5009    runtime
5010}
5011
5012/// The model-facing one-liner a queued or interrupting signal becomes.
5013///
5014/// §7.7 carries a payload and no summary, so the summary is derived — deterministically, because a
5015/// replay must produce the same context bytes. A JSON string payload is its own summary; anything
5016/// else is its canonical serialization, bounded.
5017fn signal_summary(signal: &LogicalSignal) -> String {
5018    const SIGNAL_SUMMARY_MAX_BYTES: usize = 512;
5019    match signal.payload.get() {
5020        serde_json::Value::Null => signal.signal_id.as_str().to_string(),
5021        serde_json::Value::String(text) => {
5022            truncate_on_char_boundary(text, SIGNAL_SUMMARY_MAX_BYTES)
5023        }
5024        other => truncate_on_char_boundary(&other.to_string(), SIGNAL_SUMMARY_MAX_BYTES),
5025    }
5026}
5027
5028fn core_capability_kind(
5029    kind: super::root::CapabilityKind,
5030) -> crate::types::capability::CapabilityKind {
5031    use super::root::CapabilityKind as Wire;
5032    use crate::types::capability::CapabilityKind as Core;
5033    match kind {
5034        Wire::Tool => Core::Tool,
5035        Wire::Skill => Core::Skill,
5036        Wire::Memory => Core::Memory,
5037        Wire::Knowledge => Core::Knowledge,
5038        Wire::McpServer => Core::McpServer,
5039        Wire::Command => Core::Command,
5040        Wire::Agent => Core::Agent,
5041    }
5042}
5043
5044fn live_policy_label(patch: &super::command::LivePolicyPatch) -> &'static str {
5045    use super::command::LivePolicyPatch;
5046    match patch {
5047        LivePolicyPatch::ReplaceSignalPolicy(_) => "signal",
5048        LivePolicyPatch::ReplaceGovernancePolicy(_) => "governance",
5049        LivePolicyPatch::TightenResourceQuota(_) => "resource_quota",
5050        LivePolicyPatch::ReplaceRecoveryPolicy(_) => "recovery",
5051    }
5052}
5053
5054fn logical_message(message: &super::root::LogicalMessage) -> Message {
5055    Message {
5056        role: core_role_of(message.role),
5057        content: Content::Text(message.content.clone()),
5058        tool_calls: Vec::new(),
5059        token_count: message.tokens,
5060    }
5061}
5062
5063fn core_role_of(role: MessageRole) -> Role {
5064    match role {
5065        MessageRole::System => Role::System,
5066        MessageRole::User => Role::User,
5067        MessageRole::Assistant => Role::Assistant,
5068        MessageRole::Tool => Role::Tool,
5069    }
5070}
5071
5072fn wire_role_of(role: Role) -> MessageRole {
5073    match role {
5074        Role::System => MessageRole::System,
5075        Role::User => MessageRole::User,
5076        Role::Assistant => MessageRole::Assistant,
5077        Role::Tool => MessageRole::Tool,
5078    }
5079}
5080
5081fn rendered_context(context: &crate::context::renderer::RenderedContext) -> WireRenderedContext {
5082    WireRenderedContext {
5083        system_stable: context.system_stable.clone(),
5084        system_knowledge: context.system_knowledge.clone(),
5085        turns: context.turns.iter().map(provider_message).collect(),
5086        state_turn: context.state_turn.as_ref().map(provider_message),
5087        frozen_prefix_len: context.frozen_prefix_len.map(|len| len as u32),
5088    }
5089}
5090
5091fn provider_message(message: &Message) -> ProviderMessage {
5092    let (content, tool_call_id) = message_body_parts(message)
5093        .map(|(text, tool_call_id, _is_error)| (text, tool_call_id))
5094        .unwrap_or_default();
5095    ProviderMessage {
5096        role: wire_role_of(message.role),
5097        content,
5098        tool_calls: message
5099            .tool_calls
5100            .iter()
5101            .filter_map(|call| wire_tool_call(call).ok())
5102            .collect(),
5103        tool_call_id: tool_call_id.and_then(|call_id| super::scalar::CallId::new(call_id).ok()),
5104        tokens: message.token_count,
5105    }
5106}
5107
5108fn tool_schema(schema: &crate::types::message::ToolSchema) -> WireToolSchema {
5109    WireToolSchema {
5110        name: schema.name.to_string(),
5111        description: schema.description.clone(),
5112        parameters: super::scalar::BoundedJson::new(schema.parameters.clone())
5113            .unwrap_or_else(|_| Default::default()),
5114    }
5115}
5116
5117fn workflow_budget(budget: &crate::orchestration::workflow::WorkflowBudget) -> WireWorkflowBudget {
5118    WireWorkflowBudget {
5119        max_total_tokens: budget.tokens_max.map(WireU64::new),
5120        max_turns: None,
5121        max_concurrency: budget.max_concurrent_subagents.map(|max| max as u32),
5122    }
5123}
5124
5125fn sub_agent_result(completed: &ChildCompleted) -> SubAgentResult {
5126    let termination = match completed.result.status {
5127        ChildStatus::Completed => TerminationReason::Completed,
5128        ChildStatus::Failed => TerminationReason::Error,
5129        ChildStatus::Cancelled => TerminationReason::UserAbort,
5130    };
5131    SubAgentResult {
5132        agent_id: completed.task_id.as_str().into(),
5133        result: LoopResult {
5134            termination,
5135            final_message: completed
5136                .result
5137                .output
5138                .as_ref()
5139                .map(|text| Message::assistant(text.clone())),
5140            turns_used: completed
5141                .result
5142                .usage
5143                .as_ref()
5144                .and_then(|usage| usage.turns)
5145                .unwrap_or(0),
5146            total_tokens_used: completed
5147                .result
5148                .usage
5149                .as_ref()
5150                .and_then(|usage| usage.output_tokens)
5151                .map_or(0, WireU64::get),
5152            loop_continue: None,
5153            classify_branch: None,
5154            pace_decision: None,
5155            tournament_winner: None,
5156        },
5157    }
5158}
5159
5160/// §7.12 · how an agent loop's own termination reason becomes an operation terminal.
5161///
5162/// The internal vocabulary has two reasons the wire's `TerminationReason` deliberately does not
5163/// carry: `user_abort` **is** a `Cancelled` terminal and `error` **is** a `Failed` one. Folding
5164/// either back into `Completed` would give the same event two representations, which is exactly
5165/// what the canonical union removed.
5166fn agent_terminal(result: &LoopResult) -> KernelTerminal {
5167    let usage = UsageReport {
5168        input_tokens: WireU64::new(result.total_tokens_used),
5169        output_tokens: WireU64::ZERO,
5170        turns: result.turns_used,
5171        cached_input_tokens: None,
5172    };
5173    let termination = match result.termination {
5174        TerminationReason::Completed => WireTermination::Completed,
5175        TerminationReason::MaxTurns => WireTermination::MaxTurns,
5176        TerminationReason::TokenBudget => WireTermination::TokenBudget,
5177        TerminationReason::Timeout => WireTermination::Deadline,
5178        TerminationReason::ContextOverflow => WireTermination::ContextOverflow,
5179        TerminationReason::NoProgress => WireTermination::NoProgress,
5180        TerminationReason::MilestoneExceeded => WireTermination::MilestoneExceeded,
5181        TerminationReason::UserAbort => {
5182            return KernelTerminal::Cancelled(CancelledTerminal {
5183                reason: CancellationReason::User,
5184                usage,
5185            });
5186        }
5187        TerminationReason::Error => {
5188            return KernelTerminal::Failed(FailedTerminal {
5189                failure: KernelFailure {
5190                    code: KernelFailureCode::InvariantViolated,
5191                    message: "the agent loop ended in an error state".to_string(),
5192                },
5193                usage,
5194            });
5195        }
5196    };
5197    KernelTerminal::Agent(AgentTerminal {
5198        result: WireLoopResult {
5199            termination,
5200            final_message: result.final_message.as_ref().map(provider_message),
5201            turns_used: result.turns_used,
5202            pace_decision: result.pace_decision.as_ref().map(|decision| {
5203                super::terminal::PaceDecision {
5204                    action: match decision.action {
5205                        CorePaceAction::Continue => super::terminal::PaceAction::Continue,
5206                        CorePaceAction::Sleep => super::terminal::PaceAction::Sleep,
5207                        CorePaceAction::Stop => super::terminal::PaceAction::Stop,
5208                    },
5209                    delay_ms: decision.delay_ms.map(WireU64::new),
5210                    reason: decision.reason.clone(),
5211                    coerced_from: decision.coerced_from.clone(),
5212                }
5213            }),
5214        },
5215        usage,
5216    })
5217}
5218
5219fn publishes(disposition: &StepDisposition, tag: EffectKindTag) -> bool {
5220    disposition
5221        .effects()
5222        .iter()
5223        .any(|effect| effect.tag() == tag)
5224}
5225
5226fn loop_action_label(action: &LoopAction) -> &'static str {
5227    match action {
5228        LoopAction::CallLLM { .. } => "call_provider",
5229        LoopAction::ExecuteTools { .. } => "execute_tools",
5230        LoopAction::RequestApproval { .. } => "request_approval",
5231        LoopAction::SpawnWorkflow { .. } => "spawn_tasks",
5232        LoopAction::PreemptSubAgents { .. } => "preempt_tasks",
5233        LoopAction::PersistMemory { .. } => "persist_memory",
5234        LoopAction::QueryMemory { .. } => "query_memory",
5235        LoopAction::ArchivePageOut { .. } => "archive_page_out",
5236        LoopAction::EvaluateMilestone { .. } => "evaluate_milestone",
5237        LoopAction::Done { .. } => "terminal",
5238        LoopAction::AwaitingResume => "awaiting_resume",
5239    }
5240}
5241
5242/// The model-facing answer to a P1 syscall the kernel executed.
5243///
5244/// 下一请求信息最大化: each says what happened *and* where the consequence will show up, so the
5245/// model's next turn does not have to guess whether a control-plane call took effect.
5246fn syscall_ack(name: &str) -> &'static str {
5247    match name {
5248        "start_workflow" => {
5249            "workflow accepted: its ready nodes are scheduled; each result arrives as that node \
5250             completes"
5251        }
5252        "submit_workflow_nodes" => {
5253            "nodes appended to the running workflow; each result arrives as that node completes"
5254        }
5255        "skill" => "skill activated: its guidance and tools are in this turn's context",
5256        "update_plan" => "plan updated: the new state renders in [TASK STATE] from here on",
5257        crate::context::manager::MEMORY_TOOL_NAME => {
5258            "memory search issued: matching records are added to this conversation before your \
5259             next turn"
5260        }
5261        crate::context::manager::READ_RESULT_TOOL_NAME => "page-in requested",
5262        _ => "accepted",
5263    }
5264}
5265
5266/// Wire → semantic projections for the resolution half.
5267fn core_provider_message(message: &ProviderMessage) -> Result<Message, KernelFault> {
5268    Ok(Message {
5269        role: core_role_of(message.role),
5270        content: Content::Text(message.content.clone()),
5271        tool_calls: message.tool_calls.iter().map(core_tool_call).collect(),
5272        token_count: message.tokens,
5273    })
5274}
5275
5276fn core_tool_call(call: &WireToolCall) -> crate::types::message::ToolCall {
5277    crate::types::message::ToolCall {
5278        id: call.call_id.as_str().into(),
5279        name: call.name.as_str().into(),
5280        arguments: call.arguments.get().clone(),
5281    }
5282}
5283
5284fn wire_tool_call(call: &crate::types::message::ToolCall) -> Result<WireToolCall, KernelFault> {
5285    Ok(WireToolCall {
5286        call_id: super::scalar::CallId::new(call.id.as_str()).map_err(malformed)?,
5287        name: call.name.to_string(),
5288        arguments: super::scalar::BoundedJson::new(call.arguments.clone())
5289            .unwrap_or_else(|_| Default::default()),
5290    })
5291}
5292
5293fn wire_approval_request(
5294    request: &crate::scheduler::state_machine::ApprovalRequest,
5295) -> Result<WireApprovalRequest, KernelFault> {
5296    Ok(WireApprovalRequest {
5297        call_id: super::scalar::CallId::new(request.call_id.as_str()).map_err(malformed)?,
5298        tool_name: request.tool.clone(),
5299        arguments: super::scalar::BoundedJson::new(request.arguments.clone())
5300            .unwrap_or_else(|_| Default::default()),
5301        reason: (!request.reason.is_empty()).then(|| request.reason.clone()),
5302    })
5303}
5304
5305/// §7.10 · one returned tool result.
5306///
5307/// Both arms produce the same thing: the text that enters working context. For `Inline` that is the
5308/// body; for `External` it is the preview, and the body never crosses core at all — the host
5309/// persisted it before submitting, and the kernel holds only the reference the
5310/// [`ToolsSuccess`](super::effect::ToolsSuccess) carried. The residency transfer that records
5311/// *where* the body went happens after the engine has accepted the batch (see
5312/// `record_external_payloads`), because the handle it moves does not exist until the result is in
5313/// history.
5314///
5315/// The canonical [`ToolResultDisposition`] is binary, so the projection onto core's historical
5316/// `is_fatal` + six-way `ToolErrorKind` is total and lossless in the direction that matters: only
5317/// `Recoverable` and `Fatal` are reachable, and `UserInterrupt` — the one kind that still rolls a
5318/// turn back — has no canonical spelling at all. Cancellation travels on `HostControl::Cancel`
5319/// (§7.9), so that retired retry rung is not re-expressible here.
5320///
5321/// §7.10 rule 9 · failure is orthogonal to residency, so the two failure facts are read through
5322/// [`WireToolResultPayload::disposition`] / [`WireToolResultPayload::is_error`] and land in core
5323/// identically for both arms. A tool that failed *and* produced a body over the inline threshold —
5324/// the common shape, not a rare one — is now expressible, and its fatality reaches the batch
5325/// close-out on the same path an inline one does.
5326fn core_tool_result(payload: &WireToolResultPayload) -> ToolResult {
5327    let disposition = payload.disposition();
5328    let is_error = payload.is_error();
5329    let error_kind = match disposition {
5330        ToolResultDisposition::Fatal => Some(ToolErrorKind::Fatal),
5331        ToolResultDisposition::Recoverable => is_error.then_some(ToolErrorKind::Recoverable),
5332    };
5333    match payload {
5334        WireToolResultPayload::Inline(inline) => ToolResult {
5335            call_id: inline.call_id.as_str().into(),
5336            output: Content::Text(inline.result.output.clone()),
5337            is_error,
5338            is_fatal: disposition.is_fatal(),
5339            error_kind,
5340            token_count: inline.result.tokens,
5341        },
5342        WireToolResultPayload::External(external) => ToolResult {
5343            call_id: external.call_id.as_str().into(),
5344            output: Content::Text(external.preview.clone()),
5345            is_error,
5346            is_fatal: disposition.is_fatal(),
5347            error_kind,
5348            token_count: None,
5349        },
5350    }
5351}
5352
5353/// §7.10 rules 1, 2 and 5 · the configured threshold is the **arbiter** of which arm a result may
5354/// take, checked before the engine sees anything.
5355///
5356/// `PayloadPolicy::inline_threshold_bytes` documents a total partition — "results at or above this
5357/// size are committed as `External` rather than inline" — so both directions are enforced here:
5358///
5359/// - an oversized `Inline` is refused rather than externalised by the kernel. The host must persist
5360///   before submission, so "reject" is the only answer that keeps rule 5 true.
5361/// - an undersized `External` is refused too, because it costs a `LoadPayload` round trip to read
5362///   something that would have fitted in the turn that produced it, and it makes the partition —
5363///   the one thing a host has to agree with the kernel about — untotal.
5364///
5365/// The digest must be one this kernel can *verify*: a page-in is checked by recomputing the digest
5366/// over the returned body, so a foreign algorithm would admit a payload whose restoration could
5367/// never be proved. The preview is bounded because it is the part that actually occupies context.
5368fn check_payload_policy(
5369    payload: &WireToolResultPayload,
5370    policy: &super::config::ResolvedPayloadPolicy,
5371) -> Result<(), KernelFault> {
5372    let threshold = policy.inline_threshold_bytes as u64;
5373    match payload {
5374        WireToolResultPayload::Inline(inline) => {
5375            let size = inline.result.output.len() as u64;
5376            if size >= threshold {
5377                return Err(KernelFault::new(
5378                    KernelFaultCode::ResourceLimitExceeded,
5379                    format!(
5380                        "tool result {} is {size} bytes and this operation's payload policy \
5381                         externalises at {threshold}; the host persists the body and submits an \
5382                         external result — the kernel does not spool on its behalf (§7.10)",
5383                        inline.call_id
5384                    ),
5385                ));
5386            }
5387            Ok(())
5388        }
5389        WireToolResultPayload::External(external) => {
5390            if !is_verifiable_digest(external.digest.as_str()) {
5391                return Err(KernelFault::new(
5392                    KernelFaultCode::MalformedEnvelope,
5393                    format!(
5394                        "external tool result {} carries digest {}, which this kernel cannot \
5395                         verify; a paged-in body is checked by recomputing {}:<64 hex> over it",
5396                        external.call_id,
5397                        external.digest,
5398                        super::record::DIGEST_ALGORITHM
5399                    ),
5400                ));
5401            }
5402            let size = external.original_size.get();
5403            if size < threshold {
5404                return Err(KernelFault::new(
5405                    KernelFaultCode::MalformedEnvelope,
5406                    format!(
5407                        "external tool result {} declares {size} bytes but this operation's \
5408                         payload policy inlines below {threshold}; the threshold is the single \
5409                         arbiter of which arm a result takes (§7.10)",
5410                        external.call_id
5411                    ),
5412                ));
5413            }
5414            let preview = external.preview.len() as u64;
5415            if preview > policy.preview_bytes as u64 {
5416                return Err(KernelFault::new(
5417                    KernelFaultCode::ResourceLimitExceeded,
5418                    format!(
5419                        "external tool result {} carries a {preview}-byte preview and this \
5420                         operation keeps {} bytes resident",
5421                        external.call_id, policy.preview_bytes
5422                    ),
5423                ));
5424            }
5425            Ok(())
5426        }
5427    }
5428}
5429
5430/// Whether `digest` is a digest this kernel can recompute — `sha256:` plus 64 lowercase hex.
5431fn is_verifiable_digest(digest: &str) -> bool {
5432    let Some(hex) = digest.strip_prefix(super::record::DIGEST_ALGORITHM) else {
5433        return false;
5434    };
5435    let Some(hex) = hex.strip_prefix(':') else {
5436        return false;
5437    };
5438    hex.len() == 64
5439        && hex
5440            .bytes()
5441            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
5442}
5443
5444fn core_milestone_result(
5445    result: &super::effect::MilestoneCheckResult,
5446) -> crate::types::milestone::MilestoneCheckResult {
5447    crate::types::milestone::MilestoneCheckResult {
5448        phase_id: result.phase_id.clone(),
5449        passed: result.passed,
5450        reason: (!result.passed).then(|| {
5451            if result.failed_criteria.is_empty() {
5452                result.notes.clone()
5453            } else {
5454                format!("unmet criteria: {}", result.failed_criteria.join("; "))
5455            }
5456        }),
5457    }
5458}
5459
5460/// Project a wire contract skeleton onto the engine's phase cascade.
5461///
5462/// The skeleton carries the two things core decides — phase order and unlocks — and nothing else,
5463/// so the projection fills the rest from the engine's own defaults: no criteria (the host owns
5464/// them, §5.2), the default `HarnessEval` verifier, unlimited retries, terminate-on-exhaustion.
5465/// The one lookup here is `unlocks` → capability descriptor, and it cannot fail: `resolve` already
5466/// proved every id names a declared tool or skill, so the fallback marker is unreachable and
5467/// exists only to keep the projection total.
5468fn core_milestone_contract(
5469    contract: &super::config::VerificationContract,
5470    config: &ResolvedOperationConfig,
5471) -> crate::types::milestone::MilestoneContract {
5472    use crate::types::capability::{CapabilityDescriptor, CapabilityKind as CoreCapabilityKind};
5473    use crate::types::milestone::{MilestoneContract, MilestonePhase};
5474
5475    let mut cascade = MilestoneContract::new();
5476    for phase in &contract.phases {
5477        let unlocks = phase
5478            .unlocks
5479            .iter()
5480            .map(|id| {
5481                if let Some(tool) = config.tool_catalog.iter().find(|tool| &tool.name == id) {
5482                    CapabilityDescriptor::tool(core_tool_schema(tool))
5483                } else if let Some(skill) = config.skill_catalog.iter().find(|s| &s.name == id) {
5484                    CapabilityDescriptor::skill(core_skill(skill))
5485                } else {
5486                    CapabilityDescriptor::marker(
5487                        CoreCapabilityKind::Tool,
5488                        id.as_str(),
5489                        String::new(),
5490                    )
5491                }
5492            })
5493            .collect();
5494        cascade = cascade.phase(MilestonePhase {
5495            unlocks,
5496            ..MilestonePhase::new(phase.phase_id.clone())
5497        });
5498    }
5499    cascade
5500}
5501
5502fn core_memory_kind(kind: WireMemoryKind) -> crate::mm::memory::MemoryKind {
5503    match kind {
5504        WireMemoryKind::User => crate::mm::memory::MemoryKind::User,
5505        WireMemoryKind::Feedback => crate::mm::memory::MemoryKind::Feedback,
5506        WireMemoryKind::Project => crate::mm::memory::MemoryKind::Project,
5507        WireMemoryKind::Reference => crate::mm::memory::MemoryKind::Reference,
5508    }
5509}
5510
5511fn wire_memory_kind_label(kind: WireMemoryKind) -> &'static str {
5512    core_memory_kind(kind).label()
5513}
5514
5515/// The canonical memory binding is **opaque** (§7.8): it is not a tenant, not a namespace and not a
5516/// path. Host-facing observations still need to say which binding a fact belongs to, so the binding
5517/// id rides in the namespace slot and the tenant stays empty — the kernel derives no tenant because
5518/// the contract gives it none.
5519fn binding_scope(binding_id: &MemoryBindingId) -> crate::mm::memory::MemoryScope {
5520    crate::mm::memory::MemoryScope::new(String::new(), binding_id.as_str().to_string())
5521}
5522
5523/// The audit text of a host executor failure. Classification first, host prose second — a kernel
5524/// decision was already taken on the kind alone (§7.9), and this is only what the operator reads.
5525fn host_failure_text(failure: &HostEffectFailure) -> String {
5526    if failure.message.is_empty() {
5527        failure.kind.as_str().to_string()
5528    } else {
5529        format!("{}: {}", failure.kind.as_str(), failure.message)
5530    }
5531}
5532
5533/// A resolution for an effect this driver has no record of authoring. The transaction already
5534/// refuses one for an effect that is not pending, so reaching this means the driver's own ledger
5535/// and the journal disagree — a rebuild-from-records failure, not a host protocol error.
5536fn unowned_resolution(effect_id: &EffectId, what: &str) -> KernelFault {
5537    KernelFault::new(
5538        KernelFaultCode::RecordCorrupted,
5539        format!(
5540            "effect {effect_id} resolves a {what} this runtime never authored; the driver's ledger \
5541             no longer describes the journal — rebuild from the records"
5542        ),
5543    )
5544}
5545
5546fn truncate_on_char_boundary(text: &str, max_bytes: usize) -> String {
5547    if text.len() <= max_bytes {
5548        return text.to_string();
5549    }
5550    let mut end = max_bytes;
5551    while end > 0 && !text.is_char_boundary(end) {
5552        end -= 1;
5553    }
5554    text[..end].to_string()
5555}
5556
5557// ---------------------------------------------------------------------------------------------
5558// engine construction from the resolved configuration
5559// ---------------------------------------------------------------------------------------------
5560
5561/// Build the semantic kernel this operation runs on, from the configuration its genesis record
5562/// froze. Nothing here reads a compile-time default: every value comes off the record, which is
5563/// what makes a rebuild on a newer binary reproduce the same steps (§15.2).
5564fn build_engine(config: &ResolvedOperationConfig) -> LoopStateMachine {
5565    let execution = &config.execution_policy;
5566    let mut engine = LoopStateMachine::new(SchedulerBudget {
5567        max_tokens: execution.max_context_tokens,
5568        max_turns: execution.max_turns,
5569        max_total_tokens: execution.max_total_tokens.get(),
5570        max_wall_ms: execution.max_wall_ms.map(WireU64::get),
5571    });
5572    if let Some(grant) = config.budget_grant.clone() {
5573        engine.set_budget_grant(grant);
5574    }
5575    let scheduler_policy = config.scheduler_policy;
5576    engine.set_scheduler_policy(crate::scheduler::policy::SchedulerPolicyConfig {
5577        version: crate::scheduler::policy::SCHEDULER_POLICY_VERSION,
5578        critical_path_weight: i64::from(scheduler_policy.critical_path_weight),
5579        fanout_weight: i64::from(scheduler_policy.fanout_weight),
5580        age_weight: i64::from(scheduler_policy.age_weight),
5581        token_cost_weight: i64::from(scheduler_policy.token_cost_weight),
5582    });
5583
5584    engine.set_criteria_gate(execution.criteria_gate_enabled);
5585    engine.set_repeat_fuse(crate::governance::repeat_fuse::RepeatFuseConfig {
5586        enabled: execution.repeat_fuse.enabled,
5587        deny_after: execution.repeat_fuse.deny_after,
5588        terminate_after: execution.repeat_fuse.terminate_after,
5589    });
5590    engine.set_entropy_watch(crate::scheduler::entropy::EntropyWatchConfig {
5591        enabled: execution.entropy_watch.enabled,
5592        threshold: f64::from(execution.entropy_watch.threshold_ppm.get()) / 1_000_000.0,
5593        hysteresis: f64::from(execution.entropy_watch.hysteresis_ppm.get()) / 1_000_000.0,
5594        cooldown_turns: execution.entropy_watch.cooldown_turns,
5595        notify_model: execution.entropy_watch.notify_model,
5596    });
5597    install_live_policies(&mut engine, config);
5598    engine.set_dispatch_gate_exposed(matches!(
5599        config.feature_policy.tool_dispatch_gate,
5600        super::config::ToolDispatchGate::Exposed
5601    ));
5602    engine
5603        .ctx
5604        .set_memory_enabled(config.feature_policy.memory_enabled);
5605    engine
5606        .ctx
5607        .set_knowledge_enabled(config.feature_policy.knowledge_enabled);
5608    engine
5609        .ctx
5610        .set_plan_tool_enabled(config.feature_policy.plan_tool_enabled);
5611    // §7.6 · the declared skill catalog is what makes `ActivateSkill` checkable: a name outside it
5612    // is a capability mutation with nothing behind it.
5613    engine
5614        .ctx
5615        .set_available_skills(config.skill_catalog.iter().map(core_skill).collect());
5616    engine.ctx.set_stable_core_tools(
5617        config
5618            .feature_policy
5619            .stable_core_tool_ids
5620            .iter()
5621            .map(|id| id.as_str().into()),
5622    );
5623    engine.ctx.config.knowledge_budget_ratio =
5624        config.context_policy.knowledge_budget_ppm.as_ratio();
5625    engine.ctx.config.collapse_assistant_narration =
5626        config.context_policy.collapse_old_assistant_narration;
5627    engine.tools = config.tool_catalog.iter().map(core_tool_schema).collect();
5628    engine
5629}
5630
5631/// Install the four §13.2 live-mutable policies onto an engine.
5632///
5633/// One installer, two callers: the genesis build and `HostCommand::ApplyPolicyPatch`. That is the
5634/// whole reason it exists — a patched policy that took a different code path into the engine than
5635/// the booted one is how "the same configuration means two things" starts.
5636///
5637/// A policy the operation never declared is deliberately **not** installed: §7.3's "the host never
5638/// said" is a value, distinct from an all-permissive policy the host did not state.
5639fn install_live_policies(engine: &mut LoopStateMachine, config: &ResolvedOperationConfig) {
5640    // §7.6 · the P1 gate is only a gate if the operation's declared caps actually reach it. Without
5641    // this the trap would allow every syscall on the canonical path regardless of what the genesis
5642    // record froze.
5643    if let Some(quota) = core_quota(&config.resource_quota) {
5644        engine.set_resource_quota(quota);
5645    }
5646    // The same argument for the tool gate: a governance policy the genesis record froze but the
5647    // engine never installed would make `RequestApproval` unpublishable and every declared rule
5648    // inert.
5649    if let Some(pipeline) = core_governance(&config.governance_policy) {
5650        engine.set_governance(pipeline);
5651    }
5652    engine.set_signal_policy(
5653        config.signal_policy.queue_max as usize,
5654        config.signal_policy.ttl_ms.map(WireU64::get),
5655        config.signal_policy.deadline_escalation,
5656    );
5657    // The two semantic ladders. Before this existed the resolved recovery policy was frozen into
5658    // the genesis record and then never reached the engine at all, so both the booted policy and
5659    // `ReplaceRecoveryPolicy` were inert and the engine's own compile-time defaults decided how
5660    // long a ladder ran — the exact "the record says one thing, the run does another" drift §15.2
5661    // forbids.
5662    engine.set_recovery_limits(
5663        config.recovery_policy.provider_recovery_attempts,
5664        config.recovery_policy.output_recovery_attempts,
5665    );
5666}
5667
5668/// `None` when the operation declared no axis at all. §7.3: "the host never said" is a value, and
5669/// it is *not* the same as an all-uncapped quota — an installed quota makes the workflow budget
5670/// observable, which is a statement the host did not make.
5671fn core_quota(
5672    quota: &super::config::ResourceQuota,
5673) -> Option<crate::governance::quota::ResourceQuota> {
5674    if quota == &super::config::ResourceQuota::default() {
5675        return None;
5676    }
5677    Some(crate::governance::quota::ResourceQuota {
5678        max_concurrent_subagents: quota.max_concurrent_subagents,
5679        max_total_subagents: quota.max_total_subagents,
5680        max_spawn_depth: quota.max_spawn_depth,
5681        memory_writes_per_window: quota
5682            .memory_writes_per_window
5683            .as_ref()
5684            .map(|window| (window.max_events, window.window_ms.get())),
5685        max_workflow_nodes: quota.max_workflow_nodes.map(|max| max as usize),
5686    })
5687}
5688
5689/// `None` when the operation declared no governance at all. Same "the host never said" rule as
5690/// [`core_quota`]: an installed all-allow pipeline is a statement the host did not make, and it
5691/// would silently change what a tool call means (every call would pass a gate that does not exist).
5692fn core_governance(
5693    policy: &super::config::ResolvedGovernancePolicy,
5694) -> Option<crate::governance::pipeline::GovernancePipeline> {
5695    use super::command::{ParamConstraint as WireConstraint, PolicyAction};
5696    use crate::governance::constraint::{ConstraintRule, ParamConstraint as CoreConstraint};
5697    use crate::governance::permission::PermissionRule;
5698    use crate::governance::rate_limit::RateLimit;
5699
5700    if policy.default_action == PolicyAction::Allow
5701        && policy.rules.is_empty()
5702        && policy.vetoed_tools.is_empty()
5703        && policy.rate_limits.is_empty()
5704        && policy.constraints.is_empty()
5705    {
5706        return None;
5707    }
5708    let mut pipeline = crate::governance::pipeline::GovernancePipeline::new(core_policy_action(
5709        policy.default_action,
5710    ));
5711    for rule in &policy.rules {
5712        pipeline.permission.add_rule(PermissionRule {
5713            tool_pattern: rule.tool_pattern.as_str().into(),
5714            action: core_policy_action(rule.action),
5715        });
5716    }
5717    for tool in &policy.vetoed_tools {
5718        pipeline.veto.block_tool(tool.clone());
5719    }
5720    for limit in &policy.rate_limits {
5721        pipeline.rate_limiter.set_limit(
5722            limit.tool.clone(),
5723            RateLimit {
5724                max_calls: limit.max_calls,
5725                window_ms: limit.window_ms.get(),
5726            },
5727        );
5728    }
5729    for constraint in &policy.constraints {
5730        let rule = match constraint {
5731            WireConstraint::Required(_) => ConstraintRule::Required,
5732            WireConstraint::Enum(spec) => ConstraintRule::Enum(spec.values.clone()),
5733            // §7.1.1 · the wire carries fixed-point micro-units so a bound is replayable; the
5734            // validator's own arithmetic is float, and this is the single conversion point.
5735            WireConstraint::Range(spec) => ConstraintRule::Range {
5736                min: spec.min_micros.map(|micros| micros as f64 / 1_000_000.0),
5737                max: spec.max_micros.map(|micros| micros as f64 / 1_000_000.0),
5738            },
5739        };
5740        pipeline.constraints.add(CoreConstraint {
5741            tool_name: constraint.tool().to_string(),
5742            param_path: constraint.param_path().to_string(),
5743            rule,
5744        });
5745    }
5746    Some(pipeline)
5747}
5748
5749fn core_policy_action(
5750    action: super::command::PolicyAction,
5751) -> crate::governance::permission::PermissionAction {
5752    use crate::governance::permission::PermissionAction;
5753    match action {
5754        super::command::PolicyAction::Allow => PermissionAction::Allow,
5755        super::command::PolicyAction::Deny => PermissionAction::Deny,
5756        super::command::PolicyAction::AskUser => PermissionAction::AskUser,
5757    }
5758}
5759
5760fn core_skill(skill: &super::config::SkillMetadata) -> crate::types::skill::SkillMetadata {
5761    crate::types::skill::SkillMetadata {
5762        name: skill.name.as_str().into(),
5763        description: skill.description.clone(),
5764        when_to_use: skill.when_to_use.clone(),
5765        allowed_tools: skill
5766            .allowed_tools
5767            .iter()
5768            .map(|tool| tool.as_str().into())
5769            .collect(),
5770        effort: skill.effort,
5771        estimated_tokens: skill.estimated_tokens.unwrap_or(0),
5772    }
5773}
5774
5775fn core_tool_schema(schema: &WireToolSchema) -> crate::types::message::ToolSchema {
5776    crate::types::message::ToolSchema {
5777        name: schema.name.as_str().into(),
5778        description: schema.description.clone(),
5779        parameters: schema.parameters.get().clone(),
5780    }
5781}
5782
5783fn malformed(error: super::scalar::WireScalarError) -> KernelFault {
5784    KernelFault::new(KernelFaultCode::MalformedEnvelope, error.message)
5785}
5786
5787#[cfg(test)]
5788mod tests {
5789    use std::fs;
5790    use std::path::PathBuf;
5791
5792    use serde_json::{Value, json};
5793
5794    use super::*;
5795    use crate::runtime::kernel::wire::checkpoint::{
5796        CanonicalInput, CheckpointCandidate, CheckpointDraft, KernelCheckpoint,
5797    };
5798    use crate::runtime::kernel::wire::config::ConfigDefaults;
5799    use crate::runtime::kernel::wire::config::TailBounds;
5800    use crate::runtime::kernel::wire::config::{
5801        EntropyWatchPolicy, ExecutionPolicy, HostEffectSupport,
5802        MilestonePhase as WireMilestonePhase, OperationConfig,
5803        VerificationContract as WireVerificationContract,
5804    };
5805    use crate::runtime::kernel::wire::effect::{
5806        EffectSucceeded, TaskLaunchOutcome, TaskLaunchStarted, TaskLaunchStatus,
5807        TasksSpawnedSuccess,
5808    };
5809    use crate::runtime::kernel::wire::envelope::{
5810        ConfigureOperation, DeliverExternalEvent, KernelInput, ResolveEffect, StartOperation,
5811        WireEnvelope,
5812    };
5813    use crate::runtime::kernel::wire::event::{ChildResult, DeliverSignal, LogicalSignal};
5814    use crate::runtime::kernel::wire::fault::PrepareToken;
5815    use crate::runtime::kernel::wire::record::{
5816        KernelRecord, RecordPreparation, verify_record_chain,
5817    };
5818    use crate::runtime::kernel::wire::restore::{
5819        RestoreCost, RestoredOperation, restore_operation,
5820    };
5821    use crate::runtime::kernel::wire::root::{
5822        RootAgentEntry, RootWorkflowEntry, WorkflowNode as WireNode,
5823    };
5824    use crate::runtime::kernel::wire::scalar::{
5825        AttemptId as WireAttemptId, DeliveryId, InputId, Ppm, SignalId,
5826    };
5827    use crate::runtime::kernel::wire::transaction::{
5828        CheckpointBoundary, CommittedTransition, InMemoryRecordIndex, KernelTransaction,
5829        TailPressure,
5830    };
5831    use crate::scheduler::tcb::TaskLifecycle;
5832
5833    const OPERATION: &str = "op-driver-1";
5834
5835    // -----------------------------------------------------------------------------------------
5836    // §8.2 host loop: prepare → (CAS append) → commit → fold
5837    // -----------------------------------------------------------------------------------------
5838
5839    /// The whole durable path, exactly as a host runs it. Nothing here reaches into the driver
5840    /// behind the transaction's back: a step is planned by the driver, appended by the "host"
5841    /// (this in-memory journal), committed by the transaction, and only then folded into the
5842    /// driver's root-kind/focus state.
5843    struct Runtime {
5844        tx: KernelTransaction<PlannedStep, InMemoryRecordIndex>,
5845        driver: CanonicalOperationDriver,
5846        journal: Vec<KernelRecord>,
5847        last_observations: Vec<KernelObservation>,
5848        /// What the restore that produced this runtime read, when it came from one.
5849        restore_cost: Option<RestoreCost>,
5850    }
5851
5852    impl Runtime {
5853        fn new() -> Self {
5854            Self {
5855                tx: KernelTransaction::new(ConfigDefaults::default(), InMemoryRecordIndex::new()),
5856                driver: CanonicalOperationDriver::new(),
5857                journal: Vec::new(),
5858                last_observations: Vec::new(),
5859                restore_cost: None,
5860            }
5861        }
5862
5863        fn prepare(&mut self, envelope: &WireEnvelope) -> RecordPreparation<PlannedStep> {
5864            let Self { tx, driver, .. } = self;
5865            tx.prepare(envelope, |context| driver.plan(context))
5866        }
5867
5868        fn append_and_commit(
5869            &mut self,
5870            preparation: RecordPreparation<PlannedStep>,
5871        ) -> CommittedTransition<PlannedStep> {
5872            let token: PrepareToken = preparation
5873                .token()
5874                .unwrap_or_else(|| {
5875                    panic!("expected a prepared step, got {:?}", preparation.fault())
5876                })
5877                .clone();
5878            let head = preparation.record().unwrap().record_digest().clone();
5879            let committed = self.tx.commit(&token, &head).expect("commit must succeed");
5880            self.journal.push(committed.record.clone());
5881            self.last_observations = committed.step.observations.clone();
5882            committed
5883        }
5884
5885        fn submit(&mut self, envelope: &WireEnvelope) -> CommittedTransition<PlannedStep> {
5886            let preparation = self.prepare(envelope);
5887            let committed = self.append_and_commit(preparation);
5888            self.driver
5889                .note_committed(committed.step_seq)
5890                .expect("the driver folds the step it planned");
5891            committed
5892        }
5893
5894        /// Same durable path, but the step comes from a planner the *test* supplies. Used for the
5895        /// two transitions Task 9 deliberately leaves to Task 10/12 — the P1 syscall that starts a
5896        /// nested workflow, and the provider resolution that frees the pending provider effect.
5897        fn submit_planned<F>(
5898            &mut self,
5899            envelope: &WireEnvelope,
5900            plan: F,
5901        ) -> CommittedTransition<PlannedStep>
5902        where
5903            F: FnOnce(
5904                &mut CanonicalOperationDriver,
5905                &PlanContext<'_>,
5906            ) -> Result<PlannedStep, KernelFault>,
5907        {
5908            let preparation = {
5909                let Self { tx, driver, .. } = self;
5910                tx.prepare(envelope, |context| plan(driver, context))
5911            };
5912            self.append_and_commit(preparation)
5913        }
5914
5915        fn reject(&mut self, envelope: &WireEnvelope) -> KernelFault {
5916            let preparation = self.prepare(envelope);
5917            preparation
5918                .fault()
5919                .unwrap_or_else(|| panic!("expected a rejection, got a prepared step"))
5920                .clone()
5921        }
5922
5923        /// §12.2 · the host's own restore call: a checkpoint blob plus the journal records above the
5924        /// step it covers. Nothing else — in particular, **not** the records the checkpoint covers,
5925        /// which is what makes the cost assertions meaningful.
5926        fn restore(&self, checkpoint: &KernelCheckpoint) -> Runtime {
5927            let after: Vec<KernelRecord> = self
5928                .journal
5929                .iter()
5930                .filter(|record| record.step_seq().get() > checkpoint.through_step_seq().get())
5931                .cloned()
5932                .collect();
5933            Self::restore_with(Some(checkpoint), &after)
5934        }
5935
5936        fn restore_with(
5937            checkpoint: Option<&KernelCheckpoint>,
5938            records: &[KernelRecord],
5939        ) -> Runtime {
5940            let RestoredOperation {
5941                transaction,
5942                driver,
5943                cost,
5944            } = restore_operation(
5945                checkpoint,
5946                records,
5947                ConfigDefaults::default(),
5948                InMemoryRecordIndex::from_records(records),
5949            )
5950            .expect("the restore ladder runs to completion");
5951            Runtime {
5952                tx: transaction,
5953                driver,
5954                journal: Vec::new(),
5955                last_observations: Vec::new(),
5956                restore_cost: Some(cost),
5957            }
5958        }
5959
5960        /// The whole journal, as the host holds it.
5961        fn journal_from(&self, checkpoint: &KernelCheckpoint) -> Vec<KernelRecord> {
5962            self.journal
5963                .iter()
5964                .filter(|record| record.step_seq().get() > checkpoint.through_step_seq().get())
5965                .cloned()
5966                .collect()
5967        }
5968
5969        fn pending_effect_kinds(&self) -> Vec<EffectKindTag> {
5970            self.tx.pending_effects().map(|e| e.tag()).collect()
5971        }
5972
5973        fn observations(&self) -> &[KernelObservation] {
5974            &self.last_observations
5975        }
5976
5977        /// §12.3 · exactly the host's call: project the driver's three partitions, hand them to
5978        /// the transaction, get a candidate. Nothing here reaches around either layer.
5979        fn checkpoint(&self) -> CheckpointCandidate {
5980            self.tx
5981                .checkpoint_candidate(self.driver.project_logical_state())
5982                .expect("a configured operation has a logical state to checkpoint")
5983        }
5984    }
5985
5986    // -----------------------------------------------------------------------------------------
5987    // envelopes
5988    // -----------------------------------------------------------------------------------------
5989
5990    fn operation() -> OperationId {
5991        OperationId::new(OPERATION).unwrap()
5992    }
5993
5994    fn envelope(id: &str, observed_at_ms: u64, input: KernelInput) -> WireEnvelope {
5995        WireEnvelope::new(
5996            operation(),
5997            InputId::new(id).unwrap(),
5998            WireU64::new(observed_at_ms),
5999            input,
6000        )
6001    }
6002
6003    fn boot_config(supported: impl IntoIterator<Item = EffectKindTag>) -> OperationConfig {
6004        OperationConfig {
6005            execution_policy: Some(ExecutionPolicy {
6006                max_turns: Some(12),
6007                ..ExecutionPolicy::default()
6008            }),
6009            host_effect_support: HostEffectSupport::new(supported),
6010            ..OperationConfig::default()
6011        }
6012    }
6013
6014    fn configure() -> WireEnvelope {
6015        configure_supporting([EffectKindTag::CallProvider, EffectKindTag::SpawnTasks])
6016    }
6017
6018    fn configure_supporting(supported: impl IntoIterator<Item = EffectKindTag>) -> WireEnvelope {
6019        envelope(
6020            "in-configure",
6021            1_700_000_000_000,
6022            KernelInput::ConfigureOperation(ConfigureOperation {
6023                config: boot_config(supported),
6024            }),
6025        )
6026    }
6027
6028    fn agent_start(id: &str, observed_at_ms: u64) -> WireEnvelope {
6029        envelope(
6030            id,
6031            observed_at_ms,
6032            KernelInput::StartOperation(StartOperation {
6033                entry: RootEntry::Agent(RootAgentEntry {
6034                    task: LogicalTask::new("write the research brief"),
6035                    run_spec: None,
6036                }),
6037                initial_context: InitialContext::default(),
6038            }),
6039        )
6040    }
6041
6042    fn wire_node(node_id: &str, goal: &str, depends_on: &[&str]) -> WireNode {
6043        WireNode {
6044            node_id: NodeId::new(node_id).unwrap(),
6045            task: LogicalTask::new(goal),
6046            depends_on: depends_on
6047                .iter()
6048                .map(|id| NodeId::new(*id).unwrap())
6049                .collect(),
6050            run_spec: None,
6051        }
6052    }
6053
6054    fn two_node_spec() -> WireSpec {
6055        WireSpec {
6056            name: "brief".to_string(),
6057            nodes: vec![
6058                wire_node("collect", "collect the sources", &[]),
6059                wire_node("write", "write the brief", &["collect"]),
6060            ],
6061        }
6062    }
6063
6064    fn workflow_start(id: &str, observed_at_ms: u64, spec: WireSpec) -> WireEnvelope {
6065        envelope(
6066            id,
6067            observed_at_ms,
6068            KernelInput::StartOperation(StartOperation {
6069                entry: RootEntry::Workflow(RootWorkflowEntry { spec }),
6070                initial_context: InitialContext::default(),
6071            }),
6072        )
6073    }
6074
6075    fn spawned(
6076        id: &str,
6077        observed_at_ms: u64,
6078        effect_id: &EffectId,
6079        tasks: &[&str],
6080    ) -> WireEnvelope {
6081        envelope(
6082            id,
6083            observed_at_ms,
6084            KernelInput::ResolveEffect(ResolveEffect {
6085                effect_id: effect_id.clone(),
6086                outcome: EffectOutcome::Succeeded(EffectSucceeded {
6087                    result: EffectSuccess::TasksSpawned(TasksSpawnedSuccess {
6088                        attempts: tasks
6089                            .iter()
6090                            .map(|task| TaskLaunchOutcome {
6091                                task_id: TaskId::new(*task).unwrap(),
6092                                attempt_id: WireAttemptId::new(format!("{task}:attempt:1"))
6093                                    .unwrap(),
6094                                outcome: TaskLaunchStatus::Started(TaskLaunchStarted {}),
6095                            })
6096                            .collect(),
6097                    }),
6098                }),
6099            }),
6100        )
6101    }
6102
6103    fn child_done(id: &str, observed_at_ms: u64, task: &str, output: &str) -> WireEnvelope {
6104        envelope(
6105            id,
6106            observed_at_ms,
6107            KernelInput::DeliverExternalEvent(DeliverExternalEvent {
6108                event: ExternalEvent::ChildCompleted(ChildCompleted {
6109                    task_id: TaskId::new(task).unwrap(),
6110                    attempt_id: WireAttemptId::new(format!("{task}:attempt:1")).unwrap(),
6111                    result: ChildResult {
6112                        status: ChildStatus::Completed,
6113                        output: Some(output.to_string()),
6114                        ..ChildResult::default()
6115                    },
6116                    parent_requests: Vec::new(),
6117                }),
6118            }),
6119        )
6120    }
6121
6122    /// Drives [`CanonicalOperationDriver::begin_nested_workflow`] as a direct API call, on an
6123    /// envelope that carries no request of its own.
6124    ///
6125    /// The real P1 carrier is now the provider resolution (see `provider_result` and the §7.6
6126    /// tests); these Task 9 tests keep using the direct entry point because what they assert is the
6127    /// focus/authority arc, which both paths share — `plan_provider_syscalls` reduces
6128    /// `SubmitWorkflow` onto exactly this code.
6129    fn syscall_carrier(id: &str, observed_at_ms: u64) -> WireEnvelope {
6130        envelope(
6131            id,
6132            observed_at_ms,
6133            KernelInput::DeliverExternalEvent(DeliverExternalEvent {
6134                event: ExternalEvent::DeliverSignal(DeliverSignal {
6135                    delivery_id: DeliveryId::new(format!("delivery-{id}")).unwrap(),
6136                    attempt: 1,
6137                    signal: LogicalSignal::new(SignalId::new(format!("sig-{id}")).unwrap()),
6138                }),
6139            }),
6140        )
6141    }
6142
6143    fn effect_id(step_seq: WireU64) -> EffectId {
6144        EffectId::new(format!("{OPERATION}:step:{step_seq}:effect:0")).unwrap()
6145    }
6146
6147    fn effect_id_at(step_seq: WireU64, index: u32) -> EffectId {
6148        EffectId::new(format!("{OPERATION}:step:{step_seq}:effect:{index}")).unwrap()
6149    }
6150
6151    // -----------------------------------------------------------------------------------------
6152    // §7.6 · syscall envelopes
6153    // -----------------------------------------------------------------------------------------
6154
6155    fn syscall_tool_catalog() -> Vec<WireToolSchema> {
6156        SYSCALL_TOOL_NAMES
6157            .iter()
6158            .chain(std::iter::once(&"search"))
6159            .map(|name| WireToolSchema {
6160                name: (*name).to_string(),
6161                description: String::new(),
6162                parameters: Default::default(),
6163            })
6164            .collect()
6165    }
6166
6167    /// A configuration whose operation can actually reach every P1 syscall: the meta-tool surface
6168    /// is in the catalog, memory is bound read+write, one skill is declared, and the host supports
6169    /// the effects the memory syscalls publish.
6170    fn syscall_config() -> WireEnvelope {
6171        syscall_config_with(|_| {})
6172    }
6173
6174    /// The same configuration with one edit applied — how the effect-resolution tests declare the
6175    /// extra host support (approval, page-out, milestone) or a governance policy their arc needs.
6176    fn syscall_config_with(edit: impl FnOnce(&mut OperationConfig)) -> WireEnvelope {
6177        use crate::runtime::kernel::wire::config::{
6178            MemoryPolicy, ResourceQuota, SkillMetadata as WireSkill,
6179        };
6180        use crate::runtime::kernel::wire::effect::{MemoryAccessBinding, MemoryCapabilities};
6181        use crate::runtime::kernel::wire::scalar::MemoryBindingId;
6182
6183        let mut config = {
6184            OperationConfig {
6185                execution_policy: Some(ExecutionPolicy {
6186                    max_turns: Some(12),
6187                    ..ExecutionPolicy::default()
6188                }),
6189                host_effect_support: HostEffectSupport::new([
6190                    EffectKindTag::CallProvider,
6191                    EffectKindTag::ExecuteTools,
6192                    EffectKindTag::LoadPayload,
6193                    EffectKindTag::SpawnTasks,
6194                    EffectKindTag::PreemptTasks,
6195                    EffectKindTag::PersistMemory,
6196                    EffectKindTag::QueryMemory,
6197                ]),
6198                tool_catalog: syscall_tool_catalog(),
6199                skill_catalog: vec![WireSkill {
6200                    name: "debug".to_string(),
6201                    description: "debug helper".to_string(),
6202                    when_to_use: None,
6203                    allowed_tools: Vec::new(),
6204                    effort: None,
6205                    estimated_tokens: None,
6206                }],
6207                memory_access: Some(MemoryAccessBinding {
6208                    binding_id: MemoryBindingId::new("mem-binding-1").unwrap(),
6209                    capabilities: MemoryCapabilities {
6210                        read: true,
6211                        write: true,
6212                    },
6213                }),
6214                memory_policy: Some(MemoryPolicy {
6215                    retrieval_top_k: Some(4),
6216                    ..MemoryPolicy::default()
6217                }),
6218                resource_quota: Some(ResourceQuota {
6219                    max_workflow_nodes: Some(3),
6220                    ..ResourceQuota::default()
6221                }),
6222                ..OperationConfig::default()
6223            }
6224        };
6225        edit(&mut config);
6226        envelope(
6227            "in-configure",
6228            1_700_000_000_000,
6229            KernelInput::ConfigureOperation(ConfigureOperation { config }),
6230        )
6231    }
6232
6233    fn tool_call(call_id: &str, name: &str, arguments: Value) -> WireToolCall {
6234        WireToolCall {
6235            call_id: super::super::scalar::CallId::new(call_id).unwrap(),
6236            name: name.to_string(),
6237            arguments: super::super::scalar::BoundedJson::new(arguments).unwrap(),
6238        }
6239    }
6240
6241    /// A provider result carrying tool calls — the only wire shape from which a `ProviderTool`
6242    /// causation can be derived.
6243    fn provider_result(
6244        id: &str,
6245        observed_at_ms: u64,
6246        effect: &EffectId,
6247        calls: Vec<WireToolCall>,
6248    ) -> WireEnvelope {
6249        envelope(
6250            id,
6251            observed_at_ms,
6252            KernelInput::ResolveEffect(ResolveEffect {
6253                effect_id: effect.clone(),
6254                outcome: EffectOutcome::Succeeded(EffectSucceeded {
6255                    result: EffectSuccess::Provider(super::super::effect::ProviderSuccess {
6256                        outcome: super::super::effect::ProviderOutcome::Completed(
6257                            super::super::effect::ProviderCompleted {
6258                                message: ProviderMessage {
6259                                    role: MessageRole::Assistant,
6260                                    content: String::new(),
6261                                    tool_calls: calls,
6262                                    tool_call_id: None,
6263                                    tokens: None,
6264                                },
6265                                observed_input_tokens: None,
6266                                observed_output_tokens: None,
6267                                stop_reason: None,
6268                            },
6269                        ),
6270                    }),
6271                }),
6272            }),
6273        )
6274    }
6275
6276    fn child_done_with(
6277        id: &str,
6278        observed_at_ms: u64,
6279        task: &str,
6280        attempt: &str,
6281        requests: Vec<SyscallRequest>,
6282    ) -> WireEnvelope {
6283        envelope(
6284            id,
6285            observed_at_ms,
6286            KernelInput::DeliverExternalEvent(DeliverExternalEvent {
6287                event: ExternalEvent::ChildCompleted(ChildCompleted {
6288                    task_id: TaskId::new(task).unwrap(),
6289                    attempt_id: WireAttemptId::new(attempt).unwrap(),
6290                    result: ChildResult {
6291                        status: ChildStatus::Completed,
6292                        output: Some("done".to_string()),
6293                        ..ChildResult::default()
6294                    },
6295                    parent_requests: requests,
6296                }),
6297            }),
6298        )
6299    }
6300
6301    fn node_args(nodes: &[WireNode]) -> Value {
6302        json!({ "nodes": serde_json::to_value(nodes).unwrap() })
6303    }
6304
6305    /// `(operation, subject, reason)` of every structured rejection the last transition recorded.
6306    fn rejections(runtime: &Runtime) -> Vec<(String, Option<String>, String)> {
6307        runtime
6308            .observations()
6309            .iter()
6310            .filter_map(|observation| match observation {
6311                KernelObservation::ControlRequestRejected {
6312                    operation,
6313                    subject,
6314                    reason,
6315                    ..
6316                } => Some((operation.clone(), subject.clone(), reason.clone())),
6317                _ => None,
6318            })
6319            .collect()
6320    }
6321
6322    fn sole_effect(committed: &CommittedTransition<PlannedStep>) -> &KernelEffect {
6323        let effects = committed.published_effects();
6324        assert_eq!(effects.len(), 1, "expected exactly one published effect");
6325        &effects[0]
6326    }
6327
6328    // -----------------------------------------------------------------------------------------
6329    // fixture: agent-configure-is-genesis
6330    // -----------------------------------------------------------------------------------------
6331
6332    #[test]
6333    fn the_first_accepted_input_must_be_the_configuration() {
6334        let mut runtime = Runtime::new();
6335
6336        // a business input before any configuration
6337        let fault = runtime.reject(&agent_start("in-early-start", 1_700_000_000_100));
6338        assert_eq!(fault.code, KernelFaultCode::InvalidLifecycle);
6339        assert_eq!(runtime.tx.head(), None, "a rejection moves nothing");
6340        assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Created);
6341        assert!(
6342            runtime.driver.engine().is_none(),
6343            "no semantic kernel exists"
6344        );
6345
6346        let genesis = runtime.submit(&configure());
6347        assert_eq!(genesis.step_seq, WireU64::ZERO);
6348        assert_eq!(
6349            genesis.record.previous_record_digest(),
6350            None,
6351            "the genesis record has no predecessor (§8.1)"
6352        );
6353        assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Configured);
6354        assert!(runtime.driver.engine().is_some());
6355        assert_eq!(
6356            runtime.driver.root_kind(),
6357            None,
6358            "configuring is not starting"
6359        );
6360
6361        // the genesis record stores the *resolved* configuration, not the sparse input
6362        let stored = genesis.record.normalized_input().unwrap();
6363        let resolved = stored
6364            .resolved_config()
6365            .expect("genesis carries the config");
6366        assert_eq!(resolved.execution_policy.max_turns, 12);
6367        assert_eq!(
6368            resolved.execution_policy.max_context_tokens,
6369            ConfigDefaults::default()
6370                .baseline
6371                .execution_policy
6372                .max_context_tokens,
6373            "every default this operation runs on is frozen in its first record"
6374        );
6375    }
6376
6377    // -----------------------------------------------------------------------------------------
6378    // fixture: agent-root-start-is-atomic + agent-start-issues-model-turn
6379    // -----------------------------------------------------------------------------------------
6380
6381    #[test]
6382    fn an_agent_root_start_is_one_atomic_input_that_issues_the_model_turn() {
6383        let mut runtime = Runtime::new();
6384        runtime.submit(&configure());
6385        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6386
6387        // root kind and initial context are fixed by this one accepted input
6388        assert_eq!(started.step.root_kind, Some(RootKind::Agent));
6389        assert_eq!(runtime.driver.root_kind(), Some(RootKind::Agent));
6390        assert_eq!(
6391            runtime.driver.focus(),
6392            Some(&ExecutionFocus::agent_turn(root_task_id())),
6393        );
6394        assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Running);
6395
6396        // exactly one pending effect, and it is the provider call
6397        let effect = sole_effect(&started);
6398        assert_eq!(effect.tag(), EffectKindTag::CallProvider);
6399        assert_eq!(
6400            effect.effect_id,
6401            effect_id(started.step_seq),
6402            "the effect id is minted by the kernel from the step it belongs to"
6403        );
6404        assert_eq!(
6405            effect.causation_input_id.as_str(),
6406            "in-start",
6407            "every effect names the accepted input that produced it"
6408        );
6409        assert_eq!(
6410            runtime.pending_effect_kinds(),
6411            vec![EffectKindTag::CallProvider]
6412        );
6413
6414        // the rendered context is real: the goal reached the P3 context VM
6415        let EffectKind::CallProvider(call) = &effect.effect else {
6416            panic!("expected a provider call");
6417        };
6418        let rendered = format!(
6419            "{}{}",
6420            call.context.system_stable, call.context.system_knowledge
6421        );
6422        let state_turn = call
6423            .context
6424            .state_turn
6425            .as_ref()
6426            .map(|turn| turn.content.clone())
6427            .unwrap_or_default();
6428        assert!(
6429            rendered.contains("research brief") || state_turn.contains("research brief"),
6430            "the root task's goal must be in the rendered context, not merely stored"
6431        );
6432        assert!(
6433            !call.context.turns.is_empty(),
6434            "the start seeded a first turn"
6435        );
6436    }
6437
6438    #[test]
6439    fn a_second_root_start_is_refused_with_zero_mutation() {
6440        let mut runtime = Runtime::new();
6441        runtime.submit(&configure());
6442        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6443
6444        let before = (
6445            runtime.tx.head(),
6446            runtime.tx.lifecycle(),
6447            runtime.pending_effect_kinds(),
6448            runtime.driver.root_kind(),
6449            runtime.driver.focus().cloned(),
6450        );
6451
6452        let fault = runtime.reject(&agent_start("in-start-again", 1_700_000_002_000));
6453        assert_eq!(fault.code, KernelFaultCode::InvalidLifecycle);
6454
6455        let after = (
6456            runtime.tx.head(),
6457            runtime.tx.lifecycle(),
6458            runtime.pending_effect_kinds(),
6459            runtime.driver.root_kind(),
6460            runtime.driver.focus().cloned(),
6461        );
6462        assert_eq!(before, after, "a refused root start moves nothing");
6463        assert_eq!(runtime.journal.len(), 2, "no third record exists");
6464        assert_eq!(started.step.root_kind, Some(RootKind::Agent));
6465    }
6466
6467    #[test]
6468    fn a_workflow_root_start_after_an_agent_root_cannot_re_root_the_operation() {
6469        let mut runtime = Runtime::new();
6470        runtime.submit(&configure());
6471        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6472
6473        let fault = runtime.reject(&workflow_start(
6474            "in-reroot",
6475            1_700_000_002_000,
6476            two_node_spec(),
6477        ));
6478        assert_eq!(fault.code, KernelFaultCode::InvalidLifecycle);
6479        assert_eq!(
6480            runtime.driver.root_kind(),
6481            Some(RootKind::Agent),
6482            "the root kind is immutable for the operation's lifetime (§6.1.5)"
6483        );
6484    }
6485
6486    /// DEC-8 fail-closed, at the root start rather than at the first emission. `call_provider`
6487    /// support is already mandatory at configuration time (every operation reaches a provider
6488    /// call), so the reachable half of the rule is a workflow root on a host that cannot spawn.
6489    #[test]
6490    fn a_workflow_root_is_refused_when_the_host_cannot_spawn_tasks() {
6491        let mut runtime = Runtime::new();
6492        runtime.submit(&configure_supporting([EffectKindTag::CallProvider]));
6493        let fault = runtime.reject(&workflow_start(
6494            "in-start",
6495            1_700_000_001_000,
6496            two_node_spec(),
6497        ));
6498        assert_eq!(fault.code, KernelFaultCode::UnsupportedEffect);
6499        assert_eq!(runtime.driver.root_kind(), None);
6500        assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Configured);
6501        assert_eq!(runtime.journal.len(), 1, "only the genesis record exists");
6502    }
6503
6504    // -----------------------------------------------------------------------------------------
6505    // fixture: workflow-root-entry-is-atomic
6506    // -----------------------------------------------------------------------------------------
6507
6508    #[test]
6509    fn a_workflow_root_start_spawns_tasks_and_never_calls_the_provider() {
6510        let mut runtime = Runtime::new();
6511        runtime.submit(&configure());
6512        let started = runtime.submit(&workflow_start(
6513            "in-start",
6514            1_700_000_001_000,
6515            two_node_spec(),
6516        ));
6517
6518        assert_eq!(started.step.root_kind, Some(RootKind::Workflow));
6519        let effect = sole_effect(&started);
6520        assert_eq!(
6521            effect.tag(),
6522            EffectKindTag::SpawnTasks,
6523            "a workflow root's first effect is a task spawn (§10.1)"
6524        );
6525        assert_eq!(
6526            runtime.pending_effect_kinds(),
6527            vec![EffectKindTag::SpawnTasks],
6528            "no provider effect is published, so none can be overwritten by a workflow load"
6529        );
6530
6531        // the DAG's identity is kernel-minted and its first ready node is the only launch
6532        let EffectKind::SpawnTasks(spawn) = &effect.effect else {
6533            panic!("expected a task spawn");
6534        };
6535        assert_eq!(spawn.tasks.len(), 1, "only `collect` is ready");
6536        assert_eq!(spawn.tasks[0].node_id.as_str(), "collect");
6537        assert_eq!(spawn.tasks[0].task_id.as_str(), "wf-node0");
6538        assert!(
6539            !spawn.tasks[0].launch_token.as_str().is_empty(),
6540            "the launch token exists as a committed fact before the host launches anything"
6541        );
6542
6543        // focus is the root controller, with no parent to restore
6544        assert_eq!(
6545            runtime.driver.focus(),
6546            Some(&ExecutionFocus::workflow_controller(
6547                runtime.driver.workflow_id().unwrap().clone(),
6548                None
6549            ))
6550        );
6551        assert!(
6552            !runtime.driver.focus().unwrap().is_nested_in_agent(),
6553            "the root workflow is not nested in an agent"
6554        );
6555    }
6556
6557    #[test]
6558    fn a_workflow_launch_preserves_logical_context_inheritance() {
6559        let mut spec = two_node_spec();
6560        spec.nodes[0].run_spec = Some(LogicalAgentSpec {
6561            context_inheritance: Some(WireContextInheritance::SystemOnly),
6562            ..LogicalAgentSpec::new("collect the sources")
6563        });
6564
6565        let mut runtime = Runtime::new();
6566        runtime.submit(&configure());
6567        let started = runtime.submit(&workflow_start("in-start", 1_700_000_001_000, spec));
6568
6569        let EffectKind::SpawnTasks(spawn) = &sole_effect(&started).effect else {
6570            panic!("expected a task spawn");
6571        };
6572        assert_eq!(
6573            spawn.tasks[0].spec.context_inheritance,
6574            Some(WireContextInheritance::SystemOnly),
6575        );
6576    }
6577
6578    #[test]
6579    fn a_workflow_root_with_no_nodes_has_no_first_effect_and_is_refused() {
6580        let mut runtime = Runtime::new();
6581        runtime.submit(&configure());
6582        let fault = runtime.reject(&workflow_start(
6583            "in-start",
6584            1_700_000_001_000,
6585            WireSpec::default(),
6586        ));
6587        assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
6588        assert_eq!(runtime.driver.root_kind(), None);
6589    }
6590
6591    #[test]
6592    fn a_workflow_spec_whose_dependency_names_no_declared_node_is_refused() {
6593        let mut runtime = Runtime::new();
6594        runtime.submit(&configure());
6595        let spec = WireSpec {
6596            name: "broken".to_string(),
6597            nodes: vec![wire_node("write", "write", &["collect"])],
6598        };
6599        let fault = runtime.reject(&workflow_start("in-start", 1_700_000_001_000, spec));
6600        assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
6601        assert_eq!(runtime.driver.root_kind(), None);
6602        assert_eq!(runtime.journal.len(), 1, "only the genesis record exists");
6603    }
6604
6605    // -----------------------------------------------------------------------------------------
6606    // fixture: workflow-root-completion-commits-terminal
6607    // -----------------------------------------------------------------------------------------
6608
6609    /// The full §10.1 path: configure → root start → spawn → ack → completions → workflow terminal,
6610    /// with no `LoadWorkflow`, no placeholder agent run and no host-privileged `CompleteRun`.
6611    fn drive_workflow_root_to_terminal() -> Runtime {
6612        let mut runtime = Runtime::new();
6613        runtime.submit(&configure());
6614        let started = runtime.submit(&workflow_start(
6615            "in-start",
6616            1_700_000_001_000,
6617            two_node_spec(),
6618        ));
6619
6620        runtime.submit(&spawned(
6621            "in-ack-1",
6622            1_700_000_002_000,
6623            &effect_id(started.step_seq),
6624            &["wf-node0"],
6625        ));
6626        let advanced = runtime.submit(&child_done(
6627            "in-done-1",
6628            1_700_000_003_000,
6629            "wf-node0",
6630            "sources collected",
6631        ));
6632        let second = sole_effect(&advanced);
6633        assert_eq!(second.tag(), EffectKindTag::SpawnTasks);
6634        let EffectKind::SpawnTasks(spawn) = &second.effect else {
6635            panic!("expected a task spawn");
6636        };
6637        assert_eq!(spawn.tasks[0].node_id.as_str(), "write");
6638
6639        runtime.submit(&spawned(
6640            "in-ack-2",
6641            1_700_000_004_000,
6642            &effect_id(advanced.step_seq),
6643            &["wf-node1"],
6644        ));
6645        runtime.submit(&child_done(
6646            "in-done-2",
6647            1_700_000_005_000,
6648            "wf-node1",
6649            "brief written",
6650        ));
6651        runtime
6652    }
6653
6654    #[test]
6655    fn a_root_workflow_completion_commits_the_workflow_terminal_itself() {
6656        let runtime = drive_workflow_root_to_terminal();
6657
6658        let terminal = runtime.tx.terminal().expect("the run terminated");
6659        let KernelTerminal::Workflow(workflow) = terminal else {
6660            panic!("a workflow root terminates with a workflow terminal, got {terminal:?}");
6661        };
6662        assert_eq!(workflow.outcome.status, WorkflowStatus::Completed);
6663        assert_eq!(
6664            workflow
6665                .outcome
6666                .completed_nodes
6667                .iter()
6668                .map(|node| node.as_str())
6669                .collect::<Vec<_>>(),
6670            vec!["collect", "write"],
6671            "the terminal names the wire node ids the host declared"
6672        );
6673        assert!(workflow.outcome.failed_nodes.is_empty());
6674        assert_eq!(runtime.tx.lifecycle(), OperationLifecycle::Completed);
6675        assert_eq!(
6676            runtime.pending_effect_kinds(),
6677            Vec::<EffectKindTag>::new(),
6678            "a root workflow issues no provider call after it completes"
6679        );
6680    }
6681
6682    #[test]
6683    fn a_completion_naming_an_attempt_the_kernel_never_minted_is_refused() {
6684        let mut runtime = Runtime::new();
6685        runtime.submit(&configure());
6686        let started = runtime.submit(&workflow_start(
6687            "in-start",
6688            1_700_000_001_000,
6689            two_node_spec(),
6690        ));
6691        runtime.submit(&spawned(
6692            "in-ack-1",
6693            1_700_000_002_000,
6694            &effect_id(started.step_seq),
6695            &["wf-node0"],
6696        ));
6697
6698        let forged = envelope(
6699            "in-forged",
6700            1_700_000_003_000,
6701            KernelInput::DeliverExternalEvent(DeliverExternalEvent {
6702                event: ExternalEvent::ChildCompleted(ChildCompleted {
6703                    task_id: TaskId::new("wf-node0").unwrap(),
6704                    attempt_id: WireAttemptId::new("wf-node0:attempt:7").unwrap(),
6705                    result: ChildResult::default(),
6706                    parent_requests: Vec::new(),
6707                }),
6708            }),
6709        );
6710        let before = runtime.pending_effect_kinds();
6711        let fault = runtime.reject(&forged);
6712        assert_eq!(
6713            fault.code,
6714            KernelFaultCode::InvalidAuthority,
6715            "a host does not mint or rewrite child identity (§10.4)"
6716        );
6717        assert_eq!(runtime.pending_effect_kinds(), before);
6718        assert!(runtime.tx.terminal().is_none());
6719    }
6720
6721    #[test]
6722    fn a_terminated_root_workflow_refuses_every_later_state_changing_input() {
6723        let mut runtime = drive_workflow_root_to_terminal();
6724        let fault = runtime.reject(&child_done(
6725            "in-done-late",
6726            1_700_000_006_000,
6727            "wf-node1",
6728            "again",
6729        ));
6730        assert_eq!(fault.code, KernelFaultCode::InvalidLifecycle);
6731    }
6732
6733    // -----------------------------------------------------------------------------------------
6734    // fixture: workflow-no-stack-and-root-kind-immutable
6735    // -----------------------------------------------------------------------------------------
6736
6737    /// Free the pending provider effect so the nested-workflow arc has a clean `call_provider`
6738    /// slot. The real provider reduction has its own tests below; this planner only clears the
6739    /// registration so the focus assertions read against a quiet step.
6740    fn provider_settled(
6741        driver: &mut CanonicalOperationDriver,
6742        _context: &PlanContext<'_>,
6743    ) -> Result<PlannedStep, KernelFault> {
6744        Ok(PlannedStep::quiet(
6745            driver.root_kind(),
6746            driver.focus().cloned(),
6747        ))
6748    }
6749
6750    fn agent_root_with_nested_workflow() -> (Runtime, TaskId) {
6751        let mut runtime = Runtime::new();
6752        runtime.submit(&configure());
6753        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6754
6755        // settle the provider call the agent root issued
6756        let settle = envelope(
6757            "in-provider",
6758            1_700_000_002_000,
6759            KernelInput::ResolveEffect(ResolveEffect {
6760                effect_id: effect_id(started.step_seq),
6761                outcome: EffectOutcome::Succeeded(EffectSucceeded {
6762                    result: EffectSuccess::Provider(super::super::effect::ProviderSuccess {
6763                        outcome: super::super::effect::ProviderOutcome::ContextOverflow(
6764                            super::super::effect::ProviderContextOverflow::default(),
6765                        ),
6766                    }),
6767                }),
6768            }),
6769        );
6770        runtime.submit_planned(&settle, provider_settled);
6771
6772        let spec = two_node_spec();
6773        let carrier = syscall_carrier("in-submit-workflow", 1_700_000_003_000);
6774        let entered = runtime.submit_planned(&carrier, |driver, context| {
6775            driver.begin_nested_workflow(context, &spec)
6776        });
6777        runtime
6778            .driver
6779            .note_committed(entered.step_seq)
6780            .expect("the nested start folds like any other planned step");
6781        assert_eq!(sole_effect(&entered).tag(), EffectKindTag::SpawnTasks);
6782        (runtime, root_task_id())
6783    }
6784
6785    /// The historical bootstrap replaced a live provider registration with a workflow spawn, so a
6786    /// pending provider call could disappear without ever being resolved. Here the two effects are
6787    /// separate registrations and neither evicts the other.
6788    #[test]
6789    fn starting_a_workflow_never_overwrites_a_live_provider_effect() {
6790        let mut runtime = Runtime::new();
6791        runtime.submit(&configure());
6792        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6793        let provider_effect = sole_effect(&started).effect_id.clone();
6794
6795        let spec = two_node_spec();
6796        let carrier = syscall_carrier("in-submit-workflow", 1_700_000_002_000);
6797        let entered = runtime.submit_planned(&carrier, |driver, context| {
6798            driver.begin_nested_workflow(context, &spec)
6799        });
6800        runtime.driver.note_committed(entered.step_seq).unwrap();
6801
6802        let mut pending = runtime.pending_effect_kinds();
6803        pending.sort();
6804        assert_eq!(
6805            pending,
6806            vec![EffectKindTag::CallProvider, EffectKindTag::SpawnTasks],
6807            "the provider call is still outstanding; the spawn is a second registration"
6808        );
6809        assert!(
6810            runtime
6811                .tx
6812                .pending_effects()
6813                .any(|effect| effect.effect_id == provider_effect),
6814            "the provider effect kept its identity, so the host can still resolve it"
6815        );
6816    }
6817
6818    #[test]
6819    fn an_agent_authored_workflow_moves_the_focus_but_never_the_root_kind() {
6820        let (runtime, parent) = agent_root_with_nested_workflow();
6821
6822        assert_eq!(
6823            runtime.driver.root_kind(),
6824            Some(RootKind::Agent),
6825            "a syscall never re-roots an operation (§10.2)"
6826        );
6827        let focus = runtime.driver.focus().expect("a focus exists");
6828        assert!(
6829            focus.is_nested_in_agent(),
6830            "the focus records the parent agent task it must restore"
6831        );
6832        assert_eq!(
6833            focus,
6834            &ExecutionFocus::workflow_controller(
6835                runtime.driver.workflow_id().unwrap().clone(),
6836                Some(parent),
6837            )
6838        );
6839    }
6840
6841    #[test]
6842    fn a_second_workflow_inside_a_workflow_focus_is_an_authority_refusal_with_no_spawn() {
6843        let (mut runtime, _) = agent_root_with_nested_workflow();
6844
6845        let before = (
6846            runtime.tx.head(),
6847            runtime.pending_effect_kinds(),
6848            runtime.driver.root_kind(),
6849            runtime.driver.focus().cloned(),
6850        );
6851
6852        let spec = two_node_spec();
6853        let carrier = syscall_carrier("in-submit-again", 1_700_000_004_000);
6854        let preparation = {
6855            let Runtime { tx, driver, .. } = &mut runtime;
6856            tx.prepare(&carrier, |context| {
6857                driver.begin_nested_workflow(context, &spec)
6858            })
6859        };
6860        let fault = preparation.fault().expect("expected a refusal").clone();
6861        assert_eq!(
6862            fault.code,
6863            KernelFaultCode::InvalidAuthority,
6864            "workflows do not stack — depth is at most 1 (§7.4)"
6865        );
6866
6867        let after = (
6868            runtime.tx.head(),
6869            runtime.pending_effect_kinds(),
6870            runtime.driver.root_kind(),
6871            runtime.driver.focus().cloned(),
6872        );
6873        assert_eq!(before, after, "the refusal produced no derived action");
6874    }
6875
6876    #[test]
6877    fn a_nested_workflow_completion_restores_the_parent_agent_and_resumes_its_turn() {
6878        let (mut runtime, parent) = agent_root_with_nested_workflow();
6879        let spawn_step = runtime.journal.last().unwrap().step_seq();
6880
6881        runtime.submit(&spawned(
6882            "in-ack-1",
6883            1_700_000_004_000,
6884            &effect_id(spawn_step),
6885            &["wf-node0"],
6886        ));
6887        let advanced = runtime.submit(&child_done(
6888            "in-done-1",
6889            1_700_000_005_000,
6890            "wf-node0",
6891            "sources collected",
6892        ));
6893        runtime.submit(&spawned(
6894            "in-ack-2",
6895            1_700_000_006_000,
6896            &effect_id(advanced.step_seq),
6897            &["wf-node1"],
6898        ));
6899        let finished = runtime.submit(&child_done(
6900            "in-done-2",
6901            1_700_000_007_000,
6902            "wf-node1",
6903            "brief written",
6904        ));
6905
6906        assert!(
6907            runtime.tx.terminal().is_none(),
6908            "a nested workflow's completion is not the operation's terminal (§6.1.7)"
6909        );
6910        assert_eq!(
6911            runtime.driver.focus(),
6912            Some(&ExecutionFocus::agent_turn(parent)),
6913            "the focus returns to the agent turn it left"
6914        );
6915        assert_eq!(
6916            sole_effect(&finished).tag(),
6917            EffectKindTag::CallProvider,
6918            "the parent agent's turn resumes with a provider call"
6919        );
6920        assert_eq!(runtime.driver.root_kind(), Some(RootKind::Agent));
6921    }
6922
6923    #[test]
6924    fn a_workflow_root_admits_no_nested_workflow_at_all() {
6925        let mut runtime = Runtime::new();
6926        runtime.submit(&configure());
6927        runtime.submit(&workflow_start(
6928            "in-start",
6929            1_700_000_001_000,
6930            two_node_spec(),
6931        ));
6932
6933        let spec = two_node_spec();
6934        let carrier = syscall_carrier("in-submit", 1_700_000_002_000);
6935        let preparation = {
6936            let Runtime { tx, driver, .. } = &mut runtime;
6937            tx.prepare(&carrier, |context| {
6938                driver.begin_nested_workflow(context, &spec)
6939            })
6940        };
6941        assert_eq!(
6942            preparation.fault().unwrap().code,
6943            KernelFaultCode::InvalidAuthority
6944        );
6945    }
6946
6947    #[test]
6948    fn a_workflow_roots_focus_never_moves_while_its_dag_runs() {
6949        let mut runtime = Runtime::new();
6950        runtime.submit(&configure());
6951        let started = runtime.submit(&workflow_start(
6952            "in-start",
6953            1_700_000_001_000,
6954            two_node_spec(),
6955        ));
6956        let focus = runtime.driver.focus().cloned();
6957
6958        runtime.submit(&spawned(
6959            "in-ack-1",
6960            1_700_000_002_000,
6961            &effect_id(started.step_seq),
6962            &["wf-node0"],
6963        ));
6964        assert_eq!(
6965            runtime.driver.focus().cloned(),
6966            focus,
6967            "an ack moves nothing"
6968        );
6969
6970        let advanced = runtime.submit(&child_done(
6971            "in-done-1",
6972            1_700_000_003_000,
6973            "wf-node0",
6974            "done",
6975        ));
6976        assert_eq!(
6977            runtime.driver.focus().cloned(),
6978            focus,
6979            "a DAG node's agent execution is a child attempt, not a focus change"
6980        );
6981        assert_eq!(sole_effect(&advanced).tag(), EffectKindTag::SpawnTasks);
6982    }
6983
6984    // -----------------------------------------------------------------------------------------
6985    // fixture: agent-syscall-caller-is-derived
6986    // -----------------------------------------------------------------------------------------
6987
6988    /// Configure → agent root → provider result carrying one meta-tool call. Returns the runtime
6989    /// and the provider effect the start published.
6990    fn agent_awaiting_provider() -> (Runtime, EffectId) {
6991        let mut runtime = Runtime::new();
6992        runtime.submit(&syscall_config());
6993        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
6994        let effect = sole_effect(&started);
6995        assert_eq!(effect.tag(), EffectKindTag::CallProvider);
6996        let EffectKind::CallProvider(call) = &effect.effect else {
6997            panic!("expected a provider call");
6998        };
6999        assert!(
7000            call.tools.iter().any(|tool| tool.name == "start_workflow"),
7001            "the turn must actually expose the meta-tool the model is about to call"
7002        );
7003        (runtime, effect.effect_id.clone())
7004    }
7005
7006    #[test]
7007    fn a_provider_tool_call_derives_its_caller_and_enters_p1() {
7008        let (mut runtime, provider) = agent_awaiting_provider();
7009
7010        let spec = serde_json::to_value(two_node_spec()).unwrap();
7011        let entered = runtime.submit(&provider_result(
7012            "in-authored",
7013            1_700_000_002_000,
7014            &provider,
7015            vec![tool_call("call-1", "start_workflow", spec)],
7016        ));
7017
7018        // the request became a real transition: a DAG was bootstrapped and its first node launched
7019        let effect = sole_effect(&entered);
7020        assert_eq!(effect.tag(), EffectKindTag::SpawnTasks);
7021        assert_eq!(
7022            runtime.driver.root_kind(),
7023            Some(RootKind::Agent),
7024            "a syscall never re-roots an operation (§10.2)"
7025        );
7026        assert!(
7027            runtime.driver.focus().unwrap().is_nested_in_agent(),
7028            "the focus moved to the workflow controller, under the agent turn it suspended"
7029        );
7030
7031        // and the host declared nobody: the accepted input carries no caller field at all
7032        let stored = entered.record.normalized_input().unwrap();
7033        let json = serde_json::to_value(&stored).unwrap();
7034        let text = json.to_string();
7035        for forbidden in ["submitter_agent_id", "actor_id", "parent_session_id"] {
7036            assert!(
7037                !text.contains(forbidden),
7038                "the canonical input still carries {forbidden}"
7039            );
7040        }
7041    }
7042
7043    #[test]
7044    fn a_tool_the_turn_never_exposed_has_no_caller_to_derive() {
7045        let (mut runtime, provider) = agent_awaiting_provider();
7046
7047        // An unexposed *task* tool is not an authority claim — it is a call the model made up, and
7048        // the kernel's fail-closed dispatch gate answers it the way the model is trained to read:
7049        // a visible error result, no host dispatch, and the turn continues.
7050        let committed = runtime.submit(&provider_result(
7051            "in-forged",
7052            1_700_000_002_000,
7053            &provider,
7054            vec![tool_call(
7055                "call-1",
7056                "escalate_privileges",
7057                json!({"nodes": []}),
7058            )],
7059        ));
7060        assert_eq!(
7061            committed
7062                .published_effects()
7063                .iter()
7064                .map(|effect| effect.tag())
7065                .collect::<Vec<_>>(),
7066            vec![EffectKindTag::CallProvider],
7067            "a phantom tool is never dispatched; the turn is answered and re-asked"
7068        );
7069
7070        // The authority half: a *syscall* name the turn did not advertise. Narrow the recorded
7071        // surface to nothing and ask again — the request is well-formed and still has no caller.
7072        let next = effect_id(committed.step_seq);
7073        runtime
7074            .driver
7075            .provider_calls
7076            .get_mut(&next)
7077            .expect("the driver recorded the turn it published")
7078            .exposed_tools
7079            .clear();
7080        let before = (
7081            runtime.tx.head(),
7082            runtime.pending_effect_kinds(),
7083            runtime.driver.focus().cloned(),
7084        );
7085        let fault = runtime
7086            .driver
7087            .derive_provider_syscalls(&next, &[tool_call("call-2", "start_workflow", json!({}))])
7088            .expect_err("a tool the turn never exposed has no causation");
7089        assert_eq!(fault.code, KernelFaultCode::InvalidAuthority);
7090        assert!(fault.message.contains("exposed no tool"));
7091
7092        let after = (
7093            runtime.tx.head(),
7094            runtime.pending_effect_kinds(),
7095            runtime.driver.focus().cloned(),
7096        );
7097        assert_eq!(before, after, "a forged causation moves nothing");
7098    }
7099
7100    #[test]
7101    fn a_provider_effect_this_kernel_never_published_has_no_causation() {
7102        let (runtime, _) = agent_awaiting_provider();
7103        let unknown = EffectId::new("op-driver-1:step:99:effect:0").unwrap();
7104        let fault = runtime
7105            .driver
7106            .derive_provider_syscalls(
7107                &unknown,
7108                &[tool_call("call-1", "start_workflow", json!({}))],
7109            )
7110            .expect_err("an unpublished effect names no turn");
7111        assert_eq!(fault.code, KernelFaultCode::InvalidAuthority);
7112        assert!(fault.message.contains("not a provider call"));
7113    }
7114
7115    #[test]
7116    fn a_call_id_that_already_produced_a_syscall_cannot_produce_a_second() {
7117        let (mut runtime, provider) = agent_awaiting_provider();
7118
7119        // two calls sharing one id inside a single result: the second has no causation left
7120        let fault = runtime.reject(&provider_result(
7121            "in-double",
7122            1_700_000_002_000,
7123            &provider,
7124            vec![
7125                tool_call("call-1", "skill", json!({"name": "debug"})),
7126                tool_call("call-1", "skill", json!({"name": "debug"})),
7127            ],
7128        ));
7129        assert_eq!(fault.code, KernelFaultCode::InvalidAuthority);
7130        assert!(fault.message.contains("consumed once"));
7131
7132        // and a causation that *was* spent stays spent for the operation's lifetime
7133        runtime.submit(&provider_result(
7134            "in-skill",
7135            1_700_000_002_000,
7136            &provider,
7137            vec![tool_call("call-1", "skill", json!({"name": "debug"}))],
7138        ));
7139        assert!(
7140            runtime.driver.consumed_calls.contains("call-1"),
7141            "the spent causation is remembered, so a redelivery under a fresh input id buys nothing"
7142        );
7143        assert!(
7144            !runtime.driver.provider_calls.contains_key(&provider),
7145            "a resolved provider call is no longer a surface anything can be attributed to"
7146        );
7147    }
7148
7149    #[test]
7150    fn a_skill_the_operation_never_declared_cannot_be_activated() {
7151        let (mut runtime, provider) = agent_awaiting_provider();
7152        runtime.submit(&provider_result(
7153            "in-skill",
7154            1_700_000_002_000,
7155            &provider,
7156            vec![tool_call(
7157                "call-1",
7158                "skill",
7159                json!({"name": "not-declared"}),
7160            )],
7161        ));
7162        let rejected = rejections(&runtime);
7163        assert_eq!(rejected.len(), 1, "the refusal is an audit fact");
7164        assert_eq!(rejected[0].0, "skill");
7165        assert_eq!(
7166            rejected[0].1.as_deref(),
7167            Some(ROOT_TASK_ID),
7168            "the audit fact names the caller the kernel derived, not one a host supplied"
7169        );
7170        assert!(rejected[0].2.contains("declares no skill"));
7171        assert_eq!(
7172            runtime.pending_effect_kinds(),
7173            vec![EffectKindTag::CallProvider],
7174            "a rejected capability mutation publishes no effect of its own; the turn still \
7175             continues (the §5k syscall-only continuation)"
7176        );
7177    }
7178
7179    #[test]
7180    fn an_append_with_no_graph_to_append_to_is_an_audit_fact_not_a_derived_action() {
7181        let (mut runtime, provider) = agent_awaiting_provider();
7182        runtime.submit(&provider_result(
7183            "in-append",
7184            1_700_000_002_000,
7185            &provider,
7186            vec![tool_call(
7187                "call-1",
7188                "submit_workflow_nodes",
7189                node_args(&[wire_node("stray", "stray", &[])]),
7190            )],
7191        ));
7192
7193        let rejected = rejections(&runtime);
7194        assert_eq!(rejected.len(), 1);
7195        assert_eq!(rejected[0].0, "submit_workflow_nodes");
7196        assert_eq!(
7197            rejected[0].1.as_deref(),
7198            Some(ROOT_TASK_ID),
7199            "a provider-tool causation names the task whose turn issued the call"
7200        );
7201        assert!(rejected[0].2.contains("no workflow is in flight"));
7202        assert_eq!(
7203            runtime.pending_effect_kinds(),
7204            vec![EffectKindTag::CallProvider],
7205            "a refused append spawns nothing; the turn continues with the next provider call"
7206        );
7207    }
7208
7209    // -----------------------------------------------------------------------------------------
7210    // fixture: workflow-dynamic-append-preserves-authority (+ §7.7 GAP-4)
7211    // -----------------------------------------------------------------------------------------
7212
7213    /// Workflow root, first node launched and acknowledged.
7214    fn workflow_root_awaiting_first_child() -> Runtime {
7215        let mut runtime = Runtime::new();
7216        runtime.submit(&syscall_config());
7217        let started = runtime.submit(&workflow_start(
7218            "in-start",
7219            1_700_000_001_000,
7220            two_node_spec(),
7221        ));
7222        runtime.submit(&spawned(
7223            "in-ack-1",
7224            1_700_000_002_000,
7225            &effect_id(started.step_seq),
7226            &["wf-node0"],
7227        ));
7228        runtime
7229    }
7230
7231    #[test]
7232    fn parent_requests_are_adjudicated_independently_and_never_undo_the_completion() {
7233        let mut runtime = workflow_root_awaiting_first_child();
7234
7235        let good = SyscallRequest::AppendWorkflowNodes(
7236            super::super::syscall::AppendWorkflowNodesRequest {
7237                nodes: vec![wire_node("verify", "verify the sources", &[])],
7238            },
7239        );
7240        // batch-relative dependency that names nothing in its own batch — refused on its own merits
7241        let bad = SyscallRequest::AppendWorkflowNodes(
7242            super::super::syscall::AppendWorkflowNodesRequest {
7243                nodes: vec![wire_node("orphan", "orphan", &["nowhere"])],
7244            },
7245        );
7246        let another_good = SyscallRequest::UpdateTask(super::super::syscall::UpdateTaskRequest {
7247            update: WireTaskUpdate {
7248                progress: Some("sources collected".to_string()),
7249                ..WireTaskUpdate::default()
7250            },
7251        });
7252
7253        let advanced = runtime.submit(&child_done_with(
7254            "in-done-1",
7255            1_700_000_003_000,
7256            "wf-node0",
7257            "wf-node0:attempt:1",
7258            vec![good, bad, another_good],
7259        ));
7260
7261        // the completion committed unconditionally and drained into the next ready batch
7262        assert_eq!(
7263            runtime.tx.lifecycle(),
7264            OperationLifecycle::Running,
7265            "a denied parent request does not undo the child's execution (GAP-4)"
7266        );
7267        let effect = sole_effect(&advanced);
7268        assert_eq!(effect.tag(), EffectKindTag::SpawnTasks);
7269        let EffectKind::SpawnTasks(spawn) = &effect.effect else {
7270            panic!("expected a task spawn");
7271        };
7272        let launched: Vec<&str> = spawn
7273            .tasks
7274            .iter()
7275            .map(|task| task.node_id.as_str())
7276            .collect();
7277        assert!(
7278            launched.contains(&"write") && launched.contains(&"verify"),
7279            "the admitted append reached the next ready batch alongside the original DAG, got \
7280             {launched:?}"
7281        );
7282        assert!(
7283            !launched.contains(&"orphan"),
7284            "the refused append produced no derived action"
7285        );
7286
7287        // exactly one structured rejection, and the third request was unaffected by the second
7288        let rejected = rejections(&runtime);
7289        assert_eq!(rejected.len(), 1, "each request is adjudicated on its own");
7290        assert_eq!(rejected[0].0, "submit_workflow_nodes");
7291        assert_eq!(
7292            rejected[0].1.as_deref(),
7293            Some("wf-node0"),
7294            "the refusal names the child attempt it was derived from"
7295        );
7296        assert!(
7297            runtime
7298                .driver
7299                .engine()
7300                .unwrap()
7301                .ctx
7302                .partitions
7303                .task_state
7304                .progress
7305                .contains("sources collected"),
7306            "a sibling's refusal does not stop the requests after it"
7307        );
7308    }
7309
7310    #[test]
7311    fn an_append_beyond_the_workflow_node_quota_is_denied_without_touching_the_graph() {
7312        let mut runtime = workflow_root_awaiting_first_child();
7313        let nodes_before = runtime.driver.engine().unwrap().workflow_node_count();
7314
7315        // the quota allows 3 nodes; the DAG already holds 2
7316        let oversized = SyscallRequest::AppendWorkflowNodes(
7317            super::super::syscall::AppendWorkflowNodesRequest {
7318                nodes: vec![
7319                    wire_node("a", "a", &[]),
7320                    wire_node("b", "b", &[]),
7321                    wire_node("c", "c", &[]),
7322                ],
7323            },
7324        );
7325        runtime.submit(&child_done_with(
7326            "in-done-1",
7327            1_700_000_003_000,
7328            "wf-node0",
7329            "wf-node0:attempt:1",
7330            vec![oversized],
7331        ));
7332
7333        let rejected = rejections(&runtime);
7334        assert!(
7335            rejected.iter().any(|(operation, subject, reason)| operation
7336                == "submit_workflow_nodes"
7337                && subject.as_deref() == Some("wf-node0")
7338                && reason.contains("would grow workflow")),
7339            "the resource gate refused the growth, got {rejected:?}"
7340        );
7341        assert_eq!(
7342            runtime.driver.engine().unwrap().workflow_node_count(),
7343            nodes_before,
7344            "a denied append leaves the graph exactly as it was"
7345        );
7346    }
7347
7348    #[test]
7349    fn a_quarantined_task_cannot_widen_its_authority_through_a_syscall() {
7350        let mut runtime = workflow_root_awaiting_first_child();
7351        assert!(
7352            runtime
7353                .driver
7354                .engine_mut()
7355                .unwrap()
7356                .quarantine_task_for_test("wf-node0"),
7357            "the node must exist to be quarantined"
7358        );
7359        let nodes_before = runtime.driver.engine().unwrap().workflow_node_count();
7360
7361        let append = SyscallRequest::AppendWorkflowNodes(
7362            super::super::syscall::AppendWorkflowNodesRequest {
7363                nodes: vec![wire_node("escalate", "escalate", &[])],
7364            },
7365        );
7366        let activate = SyscallRequest::ActivateSkill(super::super::syscall::ActivateSkillRequest {
7367            name: "debug".to_string(),
7368            lease_turns: None,
7369        });
7370        let remember =
7371            SyscallRequest::RequestMemoryWrite(super::super::syscall::RequestMemoryWriteRequest {
7372                proposal: super::super::syscall::MemoryWriteProposal {
7373                    name: "escalation".to_string(),
7374                    kind: WireMemoryKind::Project,
7375                    content: "trust me".to_string(),
7376                    description: String::new(),
7377                    evidence_refs: Vec::new(),
7378                },
7379            });
7380
7381        runtime.submit(&child_done_with(
7382            "in-done-1",
7383            1_700_000_003_000,
7384            "wf-node0",
7385            "wf-node0:attempt:1",
7386            vec![append, activate, remember],
7387        ));
7388
7389        let quarantine_denials = rejections(&runtime);
7390        let families: Vec<&str> = quarantine_denials
7391            .iter()
7392            .filter(|(_, _, reason)| reason.starts_with("quarantine:"))
7393            .map(|(operation, _, _)| operation.as_str())
7394            .collect();
7395        assert_eq!(
7396            families,
7397            vec!["workflow", "capability", "memory"],
7398            "every privileged family is refused for a quarantined caller"
7399        );
7400        assert_eq!(
7401            runtime.driver.engine().unwrap().workflow_node_count(),
7402            nodes_before,
7403            "no node was appended"
7404        );
7405        assert!(
7406            !runtime
7407                .driver
7408                .engine()
7409                .unwrap()
7410                .ctx
7411                .active_skills
7412                .contains_key("debug"),
7413            "no skill was activated"
7414        );
7415        assert_eq!(
7416            runtime.pending_effect_kinds(),
7417            vec![EffectKindTag::SpawnTasks],
7418            "no memory effect was published; only the DAG's own next batch"
7419        );
7420    }
7421
7422    #[test]
7423    fn a_child_request_cannot_forge_a_second_root_workflow() {
7424        let mut runtime = workflow_root_awaiting_first_child();
7425        let workflow_before = runtime.driver.workflow_id().cloned();
7426        let focus_before = runtime.driver.focus().cloned();
7427
7428        let authored =
7429            SyscallRequest::SubmitWorkflow(super::super::syscall::SubmitWorkflowRequest {
7430                spec: WireSpec {
7431                    name: "usurper".to_string(),
7432                    nodes: vec![wire_node("usurp", "take over", &[])],
7433                },
7434            });
7435        runtime.submit(&child_done_with(
7436            "in-done-1",
7437            1_700_000_003_000,
7438            "wf-node0",
7439            "wf-node0:attempt:1",
7440            vec![authored],
7441        ));
7442
7443        assert_eq!(
7444            runtime.driver.root_kind(),
7445            Some(RootKind::Workflow),
7446            "the root kind is immutable (§6.1.5)"
7447        );
7448        assert_eq!(
7449            runtime.driver.workflow_id().cloned(),
7450            workflow_before,
7451            "an authored spec flattens into the running DAG; it never becomes a second root"
7452        );
7453        assert_eq!(
7454            runtime.driver.focus().cloned(),
7455            focus_before,
7456            "a workflow root's focus never moves (§7.4)"
7457        );
7458        assert!(
7459            rejections(&runtime).is_empty(),
7460            "flattening is the admitted path, not a refusal"
7461        );
7462    }
7463
7464    // -----------------------------------------------------------------------------------------
7465    // fixture: workflow-child-identity-is-kernel-issued (TCB launch arc)
7466    // -----------------------------------------------------------------------------------------
7467
7468    #[test]
7469    fn a_child_moves_pending_launch_then_starting_then_running_on_the_acknowledgement() {
7470        let mut runtime = Runtime::new();
7471        runtime.submit(&syscall_config());
7472
7473        // the spawn effect is planned but not yet committed: the identity exists, the launch does not
7474        let preparation = runtime.prepare(&workflow_start(
7475            "in-start",
7476            1_700_000_001_000,
7477            two_node_spec(),
7478        ));
7479        assert_eq!(
7480            runtime.driver.engine().unwrap().task_lifecycle("wf-node0"),
7481            Some(TaskLifecycle::Starting),
7482            "the launch effect is planned, so the task left PendingLaunch and awaits the host"
7483        );
7484        let started = runtime.append_and_commit(preparation);
7485        runtime.driver.note_committed(started.step_seq).unwrap();
7486        assert_eq!(
7487            runtime.driver.engine().unwrap().task_lifecycle("wf-node0"),
7488            Some(TaskLifecycle::Starting),
7489            "a published launch is not a running task"
7490        );
7491
7492        runtime.submit(&spawned(
7493            "in-ack-1",
7494            1_700_000_002_000,
7495            &effect_id(started.step_seq),
7496            &["wf-node0"],
7497        ));
7498        assert_eq!(
7499            runtime.driver.engine().unwrap().task_lifecycle("wf-node0"),
7500            Some(TaskLifecycle::Running),
7501            "only the acknowledgement makes a task Running (§10.4, §15.3)"
7502        );
7503    }
7504
7505    /// The launch arc at its own layer, where all three states are separately observable. Through
7506    /// the driver `PendingLaunch` and `Starting` both happen inside one `plan` call, because minting
7507    /// identity and building the launch effect are two moments of the same transition.
7508    #[test]
7509    fn an_ack_gated_spawn_mints_identity_in_pending_launch_before_the_effect_is_published() {
7510        let mut engine = LoopStateMachine::new(SchedulerBudget::default());
7511        let action = engine.load_workflow(
7512            build_core_spec(&WireSpec {
7513                name: String::new(),
7514                nodes: vec![wire_node("only", "only", &[])],
7515            })
7516            .unwrap(),
7517        );
7518        assert!(matches!(action, LoopAction::SpawnWorkflow { .. }));
7519        assert_eq!(
7520            engine.task_lifecycle("wf-node0"),
7521            Some(TaskLifecycle::PendingLaunch),
7522            "identity is minted, the launch is not published yet"
7523        );
7524
7525        engine.mark_tasks_starting(&["wf-node0".to_string()]);
7526        assert_eq!(
7527            engine.task_lifecycle("wf-node0"),
7528            Some(TaskLifecycle::Starting),
7529            "the launch effect is published; the host has not answered"
7530        );
7531
7532        engine.resolve_workflow_spawn(vec!["wf-node0".to_string()], Vec::new());
7533        assert_eq!(
7534            engine.task_lifecycle("wf-node0"),
7535            Some(TaskLifecycle::Running),
7536            "only the acknowledgement makes it Running"
7537        );
7538    }
7539
7540    #[test]
7541    fn a_failed_launch_ends_the_attempt_and_refuses_any_later_completion_for_it() {
7542        use crate::runtime::kernel::wire::effect::{TaskLaunchFailed, TaskLaunchStatus};
7543
7544        // two independent nodes, so failing one leaves the DAG running rather than terminating it
7545        let parallel = WireSpec {
7546            name: "parallel".to_string(),
7547            nodes: vec![
7548                wire_node("left", "left", &[]),
7549                wire_node("right", "right", &[]),
7550            ],
7551        };
7552        let mut runtime = Runtime::new();
7553        runtime.submit(&syscall_config());
7554        let started = runtime.submit(&workflow_start("in-start", 1_700_000_001_000, parallel));
7555
7556        let failure = envelope(
7557            "in-ack-fail",
7558            1_700_000_002_000,
7559            KernelInput::ResolveEffect(ResolveEffect {
7560                effect_id: effect_id(started.step_seq),
7561                outcome: EffectOutcome::Succeeded(EffectSucceeded {
7562                    result: EffectSuccess::TasksSpawned(TasksSpawnedSuccess {
7563                        attempts: vec![
7564                            TaskLaunchOutcome {
7565                                task_id: TaskId::new("wf-node0").unwrap(),
7566                                attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
7567                                outcome: TaskLaunchStatus::Failed(TaskLaunchFailed {
7568                                    failure: super::super::effect::TaskLaunchFailure {
7569                                        kind:
7570                                            super::super::effect::HostEffectFailureKind::StorageUnavailable,
7571                                        message: "no worker".to_string(),
7572                                    },
7573                                }),
7574                            },
7575                            TaskLaunchOutcome {
7576                                task_id: TaskId::new("wf-node1").unwrap(),
7577                                attempt_id: WireAttemptId::new("wf-node1:attempt:1").unwrap(),
7578                                outcome: TaskLaunchStatus::Started(TaskLaunchStarted {}),
7579                            },
7580                        ],
7581                    }),
7582                }),
7583            }),
7584        );
7585        runtime.submit(&failure);
7586        assert!(
7587            runtime
7588                .driver
7589                .engine()
7590                .unwrap()
7591                .task_lifecycle("wf-node0")
7592                .is_some_and(|state| state.is_terminal()),
7593            "a failed launch terminates the attempt"
7594        );
7595
7596        let fault = runtime.reject(&child_done_with(
7597            "in-late",
7598            1_700_000_003_000,
7599            "wf-node0",
7600            "wf-node0:attempt:1",
7601            Vec::new(),
7602        ));
7603        assert_eq!(
7604            fault.code,
7605            KernelFaultCode::InvalidAuthority,
7606            "a terminated attempt is a stale causation"
7607        );
7608    }
7609
7610    #[test]
7611    fn a_second_completion_for_a_spent_attempt_carries_no_authority() {
7612        let mut runtime = workflow_root_awaiting_first_child();
7613        runtime.submit(&child_done_with(
7614            "in-done-1",
7615            1_700_000_003_000,
7616            "wf-node0",
7617            "wf-node0:attempt:1",
7618            Vec::new(),
7619        ));
7620        let nodes_before = runtime.driver.engine().unwrap().workflow_node_count();
7621
7622        let replayed = child_done_with(
7623            "in-done-1-again",
7624            1_700_000_004_000,
7625            "wf-node0",
7626            "wf-node0:attempt:1",
7627            vec![SyscallRequest::AppendWorkflowNodes(
7628                super::super::syscall::AppendWorkflowNodesRequest {
7629                    nodes: vec![wire_node("smuggled", "smuggled", &[])],
7630                },
7631            )],
7632        );
7633        let fault = runtime.reject(&replayed);
7634        assert_eq!(fault.code, KernelFaultCode::InvalidAuthority);
7635        assert_eq!(
7636            runtime.driver.engine().unwrap().workflow_node_count(),
7637            nodes_before,
7638            "a refused completion appends nothing"
7639        );
7640    }
7641
7642    // -----------------------------------------------------------------------------------------
7643    // §7.6 · memory proposals
7644    // -----------------------------------------------------------------------------------------
7645
7646    #[test]
7647    fn a_memory_proposal_becomes_a_kernel_authored_write_with_derived_provenance() {
7648        let mut runtime = workflow_root_awaiting_first_child();
7649        let write =
7650            SyscallRequest::RequestMemoryWrite(super::super::syscall::RequestMemoryWriteRequest {
7651                proposal: super::super::syscall::MemoryWriteProposal {
7652                    name: "source-set".to_string(),
7653                    kind: WireMemoryKind::Project,
7654                    content: "12 primary sources".to_string(),
7655                    description: String::new(),
7656                    evidence_refs: Vec::new(),
7657                },
7658            });
7659        let advanced = runtime.submit(&child_done_with(
7660            "in-done-1",
7661            1_700_000_003_000,
7662            "wf-node0",
7663            "wf-node0:attempt:1",
7664            vec![write],
7665        ));
7666
7667        let persisted = advanced
7668            .published_effects()
7669            .iter()
7670            .find(|effect| effect.tag() == EffectKindTag::PersistMemory)
7671            .expect("the proposal published a memory write");
7672        assert_eq!(
7673            persisted.effect_id,
7674            effect_id_at(advanced.step_seq, 0),
7675            "syscall effects mint their own identity from the step they belong to"
7676        );
7677        let EffectKind::PersistMemory(effect) = &persisted.effect else {
7678            panic!("expected a memory write");
7679        };
7680        assert_eq!(effect.binding.binding_id.as_str(), "mem-binding-1");
7681        assert_eq!(
7682            effect.memory.accepted_at_ms,
7683            WireU64::new(1_700_000_003_000),
7684            "provenance time is the envelope's accepted time, never a host clock (DEC-2)"
7685        );
7686        match &effect.memory.causation {
7687            SyscallCausation::ChildAttempt(child) => {
7688                assert_eq!(child.task_id.as_str(), "wf-node0");
7689                assert_eq!(child.attempt_id.as_str(), "wf-node0:attempt:1");
7690                assert_eq!(child.request_seq, 0, "the seq is the list's own order");
7691            }
7692            other => panic!("expected a child-attempt causation, got {other:?}"),
7693        }
7694        // the proposal contributed no security field, and the record does not grow one
7695        let json = serde_json::to_value(&effect.memory).unwrap().to_string();
7696        for forbidden in ["tenant", "author", "trust_level", "record_id", "session"] {
7697            assert!(
7698                !json.contains(forbidden),
7699                "the kernel-authored write leaked {forbidden}"
7700            );
7701        }
7702    }
7703
7704    #[test]
7705    fn a_memory_query_is_clamped_to_the_operations_retrieval_policy() {
7706        let (mut runtime, provider) = agent_awaiting_provider();
7707        let resolved = runtime.submit(&provider_result(
7708            "in-recall",
7709            1_700_000_002_000,
7710            &provider,
7711            vec![tool_call(
7712                "call-1",
7713                crate::context::manager::MEMORY_TOOL_NAME,
7714                json!({"query": "past briefs", "top_k": 999}),
7715            )],
7716        ));
7717        let queried = sole_effect(&resolved);
7718        let EffectKind::QueryMemory(effect) = &queried.effect else {
7719            panic!("expected a memory query, got {:?}", queried.tag());
7720        };
7721        assert_eq!(
7722            effect.requested_k, 4,
7723            "the model cannot widen the operation's retrieval policy by asking for more"
7724        );
7725        assert!(matches!(
7726            effect.query.causation,
7727            SyscallCausation::ProviderTool(_)
7728        ));
7729        // the query the kernel authored is binding + causation + accepted time, nothing else
7730        let json = serde_json::to_value(&effect.query).unwrap().to_string();
7731        for forbidden in ["session", "tenant", "author", "trust", "agent_id"] {
7732            assert!(
7733                !json.contains(forbidden),
7734                "the kernel-authored query leaked {forbidden}"
7735            );
7736        }
7737        assert_eq!(effect.binding.binding_id.as_str(), "mem-binding-1");
7738    }
7739
7740    // -----------------------------------------------------------------------------------------
7741    // fixture: no-host-session-identity (§22.6 · Task 11)
7742    // -----------------------------------------------------------------------------------------
7743
7744    /// Every JSON key anywhere in `value`.
7745    fn all_keys(value: &Value, into: &mut BTreeSet<String>) {
7746        match value {
7747            Value::Object(map) => {
7748                for (key, child) in map {
7749                    into.insert(key.clone());
7750                    all_keys(child, into);
7751                }
7752            }
7753            Value::Array(items) => items.iter().for_each(|item| all_keys(item, into)),
7754            _ => {}
7755        }
7756    }
7757
7758    /// One full canonical arc — configure → workflow root start → spawn effect → spawn
7759    /// acknowledgement → child completion carrying a memory proposal — as a host runs it.
7760    ///
7761    /// Returns everything the arc made durable or published: the record chain (bytes and digests),
7762    /// the effects, and the observations. Nothing here is host-timed or host-named beyond the
7763    /// opaque ids §5.3 admits.
7764    fn canonical_arc() -> (Vec<Value>, Vec<Value>, Vec<Value>) {
7765        let mut runtime = Runtime::new();
7766        let mut effects = Vec::new();
7767        let mut observations = Vec::new();
7768        let collect = |_runtime: &Runtime,
7769                       committed: &CommittedTransition<PlannedStep>,
7770                       effects: &mut Vec<Value>,
7771                       observations: &mut Vec<Value>| {
7772            for effect in committed.published_effects() {
7773                effects.push(serde_json::to_value(effect).unwrap());
7774            }
7775            for observation in &committed.step.observations {
7776                observations.push(serde_json::to_value(observation).unwrap());
7777            }
7778        };
7779
7780        let configured = runtime.submit(&syscall_config());
7781        collect(&runtime, &configured, &mut effects, &mut observations);
7782
7783        let started = runtime.submit(&workflow_start(
7784            "in-start",
7785            1_700_000_001_000,
7786            two_node_spec(),
7787        ));
7788        collect(&runtime, &started, &mut effects, &mut observations);
7789
7790        let acked = runtime.submit(&spawned(
7791            "in-ack-1",
7792            1_700_000_002_000,
7793            &effect_id(started.step_seq),
7794            &["wf-node0"],
7795        ));
7796        collect(&runtime, &acked, &mut effects, &mut observations);
7797
7798        let write =
7799            SyscallRequest::RequestMemoryWrite(super::super::syscall::RequestMemoryWriteRequest {
7800                proposal: super::super::syscall::MemoryWriteProposal {
7801                    name: "source-set".to_string(),
7802                    kind: WireMemoryKind::Project,
7803                    content: "12 primary sources".to_string(),
7804                    description: String::new(),
7805                    evidence_refs: Vec::new(),
7806                },
7807            });
7808        let advanced = runtime.submit(&child_done_with(
7809            "in-done-1",
7810            1_700_000_003_000,
7811            "wf-node0",
7812            "wf-node0:attempt:1",
7813            vec![write],
7814        ));
7815        collect(&runtime, &advanced, &mut effects, &mut observations);
7816
7817        let records = runtime
7818            .journal
7819            .iter()
7820            .map(|record| {
7821                json!({
7822                    "bytes": String::from_utf8_lossy(record.record_bytes().as_slice()).into_owned(),
7823                    "digest": record.record_digest().as_str(),
7824                })
7825            })
7826            .collect();
7827        (records, effects, observations)
7828    }
7829
7830    /// §22.6 · "host mapping does not affect kernel deterministic output", proved forwards: the arc
7831    /// is a pure function of its logical inputs, so two hosts — whatever they call their sessions —
7832    /// build the same record chain, publish the same effect bytes and read the same observations.
7833    #[test]
7834    fn the_canonical_arc_is_byte_identical_for_any_host() {
7835        let (records_a, effects_a, observations_a) = canonical_arc();
7836        let (records_b, effects_b, observations_b) = canonical_arc();
7837
7838        assert!(!records_a.is_empty() && !effects_a.is_empty() && !observations_a.is_empty());
7839        assert_eq!(records_a, records_b, "the record chain is not reproducible");
7840        assert_eq!(
7841            effects_a, effects_b,
7842            "published effects are not reproducible"
7843        );
7844        assert_eq!(
7845            observations_a, observations_b,
7846            "observations are not reproducible"
7847        );
7848    }
7849
7850    /// The scan half of Task 11: no session key, and no session *value*, exists anywhere on the
7851    /// canonical output surface — the durable records, the effects the host executes, or the
7852    /// observations it projects. A host may keep its own session mapping; the kernel holds none.
7853    #[test]
7854    fn no_canonical_record_effect_or_observation_names_a_session() {
7855        const BANNED_KEYS: [&str; 5] = [
7856            "session_id",
7857            "parent_session_id",
7858            "session",
7859            "submitter_agent_id",
7860            "actor_id",
7861        ];
7862
7863        let (records, effects, observations) = canonical_arc();
7864        for (surface, values) in [
7865            ("record", &records),
7866            ("effect", &effects),
7867            ("observation", &observations),
7868        ] {
7869            for value in values {
7870                let mut keys = BTreeSet::new();
7871                all_keys(value, &mut keys);
7872                for banned in BANNED_KEYS {
7873                    assert!(
7874                        !keys.contains(banned),
7875                        "canonical {surface} carries the host-owned key {banned:?}: {value}"
7876                    );
7877                }
7878                assert!(
7879                    !value.to_string().contains("session"),
7880                    "canonical {surface} mentions a session: {value}"
7881                );
7882            }
7883        }
7884
7885        // and the child the arc launched is correlated by logical identity alone
7886        let launch = effects
7887            .iter()
7888            .find(|effect| effect["effect"]["kind"] == "spawn_tasks")
7889            .expect("the arc launched a child");
7890        let task = &launch["effect"]["tasks"][0];
7891        assert_eq!(task["task_id"], "wf-node0");
7892        assert_eq!(task["attempt_id"], "wf-node0:attempt:1");
7893        assert!(
7894            task["launch_token"].as_str().is_some_and(|t| !t.is_empty()),
7895            "a child is named by task/attempt/launch token, never by a session"
7896        );
7897    }
7898
7899    /// The process observation the arc publishes states the logical **parent task**, and the child
7900    /// spec the kernel derives for the legacy engine carries an empty session — the canonical path
7901    /// cannot populate one because no canonical input has a field for it.
7902    #[test]
7903    fn a_spawned_process_is_reported_by_its_logical_parent_task() {
7904        let (_, _, observations) = canonical_arc();
7905        let process = observations
7906            .iter()
7907            .find(|observation| observation["kind"] == "agent_process_changed")
7908            .expect("the arc published a process observation");
7909        assert_eq!(process["agent_id"], "wf-node0");
7910        assert_eq!(process["parent_task_id"], "root");
7911
7912        let spec = agent_run_spec(&LogicalAgentSpec::new("write the brief"));
7913        assert_eq!(spec.identity.session_id.as_str(), NO_HOST_SESSION);
7914        assert!(spec.identity.parent_session_id.is_none());
7915    }
7916
7917    // -----------------------------------------------------------------------------------------
7918    // fixture: removed-self-declared-caller
7919    // -----------------------------------------------------------------------------------------
7920
7921    /// Every P1 request shape, serialized. None of them has a field through which a host could say
7922    /// who is asking — §22.10's whole point, and the reason omitting an id can no longer mean
7923    /// "skip the trust downgrade".
7924    #[test]
7925    fn no_syscall_request_shape_carries_a_self_declared_caller() {
7926        use super::super::syscall::*;
7927
7928        let requests = vec![
7929            SyscallRequest::SubmitWorkflow(SubmitWorkflowRequest {
7930                spec: two_node_spec(),
7931            }),
7932            SyscallRequest::AppendWorkflowNodes(AppendWorkflowNodesRequest {
7933                nodes: vec![wire_node("n", "n", &[])],
7934            }),
7935            SyscallRequest::ActivateSkill(ActivateSkillRequest {
7936                name: "debug".to_string(),
7937                lease_turns: Some(3),
7938            }),
7939            SyscallRequest::UpdateTask(UpdateTaskRequest {
7940                update: WireTaskUpdate::default(),
7941            }),
7942            SyscallRequest::RequestMemoryWrite(RequestMemoryWriteRequest {
7943                proposal: MemoryWriteProposal {
7944                    name: "n".to_string(),
7945                    kind: WireMemoryKind::Project,
7946                    content: "c".to_string(),
7947                    description: String::new(),
7948                    evidence_refs: Vec::new(),
7949                },
7950            }),
7951            SyscallRequest::RequestMemoryQuery(RequestMemoryQueryRequest {
7952                query: MemoryQueryProposal::default(),
7953            }),
7954            SyscallRequest::PageIn(PageInRequest {
7955                handle_id: super::super::scalar::HandleId::new("h-1").unwrap(),
7956            }),
7957        ];
7958
7959        for request in &requests {
7960            let text = serde_json::to_value(request).unwrap().to_string();
7961            for forbidden in [
7962                "submitter_agent_id",
7963                "actor_id",
7964                "agent_id",
7965                "session_id",
7966                "parent_session_id",
7967                "caller",
7968                "author",
7969                "trust",
7970            ] {
7971                assert!(
7972                    !text.contains(forbidden),
7973                    "{request:?} still exposes {forbidden}"
7974                );
7975            }
7976        }
7977
7978        // a host cannot smuggle one in either: the shapes deny unknown fields
7979        let smuggled = r#"{"kind":"append_workflow_nodes","nodes":[],"submitter_agent_id":"root"}"#;
7980        assert!(
7981            serde_json::from_str::<SyscallRequest>(smuggled).is_err(),
7982            "a self-declared submitter must not decode"
7983        );
7984    }
7985
7986    /// The three authority families a quarantined caller is refused, kept exhaustive against the
7987    /// request union so a new syscall cannot be added without a decision about it.
7988    #[test]
7989    fn every_syscall_is_classified_against_the_quarantine_rule() {
7990        use super::super::syscall::*;
7991
7992        let classified = [
7993            (
7994                SyscallRequest::SubmitWorkflow(SubmitWorkflowRequest {
7995                    spec: WireSpec::default(),
7996                }),
7997                Some("workflow"),
7998            ),
7999            (
8000                SyscallRequest::AppendWorkflowNodes(AppendWorkflowNodesRequest {
8001                    nodes: Vec::new(),
8002                }),
8003                Some("workflow"),
8004            ),
8005            (
8006                SyscallRequest::ActivateSkill(ActivateSkillRequest {
8007                    name: String::new(),
8008                    lease_turns: None,
8009                }),
8010                Some("capability"),
8011            ),
8012            (
8013                SyscallRequest::RequestMemoryWrite(RequestMemoryWriteRequest {
8014                    proposal: MemoryWriteProposal {
8015                        name: String::new(),
8016                        kind: WireMemoryKind::Project,
8017                        content: String::new(),
8018                        description: String::new(),
8019                        evidence_refs: Vec::new(),
8020                    },
8021                }),
8022                Some("memory"),
8023            ),
8024            (
8025                SyscallRequest::RequestMemoryQuery(RequestMemoryQueryRequest {
8026                    query: MemoryQueryProposal::default(),
8027                }),
8028                Some("memory"),
8029            ),
8030            (
8031                SyscallRequest::UpdateTask(UpdateTaskRequest {
8032                    update: WireTaskUpdate::default(),
8033                }),
8034                None,
8035            ),
8036            (
8037                SyscallRequest::PageIn(PageInRequest {
8038                    handle_id: super::super::scalar::HandleId::new("h-1").unwrap(),
8039                }),
8040                None,
8041            ),
8042        ];
8043        for (request, family) in &classified {
8044            assert_eq!(&privileged_family(request), family, "{request:?}");
8045        }
8046    }
8047
8048    #[test]
8049    fn a_page_in_of_a_handle_the_caller_does_not_hold_is_refused() {
8050        let (mut runtime, provider) = agent_awaiting_provider();
8051        runtime.submit(&provider_result(
8052            "in-read",
8053            1_700_000_002_000,
8054            &provider,
8055            vec![tool_call(
8056                "call-1",
8057                crate::context::manager::READ_RESULT_TOOL_NAME,
8058                json!({"call_id": "never-existed"}),
8059            )],
8060        ));
8061        let rejected = rejections(&runtime);
8062        assert_eq!(rejected.len(), 1);
8063        assert_eq!(rejected[0].0, "read_result");
8064        assert!(
8065            rejected[0].2.contains("not reachable"),
8066            "got {:?}",
8067            rejected[0].2
8068        );
8069        assert_eq!(
8070            runtime.pending_effect_kinds(),
8071            vec![EffectKindTag::CallProvider],
8072            "an address the caller does not hold produces no page-in effect; the turn continues"
8073        );
8074    }
8075
8076    // -----------------------------------------------------------------------------------------
8077    // §7.10 · external payload (Task 13)
8078    // -----------------------------------------------------------------------------------------
8079
8080    /// The body a host persists before it ever submits a result. Long enough to be over the test
8081    /// threshold, so both directions of the partition are exercised by real sizes.
8082    const BODY: &str = "the full report body, far larger than this operation keeps resident, \
8083                        repeated so it clears the inline threshold by a comfortable margin";
8084
8085    fn body_digest() -> Digest {
8086        super::super::record::canonical_digest(BODY.as_bytes())
8087    }
8088
8089    /// A payload policy small enough to test with real strings: results reaching 64 bytes must be
8090    /// externalised, and at most 32 bytes of preview stay resident.
8091    fn payload_config() -> WireEnvelope {
8092        use crate::runtime::kernel::wire::config::PayloadPolicy;
8093        syscall_config_with(|config| {
8094            config.host_effect_support = support_with([EffectKindTag::ArchivePageOut]);
8095            config.payload_policy = Some(PayloadPolicy {
8096                inline_threshold_bytes: Some(64),
8097                preview_bytes: Some(32),
8098            });
8099        })
8100    }
8101
8102    fn external_payload(
8103        call_id: &str,
8104        digest: Digest,
8105        original_size: u64,
8106        preview: &str,
8107    ) -> WireToolResultPayload {
8108        external_payload_with(
8109            call_id,
8110            digest,
8111            original_size,
8112            preview,
8113            false,
8114            ToolResultDisposition::Recoverable,
8115        )
8116    }
8117
8118    /// The same, with the two §7.10 rule 9 failure facts stated.
8119    fn external_payload_with(
8120        call_id: &str,
8121        digest: Digest,
8122        original_size: u64,
8123        preview: &str,
8124        is_error: bool,
8125        disposition: ToolResultDisposition,
8126    ) -> WireToolResultPayload {
8127        WireToolResultPayload::External(super::super::effect::ExternalToolResult {
8128            call_id: CallId::new(call_id).unwrap(),
8129            payload_ref: PayloadRef::new("payload:01J8Y2QK7C4N0V").unwrap(),
8130            digest,
8131            original_size: WireU64::new(original_size),
8132            preview: preview.to_string(),
8133            is_error,
8134            disposition,
8135        })
8136    }
8137
8138    fn payloads_resolved(
8139        id: &str,
8140        at: u64,
8141        effect: &EffectId,
8142        results: Vec<WireToolResultPayload>,
8143    ) -> WireEnvelope {
8144        resolved(
8145            id,
8146            at,
8147            effect,
8148            EffectSuccess::Tools(ToolsSuccess { results }),
8149        )
8150    }
8151
8152    /// An agent that called one host tool and is waiting for its results, under `payload_config`.
8153    fn agent_awaiting_tool_results() -> (Runtime, EffectId) {
8154        let mut runtime = Runtime::new();
8155        runtime.submit(&payload_config());
8156        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
8157        let acted = runtime.submit(&provider_result(
8158            "in-acted",
8159            1_700_000_002_000,
8160            &effect_id(started.step_seq),
8161            vec![tool_call("call-1", "search", json!({"q": "sources"}))],
8162        ));
8163        let tools = sole_effect(&acted);
8164        assert_eq!(tools.tag(), EffectKindTag::ExecuteTools);
8165        (runtime, tools.effect_id.clone())
8166    }
8167
8168    /// The whole point of the contract: the *preview* enters context and the handle says where the
8169    /// body is. Nothing about the body itself is inside this kernel.
8170    #[test]
8171    fn an_external_tool_result_lands_as_a_preview_and_an_external_handle() {
8172        let (mut runtime, tools) = agent_awaiting_tool_results();
8173        let committed = runtime.submit(&payloads_resolved(
8174            "in-results",
8175            1_700_000_003_000,
8176            &tools,
8177            vec![external_payload(
8178                "call-1",
8179                body_digest(),
8180                BODY.len() as u64,
8181                "the full report body, far la…",
8182            )],
8183        ));
8184        assert_eq!(
8185            kinds(&committed),
8186            vec![EffectKindTag::CallProvider],
8187            "an external result resumes the turn exactly like an inline one"
8188        );
8189        let EffectKind::CallProvider(provider) = &sole_effect(&committed).effect else {
8190            panic!("external residency must resume through the provider");
8191        };
8192        assert!(
8193            provider
8194                .tools
8195                .iter()
8196                .any(|tool| tool.name.as_str() == READ_RESULT_TOOL_NAME),
8197            "the refreshed provider projection must advertise the newly reachable payload"
8198        );
8199
8200        let engine = runtime.driver.engine().expect("the arc built an engine");
8201        assert_eq!(
8202            engine.ctx.payload_residency("call-1"),
8203            Some(&Residency::External {
8204                payload_ref: "payload:01J8Y2QK7C4N0V".to_string(),
8205                digest: body_digest().as_str().to_string(),
8206                original_size: BODY.len() as u64,
8207            }),
8208            "§7.10 rule 3 · the P3 handle is where the reference lives"
8209        );
8210
8211        let rendered = serde_json::to_string(&engine.ctx.partitions.history.messages).unwrap();
8212        assert!(
8213            rendered.contains("the full report body, far la"),
8214            "the preview is what occupies working context"
8215        );
8216        assert!(
8217            !rendered.contains("clears the inline threshold"),
8218            "the body must not be in context: {rendered}"
8219        );
8220        assert!(observation_kinds(&runtime).contains(&"payload_residency_changed"));
8221        assert_eq!(
8222            runtime
8223                .observations()
8224                .iter()
8225                .filter(|observation| matches!(
8226                    observation,
8227                    KernelObservation::CheckpointTaken { .. }
8228                ))
8229                .count(),
8230            1,
8231            "re-projecting external residency must not execute the provider-call boundary twice",
8232        );
8233    }
8234
8235    /// §7.10 rule 2 · a digest this kernel cannot recompute is a payload it could never prove it
8236    /// restored, so it is refused at admission rather than at page-in.
8237    #[test]
8238    fn an_external_tool_result_the_kernel_cannot_verify_is_refused() {
8239        let (mut runtime, tools) = agent_awaiting_tool_results();
8240        let fault = runtime.reject(&payloads_resolved(
8241            "in-results",
8242            1_700_000_003_000,
8243            &tools,
8244            vec![external_payload(
8245                "call-1",
8246                Digest::new("md5:deadbeef").unwrap(),
8247                BODY.len() as u64,
8248                "preview",
8249            )],
8250        ));
8251        assert_eq!(fault.code, KernelFaultCode::MalformedEnvelope);
8252        assert!(
8253            fault.message.contains("sha256:<64 hex>"),
8254            "the refusal names the only digest shape a page-in can be checked against: {}",
8255            fault.message
8256        );
8257    }
8258
8259    /// §7.10 rule 2 · the configured threshold is a **total** partition. A body small enough to
8260    /// inline may not be externalised: it would buy a `LoadPayload` round trip to read something
8261    /// that fitted in the turn that produced it, and it would leave the one rule a host and the
8262    /// kernel must agree on with a hole in the middle.
8263    #[test]
8264    fn an_external_tool_result_below_the_threshold_is_refused() {
8265        let (mut runtime, tools) = agent_awaiting_tool_results();
8266        let small = "tiny";
8267        let fault = runtime.reject(&payloads_resolved(
8268            "in-results",
8269            1_700_000_003_000,
8270            &tools,
8271            vec![external_payload(
8272                "call-1",
8273                super::super::record::canonical_digest(small.as_bytes()),
8274                small.len() as u64,
8275                small,
8276            )],
8277        ));
8278        assert_eq!(fault.code, KernelFaultCode::MalformedEnvelope);
8279        assert!(
8280            fault.message.contains("inlines below 64"),
8281            "{}",
8282            fault.message
8283        );
8284    }
8285
8286    /// The preview is the part that actually occupies context, so it is the part the policy bounds.
8287    #[test]
8288    fn an_external_preview_over_the_resident_budget_is_refused() {
8289        let (mut runtime, tools) = agent_awaiting_tool_results();
8290        let fault = runtime.reject(&payloads_resolved(
8291            "in-results",
8292            1_700_000_003_000,
8293            &tools,
8294            vec![external_payload(
8295                "call-1",
8296                body_digest(),
8297                BODY.len() as u64,
8298                BODY,
8299            )],
8300        ));
8301        assert_eq!(fault.code, KernelFaultCode::ResourceLimitExceeded);
8302        assert!(fault.message.contains("preview"), "{}", fault.message);
8303    }
8304
8305    /// §7.10 rules 1 and 5 · the kernel does not externalise on the host's behalf. Accepting an
8306    /// oversized inline result and spooling it back out is the historical round trip where the body
8307    /// crossed core twice and entered the journal twice; the only answer that keeps rule 5 true is
8308    /// to refuse it.
8309    #[test]
8310    fn an_inline_tool_result_over_the_threshold_is_refused() {
8311        let (mut runtime, tools) = agent_awaiting_tool_results();
8312        let fault = runtime.reject(&tools_resolved(
8313            "in-results",
8314            1_700_000_003_000,
8315            &tools,
8316            &[("call-1", BODY, false)],
8317        ));
8318        assert_eq!(fault.code, KernelFaultCode::ResourceLimitExceeded);
8319        assert!(
8320            fault.message.contains("externalises at 64"),
8321            "{}",
8322            fault.message
8323        );
8324        assert_eq!(
8325            runtime.pending_effect_kinds(),
8326            vec![EffectKindTag::ExecuteTools],
8327            "a rejected batch leaves the effect it was answering pending — zero mutation"
8328        );
8329    }
8330
8331    /// A batch is adjudicated whole: one illegal result and none of it lands.
8332    #[test]
8333    fn one_illegal_result_rejects_the_whole_batch() {
8334        let mut runtime = Runtime::new();
8335        runtime.submit(&payload_config());
8336        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
8337        let acted = runtime.submit(&provider_result(
8338            "in-acted",
8339            1_700_000_002_000,
8340            &effect_id(started.step_seq),
8341            vec![
8342                tool_call("call-1", "search", json!({"q": "a"})),
8343                tool_call("call-2", "search", json!({"q": "b"})),
8344            ],
8345        ));
8346        let tools = sole_effect(&acted).effect_id.clone();
8347        runtime.reject(&payloads_resolved(
8348            "in-results",
8349            1_700_000_003_000,
8350            &tools,
8351            vec![
8352                WireToolResultPayload::Inline(InlineToolResult {
8353                    call_id: CallId::new("call-1").unwrap(),
8354                    result: WireToolResult {
8355                        output: "small and legal".to_string(),
8356                        is_error: false,
8357                        disposition: ToolResultDisposition::Recoverable,
8358                        tokens: None,
8359                    },
8360                }),
8361                external_payload("call-2", Digest::new("md5:deadbeef").unwrap(), 9_000, "p"),
8362            ],
8363        ));
8364        let engine = runtime.driver.engine().expect("the arc built an engine");
8365        assert!(
8366            engine.ctx.payload_residency("call-1").is_none(),
8367            "the legal half of a refused batch must not have landed either"
8368        );
8369    }
8370
8371    /// The full §7.10 rule 4 arc: `read_result` → `LoadPayload` → `PayloadLoaded`, with the body
8372    /// entering context only once the digest proves it is the one that left.
8373    #[test]
8374    fn a_page_in_of_an_external_payload_loads_and_verifies_the_body() {
8375        let (mut runtime, tools) = agent_awaiting_tool_results();
8376        let stored = runtime.submit(&payloads_resolved(
8377            "in-results",
8378            1_700_000_003_000,
8379            &tools,
8380            vec![external_payload(
8381                "call-1",
8382                body_digest(),
8383                BODY.len() as u64,
8384                "the full report body, far la…",
8385            )],
8386        ));
8387
8388        let read = runtime.submit(&provider_result(
8389            "in-read",
8390            1_700_000_004_000,
8391            &effect_id(stored.step_seq),
8392            vec![tool_call(
8393                "call-2",
8394                READ_RESULT_TOOL_NAME,
8395                json!({"call_id": "call-1"}),
8396            )],
8397        ));
8398        let load = sole_effect(&read);
8399        assert_eq!(load.tag(), EffectKindTag::LoadPayload);
8400        let EffectKind::LoadPayload(effect) = &load.effect else {
8401            panic!("expected a payload load");
8402        };
8403        assert_eq!(effect.handle_id.as_str(), "call-1");
8404        assert_eq!(
8405            effect.payload_ref.as_str(),
8406            "payload:01J8Y2QK7C4N0V",
8407            "the effect hands back the host's own opaque locator, unread"
8408        );
8409        let load = load.effect_id.clone();
8410        assert!(
8411            rejections(&runtime).is_empty(),
8412            "a reachable, externally-backed handle is a page-in, not a refusal"
8413        );
8414
8415        let restored = runtime.submit(&resolved(
8416            "in-loaded",
8417            1_700_000_005_000,
8418            &load,
8419            EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8420                handle_id: HandleId::new("call-1").unwrap(),
8421                payload: InlinePayload {
8422                    content: BODY.to_string(),
8423                    digest: body_digest(),
8424                    original_size: WireU64::new(BODY.len() as u64),
8425                },
8426            }),
8427        ));
8428        assert_eq!(
8429            kinds(&restored),
8430            vec![EffectKindTag::CallProvider],
8431            "the paged-in body resumes the turn that asked for it"
8432        );
8433
8434        let engine = runtime.driver.engine().expect("the arc built an engine");
8435        assert_eq!(
8436            engine.ctx.payload_residency("call-1"),
8437            Some(&Residency::Resident),
8438            "§25.9 · the handle is the fact, and it says the body came home"
8439        );
8440        let rendered = serde_json::to_string(&engine.ctx.partitions.history.messages).unwrap();
8441        assert!(
8442            rendered.contains("clears the inline threshold"),
8443            "the model reads the body it asked for"
8444        );
8445        assert!(observation_kinds(&runtime).contains(&"payload_residency_changed"));
8446    }
8447
8448    /// The kernel never saw the body, so the digest is the only evidence there is. A mismatch is a
8449    /// protocol violation with zero mutation, not a degraded read.
8450    #[test]
8451    fn a_paged_in_body_that_is_not_the_one_that_left_is_refused() {
8452        let (mut runtime, load) = agent_awaiting_payload_load();
8453        // Same length, and self-consistently declared — only the digest can tell the difference,
8454        // which is exactly the property the contract rests on.
8455        let substitute = BODY.replace("margin", "MARGIN");
8456        assert_eq!(substitute.len(), BODY.len());
8457        let fault = runtime.reject(&resolved(
8458            "in-loaded",
8459            1_700_000_005_000,
8460            &load,
8461            EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8462                handle_id: HandleId::new("call-1").unwrap(),
8463                payload: InlinePayload {
8464                    content: substitute.to_string(),
8465                    digest: super::super::record::canonical_digest(substitute.as_bytes()),
8466                    original_size: WireU64::new(substitute.len() as u64),
8467                },
8468            }),
8469        ));
8470        assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
8471        assert!(fault.message.contains("digests to"), "{}", fault.message);
8472
8473        let engine = runtime.driver.engine().expect("the arc built an engine");
8474        assert!(
8475            matches!(
8476                engine.ctx.payload_residency("call-1"),
8477                Some(Residency::External { .. })
8478            ),
8479            "a refused restore leaves the handle exactly where it was"
8480        );
8481    }
8482
8483    /// A loaded payload that does not agree with itself never gets as far as the digest: the size
8484    /// it declares and the bytes it carries are the same claim stated twice.
8485    #[test]
8486    fn a_paged_in_body_that_contradicts_its_own_size_is_refused() {
8487        let (mut runtime, load) = agent_awaiting_payload_load();
8488        let fault = runtime.reject(&resolved(
8489            "in-loaded",
8490            1_700_000_005_000,
8491            &load,
8492            EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8493                handle_id: HandleId::new("call-1").unwrap(),
8494                payload: InlinePayload {
8495                    content: BODY.to_string(),
8496                    digest: body_digest(),
8497                    original_size: WireU64::new(BODY.len() as u64 + 1),
8498                },
8499            }),
8500        ));
8501        assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
8502        assert!(fault.message.contains("and carries"), "{}", fault.message);
8503    }
8504
8505    /// The outcome must name the handle its effect addressed — the same rule `verify_page_out_receipt`
8506    /// applies on the way out.
8507    #[test]
8508    fn a_paged_in_body_for_another_handle_is_refused() {
8509        let (mut runtime, load) = agent_awaiting_payload_load();
8510        let fault = runtime.reject(&resolved(
8511            "in-loaded",
8512            1_700_000_005_000,
8513            &load,
8514            EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8515                handle_id: HandleId::new("call-9").unwrap(),
8516                payload: InlinePayload {
8517                    content: BODY.to_string(),
8518                    digest: body_digest(),
8519                    original_size: WireU64::new(BODY.len() as u64),
8520                },
8521            }),
8522        ));
8523        assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
8524        assert!(fault.message.contains("names handle"), "{}", fault.message);
8525    }
8526
8527    /// DEC-5 · a body the host cannot produce leaves the operation where it was. The read is
8528    /// abandoned once, the loop continues, and the kernel does not re-issue the same load.
8529    #[test]
8530    fn a_payload_the_host_cannot_produce_abandons_the_read() {
8531        let (mut runtime, load) = agent_awaiting_payload_load();
8532        let resumed = runtime.submit(&failed(
8533            "in-load-failed",
8534            1_700_000_005_000,
8535            &load,
8536            HostEffectFailureKind::StorageUnavailable,
8537            "the blob store is offline",
8538        ));
8539        assert_eq!(
8540            kinds(&resumed),
8541            vec![EffectKindTag::CallProvider],
8542            "one page-in failure does not kill a live run"
8543        );
8544        assert!(observation_kinds(&runtime).contains(&"payload_load_failed"));
8545        let engine = runtime.driver.engine().expect("the arc built an engine");
8546        assert!(
8547            matches!(
8548                engine.ctx.payload_residency("call-1"),
8549                Some(Residency::External { .. })
8550            ),
8551            "the reference survives the failed read: the body is still out there"
8552        );
8553    }
8554
8555    /// An address whose body core still holds is refused with the reason. There is no locator to
8556    /// hand back, and inventing one is the confusion the closed union removes.
8557    #[test]
8558    fn a_page_in_of_a_resident_handle_is_refused() {
8559        let (mut runtime, tools) = agent_awaiting_tool_results();
8560        let inlined = runtime.submit(&tools_resolved(
8561            "in-results",
8562            1_700_000_003_000,
8563            &tools,
8564            &[("call-1", "three sources found", false)],
8565        ));
8566        runtime.submit(&provider_result(
8567            "in-read",
8568            1_700_000_004_000,
8569            &effect_id(inlined.step_seq),
8570            vec![tool_call(
8571                "call-2",
8572                READ_RESULT_TOOL_NAME,
8573                json!({"call_id": "call-1"}),
8574            )],
8575        ));
8576        let rejected = rejections(&runtime);
8577        assert_eq!(rejected.len(), 1);
8578        assert_eq!(rejected[0].0, "read_result");
8579        assert!(
8580            rejected[0].2.contains("nothing to page in"),
8581            "got {:?}",
8582            rejected[0].2
8583        );
8584        assert_eq!(
8585            runtime.pending_effect_kinds(),
8586            vec![EffectKindTag::CallProvider],
8587            "a body core already holds publishes no load effect"
8588        );
8589    }
8590
8591    /// B19 · the other half of the axis. A page-out archive becomes a `PagedOut` handle — a
8592    /// *different* state from `External`, reached from `Resident` — and the model can read it back
8593    /// through the same `LoadPayload` effect.
8594    #[test]
8595    fn a_page_out_archive_becomes_a_readable_paged_out_handle() {
8596        let (mut runtime, archive) = agent_awaiting_page_out();
8597        let EffectKind::ArchivePageOut(published) = &runtime
8598            .tx
8599            .pending_effects()
8600            .find(|effect| effect.effect_id == archive)
8601            .expect("the archive is pending")
8602            .effect
8603        else {
8604            panic!("expected a page-out archive");
8605        };
8606        let handle_id = published.handle_id.clone();
8607        let digest = published.payload.digest.clone();
8608        let content = published.payload.content.clone();
8609        let original_size = published.payload.original_size;
8610
8611        let archived = runtime.submit(&resolved(
8612            "in-archived",
8613            1_700_000_003_000,
8614            &archive,
8615            EffectSuccess::PageOutArchived(super::super::effect::PageOutArchivedSuccess {
8616                receipt: ArchiveReceipt {
8617                    handle_id: handle_id.clone(),
8618                    payload_ref: PayloadRef::new("payload:archive-1").unwrap(),
8619                    digest: digest.clone(),
8620                    original_size,
8621                },
8622            }),
8623        ));
8624        let engine = runtime.driver.engine().expect("the arc built an engine");
8625        assert_eq!(
8626            engine.ctx.payload_residency(handle_id.as_str()),
8627            Some(&Residency::PagedOut {
8628                payload_ref: "payload:archive-1".to_string(),
8629                digest: digest.as_str().to_string(),
8630            }),
8631            "an archived body is paged out, never external — it *was* resident"
8632        );
8633        assert!(observation_kinds(&runtime).contains(&"payload_residency_changed"));
8634
8635        // and the same read_result path addresses it
8636        let read = runtime.submit(&provider_result(
8637            "in-read",
8638            1_700_000_004_000,
8639            &effect_id(archived.step_seq),
8640            vec![tool_call(
8641                "call-7",
8642                READ_RESULT_TOOL_NAME,
8643                json!({ "call_id": handle_id.as_str() }),
8644            )],
8645        ));
8646        let load = sole_effect(&read);
8647        assert_eq!(load.tag(), EffectKindTag::LoadPayload);
8648        let load = load.effect_id.clone();
8649
8650        runtime.submit(&resolved(
8651            "in-loaded",
8652            1_700_000_005_000,
8653            &load,
8654            EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
8655                handle_id: handle_id.clone(),
8656                payload: InlinePayload {
8657                    content: content.clone(),
8658                    digest,
8659                    original_size,
8660                },
8661            }),
8662        ));
8663        let engine = runtime.driver.engine().expect("the arc built an engine");
8664        assert_eq!(
8665            engine.ctx.payload_residency(handle_id.as_str()),
8666            Some(&Residency::Resident),
8667            "the archived history came home through the same effect the external body uses"
8668        );
8669    }
8670
8671    /// An agent holding one external payload, with a page-in of it pending.
8672    fn agent_awaiting_payload_load() -> (Runtime, EffectId) {
8673        let (mut runtime, tools) = agent_awaiting_tool_results();
8674        let stored = runtime.submit(&payloads_resolved(
8675            "in-results",
8676            1_700_000_003_000,
8677            &tools,
8678            vec![external_payload(
8679                "call-1",
8680                body_digest(),
8681                BODY.len() as u64,
8682                "the full report body, far la…",
8683            )],
8684        ));
8685        let read = runtime.submit(&provider_result(
8686            "in-read",
8687            1_700_000_004_000,
8688            &effect_id(stored.step_seq),
8689            vec![tool_call(
8690                "call-2",
8691                READ_RESULT_TOOL_NAME,
8692                json!({"call_id": "call-1"}),
8693            )],
8694        ));
8695        (runtime, sole_effect(&read).effect_id.clone())
8696    }
8697
8698    // -----------------------------------------------------------------------------------------
8699    // fixture: removed-large-result-spool-effect
8700    // -----------------------------------------------------------------------------------------
8701
8702    /// §7.10 rules 5 and 6 / §25.10 · the body does not cross core, in either direction.
8703    ///
8704    /// The scan is over everything the arc made durable or published: no record, no effect and no
8705    /// observation carries the persisted body, and no effect kind exists through which the kernel
8706    /// could hand a body back out to be persisted. The full output enters only through a verified
8707    /// host-owned payload reference.
8708    #[test]
8709    fn no_canonical_record_effect_or_observation_carries_an_external_body() {
8710        let (mut runtime, tools) = agent_awaiting_tool_results();
8711        runtime.submit(&payloads_resolved(
8712            "in-results",
8713            1_700_000_003_000,
8714            &tools,
8715            vec![external_payload(
8716                "call-1",
8717                body_digest(),
8718                BODY.len() as u64,
8719                "the full report body, far la…",
8720            )],
8721        ));
8722
8723        // The record's canonical input is a base64 envelope in JSON, so scanning the record bytes
8724        // alone would pass for free. Every accepted input is decoded back out and scanned as text.
8725        let mut surfaces: Vec<(String, String)> = Vec::new();
8726        for record in &runtime.journal {
8727            surfaces.push((
8728                "record".to_string(),
8729                String::from_utf8_lossy(record.record_bytes().as_slice()).into_owned(),
8730            ));
8731            surfaces.push((
8732                "accepted input".to_string(),
8733                serde_json::to_string(&record.normalized_input().expect("the record decodes"))
8734                    .unwrap(),
8735            ));
8736        }
8737        for effect in runtime.tx.pending_effects() {
8738            surfaces.push(("effect".to_string(), serde_json::to_string(effect).unwrap()));
8739        }
8740        for observation in runtime.observations() {
8741            surfaces.push((
8742                "observation".to_string(),
8743                serde_json::to_string(observation).unwrap(),
8744            ));
8745        }
8746        assert!(surfaces.len() >= 4, "the arc produced nothing to scan");
8747        for (surface, text) in &surfaces {
8748            assert!(
8749                !text.contains("clears the inline threshold"),
8750                "the canonical {surface} carries the externalised body: {text}"
8751            );
8752            assert!(
8753                !text.contains("spool"),
8754                "the canonical {surface} still speaks of spooling: {text}"
8755            );
8756        }
8757
8758        // and no effect kind can express handing a body back out to be written
8759        for tag in EffectKindTag::ALL {
8760            assert!(
8761                !tag.as_str().contains("spool"),
8762                "{} would be the round trip §7.10 deletes",
8763                tag.as_str()
8764            );
8765        }
8766    }
8767
8768    // -----------------------------------------------------------------------------------------
8769    // fixture: removed-session-log-payload-lookup
8770    // -----------------------------------------------------------------------------------------
8771
8772    /// §7.10 rules 4 and 7 · a page-in addresses the handle table and nothing else.
8773    ///
8774    /// The historical `read_result` resolved by scanning a spool directory and then falling back to
8775    /// a linear walk of the SessionLog, so any path-shaped string was a readable address and a body
8776    /// that had left the table was still reachable. Here the two refusals are total: an address the
8777    /// caller does not hold, and a locator-shaped string that is not an address at all. Neither
8778    /// produces an effect, so there is no lookup to fall back *to*.
8779    #[test]
8780    fn a_page_in_cannot_address_anything_outside_the_handle_table() {
8781        let (mut runtime, tools) = agent_awaiting_tool_results();
8782        let stored = runtime.submit(&payloads_resolved(
8783            "in-results",
8784            1_700_000_003_000,
8785            &tools,
8786            vec![external_payload(
8787                "call-1",
8788                body_digest(),
8789                BODY.len() as u64,
8790                "the full report body, far la…",
8791            )],
8792        ));
8793        // the locator the host chose is *not* an address: only the handle is
8794        runtime.submit(&provider_result(
8795            "in-read",
8796            1_700_000_004_000,
8797            &effect_id(stored.step_seq),
8798            vec![tool_call(
8799                "call-2",
8800                READ_RESULT_TOOL_NAME,
8801                json!({"call_id": "payload:01J8Y2QK7C4N0V"}),
8802            )],
8803        ));
8804        let rejected = rejections(&runtime);
8805        assert_eq!(rejected.len(), 1);
8806        assert!(
8807            rejected[0].2.contains("not reachable"),
8808            "got {:?}",
8809            rejected[0].2
8810        );
8811        assert_eq!(
8812            runtime.pending_effect_kinds(),
8813            vec![EffectKindTag::CallProvider],
8814            "no effect is published for an address the table does not hold"
8815        );
8816    }
8817
8818    // -----------------------------------------------------------------------------------------
8819    // determinism: the record chain replays to the same steps
8820    // -----------------------------------------------------------------------------------------
8821
8822    #[test]
8823    fn the_journal_replays_to_byte_identical_records() {
8824        let runtime = drive_workflow_root_to_terminal();
8825        verify_record_chain(&runtime.journal).expect("the chain links up");
8826
8827        let mut replay = CanonicalOperationDriver::new();
8828        let rebuilt: KernelTransaction<PlannedStep, InMemoryRecordIndex> =
8829            KernelTransaction::rebuild_from_records(
8830                &runtime.journal,
8831                ConfigDefaults::default(),
8832                InMemoryRecordIndex::new(),
8833                |context| replay.fold(context),
8834            )
8835            .expect("a deterministic driver rebuilds its own journal");
8836
8837        assert_eq!(rebuilt.lifecycle(), OperationLifecycle::Completed);
8838        assert_eq!(rebuilt.terminal(), runtime.tx.terminal());
8839        assert_eq!(replay.root_kind(), runtime.driver.root_kind());
8840        assert_eq!(replay.focus(), runtime.driver.focus());
8841    }
8842
8843    /// A journal that contains syscall transitions rebuilds to the same steps — the caller a
8844    /// request was attributed to, the causation it spent and the graph it grew are all kernel state
8845    /// derived from the records, not host state a resume has to reassemble (§10.3).
8846    #[test]
8847    fn a_journal_with_syscall_transitions_replays_to_byte_identical_records() {
8848        let mut runtime = workflow_root_awaiting_first_child();
8849        runtime.submit(&child_done_with(
8850            "in-done-1",
8851            1_700_000_003_000,
8852            "wf-node0",
8853            "wf-node0:attempt:1",
8854            vec![
8855                SyscallRequest::AppendWorkflowNodes(
8856                    super::super::syscall::AppendWorkflowNodesRequest {
8857                        nodes: vec![wire_node("verify", "verify", &[])],
8858                    },
8859                ),
8860                SyscallRequest::AppendWorkflowNodes(
8861                    super::super::syscall::AppendWorkflowNodesRequest {
8862                        nodes: vec![wire_node("orphan", "orphan", &["nowhere"])],
8863                    },
8864                ),
8865            ],
8866        ));
8867        verify_record_chain(&runtime.journal).expect("the chain links up");
8868
8869        let mut replay = CanonicalOperationDriver::new();
8870        let rebuilt: KernelTransaction<PlannedStep, InMemoryRecordIndex> =
8871            KernelTransaction::rebuild_from_records(
8872                &runtime.journal,
8873                ConfigDefaults::default(),
8874                InMemoryRecordIndex::new(),
8875                |context| replay.fold(context),
8876            )
8877            .expect("a deterministic driver rebuilds its own syscall journal");
8878
8879        assert_eq!(rebuilt.lifecycle(), runtime.tx.lifecycle());
8880        assert_eq!(replay.focus(), runtime.driver.focus());
8881        assert_eq!(
8882            replay.engine().unwrap().workflow_node_count(),
8883            runtime.driver.engine().unwrap().workflow_node_count(),
8884            "the appended node is a kernel fact the replay reproduces"
8885        );
8886        assert_eq!(
8887            replay.attempts, runtime.driver.attempts,
8888            "live attempts rebuild identically, so authority after a rebuild is the same"
8889        );
8890    }
8891
8892    #[test]
8893    fn a_plan_that_never_commits_fails_closed_instead_of_drifting() {
8894        let mut runtime = Runtime::new();
8895        runtime.submit(&configure());
8896
8897        // plan a start, then abandon it: the semantic kernel already moved under that plan
8898        let preparation = runtime.prepare(&agent_start("in-start", 1_700_000_001_000));
8899        let token = preparation.token().unwrap().clone();
8900        runtime.tx.abort(&token).expect("abort before the append");
8901
8902        let fault = runtime
8903            .driver
8904            .plan(&PlanContext {
8905                input: &runtime.journal[0].normalized_input().unwrap(),
8906                step_seq: WireU64::new(1),
8907                previous_head: None,
8908                config: runtime.tx.config().unwrap(),
8909                resolving: None,
8910            })
8911            .expect_err("a discarded plan poisons the driver");
8912        assert_eq!(fault.code, KernelFaultCode::TransactionConflict);
8913        assert!(runtime.driver.poison().is_some(), "the driver is poisoned");
8914    }
8915
8916    // -----------------------------------------------------------------------------------------
8917    // §7.9 · unified effect resolution (Task 12)
8918    //
8919    // Three slices, in the order the spec suggests: provider/tool, workflow/control,
8920    // memory/page-out. Each effect kind gets success, failure and replay.
8921    // -----------------------------------------------------------------------------------------
8922
8923    use crate::runtime::kernel::wire::effect::{
8924        ApprovalSuccess, ArchiveReceipt, EffectFailed, HostEffectFailure, HostEffectFailureKind,
8925        InlinePayload, InlineToolResult, MemoryPersistReceipt, MemoryPersistedSuccess,
8926        MemoryQueriedSuccess, MemoryRecall, MilestoneCheckResult as WireMilestoneResult,
8927        MilestoneEvaluatedSuccess, PageOutArchivedSuccess, PayloadLoadedSuccess,
8928        ProviderContextOverflow, ProviderStopReason, TaskAlreadyFinished, TaskPreemptOutcome,
8929        TaskPreemptStatus, TaskPreempted, TasksPreemptedSuccess, ToolResult as WireToolResult,
8930        ToolsSuccess,
8931    };
8932    use crate::runtime::kernel::wire::scalar::{CallId, HandleId};
8933    use crate::runtime::kernel::wire::syscall::MemoryKind as SyscallMemoryKind;
8934    use crate::runtime::kernel::wire::{Digest, MemoryRecordRef, PayloadRef};
8935
8936    /// The support set `syscall_config` declares, plus whatever an arc additionally needs. Adding
8937    /// rather than replacing keeps §7.3's cross-field rules satisfied (a declared tool catalog
8938    /// implies `execute_tools` + `load_payload`).
8939    fn support_with(extra: impl IntoIterator<Item = EffectKindTag>) -> HostEffectSupport {
8940        HostEffectSupport::new(
8941            [
8942                EffectKindTag::CallProvider,
8943                EffectKindTag::ExecuteTools,
8944                EffectKindTag::LoadPayload,
8945                EffectKindTag::SpawnTasks,
8946                EffectKindTag::PreemptTasks,
8947                EffectKindTag::PersistMemory,
8948                EffectKindTag::QueryMemory,
8949            ]
8950            .into_iter()
8951            .chain(extra),
8952        )
8953    }
8954
8955    fn resolved(id: &str, at: u64, effect: &EffectId, result: EffectSuccess) -> WireEnvelope {
8956        envelope(
8957            id,
8958            at,
8959            KernelInput::ResolveEffect(ResolveEffect {
8960                effect_id: effect.clone(),
8961                outcome: EffectOutcome::Succeeded(EffectSucceeded { result }),
8962            }),
8963        )
8964    }
8965
8966    fn failed(
8967        id: &str,
8968        at: u64,
8969        effect: &EffectId,
8970        kind: HostEffectFailureKind,
8971        message: &str,
8972    ) -> WireEnvelope {
8973        envelope(
8974            id,
8975            at,
8976            KernelInput::ResolveEffect(ResolveEffect {
8977                effect_id: effect.clone(),
8978                outcome: EffectOutcome::Failed(EffectFailed {
8979                    failure: HostEffectFailure {
8980                        kind,
8981                        message: message.to_string(),
8982                        retryable: None,
8983                    },
8984                }),
8985            }),
8986        )
8987    }
8988
8989    /// A provider turn that finished with plain text and no tool calls.
8990    fn provider_answer(id: &str, at: u64, effect: &EffectId, text: &str) -> WireEnvelope {
8991        resolved(
8992            id,
8993            at,
8994            effect,
8995            EffectSuccess::Provider(super::super::effect::ProviderSuccess {
8996                outcome: ProviderOutcome::Completed(ProviderCompleted {
8997                    message: ProviderMessage {
8998                        role: MessageRole::Assistant,
8999                        content: text.to_string(),
9000                        tool_calls: Vec::new(),
9001                        tool_call_id: None,
9002                        tokens: None,
9003                    },
9004                    observed_input_tokens: None,
9005                    observed_output_tokens: None,
9006                    stop_reason: Some(ProviderStopReason::EndTurn),
9007                }),
9008            }),
9009        )
9010    }
9011
9012    fn provider_overflow(id: &str, at: u64, effect: &EffectId) -> WireEnvelope {
9013        resolved(
9014            id,
9015            at,
9016            effect,
9017            EffectSuccess::Provider(super::super::effect::ProviderSuccess {
9018                outcome: ProviderOutcome::ContextOverflow(ProviderContextOverflow {
9019                    observed_input_tokens: Some(999_999),
9020                }),
9021            }),
9022        )
9023    }
9024
9025    fn tools_resolved(
9026        id: &str,
9027        at: u64,
9028        effect: &EffectId,
9029        results: &[(&str, &str, bool)],
9030    ) -> WireEnvelope {
9031        resolved(
9032            id,
9033            at,
9034            effect,
9035            EffectSuccess::Tools(ToolsSuccess {
9036                results: results
9037                    .iter()
9038                    .map(|(call_id, output, is_error)| {
9039                        WireToolResultPayload::Inline(InlineToolResult {
9040                            call_id: CallId::new(*call_id).unwrap(),
9041                            result: WireToolResult {
9042                                output: (*output).to_string(),
9043                                is_error: *is_error,
9044                                disposition: ToolResultDisposition::Recoverable,
9045                                tokens: None,
9046                            },
9047                        })
9048                    })
9049                    .collect(),
9050            }),
9051        )
9052    }
9053
9054    fn kinds(committed: &CommittedTransition<PlannedStep>) -> Vec<EffectKindTag> {
9055        committed
9056            .published_effects()
9057            .iter()
9058            .map(|effect| effect.tag())
9059            .collect()
9060    }
9061
9062    /// Every observation the last committed transition recorded, by variant name.
9063    fn observation_kinds(runtime: &Runtime) -> Vec<&'static str> {
9064        runtime
9065            .observations()
9066            .iter()
9067            .map(observation_label)
9068            .collect()
9069    }
9070
9071    fn observation_label(observation: &KernelObservation) -> &'static str {
9072        match observation {
9073            KernelObservation::MemoryWritten { .. } => "memory_written",
9074            KernelObservation::MemoryWriteFailed { .. } => "memory_write_failed",
9075            KernelObservation::MemoryQueried { .. } => "memory_queried",
9076            KernelObservation::MemoryQueryFailed { .. } => "memory_query_failed",
9077            KernelObservation::PageOutArchived { .. } => "page_out_archived",
9078            KernelObservation::PageOutArchiveFailed { .. } => "page_out_archive_failed",
9079            KernelObservation::ApprovalResolutionFailed { .. } => "approval_resolution_failed",
9080            KernelObservation::AgentPreemptFailed { .. } => "agent_preempt_failed",
9081            KernelObservation::ControlRequestRejected { .. } => "control_request_rejected",
9082            KernelObservation::Compressed { .. } => "compressed",
9083            KernelObservation::WorkflowBatchSpawned { .. } => "workflow_batch_spawned",
9084            KernelObservation::WorkflowCompleted { .. } => "workflow_completed",
9085            KernelObservation::MilestoneAdvanced { .. } => "milestone_advanced",
9086            KernelObservation::MilestoneBlocked { .. } => "milestone_blocked",
9087            KernelObservation::Resumed { .. } => "resumed",
9088            KernelObservation::Suspended { .. } => "suspended",
9089            KernelObservation::SignalDeliveryDisposed { .. } => "signal_delivery_disposed",
9090            KernelObservation::SignalDisplaced { .. } => "signal_displaced",
9091            KernelObservation::SignalExpired { .. } => "signal_expired",
9092            KernelObservation::SignalsPending { .. } => "signals_pending",
9093            KernelObservation::OperationCancelled { .. } => "operation_cancelled",
9094            KernelObservation::LivePolicyChanged { .. } => "live_policy_changed",
9095            KernelObservation::CapabilityChanged { .. } => "capability_changed",
9096            KernelObservation::AgentPreempted { .. } => "agent_preempted",
9097            KernelObservation::PayloadResidencyChanged { .. } => "payload_residency_changed",
9098            KernelObservation::PayloadLoadFailed { .. } => "payload_load_failed",
9099            _ => "other",
9100        }
9101    }
9102
9103    /// The `(disposition, signal_id, delivery_id, attempt)` of every delivery the last transition
9104    /// disposed of.
9105    fn dispositions(runtime: &Runtime) -> Vec<(String, String, String, u32)> {
9106        runtime
9107            .observations()
9108            .iter()
9109            .filter_map(|observation| match observation {
9110                KernelObservation::SignalDeliveryDisposed {
9111                    disposition,
9112                    signal_id,
9113                    delivery_id,
9114                    attempt,
9115                    ..
9116                } => Some((
9117                    disposition.clone(),
9118                    signal_id.clone(),
9119                    delivery_id.clone(),
9120                    *attempt,
9121                )),
9122                _ => None,
9123            })
9124            .collect()
9125    }
9126
9127    /// The rendered history of the operation, as text — what the model will read next turn.
9128    fn history_text(runtime: &Runtime) -> Vec<String> {
9129        runtime
9130            .driver
9131            .engine()
9132            .map(|engine| {
9133                engine
9134                    .ctx
9135                    .partitions
9136                    .history
9137                    .messages
9138                    .iter()
9139                    .map(|message| format!("{:?}:{}", message.role, message_text(message)))
9140                    .collect()
9141            })
9142            .unwrap_or_default()
9143    }
9144
9145    fn message_text(message: &Message) -> String {
9146        match &message.content {
9147            Content::Text(text) => text.clone(),
9148            Content::Parts(parts) => parts
9149                .iter()
9150                .map(|part| match part {
9151                    crate::types::message::ContentPart::ToolResult { output, .. } => output.clone(),
9152                    other => format!("{other:?}"),
9153                })
9154                .collect::<Vec<_>>()
9155                .join(" "),
9156        }
9157    }
9158
9159    // ----- slice 1 · provider / tool -----------------------------------------------------------
9160
9161    #[test]
9162    fn a_provider_turn_with_host_tool_calls_publishes_execute_tools() {
9163        let (mut runtime, provider) = agent_awaiting_provider();
9164        let dispatched = runtime.submit(&provider_result(
9165            "in-search",
9166            1_700_000_002_000,
9167            &provider,
9168            vec![tool_call("call-1", "search", json!({"q": "sources"}))],
9169        ));
9170        assert_eq!(kinds(&dispatched), vec![EffectKindTag::ExecuteTools]);
9171        let EffectKind::ExecuteTools(execute) = &sole_effect(&dispatched).effect else {
9172            panic!("expected a tool batch");
9173        };
9174        assert_eq!(execute.calls.len(), 1);
9175        assert_eq!(execute.calls[0].name, "search");
9176        assert_eq!(execute.calls[0].call_id.as_str(), "call-1");
9177
9178        // and the results resolve that effect and re-ask the provider — the ordinary turn cycle
9179        let resumed = runtime.submit(&tools_resolved(
9180            "in-results",
9181            1_700_000_003_000,
9182            &effect_id(dispatched.step_seq),
9183            &[("call-1", "three sources found", false)],
9184        ));
9185        assert_eq!(kinds(&resumed), vec![EffectKindTag::CallProvider]);
9186        assert!(
9187            history_text(&runtime)
9188                .iter()
9189                .any(|line| line.contains("three sources found")),
9190            "the tool output is what the next turn reads"
9191        );
9192    }
9193
9194    #[test]
9195    fn a_provider_answer_with_no_tool_calls_commits_the_agent_terminal() {
9196        let (mut runtime, provider) = agent_awaiting_provider();
9197        let finished = runtime.submit(&provider_answer(
9198            "in-final",
9199            1_700_000_002_000,
9200            &provider,
9201            "the brief is written",
9202        ));
9203        let terminal = finished.terminal().expect("a final answer terminates");
9204        let KernelTerminal::Agent(agent) = terminal else {
9205            panic!("expected an agent terminal, got {terminal:?}");
9206        };
9207        assert_eq!(agent.result.termination, WireTermination::Completed);
9208        assert_eq!(
9209            agent.result.final_message.as_ref().unwrap().content,
9210            "the brief is written"
9211        );
9212        assert!(
9213            finished.published_effects().is_empty(),
9214            "§7.12 · a terminal step publishes no effect"
9215        );
9216    }
9217
9218    // fixture: budget-usage-reported-once-at-terminal
9219    #[test]
9220    fn the_usage_report_rides_the_terminal_and_only_the_terminal() {
9221        let (mut runtime, provider) = agent_awaiting_provider();
9222        let finished = runtime.submit(&provider_answer(
9223            "in-final",
9224            1_700_000_002_000,
9225            &provider,
9226            "done",
9227        ));
9228        let KernelTerminal::Agent(agent) = finished.terminal().unwrap() else {
9229            panic!("expected an agent terminal");
9230        };
9231        let reported = agent.usage.clone();
9232
9233        // Replaying the very input that committed the terminal answers with the same record and
9234        // does not mint a second report.
9235        let replayed = runtime.prepare(&provider_answer(
9236            "in-final",
9237            1_700_000_002_000,
9238            &provider,
9239            "done",
9240        ));
9241        let RecordPreparation::Replayed(replay) = replayed else {
9242            panic!("an exact replay is a replay, not a new step");
9243        };
9244        let Some(committed_step) = &replay.committed_step else {
9245            panic!("a replay above the checkpoint floor carries its step");
9246        };
9247        let StepDisposition::Terminal(terminal) = &committed_step.disposition else {
9248            panic!("the replayed step is the terminal one");
9249        };
9250        let KernelTerminal::Agent(replayed_agent) = &terminal.terminal else {
9251            panic!("expected an agent terminal");
9252        };
9253        assert_eq!(replayed_agent.usage, reported, "one report, one terminal");
9254        assert_eq!(replay.step_seq, finished.step_seq);
9255    }
9256
9257    // fixture: agent-syscall-caller-is-derived (§5k · the syscall-only continuation)
9258    #[test]
9259    fn a_syscall_only_turn_continues_with_another_provider_call() {
9260        let (mut runtime, provider) = agent_awaiting_provider();
9261        let continued = runtime.submit(&provider_result(
9262            "in-plan",
9263            1_700_000_002_000,
9264            &provider,
9265            vec![
9266                tool_call("call-1", "skill", json!({"name": "debug"})),
9267                tool_call(
9268                    "call-2",
9269                    "update_plan",
9270                    json!({"progress": "sources listed"}),
9271                ),
9272            ],
9273        ));
9274
9275        assert_eq!(
9276            kinds(&continued),
9277            vec![EffectKindTag::CallProvider],
9278            "a pure control-plane batch publishes no effect of its own, so the kernel continues \
9279             the turn itself rather than leaving the operation with nothing outstanding"
9280        );
9281        let history = history_text(&runtime);
9282        assert!(
9283            history.iter().any(|line| line.contains("skill activated")),
9284            "every syscall the kernel executed is answered so the transcript pairs: {history:?}"
9285        );
9286        assert!(
9287            history.iter().any(|line| line.contains("plan updated")),
9288            "{history:?}"
9289        );
9290        // and the assistant turn the model emitted is in history verbatim, calls included
9291        assert!(
9292            runtime
9293                .driver
9294                .engine()
9295                .unwrap()
9296                .ctx
9297                .partitions
9298                .history
9299                .messages
9300                .iter()
9301                .any(|message| message.tool_calls.len() == 2),
9302            "the model reads back the turn it actually emitted"
9303        );
9304    }
9305
9306    #[test]
9307    fn a_syscall_batch_that_published_an_effect_waits_instead_of_re_asking() {
9308        let (mut runtime, provider) = agent_awaiting_provider();
9309        let queried = runtime.submit(&provider_result(
9310            "in-memory",
9311            1_700_000_002_000,
9312            &provider,
9313            vec![tool_call(
9314                "call-1",
9315                crate::context::manager::MEMORY_TOOL_NAME,
9316                json!({"query": "prior briefs"}),
9317            )],
9318        ));
9319        assert_eq!(
9320            kinds(&queried),
9321            vec![EffectKindTag::QueryMemory],
9322            "the turn resumes when the recall it asked for resolves; a provider call now would \
9323             race the very facts it was issued to read"
9324        );
9325    }
9326
9327    #[test]
9328    fn a_mixed_batch_adjudicates_the_syscall_and_dispatches_the_tool() {
9329        let (mut runtime, provider) = agent_awaiting_provider();
9330        let mixed = runtime.submit(&provider_result(
9331            "in-mixed",
9332            1_700_000_002_000,
9333            &provider,
9334            vec![
9335                tool_call("call-1", "skill", json!({"name": "debug"})),
9336                tool_call("call-2", "search", json!({"q": "x"})),
9337            ],
9338        ));
9339        assert_eq!(kinds(&mixed), vec![EffectKindTag::ExecuteTools]);
9340        let EffectKind::ExecuteTools(execute) = &sole_effect(&mixed).effect else {
9341            panic!("expected a tool batch");
9342        };
9343        assert_eq!(
9344            execute
9345                .calls
9346                .iter()
9347                .map(|call| call.name.as_str())
9348                .collect::<Vec<_>>(),
9349            vec!["search"],
9350            "a P1 syscall is never dispatched to a host executor"
9351        );
9352        assert!(
9353            history_text(&runtime)
9354                .iter()
9355                .any(|line| line.contains("skill activated")),
9356            "the syscall half still closes its own transcript pair"
9357        );
9358    }
9359
9360    // fixture: fault-effect-resolution-fails-closed
9361    #[test]
9362    fn a_context_overflow_compacts_and_re_asks_without_reading_vendor_text() {
9363        let mut runtime = Runtime::new();
9364        runtime.submit(&syscall_config());
9365        let started = runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
9366        let provider = sole_effect(&started).effect_id.clone();
9367
9368        let recovered = runtime.submit(&provider_overflow(
9369            "in-overflow",
9370            1_700_000_002_000,
9371            &provider,
9372        ));
9373        assert_eq!(
9374            kinds(&recovered),
9375            vec![EffectKindTag::CallProvider],
9376            "an overflow is a semantic outcome the kernel recovers from, not a transport failure"
9377        );
9378        assert!(
9379            observation_kinds(&runtime).contains(&"compressed"),
9380            "the recovery ladder ran: {:?}",
9381            observation_kinds(&runtime)
9382        );
9383    }
9384
9385    #[test]
9386    fn canonical_genesis_installs_the_entropy_watch_on_the_engine() {
9387        let mut runtime = Runtime::new();
9388        runtime.submit(&syscall_config_with(|config| {
9389            config.execution_policy = Some(ExecutionPolicy {
9390                max_turns: Some(12),
9391                entropy_watch: Some(EntropyWatchPolicy {
9392                    enabled: Some(true),
9393                    threshold_ppm: Some(Ppm::new(100_000).unwrap()),
9394                    hysteresis_ppm: Some(Ppm::ZERO),
9395                    cooldown_turns: Some(0),
9396                    notify_model: Some(true),
9397                }),
9398                ..ExecutionPolicy::default()
9399            });
9400        }));
9401        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
9402        let acted = runtime.submit(&provider_result(
9403            "in-acted",
9404            1_700_000_002_000,
9405            &effect_id(started.step_seq),
9406            vec![tool_call("call-1", "search", json!({ "q": "same" }))],
9407        ));
9408        let resolved = runtime.submit(&tools_resolved(
9409            "in-results",
9410            1_700_000_003_000,
9411            &effect_id(acted.step_seq),
9412            &[("call-1", "failed", true)],
9413        ));
9414
9415        assert!(
9416            resolved
9417                .step
9418                .observations
9419                .iter()
9420                .any(|observation| matches!(observation, KernelObservation::EntropyAlert { .. })),
9421            "the resolved canonical execution policy must arm the semantic engine's entropy watch"
9422        );
9423    }
9424
9425    /// **Task 14 · core does not parse a raw vendor error string.**
9426    ///
9427    /// The legacy path classified overflow by substring-matching provider prose
9428    /// (`state_machine/eviction.rs::is_prompt_too_long`, §22.8). On the canonical face that
9429    /// reading has no entry point: the same words are ordinary content in a completion, ordinary
9430    /// diagnostics in a failure message, and neither reaches a recovery decision. Only the typed
9431    /// `ContextOverflow` outcome does.
9432    #[test]
9433    fn vendor_error_prose_is_content_and_never_a_recovery_decision() {
9434        const VENDOR_PROSE: &str = "HTTP 413: prompt is too long — context_length_exceeded, \
9435                                    maximum context length is 128000 tokens";
9436
9437        // (a) as the model's own words: an ordinary completed turn
9438        let mut runtime = Runtime::new();
9439        runtime.submit(&syscall_config());
9440        let started = runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
9441        let answered = runtime.submit(&provider_answer(
9442            "in-prose",
9443            1_700_000_002_000,
9444            &sole_effect(&started).effect_id.clone(),
9445            VENDOR_PROSE,
9446        ));
9447        assert!(
9448            answered.terminal().is_some() || kinds(&answered) == vec![EffectKindTag::CallProvider],
9449            "the words are content, not a classification"
9450        );
9451        assert!(
9452            !observation_kinds(&runtime).contains(&"compressed"),
9453            "no recovery ladder ran: {:?}",
9454            observation_kinds(&runtime)
9455        );
9456
9457        // (b) as a host failure message: a failed terminal, not an overflow recovery
9458        let (mut runtime, provider) = agent_awaiting_provider();
9459        let ended = runtime.submit(&failed(
9460            "in-prose-failure",
9461            1_700_000_002_000,
9462            &provider,
9463            HostEffectFailureKind::TransportExhausted,
9464            VENDOR_PROSE,
9465        ));
9466        let Some(KernelTerminal::Failed(failure)) = ended.terminal() else {
9467            panic!("expected a failed terminal, got {:?}", ended.terminal());
9468        };
9469        assert_eq!(failure.failure.code, KernelFailureCode::HostEffectFailed);
9470        assert!(
9471            !observation_kinds(&runtime).contains(&"compressed"),
9472            "a failure message is diagnostics; it never selects the recovery ladder: {:?}",
9473            observation_kinds(&runtime)
9474        );
9475    }
9476
9477    /// **Task 14 · no vendor vocabulary survives anywhere on the canonical face.**
9478    ///
9479    /// Every closed vocabulary the wire publishes, scanned against the words hosts historically
9480    /// forwarded verbatim. This is the regression guard for "the host maps, core never learns":
9481    /// adding a `rate_limited` failure class or a `stop` stop-reason would break it here rather
9482    /// than three releases later when something starts branching on it.
9483    #[test]
9484    fn no_canonical_vocabulary_contains_a_vendor_word() {
9485        const VENDOR_WORDS: [&str; 16] = [
9486            "rate_limit",
9487            "429",
9488            "503",
9489            "413",
9490            "overloaded",
9491            // `storage_unavailable` is the deliberate near-miss: a *storage* classification, not a
9492            // vendor's "service unavailable" — which folds into `transport_exhausted` once the
9493            // host's own ladder is spent. So the banned word is the vendor's compound, not the
9494            // bare adjective.
9495            "service_unavailable",
9496            "context_length",
9497            "max_context",
9498            "too_long",
9499            "finish_reason",
9500            "openai",
9501            "anthropic",
9502            "gemini",
9503            "deepseek",
9504            "qwen",
9505            "minimax",
9506        ];
9507
9508        let mut vocabulary: Vec<&'static str> = Vec::new();
9509        vocabulary.extend(EffectKindTag::ALL.iter().map(|tag| tag.as_str()));
9510        vocabulary.extend(
9511            super::super::effect::EffectSuccessTag::ALL
9512                .iter()
9513                .map(|tag| tag.as_str()),
9514        );
9515        vocabulary.extend(HostEffectFailureKind::ALL.iter().map(|kind| kind.as_str()));
9516        vocabulary.extend(ProviderStopReason::ALL.iter().map(|r| r.as_str()));
9517        vocabulary.extend(ToolResultDisposition::ALL.iter().map(|d| d.as_str()));
9518        assert!(vocabulary.len() >= 34, "the scan lost a vocabulary");
9519
9520        for word in vocabulary {
9521            for vendor in VENDOR_WORDS {
9522                assert!(
9523                    !word.contains(vendor),
9524                    "{word:?} carries the vendor word {vendor:?}; the canonical face is the \
9525                     host's mapping *target*, never its passthrough"
9526                );
9527            }
9528        }
9529
9530        // `storage_unavailable` is the one near-miss and it is deliberate: it is a *storage*
9531        // classification, not a vendor's "service unavailable" — which folds into
9532        // `transport_exhausted` after the host's own ladder is spent.
9533        assert!(
9534            HostEffectFailureKind::ALL
9535                .iter()
9536                .any(|kind| kind.as_str() == "storage_unavailable")
9537        );
9538        assert!(
9539            !HostEffectFailureKind::ALL
9540                .iter()
9541                .any(|kind| kind.as_str().contains("service"))
9542        );
9543    }
9544
9545    #[test]
9546    fn the_overflow_ladder_is_bounded_and_ends_in_an_honest_terminal() {
9547        let mut runtime = Runtime::new();
9548        runtime.submit(&syscall_config_with(|config| {
9549            config.recovery_policy = Some(crate::runtime::kernel::wire::command::RecoveryPolicy {
9550                provider_recovery_attempts: Some(1),
9551                ..Default::default()
9552            });
9553        }));
9554        let started = runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
9555        let mut provider = sole_effect(&started).effect_id.clone();
9556
9557        let first = runtime.submit(&provider_overflow("in-of-1", 1_700_000_002_000, &provider));
9558        assert_eq!(kinds(&first), vec![EffectKindTag::CallProvider]);
9559        provider = sole_effect(&first).effect_id.clone();
9560
9561        let exhausted = runtime.submit(&provider_overflow("in-of-2", 1_700_000_003_000, &provider));
9562        let KernelTerminal::Agent(agent) = exhausted.terminal().expect("the ladder is bounded")
9563        else {
9564            panic!("expected an agent terminal");
9565        };
9566        assert_eq!(agent.result.termination, WireTermination::ContextOverflow);
9567    }
9568
9569    // fixture: removed-kernel-auto-redispatch
9570    #[test]
9571    fn a_provider_failure_commits_a_terminal_and_never_re_asks() {
9572        let (mut runtime, provider) = agent_awaiting_provider();
9573        let ended = runtime.submit(&failed(
9574            "in-dead",
9575            1_700_000_002_000,
9576            &provider,
9577            HostEffectFailureKind::TransportExhausted,
9578            "the vendor returned 503 five times",
9579        ));
9580        assert!(
9581            ended.published_effects().is_empty(),
9582            "DEC-5 · the kernel makes one policy decision and does not re-emit the same intent"
9583        );
9584        let KernelTerminal::Failed(failure) = ended.terminal().expect("a terminal was committed")
9585        else {
9586            panic!("expected a failed terminal, got {:?}", ended.terminal());
9587        };
9588        assert_eq!(failure.failure.code, KernelFailureCode::HostEffectFailed);
9589        assert!(
9590            failure.failure.message.contains("transport_exhausted"),
9591            "the classification decides; the prose is only what an operator reads: {}",
9592            failure.failure.message
9593        );
9594    }
9595
9596    // fixture: removed-kernel-auto-redispatch
9597    #[test]
9598    fn a_tool_batch_failure_answers_every_dispatched_call_and_never_re_runs_it() {
9599        let (mut runtime, provider) = agent_awaiting_provider();
9600        let dispatched = runtime.submit(&provider_result(
9601            "in-search",
9602            1_700_000_002_000,
9603            &provider,
9604            vec![
9605                tool_call("call-1", "search", json!({"q": "a"})),
9606                tool_call("call-2", "search", json!({"q": "b"})),
9607            ],
9608        ));
9609        let tools = effect_id(dispatched.step_seq);
9610
9611        let answered = runtime.submit(&failed(
9612            "in-exec-failed",
9613            1_700_000_003_000,
9614            &tools,
9615            HostEffectFailureKind::PermissionDenied,
9616            "the executor is not allowed to run tools in this sandbox",
9617        ));
9618        assert_eq!(
9619            kinds(&answered),
9620            vec![EffectKindTag::CallProvider],
9621            "the batch is abandoned and the model is asked again — the kernel never re-dispatches \
9622             the same batch"
9623        );
9624        let history = history_text(&runtime);
9625        let refusals = history
9626            .iter()
9627            .filter(|line| line.contains("permission_denied"))
9628            .count();
9629        assert_eq!(
9630            refusals, 2,
9631            "every dispatched call still gets a result: {history:?}"
9632        );
9633    }
9634
9635    // ----- §7.10 · tool result disposition (Task 14) --------------------------------------------
9636
9637    /// A batch of two calls, dispatched and awaiting results.
9638    fn agent_dispatching_two_tools() -> (Runtime, EffectId) {
9639        let (mut runtime, provider) = agent_awaiting_provider();
9640        let dispatched = runtime.submit(&provider_result(
9641            "in-search",
9642            1_700_000_002_000,
9643            &provider,
9644            vec![
9645                tool_call("call-1", "search", json!({"q": "a"})),
9646                tool_call("call-2", "search", json!({"q": "b"})),
9647            ],
9648        ));
9649        assert_eq!(kinds(&dispatched), vec![EffectKindTag::ExecuteTools]);
9650        let tools = effect_id(dispatched.step_seq);
9651        (runtime, tools)
9652    }
9653
9654    fn tool_batch(
9655        id: &str,
9656        at: u64,
9657        effect: &EffectId,
9658        results: &[(&str, &str, bool, ToolResultDisposition)],
9659    ) -> WireEnvelope {
9660        resolved(
9661            id,
9662            at,
9663            effect,
9664            EffectSuccess::Tools(ToolsSuccess {
9665                results: results
9666                    .iter()
9667                    .map(|(call_id, output, is_error, disposition)| {
9668                        WireToolResultPayload::Inline(InlineToolResult {
9669                            call_id: CallId::new(*call_id).unwrap(),
9670                            result: WireToolResult {
9671                                output: (*output).to_string(),
9672                                is_error: *is_error,
9673                                disposition: *disposition,
9674                                tokens: None,
9675                            },
9676                        })
9677                    })
9678                    .collect(),
9679            }),
9680        )
9681    }
9682
9683    #[test]
9684    fn a_fatal_result_closes_out_the_calls_its_batch_never_answered() {
9685        // `fatal` = the executor stopped. The unanswered call is not pending — it will never be
9686        // answered — so leaving it alone would produce an assistant turn whose tool_call has no
9687        // matching tool_result, which is malformed on every vendor wire.
9688        let (mut runtime, tools) = agent_dispatching_two_tools();
9689        let settled = runtime.submit(&tool_batch(
9690            "in-fatal",
9691            1_700_000_003_000,
9692            &tools,
9693            &[(
9694                "call-1",
9695                "disk corrupt, aborting",
9696                true,
9697                ToolResultDisposition::Fatal,
9698            )],
9699        ));
9700        assert_eq!(
9701            kinds(&settled),
9702            vec![EffectKindTag::CallProvider],
9703            "the turn still completes and the model is asked again — a fatal result is model \
9704             feedback, not a rollback (v0.2.42)"
9705        );
9706
9707        let history = history_text(&runtime);
9708        assert!(
9709            history.iter().any(|line| line.contains("disk corrupt")),
9710            "the failure the host reported stays visible: {history:?}"
9711        );
9712        // The pairing invariant is the point: both dispatched calls end the turn with a result.
9713        let answered = tool_result_call_ids(&runtime);
9714        assert_eq!(
9715            answered,
9716            vec!["call-1".to_string(), "call-2".to_string()],
9717            "the call the batch never answered is closed out"
9718        );
9719        assert!(
9720            history
9721                .iter()
9722                .any(|line| line.contains("not executed") && line.contains("fatally")),
9723            "the close-out says why the call did not run: {history:?}"
9724        );
9725    }
9726
9727    /// Every `call_id` that has a committed tool result in history, in order.
9728    fn tool_result_call_ids(runtime: &Runtime) -> Vec<String> {
9729        runtime
9730            .driver
9731            .engine()
9732            .map(|engine| {
9733                engine
9734                    .ctx
9735                    .partitions
9736                    .history
9737                    .messages
9738                    .iter()
9739                    .flat_map(|message| match &message.content {
9740                        Content::Parts(parts) => parts
9741                            .iter()
9742                            .filter_map(|part| match part {
9743                                crate::types::message::ContentPart::ToolResult {
9744                                    call_id, ..
9745                                } => Some(call_id.to_string()),
9746                                _ => None,
9747                            })
9748                            .collect::<Vec<_>>(),
9749                        Content::Text(_) => Vec::new(),
9750                    })
9751                    .collect()
9752            })
9753            .unwrap_or_default()
9754    }
9755
9756    #[test]
9757    fn an_ordinary_batch_closes_out_nothing() {
9758        // The close-out is fatal-only. A short *recoverable* batch says nothing about the calls it
9759        // omitted, so the kernel must not invent results for them.
9760        let (mut runtime, tools) = agent_dispatching_two_tools();
9761        runtime.submit(&tool_batch(
9762            "in-partial",
9763            1_700_000_003_000,
9764            &tools,
9765            &[(
9766                "call-1",
9767                "no matches",
9768                false,
9769                ToolResultDisposition::Recoverable,
9770            )],
9771        ));
9772        let history = history_text(&runtime);
9773        assert!(
9774            !history.iter().any(|line| line.contains("not executed")),
9775            "a recoverable batch closes nothing out: {history:?}"
9776        );
9777    }
9778
9779    /// §7.10 rule 9 · the close-out is **total over residency**.
9780    ///
9781    /// A tool that fails after producing a huge diagnostic body is the common shape, not a rare
9782    /// one. If the fatality scan read only the inline arm, whether a fatal failure stopped the
9783    /// batch would depend on how many bytes its traceback happened to be.
9784    #[test]
9785    fn an_externalised_fatal_stops_the_batch_exactly_as_an_inline_one_does() {
9786        let (mut runtime, tools) = agent_dispatching_two_tools();
9787        let settled = runtime.submit(&resolved(
9788            "in-fatal-external",
9789            1_700_000_003_000,
9790            &tools,
9791            EffectSuccess::Tools(ToolsSuccess {
9792                results: vec![external_payload_with(
9793                    "call-1",
9794                    Digest::new(
9795                        "sha256:3b1f4a7c9e2d05186a4c7f0b9d3e8c25714f6a0b8c5d2e9f1a3b6c8d0e2f4a61",
9796                    )
9797                    .unwrap(),
9798                    524_288,
9799                    "Traceback (most recent call last):",
9800                    true,
9801                    ToolResultDisposition::Fatal,
9802                )],
9803            }),
9804        ));
9805        assert_eq!(kinds(&settled), vec![EffectKindTag::CallProvider]);
9806        assert_eq!(
9807            tool_result_call_ids(&runtime),
9808            vec!["call-1".to_string(), "call-2".to_string()],
9809            "an externalised fatal closes the batch out just like an inline one"
9810        );
9811        let history = history_text(&runtime);
9812        assert!(
9813            history
9814                .iter()
9815                .any(|line| line.contains("not executed") && line.contains("fatally")),
9816            "{history:?}"
9817        );
9818
9819        // …and the failure itself stays visible as an error, which the old shape could not express
9820        // at all: an externalised failure was indistinguishable from an externalised success.
9821        assert!(
9822            runtime
9823                .driver
9824                .engine()
9825                .expect("engine")
9826                .ctx
9827                .partitions
9828                .history
9829                .messages
9830                .iter()
9831                .any(
9832                    |message| matches!(&message.content, Content::Parts(parts) if parts
9833                    .iter()
9834                    .any(|part| matches!(
9835                        part,
9836                        crate::types::message::ContentPart::ToolResult {
9837                            call_id,
9838                            is_error: true,
9839                            ..
9840                        } if call_id.as_str() == "call-1"
9841                    )))
9842                ),
9843            "the externalised failure is committed as an error result"
9844        );
9845    }
9846
9847    #[test]
9848    fn a_fatal_result_does_not_synthesise_an_answer_the_host_already_gave() {
9849        // The close-out is keyed on *unanswered* calls, so a complete fatal batch adds nothing —
9850        // otherwise a call would get two results.
9851        let (mut runtime, tools) = agent_dispatching_two_tools();
9852        runtime.submit(&tool_batch(
9853            "in-fatal-complete",
9854            1_700_000_003_000,
9855            &tools,
9856            &[
9857                ("call-1", "boom", true, ToolResultDisposition::Fatal),
9858                (
9859                    "call-2",
9860                    "ok anyway",
9861                    false,
9862                    ToolResultDisposition::Recoverable,
9863                ),
9864            ],
9865        ));
9866        let history = history_text(&runtime);
9867        assert!(
9868            !history.iter().any(|line| line.contains("not executed")),
9869            "every call was answered by the host: {history:?}"
9870        );
9871        assert_eq!(
9872            history
9873                .iter()
9874                .filter(|line| line.contains("ok anyway"))
9875                .count(),
9876            1,
9877            "no call gets two results: {history:?}"
9878        );
9879    }
9880
9881    // ----- §7.9 · DEC-5 differential (Task 14) --------------------------------------------------
9882
9883    /// **Recovery exhaustion differential.** The kernel's decision on a failure is chosen by the
9884    /// effect kind *it* published, never by the failure kind the host reports and never by
9885    /// `retryable`. The strongest statement of that is byte equality: for every one of the ten
9886    /// effect kinds, all six failure classes × three `retryable` values must plan the same step.
9887    ///
9888    /// If any of those ever became an input, this is the test that breaks — which is the point,
9889    /// because "the kernel does not retry" is otherwise only a comment.
9890    #[test]
9891    fn a_host_failure_plans_the_same_step_whatever_the_host_advises() {
9892        for kind in HostEffectFailureKind::ALL {
9893            let mut planned: Option<Value> = None;
9894            for retryable in [None, Some(true), Some(false)] {
9895                let (mut runtime, tools) = agent_dispatching_two_tools();
9896                let settled = runtime.submit(&envelope(
9897                    "in-failed",
9898                    1_700_000_003_000,
9899                    KernelInput::ResolveEffect(ResolveEffect {
9900                        effect_id: tools.clone(),
9901                        outcome: EffectOutcome::Failed(EffectFailed {
9902                            failure: HostEffectFailure {
9903                                kind,
9904                                // the message is fixed: only the advice varies
9905                                message: "the executor could not run".to_string(),
9906                                retryable,
9907                            },
9908                        }),
9909                    }),
9910                ));
9911                let step = serde_json::to_value(&settled.step).unwrap();
9912                match &planned {
9913                    None => planned = Some(step),
9914                    Some(first) => assert_eq!(
9915                        first, &step,
9916                        "{kind:?} with retryable={retryable:?} planned a different step; \
9917                         `retryable` is advice and DEC-5 leaves it no branch to select"
9918                    ),
9919                }
9920            }
9921        }
9922    }
9923
9924    #[test]
9925    fn the_recovery_decision_reads_the_effect_kind_the_kernel_published() {
9926        // The other half of the same claim: the *kind* the host reports does not select the
9927        // decision either. Six wildly different failure classes on the same effect all produce the
9928        // one decision that effect kind gets — here, "answer every call and ask the model again".
9929        for kind in HostEffectFailureKind::ALL {
9930            let (mut runtime, tools) = agent_dispatching_two_tools();
9931            let settled = runtime.submit(&failed(
9932                "in-failed",
9933                1_700_000_003_000,
9934                &tools,
9935                kind,
9936                "the executor could not run",
9937            ));
9938            assert_eq!(
9939                kinds(&settled),
9940                vec![EffectKindTag::CallProvider],
9941                "{kind:?} must take the same decision as every other failure class"
9942            );
9943            assert!(
9944                runtime.pending_effect_kinds() == vec![EffectKindTag::CallProvider],
9945                "{kind:?}: the failed batch is never re-issued (DEC-5)"
9946            );
9947        }
9948    }
9949
9950    // fixture: fault-effect-resolution-fails-closed
9951    #[test]
9952    fn duplicate_and_conflicting_resolutions_are_settled_before_the_driver_sees_them() {
9953        let (mut runtime, provider) = agent_awaiting_provider();
9954        let first = runtime.submit(&provider_result(
9955            "in-skill",
9956            1_700_000_002_000,
9957            &provider,
9958            vec![tool_call("call-1", "skill", json!({"name": "debug"}))],
9959        ));
9960
9961        // the same resolution under a fresh input id is a replay of the existing record (DEC-1)
9962        let replay = runtime.prepare(&provider_result(
9963            "in-skill-again",
9964            1_700_000_002_500,
9965            &provider,
9966            vec![tool_call("call-1", "skill", json!({"name": "debug"}))],
9967        ));
9968        let RecordPreparation::Replayed(replayed) = replay else {
9969            panic!("a semantically identical redelivery is a replay");
9970        };
9971        assert_eq!(replayed.step_seq, first.step_seq);
9972
9973        // a *different* payload for the same effect is a conflict
9974        let conflict = runtime.reject(&provider_result(
9975            "in-skill-conflict",
9976            1_700_000_002_600,
9977            &provider,
9978            vec![tool_call("call-9", "skill", json!({"name": "debug"}))],
9979        ));
9980        assert_eq!(conflict.code, KernelFaultCode::UnexpectedEffectOutcome);
9981
9982        // an effect nobody is waiting on
9983        let unknown = EffectId::new("op-driver-1:step:77:effect:0").unwrap();
9984        let stray = runtime.reject(&provider_answer(
9985            "in-stray",
9986            1_700_000_002_700,
9987            &unknown,
9988            "hello",
9989        ));
9990        assert_eq!(stray.code, KernelFaultCode::UnexpectedEffectOutcome);
9991
9992        // and a result of the wrong *kind* for the pending effect
9993        let pending = effect_id(first.step_seq);
9994        let mismatched = runtime.reject(&tools_resolved(
9995            "in-mismatch",
9996            1_700_000_002_800,
9997            &pending,
9998            &[("call-1", "x", false)],
9999        ));
10000        assert_eq!(mismatched.code, KernelFaultCode::UnexpectedEffectOutcome);
10001    }
10002
10003    // ----- slice 2 · workflow / control ---------------------------------------------------------
10004
10005    // fixture: removed-kernel-auto-redispatch
10006    #[test]
10007    fn a_spawn_failure_fails_the_whole_batch_and_never_re_launches_it() {
10008        let mut runtime = Runtime::new();
10009        runtime.submit(&configure());
10010        let started = runtime.submit(&workflow_start(
10011            "in-start",
10012            1_700_000_001_000,
10013            two_node_spec(),
10014        ));
10015        let spawn = effect_id(started.step_seq);
10016
10017        let after = runtime.submit(&failed(
10018            "in-launch-failed",
10019            1_700_000_002_000,
10020            &spawn,
10021            HostEffectFailureKind::ResourceExhausted,
10022            "no worker slots",
10023        ));
10024        assert!(
10025            !after
10026                .published_effects()
10027                .iter()
10028                .any(|effect| effect.tag() == EffectKindTag::SpawnTasks),
10029            "DEC-5 · the same launch intent is never re-emitted"
10030        );
10031        let KernelTerminal::Workflow(workflow) = after
10032            .terminal()
10033            .expect("a DAG whose only ready node could not start drains")
10034        else {
10035            panic!("expected a workflow terminal, got {:?}", after.terminal());
10036        };
10037        assert_eq!(workflow.outcome.status, WorkflowStatus::Failed);
10038        assert!(
10039            runtime.driver.attempts.is_empty(),
10040            "a launch that never happened leaves no live attempt to complete later"
10041        );
10042    }
10043
10044    #[test]
10045    fn an_approval_resolution_dispatches_exactly_what_was_approved() {
10046        let (mut runtime, provider) = agent_awaiting_approval();
10047        let requested = runtime.submit(&provider_result(
10048            "in-gated",
10049            1_700_000_002_000,
10050            &provider,
10051            vec![
10052                tool_call("call-1", "search", json!({"q": "a"})),
10053                tool_call("call-2", "search", json!({"q": "b"})),
10054            ],
10055        ));
10056        assert_eq!(kinds(&requested), vec![EffectKindTag::RequestApproval]);
10057        let approval = effect_id(requested.step_seq);
10058
10059        let resumed = runtime.submit(&resolved(
10060            "in-approved",
10061            1_700_000_003_000,
10062            &approval,
10063            EffectSuccess::Approval(ApprovalSuccess {
10064                approved_call_ids: vec![CallId::new("call-1").unwrap()],
10065                denied_call_ids: vec![CallId::new("call-2").unwrap()],
10066            }),
10067        ));
10068        assert_eq!(kinds(&resumed), vec![EffectKindTag::ExecuteTools]);
10069        let EffectKind::ExecuteTools(execute) = &sole_effect(&resumed).effect else {
10070            panic!("expected a tool batch");
10071        };
10072        assert_eq!(
10073            execute
10074                .calls
10075                .iter()
10076                .map(|call| call.call_id.as_str())
10077                .collect::<Vec<_>>(),
10078            vec!["call-1"],
10079            "only the approved call reaches a host"
10080        );
10081    }
10082
10083    // fixture: removed-kernel-auto-redispatch
10084    #[test]
10085    fn an_approval_failure_denies_every_gated_call_and_never_re_asks() {
10086        let (mut runtime, provider) = agent_awaiting_approval();
10087        let requested = runtime.submit(&provider_result(
10088            "in-gated",
10089            1_700_000_002_000,
10090            &provider,
10091            vec![tool_call("call-1", "search", json!({"q": "a"}))],
10092        ));
10093        let approval = effect_id(requested.step_seq);
10094
10095        let after = runtime.submit(&failed(
10096            "in-approval-failed",
10097            1_700_000_003_000,
10098            &approval,
10099            HostEffectFailureKind::StorageUnavailable,
10100            "the approval queue is down",
10101        ));
10102        assert!(
10103            !after
10104                .published_effects()
10105                .iter()
10106                .any(|effect| effect.tag() == EffectKindTag::RequestApproval),
10107            "DEC-5 · `retry_approval` is deleted on this wire"
10108        );
10109        assert_eq!(kinds(&after), vec![EffectKindTag::CallProvider]);
10110        assert!(
10111            observation_kinds(&runtime).contains(&"approval_resolution_failed"),
10112            "the failure is a typed audit fact: {:?}",
10113            observation_kinds(&runtime)
10114        );
10115        assert!(
10116            history_text(&runtime)
10117                .iter()
10118                .any(|line| line.contains("permission denied")),
10119            "fail closed: an approval that never arrived approves nothing"
10120        );
10121    }
10122
10123    #[test]
10124    fn a_preempt_resolution_settles_every_attempt_it_names() {
10125        let (mut runtime, spawn_effect) = workflow_with_live_child();
10126        let attempts = vec![
10127            TaskPreemptOutcome {
10128                task_id: TaskId::new("wf-node0").unwrap(),
10129                attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
10130                outcome: TaskPreemptStatus::Preempted(TaskPreempted {}),
10131            },
10132            TaskPreemptOutcome {
10133                task_id: TaskId::new("wf-node1").unwrap(),
10134                attempt_id: WireAttemptId::new("wf-node1:attempt:1").unwrap(),
10135                outcome: TaskPreemptStatus::AlreadyFinished(TaskAlreadyFinished {}),
10136            },
10137        ];
10138        // The preempt effect has no canonical producer yet (its only trigger is the signal /
10139        // cancel path). Publishing it through the driver's own mint is what makes the *resolution*
10140        // contract — the half Task 12 owns — reachable.
10141        let carrier = syscall_carrier("in-preempt-effect", 1_700_000_004_000);
10142        let published = runtime.submit_planned(&carrier, |driver, context| {
10143            let mut index = 0;
10144            let effect = driver.mint_effect(
10145                context,
10146                EffectKind::PreemptTasks(PreemptTasksEffect {
10147                    attempts: vec![TaskAttemptRef {
10148                        task_id: TaskId::new("wf-node0").unwrap(),
10149                        attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
10150                    }],
10151                    reason: "cancelled".to_string(),
10152                }),
10153                &mut index,
10154            );
10155            Ok(PlannedStep {
10156                root_kind: Some(RootKind::Workflow),
10157                focus: driver.focus().cloned(),
10158                observations: Vec::new(),
10159                disposition: StepDisposition::Effects(EffectsDisposition {
10160                    effects: vec![effect],
10161                }),
10162            })
10163        });
10164        let preempt = effect_id(published.step_seq);
10165        assert!(runtime.driver.attempts.contains_key("wf-node0"));
10166
10167        runtime.submit(&resolved(
10168            "in-preempted",
10169            1_700_000_005_000,
10170            &preempt,
10171            EffectSuccess::TasksPreempted(TasksPreemptedSuccess { attempts }),
10172        ));
10173        assert!(
10174            !runtime.driver.attempts.contains_key("wf-node0"),
10175            "a preempted attempt is spent, so a later completion naming it is a stale causation"
10176        );
10177        let _ = spawn_effect;
10178    }
10179
10180    // fixture: removed-kernel-auto-redispatch
10181    #[test]
10182    fn a_preempt_failure_is_an_audit_fact_and_not_a_second_preemption() {
10183        let (mut runtime, _) = workflow_with_live_child();
10184        let carrier = syscall_carrier("in-preempt-effect", 1_700_000_004_000);
10185        let published = runtime.submit_planned(&carrier, |driver, context| {
10186            let mut index = 0;
10187            let effect = driver.mint_effect(
10188                context,
10189                EffectKind::PreemptTasks(PreemptTasksEffect {
10190                    attempts: vec![TaskAttemptRef {
10191                        task_id: TaskId::new("wf-node0").unwrap(),
10192                        attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
10193                    }],
10194                    reason: "cancelled".to_string(),
10195                }),
10196                &mut index,
10197            );
10198            Ok(PlannedStep {
10199                root_kind: Some(RootKind::Workflow),
10200                focus: driver.focus().cloned(),
10201                observations: Vec::new(),
10202                disposition: StepDisposition::Effects(EffectsDisposition {
10203                    effects: vec![effect],
10204                }),
10205            })
10206        });
10207        let preempt = effect_id(published.step_seq);
10208
10209        let after = runtime.submit(&failed(
10210            "in-preempt-failed",
10211            1_700_000_005_000,
10212            &preempt,
10213            HostEffectFailureKind::Unknown,
10214            "the supervisor did not answer",
10215        ));
10216        assert!(
10217            after.published_effects().is_empty(),
10218            "DEC-5 · `retry_preempt` is deleted on this wire"
10219        );
10220        assert!(
10221            observation_kinds(&runtime).contains(&"agent_preempt_failed"),
10222            "{:?}",
10223            observation_kinds(&runtime)
10224        );
10225    }
10226
10227    #[test]
10228    fn a_milestone_verdict_advances_the_contract_it_belongs_to() {
10229        let (mut runtime, provider) = agent_awaiting_milestone_check();
10230        let requested = runtime.submit(&provider_answer(
10231            "in-claim",
10232            1_700_000_002_000,
10233            &provider,
10234            "phase one is done",
10235        ));
10236        assert_eq!(kinds(&requested), vec![EffectKindTag::EvaluateMilestone]);
10237        let milestone = effect_id(requested.step_seq);
10238
10239        let blocked = runtime.submit(&resolved(
10240            "in-verdict",
10241            1_700_000_003_000,
10242            &milestone,
10243            EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
10244                result: WireMilestoneResult {
10245                    phase_id: "collect".to_string(),
10246                    passed: false,
10247                    failed_criteria: vec!["no sources cited".to_string()],
10248                    score: None,
10249                    notes: String::new(),
10250                },
10251            }),
10252        ));
10253        assert_eq!(kinds(&blocked), vec![EffectKindTag::CallProvider]);
10254        assert!(
10255            observation_kinds(&runtime).contains(&"milestone_blocked"),
10256            "{:?}",
10257            observation_kinds(&runtime)
10258        );
10259    }
10260
10261    /// **`EvaluateMilestone`'s canonical producer, end to end** (Task 12 SPEC-ISSUE-4).
10262    ///
10263    /// Contract declared in `verification_contracts` → referenced by the root run spec → loaded as
10264    /// the engine's phase cascade → phase 0 published as an `EvaluateMilestone` → a passing verdict
10265    /// mounts that phase's `unlocks` and moves the cascade to phase 1. Before Task 14 the middle
10266    /// three links did not exist on the wire at all: the effect, its resolution and its failure
10267    /// path were reachable only by reaching into the engine from outside.
10268    #[test]
10269    fn a_declared_contract_drives_the_whole_milestone_cascade() {
10270        let (mut runtime, provider) = agent_awaiting_milestone_check();
10271
10272        // link 3: the cascade is installed, so the first turn's completion asks for phase 0
10273        let requested = runtime.submit(&provider_answer(
10274            "in-claim",
10275            1_700_000_002_000,
10276            &provider,
10277            "sources collected",
10278        ));
10279        assert_eq!(kinds(&requested), vec![EffectKindTag::EvaluateMilestone]);
10280        let EffectKind::EvaluateMilestone(evaluate) = &sole_effect(&requested).effect else {
10281            panic!("expected a milestone request");
10282        };
10283        assert_eq!(
10284            evaluate.request.phase_id, "collect",
10285            "the request names the phase the declared cascade is on"
10286        );
10287        assert_eq!(
10288            evaluate.request.contract_id, "brief-quality-v1",
10289            "a phase id is unique only inside its contract, so the request carries the pair the \
10290             host looks its verifier up by"
10291        );
10292        // and nothing else: criteria, evidence and verifier are host-owned (§5.2)
10293        let request = serde_json::to_value(&evaluate.request).unwrap();
10294        assert_eq!(
10295            request.as_object().unwrap().keys().collect::<Vec<_>>(),
10296            vec!["contract_id", "phase_id"],
10297            "{request}"
10298        );
10299
10300        // link 4: a pass mounts the phase's unlocks and advances to phase 1
10301        let advanced = runtime.submit(&resolved(
10302            "in-verdict",
10303            1_700_000_003_000,
10304            &effect_id(requested.step_seq),
10305            EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
10306                result: WireMilestoneResult {
10307                    phase_id: "collect".to_string(),
10308                    passed: true,
10309                    failed_criteria: Vec::new(),
10310                    score: None,
10311                    notes: String::new(),
10312                },
10313            }),
10314        ));
10315        assert_eq!(kinds(&advanced), vec![EffectKindTag::CallProvider]);
10316        let advance = runtime
10317            .observations()
10318            .iter()
10319            .find_map(|observation| match observation {
10320                KernelObservation::MilestoneAdvanced {
10321                    phase_id,
10322                    capabilities_unlocked,
10323                    ..
10324                } => Some((phase_id.clone(), capabilities_unlocked.clone())),
10325                _ => None,
10326            })
10327            .unwrap_or_else(|| panic!("{:?}", observation_kinds(&runtime)));
10328        assert_eq!(advance.0, "collect");
10329        assert_eq!(
10330            advance.1,
10331            vec!["Tool:search".to_string()],
10332            "the phase's declared unlocks are the ones mounted"
10333        );
10334        assert_eq!(
10335            runtime
10336                .driver
10337                .engine()
10338                .expect("engine")
10339                .current_milestone_phase_id(),
10340            Some("write"),
10341            "the cascade advanced to the next declared phase"
10342        );
10343
10344        // link 5: phase 1 unlocks a *skill*, proving the projection covers both directories
10345        let requested_2 = runtime.submit(&provider_answer(
10346            "in-claim-2",
10347            1_700_000_004_000,
10348            &effect_id(advanced.step_seq),
10349            "brief written",
10350        ));
10351        assert_eq!(kinds(&requested_2), vec![EffectKindTag::EvaluateMilestone]);
10352        runtime.submit(&resolved(
10353            "in-verdict-2",
10354            1_700_000_005_000,
10355            &effect_id(requested_2.step_seq),
10356            EffectSuccess::MilestoneEvaluated(MilestoneEvaluatedSuccess {
10357                result: WireMilestoneResult {
10358                    phase_id: "write".to_string(),
10359                    passed: true,
10360                    failed_criteria: Vec::new(),
10361                    score: None,
10362                    notes: String::new(),
10363                },
10364            }),
10365        ));
10366        let unlocked_by_phase_two =
10367            runtime
10368                .observations()
10369                .iter()
10370                .find_map(|observation| match observation {
10371                    KernelObservation::MilestoneAdvanced {
10372                        capabilities_unlocked,
10373                        ..
10374                    } => Some(capabilities_unlocked.clone()),
10375                    _ => None,
10376                });
10377        assert_eq!(unlocked_by_phase_two, Some(vec!["Skill:debug".to_string()]));
10378    }
10379
10380    #[test]
10381    fn a_run_spec_naming_an_undeclared_contract_is_refused_before_anything_moves() {
10382        // A reference that resolves to nothing is a gate the run believes it has: the agent would
10383        // start with no cascade, never publish an `EvaluateMilestone`, and finish having "passed"
10384        // a contract that was never evaluated.
10385        let mut runtime = Runtime::new();
10386        runtime.submit(&syscall_config_with(|config| {
10387            config.host_effect_support = support_with([EffectKindTag::EvaluateMilestone]);
10388            config.verification_contracts = vec![brief_contract()];
10389        }));
10390        let head_before = runtime.tx.head();
10391
10392        let fault = runtime.reject(&agent_start_under_contract(
10393            "in-start",
10394            1_700_000_001_000,
10395            "brief-quality-v2",
10396        ));
10397        assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
10398        assert!(
10399            fault.message.contains("brief-quality-v2"),
10400            "{}",
10401            fault.message
10402        );
10403        assert_eq!(runtime.tx.head(), head_before, "nothing moved");
10404        assert!(
10405            runtime.driver.root_kind().is_none(),
10406            "the operation is still free to start with a spec that resolves"
10407        );
10408
10409        // …and the same spec with the declared id starts normally
10410        runtime.submit(&agent_start_under_contract(
10411            "in-start-ok",
10412            1_700_000_001_500,
10413            "brief-quality-v1",
10414        ));
10415        assert_eq!(runtime.driver.root_kind(), Some(RootKind::Agent));
10416    }
10417
10418    #[test]
10419    fn a_workflow_node_may_not_name_an_undeclared_contract_either() {
10420        // Same rule wherever a `LogicalAgentSpec` enters: a root, a DAG node, an authored append.
10421        let mut runtime = Runtime::new();
10422        runtime.submit(&syscall_config_with(|config| {
10423            config.host_effect_support = support_with([EffectKindTag::EvaluateMilestone]);
10424            config.verification_contracts = vec![brief_contract()];
10425        }));
10426        let mut spec = two_node_spec();
10427        spec.nodes[1].run_spec = Some(LogicalAgentSpec {
10428            verification_contract_id: Some("no-such-contract".to_string()),
10429            ..LogicalAgentSpec::new("write the brief")
10430        });
10431        let fault = runtime.reject(&workflow_start("in-start", 1_700_000_001_000, spec));
10432        assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
10433        assert!(
10434            fault.message.contains("no-such-contract"),
10435            "{}",
10436            fault.message
10437        );
10438    }
10439
10440    // fixture: removed-kernel-auto-redispatch
10441    #[test]
10442    fn a_milestone_that_could_not_be_evaluated_terminates_instead_of_advancing() {
10443        let (mut runtime, provider) = agent_awaiting_milestone_check();
10444        let requested = runtime.submit(&provider_answer(
10445            "in-claim",
10446            1_700_000_002_000,
10447            &provider,
10448            "phase one is done",
10449        ));
10450        let milestone = effect_id(requested.step_seq);
10451
10452        let ended = runtime.submit(&failed(
10453            "in-verifier-down",
10454            1_700_000_003_000,
10455            &milestone,
10456            HostEffectFailureKind::StorageUnavailable,
10457            "the verifier could not be reached",
10458        ));
10459        assert!(ended.published_effects().is_empty());
10460        let KernelTerminal::Failed(failure) = ended.terminal().expect("a terminal was committed")
10461        else {
10462            panic!("expected a failed terminal, got {:?}", ended.terminal());
10463        };
10464        assert_eq!(failure.failure.code, KernelFailureCode::HostEffectFailed);
10465        assert!(
10466            failure.failure.message.contains("evaluate_milestone"),
10467            "{}",
10468            failure.failure.message
10469        );
10470    }
10471
10472    // ----- slice 3 · memory / page-out ----------------------------------------------------------
10473
10474    /// §22.13 · a receipt is a locator, never a rewrite of the record the kernel authored.
10475    #[test]
10476    fn a_memory_receipt_cannot_restate_what_the_kernel_authored() {
10477        let (mut runtime, effect) = agent_awaiting_memory_write();
10478        let settled = runtime.submit(&resolved(
10479            "in-persisted",
10480            1_700_000_003_000,
10481            &effect,
10482            EffectSuccess::MemoryPersisted(MemoryPersistedSuccess {
10483                receipt: MemoryPersistReceipt {
10484                    binding_id: MemoryBindingId::new("some-other-binding").unwrap(),
10485                    record_ref: MemoryRecordRef::new("rec-7").unwrap(),
10486                    digest: Digest::new("sha256:".to_string() + &"0".repeat(64)).unwrap(),
10487                },
10488            }),
10489        ));
10490        assert!(
10491            settled.published_effects().is_empty(),
10492            "a persisted record is a fact, not a new obligation"
10493        );
10494        let written = runtime
10495            .observations()
10496            .iter()
10497            .find_map(|observation| match observation {
10498                KernelObservation::MemoryWritten {
10499                    record_id,
10500                    scope,
10501                    name,
10502                    memory_kind,
10503                    size_bytes,
10504                    ..
10505                } => Some((
10506                    record_id.clone(),
10507                    scope.clone(),
10508                    name.clone(),
10509                    *memory_kind,
10510                    *size_bytes,
10511                )),
10512                _ => None,
10513            })
10514            .expect("the resolution records the write");
10515        assert_eq!(written.0, "rec-7", "the host contributes its own locator");
10516        assert_eq!(
10517            written.1.namespace, "mem-binding-1",
10518            "and nothing else: the binding is the one the operation holds, not the one echoed back"
10519        );
10520        assert_eq!(written.2, "brief-style");
10521        assert_eq!(written.3, crate::mm::memory::MemoryKind::Project);
10522        assert_eq!(written.4, "prefers numbered sections".len() as u32);
10523    }
10524
10525    #[test]
10526    fn a_memory_write_failure_names_the_intent_the_kernel_authored() {
10527        let (mut runtime, effect) = agent_awaiting_memory_write();
10528        let settled = runtime.submit(&failed(
10529            "in-persist-failed",
10530            1_700_000_003_000,
10531            &effect,
10532            HostEffectFailureKind::StorageUnavailable,
10533            "the memory store is offline",
10534        ));
10535        assert!(settled.published_effects().is_empty());
10536        assert!(
10537            observation_kinds(&runtime).contains(&"memory_write_failed"),
10538            "{:?}",
10539            observation_kinds(&runtime)
10540        );
10541    }
10542
10543    #[test]
10544    fn a_memory_recall_enters_context_before_the_turn_resumes() {
10545        let (mut runtime, provider) = agent_awaiting_provider();
10546        let queried = runtime.submit(&provider_result(
10547            "in-memory",
10548            1_700_000_002_000,
10549            &provider,
10550            vec![tool_call(
10551                "call-1",
10552                crate::context::manager::MEMORY_TOOL_NAME,
10553                json!({"query": "prior briefs", "top_k": 9}),
10554            )],
10555        ));
10556        let query_effect = sole_effect(&queried);
10557        let EffectKind::QueryMemory(query) = &query_effect.effect else {
10558            panic!("expected a memory query");
10559        };
10560        assert_eq!(
10561            query.requested_k, 4,
10562            "retrieval width is the operation's policy, clamped — a model cannot widen it"
10563        );
10564        let effect = query_effect.effect_id.clone();
10565
10566        let resumed = runtime.submit(&resolved(
10567            "in-recalls",
10568            1_700_000_003_000,
10569            &effect,
10570            EffectSuccess::MemoryQueried(MemoryQueriedSuccess {
10571                recalls: vec![MemoryRecall {
10572                    record_ref: MemoryRecordRef::new("rec-1").unwrap(),
10573                    name: "brief-style".to_string(),
10574                    kind: SyscallMemoryKind::Project,
10575                    content: "prefers numbered sections".to_string(),
10576                    score: None,
10577                }],
10578            }),
10579        ));
10580        assert_eq!(kinds(&resumed), vec![EffectKindTag::CallProvider]);
10581        assert!(
10582            history_text(&runtime)
10583                .iter()
10584                .any(|line| line.contains("prefers numbered sections")),
10585            "the recall is in the context the resumed turn renders"
10586        );
10587        assert!(
10588            observation_kinds(&runtime).contains(&"memory_queried"),
10589            "{:?}",
10590            observation_kinds(&runtime)
10591        );
10592    }
10593
10594    #[test]
10595    fn a_memory_query_failure_resumes_the_turn_without_recalls() {
10596        let (mut runtime, provider) = agent_awaiting_provider();
10597        let queried = runtime.submit(&provider_result(
10598            "in-memory",
10599            1_700_000_002_000,
10600            &provider,
10601            vec![tool_call(
10602                "call-1",
10603                crate::context::manager::MEMORY_TOOL_NAME,
10604                json!({"query": "prior briefs"}),
10605            )],
10606        ));
10607        let effect = effect_id(queried.step_seq);
10608
10609        let resumed = runtime.submit(&failed(
10610            "in-query-failed",
10611            1_700_000_003_000,
10612            &effect,
10613            HostEffectFailureKind::StorageUnavailable,
10614            "the memory store is offline",
10615        ));
10616        assert_eq!(
10617            kinds(&resumed),
10618            vec![EffectKindTag::CallProvider],
10619            "a store that could not answer is not a reason to stall the run"
10620        );
10621        assert!(
10622            observation_kinds(&runtime).contains(&"memory_query_failed"),
10623            "{:?}",
10624            observation_kinds(&runtime)
10625        );
10626    }
10627
10628    #[test]
10629    fn a_page_out_archive_holds_the_continuation_until_the_host_commits_it() {
10630        let (mut runtime, archive) = agent_awaiting_page_out();
10631        let EffectKind::ArchivePageOut(published) = &runtime
10632            .tx
10633            .pending_effects()
10634            .find(|effect| effect.effect_id == archive)
10635            .expect("the archive is pending")
10636            .effect
10637            .clone()
10638        else {
10639            panic!("expected a page-out effect");
10640        };
10641
10642        let resumed = runtime.submit(&resolved(
10643            "in-archived",
10644            1_700_000_004_000,
10645            &archive,
10646            EffectSuccess::PageOutArchived(PageOutArchivedSuccess {
10647                receipt: ArchiveReceipt {
10648                    handle_id: published.handle_id.clone(),
10649                    payload_ref: PayloadRef::new("blob-1").unwrap(),
10650                    digest: published.payload.digest.clone(),
10651                    original_size: published.payload.original_size,
10652                },
10653            }),
10654        ));
10655        assert_eq!(
10656            kinds(&resumed),
10657            vec![EffectKindTag::CallProvider],
10658            "the provider retry the compaction deferred is released by the archive's commit"
10659        );
10660        assert!(
10661            observation_kinds(&runtime).contains(&"page_out_archived"),
10662            "{:?}",
10663            observation_kinds(&runtime)
10664        );
10665    }
10666
10667    #[test]
10668    fn an_archive_receipt_for_another_body_is_refused() {
10669        let (mut runtime, archive) = agent_awaiting_page_out();
10670        let fault = runtime.reject(&resolved(
10671            "in-wrong-archive",
10672            1_700_000_004_000,
10673            &archive,
10674            EffectSuccess::PageOutArchived(PageOutArchivedSuccess {
10675                receipt: ArchiveReceipt {
10676                    handle_id: HandleId::new("some-other-handle").unwrap(),
10677                    payload_ref: PayloadRef::new("blob-1").unwrap(),
10678                    digest: Digest::new("sha256:".to_string() + &"1".repeat(64)).unwrap(),
10679                    original_size: WireU64::new(1),
10680                },
10681            }),
10682        ));
10683        assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
10684    }
10685
10686    // fixture: removed-kernel-auto-redispatch
10687    #[test]
10688    fn a_failed_archive_is_abandoned_and_the_run_stays_live() {
10689        let (mut runtime, archive) = agent_awaiting_page_out();
10690        let resumed = runtime.submit(&failed(
10691            "in-archive-failed",
10692            1_700_000_004_000,
10693            &archive,
10694            HostEffectFailureKind::StorageUnavailable,
10695            "the blob store is offline",
10696        ));
10697        assert_eq!(
10698            kinds(&resumed),
10699            vec![EffectKindTag::CallProvider],
10700            "DEC-5 · the archive is abandoned once; the compaction it belongs to already happened, \
10701             so the run continues degraded rather than dying on a best-effort durability effect"
10702        );
10703        assert!(
10704            observation_kinds(&runtime).contains(&"page_out_archive_failed"),
10705            "{:?}",
10706            observation_kinds(&runtime)
10707        );
10708    }
10709
10710    /// A load outcome that answers no pending `LoadPayload` effect fails closed: since Task 13 the
10711    /// producer is the P1 `PageIn { handle_id }` syscall, and a resolution is only reducible
10712    /// against the effect it actually answers.
10713    #[test]
10714    fn a_payload_load_outcome_is_refused_with_its_reason() {
10715        let (mut runtime, provider) = agent_awaiting_provider();
10716        let fault = runtime.reject(&resolved(
10717            "in-loaded",
10718            1_700_000_002_000,
10719            &provider,
10720            EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
10721                handle_id: HandleId::new("call-1").unwrap(),
10722                payload: InlinePayload {
10723                    content: "body".to_string(),
10724                    digest: Digest::new("sha256:".to_string() + &"2".repeat(64)).unwrap(),
10725                    original_size: WireU64::new(4),
10726                },
10727            }),
10728        ));
10729        // the transaction refuses it first: a provider effect does not accept a payload result
10730        assert_eq!(fault.code, KernelFaultCode::UnexpectedEffectOutcome);
10731    }
10732
10733    // ----- fixtures the failure vocabulary itself has to satisfy --------------------------------
10734
10735    // fixture: removed-cancellation-as-effect-failure
10736    #[test]
10737    fn the_failure_vocabulary_cannot_express_a_cancellation() {
10738        for kind in HostEffectFailureKind::ALL {
10739            let label = kind.as_str();
10740            assert!(
10741                !label.contains("cancel"),
10742                "cancellation is a control-plane fact and only reaches the kernel through \
10743                 HostControl::Cancel; {label} would give one pending effect two meanings"
10744            );
10745        }
10746        assert_eq!(HostEffectFailureKind::ALL.len(), 6);
10747    }
10748
10749    // ----- arcs the tests above start from ------------------------------------------------------
10750
10751    fn agent_start_with_history(id: &str, at: u64, messages: usize) -> WireEnvelope {
10752        envelope(
10753            id,
10754            at,
10755            KernelInput::StartOperation(StartOperation {
10756                entry: RootEntry::Agent(RootAgentEntry {
10757                    task: LogicalTask::new("write the research brief"),
10758                    run_spec: None,
10759                }),
10760                initial_context: InitialContext {
10761                    messages: (0..messages)
10762                        .map(|index| super::super::root::LogicalMessage {
10763                            role: if index % 2 == 0 {
10764                                MessageRole::User
10765                            } else {
10766                                MessageRole::Assistant
10767                            },
10768                            content: format!(
10769                                "turn {index}: a long enough body that compaction has something \
10770                                 to reclaim when the prompt stops fitting"
10771                            ),
10772                            tokens: Some(64),
10773                            tool_call_id: None,
10774                        })
10775                        .collect(),
10776                    ..InitialContext::default()
10777                },
10778            }),
10779        )
10780    }
10781
10782    /// An agent whose governance policy gates `search` behind an approval.
10783    fn agent_awaiting_approval() -> (Runtime, EffectId) {
10784        use crate::runtime::kernel::wire::command::{GovernancePolicy, PolicyAction, PolicyRule};
10785
10786        let mut runtime = Runtime::new();
10787        runtime.submit(&syscall_config_with(|config| {
10788            config.host_effect_support = support_with([EffectKindTag::RequestApproval]);
10789            config.governance_policy = Some(GovernancePolicy {
10790                rules: vec![PolicyRule {
10791                    tool_pattern: "search".to_string(),
10792                    action: PolicyAction::AskUser,
10793                }],
10794                ..GovernancePolicy::default()
10795            });
10796        }));
10797        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
10798        (runtime, sole_effect(&started).effect_id.clone())
10799    }
10800
10801    /// The two-phase skeleton the milestone arcs run on: `collect` unlocks the `search` tool,
10802    /// `write` unlocks the `debug` skill — one of each capability directory, so the projection is
10803    /// exercised in both directions.
10804    fn brief_contract() -> WireVerificationContract {
10805        WireVerificationContract {
10806            contract_id: "brief-quality-v1".to_string(),
10807            phases: vec![
10808                WireMilestonePhase {
10809                    phase_id: "collect".to_string(),
10810                    unlocks: vec!["search".to_string()],
10811                },
10812                WireMilestonePhase {
10813                    phase_id: "write".to_string(),
10814                    unlocks: vec!["debug".to_string()],
10815                },
10816            ],
10817        }
10818    }
10819
10820    fn agent_start_under_contract(id: &str, at: u64, contract_id: &str) -> WireEnvelope {
10821        envelope(
10822            id,
10823            at,
10824            KernelInput::StartOperation(StartOperation {
10825                entry: RootEntry::Agent(RootAgentEntry {
10826                    task: LogicalTask::new("write the research brief"),
10827                    run_spec: Some(LogicalAgentSpec {
10828                        verification_contract_id: Some(contract_id.to_string()),
10829                        ..LogicalAgentSpec::new("write the research brief")
10830                    }),
10831                }),
10832                initial_context: InitialContext::default(),
10833            }),
10834        )
10835    }
10836
10837    /// An agent carrying a two-phase milestone contract, declared the canonical way: the
10838    /// operation's `verification_contracts` catalog holds the skeleton and the root run spec
10839    /// references it by id (Task 14 · closes Task 12's SPEC-ISSUE-4, where nothing on the wire
10840    /// could make the kernel ask for a verdict).
10841    fn agent_awaiting_milestone_check() -> (Runtime, EffectId) {
10842        let mut runtime = Runtime::new();
10843        runtime.submit(&syscall_config_with(|config| {
10844            config.host_effect_support = support_with([EffectKindTag::EvaluateMilestone]);
10845            config.verification_contracts = vec![brief_contract()];
10846        }));
10847        let started = runtime.submit(&agent_start_under_contract(
10848            "in-start",
10849            1_700_000_001_000,
10850            "brief-quality-v1",
10851        ));
10852        (runtime, sole_effect(&started).effect_id.clone())
10853    }
10854
10855    /// An agent that has published a `PersistMemory` effect through the child→parent request path.
10856    fn agent_awaiting_memory_write() -> (Runtime, EffectId) {
10857        use crate::runtime::kernel::wire::syscall::{
10858            MemoryWriteProposal, RequestMemoryWriteRequest,
10859        };
10860
10861        let mut runtime = Runtime::new();
10862        runtime.submit(&syscall_config());
10863        let started = runtime.submit(&workflow_start(
10864            "in-start",
10865            1_700_000_001_000,
10866            two_node_spec(),
10867        ));
10868        runtime.submit(&spawned(
10869            "in-ack",
10870            1_700_000_002_000,
10871            &effect_id(started.step_seq),
10872            &["wf-node0"],
10873        ));
10874        let completed = runtime.submit(&child_done_with(
10875            "in-done",
10876            1_700_000_002_500,
10877            "wf-node0",
10878            "wf-node0:attempt:1",
10879            vec![SyscallRequest::RequestMemoryWrite(
10880                RequestMemoryWriteRequest {
10881                    proposal: MemoryWriteProposal {
10882                        name: "brief-style".to_string(),
10883                        kind: SyscallMemoryKind::Project,
10884                        content: "prefers numbered sections".to_string(),
10885                        description: String::new(),
10886                        evidence_refs: Vec::new(),
10887                    },
10888                },
10889            )],
10890        ));
10891        let effect = completed
10892            .published_effects()
10893            .iter()
10894            .find(|effect| effect.tag() == EffectKindTag::PersistMemory)
10895            .expect("the child's request published a memory write")
10896            .effect_id
10897            .clone();
10898        (runtime, effect)
10899    }
10900
10901    /// An agent whose context overflowed hard enough to compact, so a page-out archive is pending
10902    /// and the provider retry it deferred is waiting behind it.
10903    fn agent_awaiting_page_out() -> (Runtime, EffectId) {
10904        let mut runtime = Runtime::new();
10905        runtime.submit(&syscall_config_with(|config| {
10906            config.host_effect_support = support_with([EffectKindTag::ArchivePageOut]);
10907        }));
10908        let started = runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
10909        let provider = sole_effect(&started).effect_id.clone();
10910        let compacted = runtime.submit(&provider_overflow(
10911            "in-overflow",
10912            1_700_000_002_000,
10913            &provider,
10914        ));
10915        let archive = compacted
10916            .published_effects()
10917            .iter()
10918            .find(|effect| effect.tag() == EffectKindTag::ArchivePageOut)
10919            .expect("the compaction externalised its archive")
10920            .effect_id
10921            .clone();
10922        (runtime, archive)
10923    }
10924
10925    /// A workflow root with its first node launched and acknowledged, so a live attempt exists.
10926    fn workflow_with_live_child() -> (Runtime, EffectId) {
10927        let mut runtime = Runtime::new();
10928        runtime.submit(&syscall_config());
10929        let started = runtime.submit(&workflow_start(
10930            "in-start",
10931            1_700_000_001_000,
10932            two_node_spec(),
10933        ));
10934        let spawn = effect_id(started.step_seq);
10935        runtime.submit(&spawned("in-ack", 1_700_000_002_000, &spawn, &["wf-node0"]));
10936        (runtime, spawn)
10937    }
10938
10939    // -----------------------------------------------------------------------------------------
10940    // §7.5 · host control plane, and §7.7 · signal delivery (Task 12b)
10941    // -----------------------------------------------------------------------------------------
10942
10943    fn control(id: &str, at: u64, command: HostCommand) -> WireEnvelope {
10944        envelope(
10945            id,
10946            at,
10947            KernelInput::HostControl(super::super::envelope::HostControl { command }),
10948        )
10949    }
10950
10951    fn cancel_with(id: &str, at: u64, reason: CancellationReason) -> WireEnvelope {
10952        control(
10953            id,
10954            at,
10955            HostCommand::Cancel(CancelCommand {
10956                reason,
10957                pending_call_ids: Vec::new(),
10958            }),
10959        )
10960    }
10961
10962    fn cancel(id: &str, at: u64) -> WireEnvelope {
10963        cancel_with(id, at, CancellationReason::User)
10964    }
10965
10966    /// One signal delivery. `delivery` and `signal` are separate arguments precisely because they
10967    /// are separate identities (§7.7).
10968    fn signal_delivery(
10969        id: &str,
10970        at: u64,
10971        delivery: &str,
10972        attempt: u32,
10973        signal: LogicalSignal,
10974    ) -> WireEnvelope {
10975        envelope(
10976            id,
10977            at,
10978            KernelInput::DeliverExternalEvent(DeliverExternalEvent {
10979                event: ExternalEvent::DeliverSignal(DeliverSignal {
10980                    delivery_id: DeliveryId::new(delivery).unwrap(),
10981                    attempt,
10982                    signal,
10983                }),
10984            }),
10985        )
10986    }
10987
10988    fn logical_signal(id: &str, urgency: SignalUrgency) -> LogicalSignal {
10989        LogicalSignal {
10990            urgency: Some(urgency),
10991            ..LogicalSignal::new(SignalId::new(id).unwrap())
10992        }
10993    }
10994
10995    /// A configuration with a signal policy, so queue capacity and TTL are the operation's own
10996    /// facts rather than a compile-time default.
10997    fn signal_config(queue_max: u32, ttl_ms: Option<u64>) -> WireEnvelope {
10998        syscall_config_with(|config| {
10999            config.signal_policy = Some(super::super::command::SignalPolicy {
11000                queue_max,
11001                ttl_ms: ttl_ms.map(WireU64::new),
11002                deadline_escalation: None,
11003            });
11004        })
11005    }
11006
11007    // fixture: cancel-flows-only-through-host-control
11008    #[test]
11009    fn cancellation_enters_only_through_the_control_plane_and_never_as_a_failure() {
11010        // an effect failure on the very call a cancel would abandon is a *failed* terminal, and no
11011        // wire shape lets it become a cancellation
11012        let (mut runtime, provider) = agent_awaiting_provider();
11013        let failed_out = runtime.submit(&failed(
11014            "in-transport-dead",
11015            1_700_000_002_000,
11016            &provider,
11017            HostEffectFailureKind::TransportExhausted,
11018            "the vendor gave up",
11019        ));
11020        assert!(
11021            matches!(failed_out.terminal(), Some(KernelTerminal::Failed(_))),
11022            "a host effect failure is never a cancellation (§14.3)"
11023        );
11024
11025        // the control plane is the path that produces a cancellation
11026        let (mut runtime, _) = agent_awaiting_provider();
11027        let cancelled = runtime.submit(&cancel_with(
11028            "in-cancel",
11029            1_700_000_002_000,
11030            CancellationReason::Deadline,
11031        ));
11032        let Some(KernelTerminal::Cancelled(terminal)) = cancelled.terminal() else {
11033            panic!(
11034                "expected a cancelled terminal, got {:?}",
11035                cancelled.terminal()
11036            );
11037        };
11038        assert_eq!(
11039            terminal.reason,
11040            CancellationReason::Deadline,
11041            "the reason is the host's, not the loop's internal user-abort"
11042        );
11043        assert!(
11044            observation_kinds(&runtime).contains(&"operation_cancelled"),
11045            "{:?}",
11046            observation_kinds(&runtime)
11047        );
11048
11049        // identity comes from the envelope alone: a cancel that repeats it does not decode, and a
11050        // cancel addressed at another operation never reaches the driver
11051        assert!(
11052            serde_json::from_value::<CancelCommand>(
11053                json!({ "reason": "user", "operation_id": "op-driver-1" })
11054            )
11055            .is_err(),
11056            "the envelope owns the operation id (§7.5)"
11057        );
11058        let (mut runtime, _) = agent_awaiting_provider();
11059        let mut foreign = cancel("in-foreign", 1_700_000_002_000);
11060        foreign.operation_id = OperationId::new("op-someone-else").unwrap();
11061        assert_eq!(
11062            runtime.reject(&foreign).code,
11063            KernelFaultCode::OperationMismatch
11064        );
11065    }
11066
11067    // fixture: cancel-order-is-downstream-first
11068    #[test]
11069    fn cancellation_settles_every_downstream_wait_before_it_commits_the_root_terminal() {
11070        let (mut runtime, _) = workflow_with_live_child();
11071        assert!(
11072            runtime.driver.attempts.contains_key("wf-node0"),
11073            "the arc starts with a live child attempt"
11074        );
11075
11076        let cancelled = runtime.submit(&cancel("in-cancel", 1_700_000_003_000));
11077
11078        // downstream first, inside this one transition
11079        assert_eq!(
11080            runtime
11081                .driver
11082                .engine()
11083                .and_then(|engine| engine.task_lifecycle("wf-node0"))
11084                .map(TaskLifecycle::is_terminal),
11085            Some(true),
11086            "a running child is settled by the same step that cancels its parent"
11087        );
11088        assert!(
11089            runtime.driver.attempts.is_empty(),
11090            "a settled attempt is spent, so nothing downstream can be resumed by a late completion"
11091        );
11092        assert_eq!(
11093            runtime.pending_effect_kinds(),
11094            Vec::<EffectKindTag>::new(),
11095            "§11.1 · the cancelling step leaves nothing waiting on the host"
11096        );
11097
11098        // and the root terminal is what that same step committed
11099        assert!(matches!(
11100            cancelled.terminal(),
11101            Some(KernelTerminal::Cancelled(_))
11102        ));
11103        assert!(
11104            cancelled.published_effects().is_empty(),
11105            "§7.12 · effects or a terminal, never both"
11106        );
11107
11108        // the child's late completion cannot revive the operation
11109        assert_eq!(
11110            runtime
11111                .reject(&child_done(
11112                    "in-late-done",
11113                    1_700_000_004_000,
11114                    "wf-node0",
11115                    "finished anyway"
11116                ))
11117                .code,
11118            KernelFaultCode::InvalidLifecycle,
11119        );
11120    }
11121
11122    // fixture: cancel-is-idempotent
11123    #[test]
11124    fn a_second_cancellation_commits_no_second_terminal() {
11125        let (mut runtime, _) = agent_awaiting_provider();
11126        let first = runtime.submit(&cancel("in-cancel", 1_700_000_002_000));
11127        let head = runtime.tx.head().map(|head| head.step_seq);
11128
11129        // a re-issued cancellation under a fresh input id replays the record that already
11130        // committed the terminal (§18.3) — no second record, no second terminal, no new effect
11131        let replay = runtime.prepare(&cancel("in-cancel-again", 1_700_000_003_000));
11132        assert_eq!(replay.step_seq(), Some(first.step_seq));
11133        assert!(
11134            replay.record().unwrap().record_digest() == first.record.record_digest(),
11135            "the replay names the existing record"
11136        );
11137        assert_eq!(
11138            runtime.tx.head().map(|head| head.step_seq),
11139            head,
11140            "a replay moves no head"
11141        );
11142
11143        // a cancellation that says something *different* is a conflict, not an overwrite
11144        assert_eq!(
11145            runtime
11146                .reject(&cancel_with(
11147                    "in-cancel-other",
11148                    1_700_000_003_000,
11149                    CancellationReason::LeaseLost
11150                ))
11151                .code,
11152            KernelFaultCode::DuplicateInputConflict,
11153        );
11154    }
11155
11156    // fixture: cancel-terminal-rejects-all-state-changing-input
11157    #[test]
11158    fn a_terminal_refuses_every_state_changing_input_including_signal_delivery() {
11159        let (mut runtime, provider) = agent_awaiting_provider();
11160        runtime.submit(&cancel("in-cancel", 1_700_000_002_000));
11161
11162        let head = runtime.tx.head().map(|head| head.step_seq);
11163        let queue_before = signal_queue_depth(&runtime);
11164        let journal_len = runtime.journal.len();
11165
11166        for envelope in [
11167            signal_delivery(
11168                "in-late-signal",
11169                1_700_000_003_000,
11170                "delivery-late",
11171                1,
11172                logical_signal("sig-late", SignalUrgency::Critical),
11173            ),
11174            child_done("in-late-child", 1_700_000_003_000, "wf-node0", "done"),
11175            provider_answer("in-late-answer", 1_700_000_003_000, &provider, "too late"),
11176            agent_start("in-restart", 1_700_000_003_000),
11177            control(
11178                "in-late-compact",
11179                1_700_000_003_000,
11180                HostCommand::ForceCompact(super::super::command::ForceCompactCommand {}),
11181            ),
11182            control(
11183                "in-late-task",
11184                1_700_000_003_000,
11185                HostCommand::UpdateTask(UpdateTaskCommand {
11186                    update: WireTaskUpdate {
11187                        progress: Some("still going".to_string()),
11188                        ..WireTaskUpdate::default()
11189                    },
11190                }),
11191            ),
11192        ] {
11193            let input_id = envelope.input_id.clone();
11194            assert_eq!(
11195                runtime.reject(&envelope).code,
11196                KernelFaultCode::InvalidLifecycle,
11197                "{input_id} must be refused after the terminal",
11198            );
11199        }
11200
11201        assert_eq!(
11202            runtime.tx.head().map(|head| head.step_seq),
11203            head,
11204            "no step sequence advanced"
11205        );
11206        assert_eq!(runtime.journal.len(), journal_len, "nothing was journaled");
11207        assert_eq!(
11208            signal_queue_depth(&runtime),
11209            queue_before,
11210            "a refused signal never reaches the queue"
11211        );
11212        assert!(
11213            runtime.driver.poison().is_none(),
11214            "a typed rejection is not a driver failure"
11215        );
11216    }
11217
11218    fn signal_queue_depth(runtime: &Runtime) -> usize {
11219        runtime
11220            .driver
11221            .engine()
11222            .map(LoopStateMachine::signal_queue_depth)
11223            .unwrap_or(0)
11224    }
11225
11226    // fixture: signal-delivery-identity-is-distinct
11227    #[test]
11228    fn delivery_identity_and_signal_identity_are_two_separate_facts() {
11229        let mut runtime = Runtime::new();
11230        runtime.submit(&signal_config(8, None));
11231        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11232
11233        let first = signal_delivery(
11234            "in-sig-1",
11235            1_700_000_002_000,
11236            "delivery-a",
11237            1,
11238            LogicalSignal {
11239                dedupe_key: Some("nightly".to_string()),
11240                ..logical_signal("sig-nightly", SignalUrgency::Normal)
11241            },
11242        );
11243        runtime.submit(&first);
11244        assert_eq!(
11245            dispositions(&runtime),
11246            vec![(
11247                "queue".to_string(),
11248                "sig-nightly".to_string(),
11249                "delivery-a".to_string(),
11250                1
11251            )],
11252            "the audit fact names the caller's own signal id, never a minted one"
11253        );
11254
11255        // the same delivery, retried: the envelope's idempotency key answers it as a replay, so the
11256        // signal is not disposed of twice
11257        let depth = signal_queue_depth(&runtime);
11258        let replay = runtime.prepare(&first);
11259        assert!(matches!(
11260            replay,
11261            super::super::fault::KernelPreparation::Replayed(_)
11262        ));
11263        assert_eq!(signal_queue_depth(&runtime), depth);
11264
11265        // a *new* delivery of the same business signal is a distinct delivery attempt, and the
11266        // business dedupe key is what stops it becoming a second queued signal
11267        runtime.submit(&signal_delivery(
11268            "in-sig-2",
11269            1_700_000_003_000,
11270            "delivery-b",
11271            2,
11272            LogicalSignal {
11273                dedupe_key: Some("nightly".to_string()),
11274                ..logical_signal("sig-nightly", SignalUrgency::Normal)
11275            },
11276        ));
11277        assert_eq!(
11278            dispositions(&runtime),
11279            vec![(
11280                "ignore".to_string(),
11281                "sig-nightly".to_string(),
11282                "delivery-b".to_string(),
11283                2
11284            )],
11285            "a redelivery is recognisable as the same signal and a different delivery"
11286        );
11287        assert_eq!(
11288            signal_queue_depth(&runtime),
11289            depth,
11290            "and it does not queue a second copy"
11291        );
11292
11293        // a delivery that cannot say which attempt it is fails closed
11294        assert_eq!(
11295            runtime
11296                .reject(&signal_delivery(
11297                    "in-sig-0",
11298                    1_700_000_004_000,
11299                    "delivery-c",
11300                    0,
11301                    logical_signal("sig-other", SignalUrgency::Normal),
11302                ))
11303                .code,
11304            KernelFaultCode::MalformedEnvelope,
11305        );
11306    }
11307
11308    // fixture: signal-admission-uses-accepted-time
11309    #[test]
11310    fn signal_ttl_is_measured_from_the_accepted_envelope_time() {
11311        let mut runtime = Runtime::new();
11312        runtime.submit(&signal_config(8, Some(60_000)));
11313        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11314
11315        // a source timestamp far older than the TTL — if admission read it, this signal would be
11316        // born expired
11317        runtime.submit(&signal_delivery(
11318            "in-sig-1",
11319            1_700_000_002_000,
11320            "delivery-a",
11321            1,
11322            LogicalSignal {
11323                source_timestamp_ms: Some(WireU64::new(1_600_000_000_000)),
11324                ..logical_signal("sig-stale-source", SignalUrgency::Normal)
11325            },
11326        ));
11327        assert_eq!(
11328            dispositions(&runtime)
11329                .iter()
11330                .map(|(disposition, ..)| disposition.clone())
11331                .collect::<Vec<_>>(),
11332            vec!["queue".to_string()],
11333            "admission uses the accepted envelope time; the source timestamp is metadata"
11334        );
11335        assert_eq!(signal_queue_depth(&runtime), 1);
11336
11337        // and expiry is measured on the same clock: the next accepted input is far enough past the
11338        // *accepted* time of the first signal to expire it
11339        runtime.submit(&signal_delivery(
11340            "in-sig-2",
11341            1_700_000_200_000,
11342            "delivery-b",
11343            1,
11344            logical_signal("sig-fresh", SignalUrgency::Normal),
11345        ));
11346        assert!(
11347            observation_kinds(&runtime).contains(&"signal_expired"),
11348            "{:?}",
11349            observation_kinds(&runtime)
11350        );
11351        assert_eq!(
11352            signal_queue_depth(&runtime),
11353            1,
11354            "the stale signal left, the fresh one stayed"
11355        );
11356    }
11357
11358    // fixture: signal-target-is-operation-or-task
11359    #[test]
11360    fn a_signal_addresses_the_operation_or_one_of_its_own_tasks() {
11361        let (mut runtime, _) = workflow_with_live_child();
11362
11363        // the operation itself
11364        runtime.submit(&signal_delivery(
11365            "in-sig-op",
11366            1_700_000_003_000,
11367            "delivery-a",
11368            1,
11369            logical_signal("sig-op", SignalUrgency::Normal),
11370        ));
11371        assert_eq!(dispositions(&runtime).len(), 1);
11372
11373        // one of its own logical tasks
11374        runtime.submit(&signal_delivery(
11375            "in-sig-task",
11376            1_700_000_004_000,
11377            "delivery-b",
11378            1,
11379            LogicalSignal {
11380                target: SignalTarget::Task(super::super::event::TaskTarget {
11381                    task_id: TaskId::new("wf-node0").unwrap(),
11382                }),
11383                ..logical_signal("sig-task", SignalUrgency::Normal)
11384            },
11385        ));
11386        assert_eq!(dispositions(&runtime).len(), 1);
11387
11388        // a task this operation does not have
11389        assert_eq!(
11390            runtime
11391                .reject(&signal_delivery(
11392                    "in-sig-ghost",
11393                    1_700_000_005_000,
11394                    "delivery-c",
11395                    1,
11396                    LogicalSignal {
11397                        target: SignalTarget::Task(super::super::event::TaskTarget {
11398                            task_id: TaskId::new("ghost").unwrap(),
11399                        }),
11400                        ..logical_signal("sig-ghost", SignalUrgency::Normal)
11401                    },
11402                ))
11403                .code,
11404            KernelFaultCode::InvalidAuthority,
11405        );
11406
11407        // a host session is not an address, and the wire has no slot for one
11408        assert!(
11409            serde_json::from_value::<SignalTarget>(
11410                json!({ "kind": "task", "task_id": "wf-node0", "session_id": "sess-1" })
11411            )
11412            .is_err(),
11413            "host session identity does not enter the event (§7.7)"
11414        );
11415    }
11416
11417    // fixture: signal-target-is-operation-or-task
11418    #[test]
11419    fn a_full_signal_queue_drops_by_policy_and_leaves_an_audit_fact() {
11420        let mut runtime = Runtime::new();
11421        runtime.submit(&signal_config(1, None));
11422        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11423
11424        runtime.submit(&signal_delivery(
11425            "in-sig-1",
11426            1_700_000_002_000,
11427            "delivery-a",
11428            1,
11429            logical_signal("sig-1", SignalUrgency::Normal),
11430        ));
11431        runtime.submit(&signal_delivery(
11432            "in-sig-2",
11433            1_700_000_003_000,
11434            "delivery-b",
11435            1,
11436            logical_signal("sig-2", SignalUrgency::Normal),
11437        ));
11438        assert_eq!(
11439            dispositions(&runtime)
11440                .iter()
11441                .map(|(disposition, ..)| disposition.clone())
11442                .collect::<Vec<_>>(),
11443            vec!["dropped".to_string()],
11444            "the configured capacity decides, and the loss is an audit fact"
11445        );
11446        assert_eq!(signal_queue_depth(&runtime), 1);
11447    }
11448
11449    // fixture: signal-disposition-is-a-fact
11450    #[test]
11451    fn a_signal_disposition_is_a_fact_and_only_a_preemption_asks_the_host_for_anything() {
11452        // an ordinary signal: an audit fact and nothing to execute
11453        let mut runtime = Runtime::new();
11454        runtime.submit(&signal_config(8, None));
11455        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11456        let queued = runtime.submit(&signal_delivery(
11457            "in-sig-normal",
11458            1_700_000_002_000,
11459            "delivery-a",
11460            1,
11461            logical_signal("sig-normal", SignalUrgency::Normal),
11462        ));
11463        assert!(
11464            queued.published_effects().is_empty(),
11465            "queueing is a fact; it asks the host for nothing"
11466        );
11467        assert!(observation_kinds(&runtime).contains(&"signal_delivery_disposed"));
11468
11469        // an urgent one that must stop running children *is* a host action, published as an effect
11470        let (mut runtime, _) = workflow_with_live_child();
11471        let interrupted = runtime.submit(&signal_delivery(
11472            "in-sig-critical",
11473            1_700_000_003_000,
11474            "delivery-b",
11475            1,
11476            logical_signal("sig-critical", SignalUrgency::Critical),
11477        ));
11478        assert_eq!(kinds(&interrupted), vec![EffectKindTag::PreemptTasks]);
11479        assert!(
11480            !observation_kinds(&runtime).contains(&"agent_preempted"),
11481            "the preemption is requested here, not committed: {:?}",
11482            observation_kinds(&runtime)
11483        );
11484
11485        // only the host's resolution commits it
11486        let preempt = effect_id(interrupted.step_seq);
11487        runtime.submit(&resolved(
11488            "in-preempted",
11489            1_700_000_004_000,
11490            &preempt,
11491            EffectSuccess::TasksPreempted(super::super::effect::TasksPreemptedSuccess {
11492                attempts: vec![super::super::effect::TaskPreemptOutcome {
11493                    task_id: TaskId::new("wf-node0").unwrap(),
11494                    attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
11495                    outcome: super::super::effect::TaskPreemptStatus::Preempted(
11496                        super::super::effect::TaskPreempted {},
11497                    ),
11498                }],
11499            }),
11500        ));
11501        assert!(
11502            observation_kinds(&runtime).contains(&"agent_preempted"),
11503            "{:?}",
11504            observation_kinds(&runtime)
11505        );
11506    }
11507
11508    #[test]
11509    fn an_operation_that_can_be_interrupted_is_one_that_can_stop_its_children() {
11510        // DEC-8 · the driver's own pre-check for `preempt_tasks` support before a critical signal
11511        // routes can never fire, and this is why: an operation that declares spawn capacity is
11512        // refused at genesis unless it also declares that it can stop what it started. The
11513        // guarantee lives at config time, so the signal path cannot plan an effect the host would
11514        // have to refuse.
11515        let mut runtime = Runtime::new();
11516        let fault = runtime.reject(&syscall_config_with(|config| {
11517            config.host_effect_support = HostEffectSupport::new([
11518                EffectKindTag::CallProvider,
11519                EffectKindTag::ExecuteTools,
11520                EffectKindTag::LoadPayload,
11521                EffectKindTag::SpawnTasks,
11522                EffectKindTag::PersistMemory,
11523                EffectKindTag::QueryMemory,
11524            ]);
11525        }));
11526        assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
11527        assert!(fault.message.contains("preempt_tasks"), "{fault:?}");
11528    }
11529
11530    // ----- §7.7 · escalate_after_ms (Task 14, adjudication §5n item 1) --------------------------
11531
11532    /// A signal config with deadline escalation switched on.
11533    fn escalating_signal_config(queue_max: u32) -> WireEnvelope {
11534        syscall_config_with(|config| {
11535            config.signal_policy = Some(super::super::command::SignalPolicy {
11536                queue_max,
11537                ttl_ms: None,
11538                deadline_escalation: Some(true),
11539            });
11540        })
11541    }
11542
11543    fn signal_escalating_after(id: &str, urgency: SignalUrgency, after_ms: u64) -> LogicalSignal {
11544        LogicalSignal {
11545            escalate_after_ms: Some(WireU64::new(after_ms)),
11546            ..logical_signal(id, urgency)
11547        }
11548    }
11549
11550    #[test]
11551    fn a_due_escalation_raises_urgency_one_tier_and_is_anchored_to_the_accepted_time() {
11552        // A `low` signal is only observed. The same signal with a due `escalate_after_ms` is
11553        // `normal`, which queues for the next turn boundary — one tier, exactly.
11554        let mut runtime = Runtime::new();
11555        runtime.submit(&escalating_signal_config(8));
11556        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11557        runtime.submit(&signal_delivery(
11558            "in-sig-due",
11559            1_700_000_002_000,
11560            "delivery-a",
11561            1,
11562            signal_escalating_after("sig-due", SignalUrgency::Low, 0),
11563        ));
11564        assert_eq!(
11565            dispositions(&runtime)
11566                .iter()
11567                .map(|(disposition, ..)| disposition.clone())
11568                .collect::<Vec<_>>(),
11569            vec!["queue".to_string()],
11570            "a due deadline escalated low → normal"
11571        );
11572
11573        // Not yet due: the same signal, same accepted time, a deadline in the future. This is what
11574        // "anchored to the envelope's accepted time" buys — the kernel needs no clock of its own to
11575        // tell the two apart.
11576        let mut runtime = Runtime::new();
11577        runtime.submit(&escalating_signal_config(8));
11578        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11579        runtime.submit(&signal_delivery(
11580            "in-sig-waiting",
11581            1_700_000_002_000,
11582            "delivery-a",
11583            1,
11584            signal_escalating_after("sig-waiting", SignalUrgency::Low, 60_000),
11585        ));
11586        assert_eq!(
11587            dispositions(&runtime)
11588                .iter()
11589                .map(|(disposition, ..)| disposition.clone())
11590                .collect::<Vec<_>>(),
11591            vec!["observe".to_string()],
11592            "a deadline that has not come due changes nothing"
11593        );
11594    }
11595
11596    #[test]
11597    fn escalation_is_inert_unless_the_operation_asked_for_it() {
11598        // The field is a request; `signal_policy.deadline_escalation` is the operation's consent.
11599        // Without it the same bytes decode to the same signal and route the same way.
11600        let mut runtime = Runtime::new();
11601        runtime.submit(&signal_config(8, None));
11602        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11603        runtime.submit(&signal_delivery(
11604            "in-sig",
11605            1_700_000_002_000,
11606            "delivery-a",
11607            1,
11608            signal_escalating_after("sig-inert", SignalUrgency::Low, 0),
11609        ));
11610        assert_eq!(
11611            dispositions(&runtime)
11612                .iter()
11613                .map(|(disposition, ..)| disposition.clone())
11614                .collect::<Vec<_>>(),
11615            vec!["observe".to_string()],
11616            "escalation without the policy is inert"
11617        );
11618    }
11619
11620    #[test]
11621    fn an_escalation_that_reaches_critical_takes_the_whole_interrupt_arc() {
11622        // The full arc: a `high` signal that waited long enough becomes `critical`, and critical
11623        // while children are running is the one disposition that asks the host for something. This
11624        // is also what the driver's pre-admission `effective_urgency` check has to agree with — it
11625        // reads the escalated value precisely so a delivery that will publish a `PreemptTasks` is
11626        // adjudicated against DEC-8 before the router moves.
11627        let mut runtime = Runtime::new();
11628        runtime.submit(&escalating_signal_config(8));
11629        let started = runtime.submit(&workflow_start(
11630            "in-start",
11631            1_700_000_001_000,
11632            two_node_spec(),
11633        ));
11634        runtime.submit(&spawned(
11635            "in-ack",
11636            1_700_000_002_000,
11637            &effect_id(started.step_seq),
11638            &["wf-node0"],
11639        ));
11640
11641        let interrupted = runtime.submit(&signal_delivery(
11642            "in-sig-escalated",
11643            1_700_000_003_000,
11644            "delivery-a",
11645            1,
11646            signal_escalating_after("sig-escalated", SignalUrgency::High, 0),
11647        ));
11648        assert_eq!(
11649            kinds(&interrupted),
11650            vec![EffectKindTag::PreemptTasks],
11651            "high + due deadline = critical, and critical while busy preempts"
11652        );
11653
11654        // …and without the escalation the same signal is only a soft interrupt: nothing published.
11655        let mut runtime = Runtime::new();
11656        runtime.submit(&escalating_signal_config(8));
11657        let started = runtime.submit(&workflow_start(
11658            "in-start",
11659            1_700_000_001_000,
11660            two_node_spec(),
11661        ));
11662        runtime.submit(&spawned(
11663            "in-ack",
11664            1_700_000_002_000,
11665            &effect_id(started.step_seq),
11666            &["wf-node0"],
11667        ));
11668        let soft = runtime.submit(&signal_delivery(
11669            "in-sig-plain",
11670            1_700_000_003_000,
11671            "delivery-a",
11672            1,
11673            logical_signal("sig-plain", SignalUrgency::High),
11674        ));
11675        assert!(
11676            soft.published_effects().is_empty(),
11677            "an unescalated high signal waits for the next boundary"
11678        );
11679    }
11680
11681    #[test]
11682    fn a_signal_carries_a_duration_not_a_deadline() {
11683        // DEC-2 · an absolute instant would be a second host clock on the wire, and the same
11684        // intent would decode to a different deadline on every redelivery. Only the duration
11685        // spelling exists.
11686        let signal = signal_escalating_after("sig", SignalUrgency::Normal, 30_000);
11687        let value = serde_json::to_value(&signal).unwrap();
11688        assert_eq!(value["escalate_after_ms"], json!("30000"));
11689        for banned in ["deadline_ms", "escalate_at_ms", "expires_at_ms", "now_ms"] {
11690            assert!(
11691                value.get(banned).is_none(),
11692                "a signal must not carry {banned}"
11693            );
11694            let mut with_instant = value.clone();
11695            with_instant
11696                .as_object_mut()
11697                .unwrap()
11698                .insert(banned.to_string(), json!("1700000000000"));
11699            assert!(
11700                serde_json::from_value::<LogicalSignal>(with_instant).is_err(),
11701                "{banned} must not decode"
11702            );
11703        }
11704
11705        // and the field is optional: a signal that never escalates is the common case
11706        let plain = serde_json::to_value(logical_signal("sig", SignalUrgency::Normal)).unwrap();
11707        assert!(plain.get("escalate_after_ms").is_none());
11708    }
11709
11710    #[test]
11711    fn an_urgent_signal_never_publishes_a_second_provider_request() {
11712        // DEC-3 · a provider call is already pending, so the interrupt is admitted to the attention
11713        // partition and read at the next turn boundary rather than re-asking now.
11714        let (mut runtime, provider) = agent_awaiting_provider();
11715        let interrupted = runtime.submit(&signal_delivery(
11716            "in-sig-critical",
11717            1_700_000_002_000,
11718            "delivery-a",
11719            1,
11720            logical_signal("sig-critical", SignalUrgency::Critical),
11721        ));
11722        assert!(
11723            interrupted.published_effects().is_empty(),
11724            "a second provider call would be refused by §15.3, so none is planned"
11725        );
11726        assert_eq!(
11727            dispositions(&runtime)
11728                .iter()
11729                .map(|(disposition, ..)| disposition.clone())
11730                .collect::<Vec<_>>(),
11731            vec!["interrupt".to_string()],
11732            "the disposition reports what actually happened"
11733        );
11734
11735        // and the pending call still resolves normally, carrying the interrupt into that turn
11736        let answered = runtime.submit(&provider_result(
11737            "in-answer",
11738            1_700_000_003_000,
11739            &provider,
11740            vec![tool_call("call-1", "search", json!({"q": "now what"}))],
11741        ));
11742        assert_eq!(kinds(&answered), vec![EffectKindTag::ExecuteTools]);
11743    }
11744
11745    // -----------------------------------------------------------------------------------------
11746    // §7.5 · the remaining host commands
11747    // -----------------------------------------------------------------------------------------
11748
11749    #[test]
11750    fn a_host_task_update_and_a_model_task_update_share_a_payload_but_not_an_authority() {
11751        let (mut runtime, provider) = agent_awaiting_provider();
11752        let committed = runtime.submit(&control(
11753            "in-host-plan",
11754            1_700_000_002_000,
11755            HostCommand::UpdateTask(UpdateTaskCommand {
11756                update: WireTaskUpdate {
11757                    plan: Some(vec!["collect".to_string(), "write".to_string()]),
11758                    progress: Some("host set the plan".to_string()),
11759                    ..WireTaskUpdate::default()
11760                },
11761            }),
11762        ));
11763        assert!(
11764            committed.published_effects().is_empty() && committed.terminal().is_none(),
11765            "a control command changes kernel state and publishes nothing"
11766        );
11767        assert_eq!(
11768            runtime
11769                .driver
11770                .engine()
11771                .map(|engine| engine.ctx.partitions.task_state.progress.clone()),
11772            Some("host set the plan".to_string()),
11773        );
11774
11775        // the model reaches the same mutation through the P1 syscall path, which *is* gated —
11776        // it must be a call the turn advertised, attributed to a derived caller
11777        let acted = runtime.submit(&provider_result(
11778            "in-model-plan",
11779            1_700_000_003_000,
11780            &provider,
11781            vec![tool_call(
11782                "call-1",
11783                "update_plan",
11784                json!({ "progress": "model set the plan" }),
11785            )],
11786        ));
11787        assert_eq!(kinds(&acted), vec![EffectKindTag::CallProvider]);
11788        assert_eq!(
11789            runtime
11790                .driver
11791                .engine()
11792                .map(|engine| engine.ctx.partitions.task_state.progress.clone()),
11793            Some("model set the plan".to_string()),
11794        );
11795    }
11796
11797    #[test]
11798    fn seeded_and_mutated_knowledge_enter_the_same_partition() {
11799        let (mut runtime, _) = agent_awaiting_provider();
11800        runtime.submit(&control(
11801            "in-seed",
11802            1_700_000_002_000,
11803            HostCommand::SeedKnowledge(SeedKnowledgeCommand {
11804                entries: vec![super::super::root::KnowledgeEntry {
11805                    content: "the house style forbids bullet lists".to_string(),
11806                    key: Some("style".to_string()),
11807                    tokens: Some(9),
11808                    pinned: true,
11809                }],
11810            }),
11811        ));
11812        assert!(
11813            knowledge_text(&runtime)
11814                .iter()
11815                .any(|text| text.contains("house style")),
11816            "{:?}",
11817            knowledge_text(&runtime)
11818        );
11819
11820        // the keyed removal half is boundary-deferred, so it is accepted here and swept later
11821        let committed = runtime.submit(&control(
11822            "in-knowledge",
11823            1_700_000_003_000,
11824            HostCommand::ApplyKnowledgeMutation(ApplyKnowledgeMutationCommand {
11825                mutation: super::super::command::KnowledgeMutation {
11826                    upsert: vec![super::super::root::KnowledgeEntry {
11827                        content: "cite at least three sources".to_string(),
11828                        key: Some("sources".to_string()),
11829                        tokens: Some(6),
11830                        pinned: false,
11831                    }],
11832                    remove: vec!["style".to_string(), "never-seen".to_string()],
11833                },
11834            }),
11835        ));
11836        assert!(committed.published_effects().is_empty());
11837        assert!(
11838            knowledge_text(&runtime)
11839                .iter()
11840                .any(|text| text.contains("three sources")),
11841            "{:?}",
11842            knowledge_text(&runtime)
11843        );
11844    }
11845
11846    fn knowledge_text(runtime: &Runtime) -> Vec<String> {
11847        runtime
11848            .driver
11849            .engine()
11850            .map(|engine| {
11851                engine
11852                    .ctx
11853                    .partitions
11854                    .knowledge
11855                    .messages()
11856                    .map(message_text)
11857                    .collect()
11858            })
11859            .unwrap_or_default()
11860    }
11861
11862    #[test]
11863    fn a_capability_patch_mounts_and_unmounts_in_one_step() {
11864        use super::super::root::{CapabilityGrant, CapabilityKind as WireCapabilityKind};
11865
11866        let (mut runtime, _) = agent_awaiting_provider();
11867        runtime.submit(&control(
11868            "in-mount",
11869            1_700_000_002_000,
11870            HostCommand::ApplyCapabilityPatch(ApplyCapabilityPatchCommand {
11871                patch: super::super::command::CapabilityPatch {
11872                    mount: vec![CapabilityGrant {
11873                        kind: WireCapabilityKind::McpServer,
11874                        id: "github".to_string(),
11875                        description: Some("issue and PR access".to_string()),
11876                    }],
11877                    unmount: Vec::new(),
11878                },
11879            }),
11880        ));
11881        assert!(observation_kinds(&runtime).contains(&"capability_changed"));
11882        assert!(
11883            runtime
11884                .driver
11885                .engine()
11886                .unwrap()
11887                .ctx
11888                .capabilities
11889                .capabilities()
11890                .iter()
11891                .any(|capability| capability.id == "github")
11892        );
11893
11894        // withdrawing something already absent errs open, so a retry is safe
11895        runtime.submit(&control(
11896            "in-unmount",
11897            1_700_000_003_000,
11898            HostCommand::ApplyCapabilityPatch(ApplyCapabilityPatchCommand {
11899                patch: super::super::command::CapabilityPatch {
11900                    mount: Vec::new(),
11901                    unmount: vec![
11902                        super::super::root::CapabilityRef {
11903                            kind: WireCapabilityKind::McpServer,
11904                            id: "github".to_string(),
11905                        },
11906                        super::super::root::CapabilityRef {
11907                            kind: WireCapabilityKind::McpServer,
11908                            id: "never-mounted".to_string(),
11909                        },
11910                    ],
11911                },
11912            }),
11913        ));
11914        assert!(
11915            !runtime
11916                .driver
11917                .engine()
11918                .unwrap()
11919                .ctx
11920                .capabilities
11921                .capabilities()
11922                .iter()
11923                .any(|capability| capability.id == "github")
11924        );
11925    }
11926
11927    #[test]
11928    fn a_skill_swap_is_atomic_and_refuses_a_name_outside_the_catalog() {
11929        let (mut runtime, _) = agent_awaiting_provider();
11930        let before = runtime.driver.engine().unwrap().ctx.active_skills.len();
11931
11932        // one undeclared name refuses the whole swap — nothing is half-applied
11933        assert_eq!(
11934            runtime
11935                .reject(&control(
11936                    "in-bad-skill",
11937                    1_700_000_002_000,
11938                    HostCommand::ApplySkillActivation(ApplySkillActivationCommand {
11939                        activate: vec![
11940                            super::super::command::SkillActivation {
11941                                name: "debug".to_string(),
11942                                lease_turns: None,
11943                            },
11944                            super::super::command::SkillActivation {
11945                                name: "invented".to_string(),
11946                                lease_turns: None,
11947                            },
11948                        ],
11949                        deactivate: Vec::new(),
11950                    }),
11951                ))
11952                .code,
11953            KernelFaultCode::InvalidConfig,
11954        );
11955        assert_eq!(
11956            runtime.driver.engine().unwrap().ctx.active_skills.len(),
11957            before,
11958            "a refused swap left nothing behind"
11959        );
11960
11961        runtime.submit(&control(
11962            "in-skill",
11963            1_700_000_002_000,
11964            HostCommand::ApplySkillActivation(ApplySkillActivationCommand {
11965                activate: vec![super::super::command::SkillActivation {
11966                    name: "debug".to_string(),
11967                    lease_turns: Some(2),
11968                }],
11969                deactivate: Vec::new(),
11970            }),
11971        ));
11972        assert!(
11973            runtime
11974                .driver
11975                .engine()
11976                .unwrap()
11977                .ctx
11978                .active_skills
11979                .iter()
11980                .any(|(skill, _)| skill == "debug")
11981        );
11982    }
11983
11984    #[test]
11985    fn a_live_policy_patch_is_revision_guarded_and_takes_effect() {
11986        let mut runtime = Runtime::new();
11987        runtime.submit(&signal_config(8, None));
11988        runtime.submit(&agent_start("in-start", 1_700_000_001_000));
11989
11990        // a stale revision changes nothing at all
11991        let stale = control(
11992            "in-stale-policy",
11993            1_700_000_002_000,
11994            HostCommand::ApplyPolicyPatch(ApplyPolicyPatchCommand {
11995                expected_revision: WireU64::new(7),
11996                patch: super::super::command::LivePolicyPatch::ReplaceSignalPolicy(
11997                    super::super::command::ReplaceSignalPolicy {
11998                        policy: super::super::command::SignalPolicy {
11999                            queue_max: 1,
12000                            ttl_ms: None,
12001                            deadline_escalation: None,
12002                        },
12003                    },
12004                ),
12005            }),
12006        );
12007        let fault = runtime.reject(&stale);
12008        assert_eq!(fault.code, KernelFaultCode::InvalidConfig);
12009        assert!(fault.message.contains("revision mismatch"), "{fault:?}");
12010        assert_eq!(
12011            runtime
12012                .driver
12013                .policy
12014                .as_ref()
12015                .map(LivePolicyState::revision),
12016            Some(WireU64::ZERO),
12017        );
12018
12019        // a widening quota is refused rather than clamped
12020        let widening = control(
12021            "in-widen",
12022            1_700_000_002_000,
12023            HostCommand::ApplyPolicyPatch(ApplyPolicyPatchCommand {
12024                expected_revision: WireU64::ZERO,
12025                patch: super::super::command::LivePolicyPatch::TightenResourceQuota(
12026                    super::super::command::TightenResourceQuota {
12027                        max_workflow_nodes: Some(999),
12028                        ..Default::default()
12029                    },
12030                ),
12031            }),
12032        );
12033        assert!(
12034            runtime
12035                .reject(&widening)
12036                .message
12037                .contains("may only tighten")
12038        );
12039
12040        // a well-formed patch applies, advances the revision, and reaches the running engine
12041        runtime.submit(&control(
12042            "in-policy",
12043            1_700_000_002_000,
12044            HostCommand::ApplyPolicyPatch(ApplyPolicyPatchCommand {
12045                expected_revision: WireU64::ZERO,
12046                patch: super::super::command::LivePolicyPatch::ReplaceSignalPolicy(
12047                    super::super::command::ReplaceSignalPolicy {
12048                        policy: super::super::command::SignalPolicy {
12049                            queue_max: 1,
12050                            ttl_ms: None,
12051                            deadline_escalation: None,
12052                        },
12053                    },
12054                ),
12055            }),
12056        ));
12057        assert_eq!(
12058            runtime
12059                .driver
12060                .policy
12061                .as_ref()
12062                .map(LivePolicyState::revision),
12063            Some(WireU64::new(1)),
12064        );
12065        assert!(
12066            observation_kinds(&runtime).contains(&"live_policy_changed"),
12067            "{:?}",
12068            observation_kinds(&runtime)
12069        );
12070
12071        // the new capacity is the one the router enforces from here on
12072        runtime.submit(&signal_delivery(
12073            "in-sig-1",
12074            1_700_000_003_000,
12075            "delivery-a",
12076            1,
12077            logical_signal("sig-1", SignalUrgency::Normal),
12078        ));
12079        runtime.submit(&signal_delivery(
12080            "in-sig-2",
12081            1_700_000_004_000,
12082            "delivery-b",
12083            1,
12084            logical_signal("sig-2", SignalUrgency::Normal),
12085        ));
12086        assert_eq!(
12087            dispositions(&runtime)
12088                .iter()
12089                .map(|(disposition, ..)| disposition.clone())
12090                .collect::<Vec<_>>(),
12091            vec!["dropped".to_string()],
12092            "the patched queue_max is what the next admission decision reads"
12093        );
12094    }
12095
12096    #[test]
12097    fn an_absolute_deadline_becomes_the_wall_budget_axis() {
12098        let (mut runtime, provider) = agent_awaiting_provider();
12099        // the operation's clock started at the root start
12100        runtime.submit(&control(
12101            "in-deadline",
12102            1_700_000_002_000,
12103            HostCommand::UpdateDeadline(UpdateDeadlineCommand {
12104                deadline_ms: Some(WireU64::new(1_700_000_001_500)),
12105            }),
12106        ));
12107
12108        // the axis is read at the one funnel that issues a provider request, so the deadline is
12109        // seen the next time the loop asks a question rather than mid-flight
12110        let acted = runtime.submit(&provider_result(
12111            "in-acted",
12112            1_700_000_003_000,
12113            &provider,
12114            vec![tool_call("call-1", "search", json!({ "q": "sources" }))],
12115        ));
12116        let results = runtime.submit(&tools_resolved(
12117            "in-results",
12118            1_700_000_004_000,
12119            &effect_id(acted.step_seq),
12120            &[("call-1", "three sources", false)],
12121        ));
12122        assert_eq!(
12123            kinds(&results),
12124            vec![EffectKindTag::CallProvider],
12125            "an exhausted budget still buys exactly one bounded final turn"
12126        );
12127
12128        let ended = runtime.submit(&provider_answer(
12129            "in-answer",
12130            1_700_000_005_000,
12131            &effect_id(results.step_seq),
12132            "half a thought",
12133        ));
12134        let Some(KernelTerminal::Agent(agent)) = ended.terminal() else {
12135            panic!("expected the deadline to end the operation, got {ended:?}");
12136        };
12137        assert_eq!(agent.result.termination, WireTermination::Deadline);
12138    }
12139
12140    #[test]
12141    fn a_forced_compaction_publishes_the_archive_it_produced() {
12142        let mut runtime = Runtime::new();
12143        runtime.submit(&syscall_config_with(|config| {
12144            config.host_effect_support = support_with([EffectKindTag::ArchivePageOut]);
12145        }));
12146        runtime.submit(&agent_start_with_history("in-start", 1_700_000_001_000, 14));
12147
12148        let compacted = runtime.submit(&control(
12149            "in-compact",
12150            1_700_000_002_000,
12151            HostCommand::ForceCompact(super::super::command::ForceCompactCommand {}),
12152        ));
12153        assert_eq!(kinds(&compacted), vec![EffectKindTag::ArchivePageOut]);
12154        assert!(observation_kinds(&runtime).contains(&"compressed"));
12155        assert!(
12156            compacted
12157                .step
12158                .observations
12159                .iter()
12160                .any(|observation| matches!(observation, KernelObservation::Compressed { .. })),
12161            "the committed planned step is the host publication channel for observations"
12162        );
12163    }
12164
12165    // -----------------------------------------------------------------------------------------
12166    // lifecycle goldens
12167    // -----------------------------------------------------------------------------------------
12168
12169    fn fixture_dir() -> PathBuf {
12170        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/kernel-wire")
12171    }
12172
12173    fn golden(name: &str, produced: &Value) -> Value {
12174        let path = fixture_dir().join(name);
12175        if std::env::var("BLESS_KERNEL_RECORD_FIXTURES").as_deref() == Ok("1") {
12176            let mut text = serde_json::to_string_pretty(produced).unwrap();
12177            text.push('\n');
12178            fs::write(&path, text).unwrap_or_else(|e| panic!("cannot bless {name}: {e}"));
12179            return produced.clone();
12180        }
12181        let raw = fs::read_to_string(&path).unwrap_or_else(|e| {
12182            panic!("missing golden {name} ({e}); re-bless with BLESS_KERNEL_RECORD_FIXTURES=1")
12183        });
12184        serde_json::from_str(&raw).unwrap_or_else(|e| panic!("{name} is not JSON: {e}"))
12185    }
12186
12187    fn link(envelope: &WireEnvelope, committed: &CommittedTransition<PlannedStep>) -> Value {
12188        json!({
12189            "envelope": serde_json::to_value(envelope).unwrap(),
12190            "step": serde_json::to_value(&committed.step).unwrap(),
12191            "record": serde_json::to_value(&committed.record).unwrap(),
12192        })
12193    }
12194
12195    #[test]
12196    fn golden_lifecycle_agent_root() {
12197        let mut runtime = Runtime::new();
12198        let configure_envelope = configure();
12199        let start_envelope = agent_start("in-start", 1_700_000_001_000);
12200        let genesis = runtime.submit(&configure_envelope);
12201        let started = runtime.submit(&start_envelope);
12202
12203        let produced = json!({
12204            "description":
12205                "Configure → atomic agent root start (spec 6, 7.4). Two accepted inputs reach the \
12206                 first provider call: the genesis record freezes the resolved configuration, and \
12207                 the start record's step carries the immutable root kind, the initial execution \
12208                 focus and the one CallProvider effect the transition published.",
12209            "genesis_digest": genesis.record.record_digest().as_str(),
12210            "head_digest": started.record.record_digest().as_str(),
12211            "links": [
12212                link(&configure_envelope, &genesis),
12213                link(&start_envelope, &started),
12214            ],
12215        });
12216
12217        let expected = golden("golden_lifecycle_agent_root.json", &produced);
12218        assert_eq!(produced, expected, "the agent root lifecycle drifted");
12219        assert_eq!(expected["links"][1]["step"]["root_kind"], json!("agent"));
12220        assert_eq!(
12221            expected["links"][1]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12222            json!("call_provider"),
12223        );
12224    }
12225
12226    #[test]
12227    fn golden_lifecycle_agent_full_turn() {
12228        let mut runtime = Runtime::new();
12229        let configure_envelope = syscall_config();
12230        let start_envelope = agent_start("in-start", 1_700_000_001_000);
12231        let genesis = runtime.submit(&configure_envelope);
12232        let started = runtime.submit(&start_envelope);
12233
12234        let acted_envelope = provider_result(
12235            "in-acted",
12236            1_700_000_002_000,
12237            &effect_id(started.step_seq),
12238            vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12239        );
12240        let acted = runtime.submit(&acted_envelope);
12241        let results_envelope = tools_resolved(
12242            "in-results",
12243            1_700_000_003_000,
12244            &effect_id(acted.step_seq),
12245            &[("call-1", "three sources found", false)],
12246        );
12247        let results = runtime.submit(&results_envelope);
12248        let answer_envelope = provider_answer(
12249            "in-answer",
12250            1_700_000_004_000,
12251            &effect_id(results.step_seq),
12252            "the brief cites three sources",
12253        );
12254        let answered = runtime.submit(&answer_envelope);
12255
12256        let produced = json!({
12257            "description":
12258                "One whole agent turn cycle on the canonical wire (spec 7.9 · Task 12): configure → \
12259                 atomic agent start → provider result carrying a tool call → tool results → final \
12260                 provider answer → agent terminal. Every step after the start is a single \
12261                 `ResolveEffect` input, and each one publishes exactly the next effect the loop is \
12262                 waiting on — until the last, whose disposition is the terminal itself and which \
12263                 publishes nothing.",
12264            "genesis_digest": genesis.record.record_digest().as_str(),
12265            "head_digest": answered.record.record_digest().as_str(),
12266            "links": [
12267                link(&configure_envelope, &genesis),
12268                link(&start_envelope, &started),
12269                link(&acted_envelope, &acted),
12270                link(&results_envelope, &results),
12271                link(&answer_envelope, &answered),
12272            ],
12273        });
12274
12275        let expected = golden("golden_lifecycle_agent_full_turn.json", &produced);
12276        assert_eq!(produced, expected, "the agent turn cycle drifted");
12277        assert_eq!(
12278            expected["links"][2]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12279            json!("execute_tools"),
12280            "a provider result carrying a host tool call publishes exactly one tool batch",
12281        );
12282        assert_eq!(
12283            expected["links"][3]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12284            json!("call_provider"),
12285            "the tool results resume the turn with the next provider call",
12286        );
12287        assert_eq!(
12288            expected["links"][4]["step"]["disposition"]["terminal"]["kind"],
12289            json!("agent"),
12290        );
12291        assert_eq!(
12292            expected["links"][4]["step"]["disposition"]["effects"],
12293            Value::Null,
12294            "§7.12 · effects or a terminal, never both",
12295        );
12296    }
12297
12298    #[test]
12299    fn golden_lifecycle_workflow_root() {
12300        let mut runtime = Runtime::new();
12301        let configure_envelope = configure();
12302        let start_envelope = workflow_start("in-start", 1_700_000_001_000, two_node_spec());
12303        let genesis = runtime.submit(&configure_envelope);
12304        let started = runtime.submit(&start_envelope);
12305
12306        let ack_envelope = spawned(
12307            "in-ack-1",
12308            1_700_000_002_000,
12309            &effect_id(started.step_seq),
12310            &["wf-node0"],
12311        );
12312        let ack = runtime.submit(&ack_envelope);
12313        let done_envelope = child_done("in-done-1", 1_700_000_003_000, "wf-node0", "collected");
12314        let advanced = runtime.submit(&done_envelope);
12315        let ack2_envelope = spawned(
12316            "in-ack-2",
12317            1_700_000_004_000,
12318            &effect_id(advanced.step_seq),
12319            &["wf-node1"],
12320        );
12321        let ack2 = runtime.submit(&ack2_envelope);
12322        let done2_envelope = child_done("in-done-2", 1_700_000_005_000, "wf-node1", "written");
12323        let finished = runtime.submit(&done2_envelope);
12324
12325        let produced = json!({
12326            "description":
12327                "Configure → atomic workflow root start → spawn ack → child completions → workflow \
12328                 terminal (spec 10.1). No LoadWorkflow, no placeholder agent run, and no host \
12329                 CompleteRun: the last committed step's disposition is the terminal itself.",
12330            "genesis_digest": genesis.record.record_digest().as_str(),
12331            "head_digest": finished.record.record_digest().as_str(),
12332            "links": [
12333                link(&configure_envelope, &genesis),
12334                link(&start_envelope, &started),
12335                link(&ack_envelope, &ack),
12336                link(&done_envelope, &advanced),
12337                link(&ack2_envelope, &ack2),
12338                link(&done2_envelope, &finished),
12339            ],
12340        });
12341
12342        let expected = golden("golden_lifecycle_workflow_root.json", &produced);
12343        assert_eq!(produced, expected, "the workflow root lifecycle drifted");
12344        assert_eq!(expected["links"][1]["step"]["root_kind"], json!("workflow"));
12345        assert_eq!(
12346            expected["links"][1]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12347            json!("spawn_tasks"),
12348        );
12349        assert_eq!(
12350            expected["links"][5]["step"]["disposition"]["terminal"]["kind"],
12351            json!("workflow"),
12352        );
12353    }
12354
12355    #[test]
12356    fn golden_lifecycle_cancel_arc() {
12357        let mut runtime = Runtime::new();
12358        let configure_envelope = signal_config(8, None);
12359        let start_envelope = workflow_start("in-start", 1_700_000_001_000, two_node_spec());
12360        let genesis = runtime.submit(&configure_envelope);
12361        let started = runtime.submit(&start_envelope);
12362
12363        let ack_envelope = spawned(
12364            "in-ack",
12365            1_700_000_002_000,
12366            &effect_id(started.step_seq),
12367            &["wf-node0"],
12368        );
12369        let ack = runtime.submit(&ack_envelope);
12370
12371        let signal_envelope = signal_delivery(
12372            "in-sig",
12373            1_700_000_003_000,
12374            "delivery-a",
12375            1,
12376            logical_signal("sig-abort", SignalUrgency::Critical),
12377        );
12378        let interrupted = runtime.submit(&signal_envelope);
12379
12380        let preempted_envelope = resolved(
12381            "in-preempted",
12382            1_700_000_004_000,
12383            &effect_id(interrupted.step_seq),
12384            EffectSuccess::TasksPreempted(super::super::effect::TasksPreemptedSuccess {
12385                attempts: vec![super::super::effect::TaskPreemptOutcome {
12386                    task_id: TaskId::new("wf-node0").unwrap(),
12387                    attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
12388                    outcome: super::super::effect::TaskPreemptStatus::Preempted(
12389                        super::super::effect::TaskPreempted {},
12390                    ),
12391                }],
12392            }),
12393        );
12394        let preempted = runtime.submit(&preempted_envelope);
12395
12396        let cancel_envelope = cancel("in-cancel", 1_700_000_005_000);
12397        let cancelled = runtime.submit(&cancel_envelope);
12398
12399        let produced = json!({
12400            "description":
12401                "The cancellation arc on the canonical wire (spec 7.5 / 7.7 / 11.1 · Task 12b): \
12402                 configure → workflow root start → spawn ack → a critical signal that preempts the \
12403                 running child → the host's preempt resolution → `HostControl::Cancel`, whose step \
12404                 is the cancelled terminal itself. Two things this chain fixes in place: the only \
12405                 host action a signal can cause is the preemption (its queueing/dropping/expiry are \
12406                 audit facts and publish nothing), and cancellation settles every downstream wait \
12407                 inside the same transition that commits the root terminal — §7.12 admits effects or \
12408                 a terminal and never both, so there is no second round trip in which the operation \
12409                 is neither running nor cancelled.",
12410            "genesis_digest": genesis.record.record_digest().as_str(),
12411            "head_digest": cancelled.record.record_digest().as_str(),
12412            "links": [
12413                link(&configure_envelope, &genesis),
12414                link(&start_envelope, &started),
12415                link(&ack_envelope, &ack),
12416                link(&signal_envelope, &interrupted),
12417                link(&preempted_envelope, &preempted),
12418                link(&cancel_envelope, &cancelled),
12419            ],
12420        });
12421
12422        let expected = golden("golden_lifecycle_cancel_arc.json", &produced);
12423        assert_eq!(produced, expected, "the cancellation arc drifted");
12424        assert_eq!(
12425            expected["links"][3]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12426            json!("preempt_tasks"),
12427            "an urgent signal's one host action is stopping the running child",
12428        );
12429        assert_eq!(
12430            expected["links"][5]["step"]["disposition"]["terminal"]["kind"],
12431            json!("cancelled"),
12432        );
12433        assert_eq!(
12434            expected["links"][5]["step"]["disposition"]["effects"],
12435            Value::Null,
12436            "§11.1 · the cancelling step leaves nothing waiting on the host",
12437        );
12438        assert_eq!(
12439            runtime.pending_effect_kinds(),
12440            Vec::<EffectKindTag>::new(),
12441            "and the transaction holds no pending effect after it",
12442        );
12443    }
12444
12445    #[test]
12446    fn golden_lifecycle_external_payload() {
12447        let mut runtime = Runtime::new();
12448        let configure_envelope = payload_config();
12449        let start_envelope = agent_start("in-start", 1_700_000_001_000);
12450        let genesis = runtime.submit(&configure_envelope);
12451        let started = runtime.submit(&start_envelope);
12452
12453        let acted_envelope = provider_result(
12454            "in-acted",
12455            1_700_000_002_000,
12456            &effect_id(started.step_seq),
12457            vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12458        );
12459        let acted = runtime.submit(&acted_envelope);
12460        let results_envelope = payloads_resolved(
12461            "in-results",
12462            1_700_000_003_000,
12463            &effect_id(acted.step_seq),
12464            vec![external_payload(
12465                "call-1",
12466                body_digest(),
12467                BODY.len() as u64,
12468                "the full report body, far la…",
12469            )],
12470        );
12471        let results = runtime.submit(&results_envelope);
12472        let read_envelope = provider_result(
12473            "in-read",
12474            1_700_000_004_000,
12475            &effect_id(results.step_seq),
12476            vec![tool_call(
12477                "call-2",
12478                READ_RESULT_TOOL_NAME,
12479                json!({"call_id": "call-1"}),
12480            )],
12481        );
12482        let read = runtime.submit(&read_envelope);
12483        let loaded_envelope = resolved(
12484            "in-loaded",
12485            1_700_000_005_000,
12486            &effect_id(read.step_seq),
12487            EffectSuccess::PayloadLoaded(PayloadLoadedSuccess {
12488                handle_id: HandleId::new("call-1").unwrap(),
12489                payload: InlinePayload {
12490                    content: BODY.to_string(),
12491                    digest: body_digest(),
12492                    original_size: WireU64::new(BODY.len() as u64),
12493                },
12494            }),
12495        );
12496        let loaded = runtime.submit(&loaded_envelope);
12497
12498        let produced = json!({
12499            "description":
12500                "The §7.10 external-payload arc (Task 13): configure → agent start → a provider \
12501                 turn calling one host tool → an **external** tool result → the model's \
12502                 `read_result` page-in → the loaded body. Read the records: the persisted body \
12503                 appears in exactly one place on this whole chain — the `payload_loaded` outcome \
12504                 the host sent when the kernel asked for it. It is in no effect the kernel \
12505                 published and in no accepted input the kernel did not ask for, which is what \
12506                 §25.10 means by \"large bodies do not enter the journal\". What the kernel holds \
12507                 instead is the reference: an opaque `payload_ref` it never interprets, a digest \
12508                 it checks the restored body against, and a bounded preview that is the only part \
12509                 to occupy context.",
12510            "genesis_digest": genesis.record.record_digest().as_str(),
12511            "head_digest": loaded.record.record_digest().as_str(),
12512            "links": [
12513                link(&configure_envelope, &genesis),
12514                link(&start_envelope, &started),
12515                link(&acted_envelope, &acted),
12516                link(&results_envelope, &results),
12517                link(&read_envelope, &read),
12518                link(&loaded_envelope, &loaded),
12519            ],
12520        });
12521
12522        let expected = golden("golden_lifecycle_external_payload.json", &produced);
12523        assert_eq!(produced, expected, "the external-payload arc drifted");
12524        assert_eq!(
12525            expected["links"][3]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12526            json!("call_provider"),
12527            "an external result resumes the turn like any other — the body never comes back out",
12528        );
12529        assert_eq!(
12530            expected["links"][4]["step"]["disposition"]["effects"][0]["effect"]["kind"],
12531            json!("load_payload"),
12532            "§7.10 rule 4 · `read_result` reduces to exactly one effect",
12533        );
12534        assert_eq!(
12535            expected["links"][4]["step"]["disposition"]["effects"][0]["effect"]["payload_ref"],
12536            json!("payload:01J8Y2QK7C4N0V"),
12537            "the effect hands back the host's own opaque locator, unread and unjoined",
12538        );
12539
12540        // The body appears exactly once on the whole chain, and only where the model asked for it.
12541        //
12542        // SPEC-ISSUE: §25.10 states flatly that large bodies do not enter the journal, but §7.9's
12543        // `PayloadLoaded` carries `InlinePayload.content` as an accepted input, and §8.1 makes the
12544        // accepted input part of the durable record. So a *paged-in* body is journalled by the
12545        // contract's own construction. The invariant that is actually achievable — and the one this
12546        // golden pins — is narrower: no body enters on the **ingestion** path, and no body is ever
12547        // carried by an effect the kernel publishes. Either §25.10 should be scoped to those two,
12548        // or `PayloadLoaded` needs a shape that resolves an effect without riding the record.
12549        for (index, link) in expected["links"].as_array().unwrap().iter().enumerate() {
12550            assert_eq!(
12551                link["envelope"]
12552                    .to_string()
12553                    .contains("clears the inline threshold"),
12554                index == 5,
12555                "link {index} disagrees about where a large body may enter",
12556            );
12557            // A `call_provider` effect legitimately carries whatever is in context once the model
12558            // has paged a body in. No effect may hand the body back out to be persisted.
12559            for effect in link["step"]["disposition"]["effects"]
12560                .as_array()
12561                .unwrap_or(&Vec::new())
12562            {
12563                assert!(
12564                    !effect.to_string().contains("clears the inline threshold")
12565                        || effect["effect"]["kind"] == json!("call_provider"),
12566                    "link {index} hands the body back out in a {} effect",
12567                    effect["effect"]["kind"],
12568                );
12569            }
12570        }
12571        for record in &runtime.journal {
12572            let input = serde_json::to_string(&record.normalized_input().unwrap()).unwrap();
12573            assert_eq!(
12574                input.contains("clears the inline threshold"),
12575                record.step_seq().get() == 5,
12576                "only the page-in the model asked for may make a body durable, got step {}",
12577                record.step_seq()
12578            );
12579        }
12580    }
12581
12582    // -----------------------------------------------------------------------------------------
12583    // §12 · logical checkpoint goldens
12584    // -----------------------------------------------------------------------------------------
12585
12586    /// Drive one whole agent turn and stop mid-flight, so the checkpoint has something to hold:
12587    /// a live pending effect, a replay ledger, a task table and a live policy.
12588    fn agent_mid_turn() -> Runtime {
12589        let mut runtime = Runtime::new();
12590        runtime.submit(&syscall_config());
12591        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
12592        let acted = runtime.submit(&provider_result(
12593            "in-acted",
12594            1_700_000_002_000,
12595            &effect_id(started.step_seq),
12596            vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12597        ));
12598        runtime.submit(&tools_resolved(
12599            "in-results",
12600            1_700_000_003_000,
12601            &effect_id(acted.step_seq),
12602            &[("call-1", "three sources found", false)],
12603        ));
12604        runtime
12605    }
12606
12607    #[test]
12608    fn golden_checkpoint_agent_turn() {
12609        let runtime = agent_mid_turn();
12610        let candidate = runtime.checkpoint();
12611        let checkpoint = candidate.decode().expect("the candidate blob verifies");
12612
12613        let produced = json!({
12614            "description":
12615                "A full-state logical checkpoint taken mid-turn (spec 12.1, 12.3). base == through \
12616                 == the durable head, so the bounded tail is empty and a restore replays nothing \
12617                 before the post-checkpoint records. The logical state is partitioned four ways and \
12618                 the header repeats none of it: the pending `call_provider` effect, the replay \
12619                 ledger and the terminal slot live in `transition`, the task table in `scheduler`, \
12620                 the handle table in `context_vm`, the live policy and the provider-tool causation \
12621                 in `syscall`.",
12622            "through_step_seq": candidate.through_step_seq.to_string(),
12623            "covered_head": candidate.covered_head.as_str(),
12624            "state_digest": candidate.state_digest.as_str(),
12625            "ack_token": candidate.ack_token.as_str(),
12626            "checkpoint": serde_json::to_value(&checkpoint).unwrap(),
12627        });
12628
12629        let expected = golden("golden_checkpoint_agent_turn.json", &produced);
12630        assert_eq!(produced, expected, "the logical checkpoint drifted");
12631        assert_eq!(expected["checkpoint"]["checkpoint_version"], json!(1));
12632        assert_eq!(
12633            expected["checkpoint"]["abi_version"],
12634            json!(super::super::KERNEL_ABI_VERSION),
12635        );
12636        assert_eq!(
12637            expected["checkpoint"]["base_step_seq"], expected["checkpoint"]["through_step_seq"],
12638            "a full-state candidate carries no tail",
12639        );
12640        assert_eq!(expected["checkpoint"]["tail_inputs"], json!([]));
12641        assert_eq!(
12642            expected["checkpoint"]["logical_state"]["transition"]["pending_effects"][0]["effect"]["kind"],
12643            json!("call_provider"),
12644            "the effect the operation is waiting on is inside the checkpoint, not beside it",
12645        );
12646    }
12647
12648    /// The incremental form of §12.1: an older logical state plus the canonical inputs that carry
12649    /// it forward to the covered head. This is exactly the shape a Task 16 rebase produces, and
12650    /// pinning it now is what stops the tail contract from being invented twice.
12651    #[test]
12652    fn golden_checkpoint_bounded_tail() {
12653        let mut runtime = Runtime::new();
12654        runtime.submit(&syscall_config());
12655        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
12656
12657        // the state at the root start — the base a later rebase keeps
12658        let base = runtime
12659            .checkpoint()
12660            .decode()
12661            .expect("the base candidate verifies");
12662
12663        let acted = runtime.submit(&provider_result(
12664            "in-acted",
12665            1_700_000_002_000,
12666            &effect_id(started.step_seq),
12667            vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12668        ));
12669        let results = runtime.submit(&tools_resolved(
12670            "in-results",
12671            1_700_000_003_000,
12672            &effect_id(acted.step_seq),
12673            &[("call-1", "three sources found", false)],
12674        ));
12675        runtime.submit(&provider_answer(
12676            "in-answer",
12677            1_700_000_004_000,
12678            &effect_id(results.step_seq),
12679            "the brief cites three sources",
12680        ));
12681
12682        let tail: Vec<CanonicalInput> = runtime.journal[2..]
12683            .iter()
12684            .map(|record| CanonicalInput::from_record(record).expect("a record projects"))
12685            .collect();
12686        // The transaction is the authority on what the tail still holds; harvesting from the
12687        // journal and asking the transaction must give the same answer, or a rebase built from
12688        // either source would produce a different checkpoint.
12689        assert_eq!(
12690            runtime
12691                .tx
12692                .tail_inputs()
12693                .into_iter()
12694                .filter(|entry| entry.step_seq.get() > base.through_step_seq().get())
12695                .collect::<Vec<_>>(),
12696            tail,
12697            "the transaction's own tail and the journal agree on (base, through]",
12698        );
12699        let rebased = runtime
12700            .tx
12701            .checkpoint_rebase(
12702                &CheckpointBoundary {
12703                    through_step_seq: base.through_step_seq(),
12704                    covered_head: base.covered_transaction_head_digest().clone(),
12705                },
12706                base.logical_state().clone(),
12707            )
12708            .expect("an older state plus its exact tail is a checkpoint")
12709            .decode()
12710            .expect("the rebase blob verifies");
12711
12712        let produced = json!({
12713            "description":
12714                "The incremental form of a logical checkpoint (spec 12.1, 12.2): `logical_state` is \
12715                 the state after `base_step_seq`, and `tail_inputs` covers (base, through] exactly \
12716                 — no hole, no duplicate, nothing outside the range. A restore replays this tail on \
12717                 top of the state, then continues with the journal records after `through_step_seq`. \
12718                 The state digest is byte-identical to the base checkpoint's, because the state is \
12719                 the same state; only the tail and the header moved.",
12720            "base_state_digest": base.state_digest().as_str(),
12721            "checkpoint": serde_json::to_value(&rebased).unwrap(),
12722        });
12723
12724        let expected = golden("golden_checkpoint_bounded_tail.json", &produced);
12725        assert_eq!(produced, expected, "the bounded-tail checkpoint drifted");
12726        assert_eq!(
12727            expected["checkpoint"]["state_digest"], expected["base_state_digest"],
12728            "a rebase carries the base state forward untouched",
12729        );
12730        assert_eq!(
12731            expected["checkpoint"]["base_step_seq"],
12732            json!("1"),
12733            "the base is the root start",
12734        );
12735        assert_eq!(expected["checkpoint"]["through_step_seq"], json!("4"));
12736        let tail = expected["checkpoint"]["tail_inputs"].as_array().unwrap();
12737        assert_eq!(
12738            tail.iter()
12739                .map(|entry| entry["step_seq"].as_str().unwrap())
12740                .collect::<Vec<_>>(),
12741            vec!["2", "3", "4"],
12742            "(1, 4] is exactly steps 2, 3 and 4",
12743        );
12744    }
12745
12746    /// §12.3 rule 1 · a candidate is a read. Appends continue, and the candidate that was handed
12747    /// out still describes the prefix it was taken over.
12748    #[test]
12749    fn a_candidate_neither_blocks_nor_is_invalidated_by_later_appends() {
12750        let mut runtime = agent_mid_turn();
12751        let candidate = runtime.checkpoint();
12752        let before = runtime.tx.head().expect("a head");
12753        assert_eq!(candidate.through_step_seq, before.step_seq);
12754
12755        let results_effect = effect_id(before.step_seq);
12756        runtime.submit(&provider_answer(
12757            "in-answer",
12758            1_700_000_004_000,
12759            &results_effect,
12760            "the brief cites three sources",
12761        ));
12762
12763        let after = runtime.tx.head().expect("a head");
12764        assert_ne!(after.step_seq, before.step_seq, "the journal moved on");
12765        assert_eq!(
12766            candidate.through_step_seq, before.step_seq,
12767            "the candidate still covers the prefix it was taken over",
12768        );
12769        candidate
12770            .decode()
12771            .expect("and it still verifies after the journal moved")
12772            .verify_belongs_to(&operation(), runtime.journal[0].record_digest())
12773            .expect("it is still this operation's checkpoint");
12774    }
12775
12776    // -----------------------------------------------------------------------------------------
12777    // §12.2 · bounded-tail restore
12778    // -----------------------------------------------------------------------------------------
12779
12780    /// Drive an operation to a fixed point through a fixed envelope sequence.
12781    ///
12782    /// One function so the two sides of a differential are *the same* sequence by construction
12783    /// rather than by two copies that agree today. `stop_after` is how many envelopes to submit.
12784    fn drive(runtime: &mut Runtime, envelopes: &[WireEnvelope]) {
12785        for envelope in envelopes {
12786            runtime.submit(envelope);
12787        }
12788    }
12789
12790    /// The canonical agent turn, as a list of envelopes.
12791    ///
12792    /// Deterministic in every field a record digests: ids, clocks and payloads are literals, and
12793    /// the effect ids are derived from the step sequence exactly as the kernel derives them. That is
12794    /// what makes "byte-identical" a checkable claim rather than a hope.
12795    fn turn_envelopes() -> Vec<WireEnvelope> {
12796        vec![
12797            syscall_config(),
12798            agent_start("in-start", 1_700_000_001_000),
12799            provider_result(
12800                "in-acted",
12801                1_700_000_002_000,
12802                &effect_id(WireU64::new(1)),
12803                vec![tool_call("call-1", "search", json!({"q": "sources"}))],
12804            ),
12805            tools_resolved(
12806                "in-results",
12807                1_700_000_003_000,
12808                &effect_id(WireU64::new(2)),
12809                &[("call-1", "three sources found", false)],
12810            ),
12811            provider_result(
12812                "in-acted-2",
12813                1_700_000_004_000,
12814                &effect_id(WireU64::new(3)),
12815                vec![tool_call("call-2", "search", json!({"q": "more"}))],
12816            ),
12817            tools_resolved(
12818                "in-results-2",
12819                1_700_000_005_000,
12820                &effect_id(WireU64::new(4)),
12821                &[("call-2", "two more sources", false)],
12822            ),
12823            provider_answer(
12824                "in-answer",
12825                1_700_000_006_000,
12826                &effect_id(WireU64::new(5)),
12827                "the brief cites five sources",
12828            ),
12829        ]
12830    }
12831
12832    fn digests(records: &[KernelRecord]) -> Vec<String> {
12833        records
12834            .iter()
12835            .map(|record| record.record_digest().to_string())
12836            .collect()
12837    }
12838
12839    /// The whole observable surface of a runtime, as bytes.
12840    ///
12841    /// Everything a differential must compare and nothing that is allowed to differ: the state
12842    /// digest a checkpoint would take, the pending effect identities, the terminal, and the head.
12843    /// If a restored runtime matches an uninterrupted one on all four *and* keeps producing the same
12844    /// records, "behaves identically" is not a judgement call.
12845    fn surface(runtime: &Runtime) -> Value {
12846        json!({
12847            "head": runtime.tx.head().map(|head| json!({
12848                "digest": head.digest.as_str(),
12849                "step_seq": head.step_seq.to_string(),
12850            })),
12851            "lifecycle": format!("{:?}", runtime.tx.lifecycle()),
12852            "pending_effects": runtime
12853                .tx
12854                .pending_effects()
12855                .map(|effect| serde_json::to_value(effect).unwrap())
12856                .collect::<Vec<_>>(),
12857            "terminal": runtime.tx.terminal().map(|t| serde_json::to_value(t).unwrap()),
12858            "logical_state": serde_json::to_value(
12859                runtime
12860                    .tx
12861                    .transition_state_for_restore(
12862                        runtime.driver.root_kind(),
12863                        runtime.driver.focus().cloned(),
12864                    )
12865                    .expect("a configured runtime has a transition state"),
12866            )
12867            .unwrap(),
12868            "context_vm": serde_json::to_value(
12869                runtime.driver.project_logical_state().context_vm
12870            ).unwrap(),
12871            "scheduler": serde_json::to_value(
12872                runtime.driver.project_logical_state().scheduler
12873            ).unwrap(),
12874        })
12875    }
12876
12877    /// Task 16b · a checkpoint owns the active DAG and every child process identity needed to
12878    /// continue it. Restoring while the first node is live must neither forget the second node nor
12879    /// rebuild the child under different role/isolation/inheritance.
12880    #[test]
12881    fn an_active_workflow_and_its_child_restore_to_the_same_completion() {
12882        let (mut uninterrupted, _) = workflow_with_live_child();
12883        let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
12884        let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
12885
12886        let first_done = child_done(
12887            "in-done-1",
12888            1_700_000_003_000,
12889            "wf-node0",
12890            "sources collected",
12891        );
12892        let uninterrupted_next = uninterrupted.submit(&first_done);
12893        let restored_next = restored.submit(&first_done);
12894        assert_eq!(
12895            restored_next, uninterrupted_next,
12896            "the restored DAG schedules the same second node",
12897        );
12898
12899        let uninterrupted_spawn = effect_id(uninterrupted_next.step_seq);
12900        let restored_spawn = effect_id(restored_next.step_seq);
12901        uninterrupted.submit(&spawned(
12902            "in-ack-2",
12903            1_700_000_004_000,
12904            &uninterrupted_spawn,
12905            &["wf-node1"],
12906        ));
12907        restored.submit(&spawned(
12908            "in-ack-2",
12909            1_700_000_004_000,
12910            &restored_spawn,
12911            &["wf-node1"],
12912        ));
12913        let second_done = child_done("in-done-2", 1_700_000_005_000, "wf-node1", "brief written");
12914        uninterrupted.submit(&second_done);
12915        restored.submit(&second_done);
12916
12917        assert_eq!(
12918            surface(&restored),
12919            surface(&uninterrupted),
12920            "workflow state and child permission identity are reversible",
12921        );
12922    }
12923
12924    /// Task 16b · queued signal source state and the router's business-dedupe memory survive a
12925    /// checkpoint. The queued payload must drive the same follow-up provider request, while a new
12926    /// delivery with the same key remains ignored on both sides.
12927    #[test]
12928    fn a_queued_signal_and_its_dedupe_key_restore_to_the_same_follow_up() {
12929        let mut uninterrupted = Runtime::new();
12930        uninterrupted.submit(&signal_config(8, None));
12931        let started = uninterrupted.submit(&agent_start("in-start", 1_700_000_001_000));
12932        uninterrupted.submit(&signal_delivery(
12933            "in-sig-1",
12934            1_700_000_002_000,
12935            "delivery-a",
12936            1,
12937            LogicalSignal {
12938                payload: super::super::scalar::BoundedJson::new(json!({
12939                    "job": "nightly-index"
12940                }))
12941                .unwrap(),
12942                dedupe_key: Some("nightly-index".to_string()),
12943                ..logical_signal("sig-nightly", SignalUrgency::Normal)
12944            },
12945        ));
12946
12947        let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
12948        let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
12949
12950        let duplicate = signal_delivery(
12951            "in-sig-2",
12952            1_700_000_003_000,
12953            "delivery-b",
12954            2,
12955            LogicalSignal {
12956                payload: super::super::scalar::BoundedJson::new(json!({
12957                    "job": "nightly-index"
12958                }))
12959                .unwrap(),
12960                dedupe_key: Some("nightly-index".to_string()),
12961                ..logical_signal("sig-nightly", SignalUrgency::Normal)
12962            },
12963        );
12964        assert_eq!(
12965            restored.submit(&duplicate),
12966            uninterrupted.submit(&duplicate),
12967            "the restored router remembers the business dedupe key",
12968        );
12969
12970        let provider = effect_id(started.step_seq);
12971        let answer = provider_answer(
12972            "in-answer",
12973            1_700_000_004_000,
12974            &provider,
12975            "the first request completed",
12976        );
12977        assert_eq!(
12978            restored.submit(&answer),
12979            uninterrupted.submit(&answer),
12980            "the queued payload produces the same follow-up provider request",
12981        );
12982        assert_eq!(surface(&restored), surface(&uninterrupted));
12983    }
12984
12985    /// Task 16b · a completed child remains the same process fact after restore: its role,
12986    /// isolation, context inheritance, capability ceiling and join result are not inferred anew.
12987    #[test]
12988    fn a_subagent_process_and_join_result_restore_without_permission_drift() {
12989        let mut spec = two_node_spec();
12990        spec.nodes[0].run_spec = Some(LogicalAgentSpec {
12991            role: Some(WireRole::Verify),
12992            isolation: Some(WireIsolation::ReadOnly),
12993            ..LogicalAgentSpec::new("verify the collected sources")
12994        });
12995
12996        let mut uninterrupted = Runtime::new();
12997        uninterrupted.submit(&syscall_config());
12998        let started = uninterrupted.submit(&workflow_start("in-start", 1_700_000_001_000, spec));
12999        uninterrupted.submit(&spawned(
13000            "in-ack-1",
13001            1_700_000_002_000,
13002            &effect_id(started.step_seq),
13003            &["wf-node0"],
13004        ));
13005        let completed = uninterrupted.submit(&child_done(
13006            "in-done-1",
13007            1_700_000_003_000,
13008            "wf-node0",
13009            "sources verified",
13010        ));
13011
13012        let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
13013        let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13014        let original_task = uninterrupted
13015            .driver
13016            .project_logical_state()
13017            .scheduler
13018            .tasks
13019            .into_iter()
13020            .find(|task| task.task_id.as_str() == "wf-node0")
13021            .expect("the completed child remains projected");
13022        let restored_task = restored
13023            .driver
13024            .project_logical_state()
13025            .scheduler
13026            .tasks
13027            .into_iter()
13028            .find(|task| task.task_id.as_str() == "wf-node0")
13029            .expect("the restored child remains projected");
13030        assert_eq!(restored_task, original_task);
13031        let process = restored_task.process.expect("it is still a child process");
13032        assert_eq!(process.role, "verify");
13033        assert_eq!(process.isolation, "read_only");
13034        assert_eq!(process.context_inheritance, "none");
13035        assert!(
13036            process.join_result.is_some(),
13037            "the join result is source state"
13038        );
13039
13040        let next_spawn = effect_id(completed.step_seq);
13041        let ack = spawned("in-ack-2", 1_700_000_004_000, &next_spawn, &["wf-node1"]);
13042        assert_eq!(
13043            restored.submit(&ack),
13044            uninterrupted.submit(&ack),
13045            "the restored process table authorizes the same next transition",
13046        );
13047        assert_eq!(surface(&restored), surface(&uninterrupted));
13048    }
13049
13050    #[test]
13051    fn a_pending_subagent_preemption_restores_from_the_transition_effect() {
13052        let (mut uninterrupted, _) = workflow_with_live_child();
13053        let requested = uninterrupted.submit(&signal_delivery(
13054            "in-sig-critical",
13055            1_700_000_003_000,
13056            "delivery-critical",
13057            1,
13058            logical_signal("sig-critical", SignalUrgency::Critical),
13059        ));
13060        let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
13061        let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13062
13063        let resolved = resolved(
13064            "in-preempted",
13065            1_700_000_004_000,
13066            &effect_id(requested.step_seq),
13067            EffectSuccess::TasksPreempted(super::super::effect::TasksPreemptedSuccess {
13068                attempts: vec![super::super::effect::TaskPreemptOutcome {
13069                    task_id: TaskId::new("wf-node0").unwrap(),
13070                    attempt_id: WireAttemptId::new("wf-node0:attempt:1").unwrap(),
13071                    outcome: super::super::effect::TaskPreemptStatus::Preempted(
13072                        super::super::effect::TaskPreempted {},
13073                    ),
13074                }],
13075            }),
13076        );
13077        assert_eq!(
13078            restored.submit(&resolved),
13079            uninterrupted.submit(&resolved),
13080            "the transition-owned preempt intent remains resolvable after restore",
13081        );
13082        assert_eq!(surface(&restored), surface(&uninterrupted));
13083    }
13084
13085    /// Task 16b · a `PagedOut` handle restored from a checkpoint projects the archived tool body
13086    /// exactly as the uninterrupted renderer does on the next provider call.
13087    #[test]
13088    fn a_paged_out_result_restores_to_the_same_rendered_provider_context() {
13089        let mut uninterrupted = Runtime::new();
13090        uninterrupted.submit(&syscall_config());
13091        let started = uninterrupted.submit(&agent_start("in-start", 1_700_000_001_000));
13092        let archived_body = "ARCHIVED TOOL OUTPUT ".repeat(300);
13093        {
13094            let engine = uninterrupted
13095                .driver
13096                .engine
13097                .as_mut()
13098                .expect("the configured agent has an engine");
13099            let mut assistant = Message::assistant("I checked the archive.");
13100            assistant.tool_calls = vec![crate::types::message::ToolCall {
13101                id: "call-archived".into(),
13102                name: "search".into(),
13103                arguments: json!({"q": "archived evidence"}),
13104            }];
13105            engine.ctx.push_history(assistant, 8);
13106            engine.ctx.push_history(
13107                Message::tool(vec![ContentPart::ToolResult {
13108                    call_id: "call-archived".into(),
13109                    output: archived_body,
13110                    is_error: false,
13111                }]),
13112                1_200,
13113            );
13114            let handle_id = engine
13115                .ctx
13116                .handles
13117                .all()
13118                .iter()
13119                .find(|handle| handle.source.as_deref() == Some("call-archived"))
13120                .expect("the tool result is addressable")
13121                .id;
13122            engine
13123                .ctx
13124                .handles
13125                .get_mut(handle_id)
13126                .expect("the handle remains live")
13127                .residency = Residency::PagedOut {
13128                payload_ref: "payload:checkpoint-archive".to_string(),
13129                digest: format!("sha256:{}", "1".repeat(64)),
13130            };
13131        }
13132        let checkpoint = uninterrupted.checkpoint().decode().expect("verifies");
13133        let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13134        assert_eq!(
13135            serde_json::to_value(
13136                &restored
13137                    .driver
13138                    .engine()
13139                    .expect("restored engine")
13140                    .ctx
13141                    .render()
13142                    .turns
13143            )
13144            .unwrap(),
13145            serde_json::to_value(
13146                &uninterrupted
13147                    .driver
13148                    .engine()
13149                    .expect("uninterrupted engine")
13150                    .ctx
13151                    .render()
13152                    .turns
13153            )
13154            .unwrap(),
13155            "the restored PagedOut preview itself matches before either run advances",
13156        );
13157
13158        let acted = provider_result(
13159            "in-acted",
13160            1_700_000_002_000,
13161            &effect_id(started.step_seq),
13162            vec![tool_call(
13163                "call-after-page-out",
13164                "search",
13165                json!({
13166                    "q": "fresh evidence"
13167                }),
13168            )],
13169        );
13170        let uninterrupted_tools = uninterrupted.submit(&acted);
13171        let restored_tools = restored.submit(&acted);
13172        assert_eq!(restored_tools, uninterrupted_tools);
13173
13174        let results = tools_resolved(
13175            "in-results",
13176            1_700_000_003_000,
13177            &effect_id(uninterrupted_tools.step_seq),
13178            &[("call-after-page-out", "fresh evidence found", false)],
13179        );
13180        let uninterrupted_provider = uninterrupted.submit(&results);
13181        let restored_provider = restored.submit(&results);
13182        assert_eq!(
13183            restored_provider, uninterrupted_provider,
13184            "the post-restore renderer emits the same provider context with a PagedOut preview",
13185        );
13186        assert_eq!(surface(&restored), surface(&uninterrupted));
13187    }
13188
13189    /// **Verification 1 (full-state form)** · an uninterrupted run and a restored one are
13190    /// byte-identical.
13191    ///
13192    /// Both sides submit the *same* envelope list. The second takes a full-state checkpoint
13193    /// half-way, throws the runtime away, restores from the blob plus the records above it, and
13194    /// finishes. Every record digest on both journals must match, and so must the whole observable
13195    /// surface — including the digest of the logical state, which is the strongest statement
13196    /// available: the two runtimes would produce the same checkpoint.
13197    #[test]
13198    fn an_uninterrupted_run_and_a_full_state_restore_are_byte_identical() {
13199        let envelopes = turn_envelopes();
13200
13201        let mut uninterrupted = Runtime::new();
13202        drive(&mut uninterrupted, &envelopes);
13203
13204        let mut interrupted = Runtime::new();
13205        drive(&mut interrupted, &envelopes[..4]);
13206        let candidate = interrupted.checkpoint();
13207        let checkpoint = candidate.decode().expect("the candidate blob verifies");
13208        assert_eq!(
13209            checkpoint.base_step_seq(),
13210            checkpoint.through_step_seq(),
13211            "this half of the differential exercises the full-state form",
13212        );
13213
13214        // The crash: everything in memory is gone, and all the host still holds is the blob and the
13215        // journal. Records at or below `through_step_seq` are deliberately *not* handed back.
13216        let mut restored = interrupted.restore(&checkpoint);
13217        assert_eq!(
13218            restored.restore_cost.unwrap().records_before_checkpoint,
13219            0,
13220            "a restore with a checkpoint reads nothing below it",
13221        );
13222        assert_eq!(
13223            surface(&restored),
13224            surface(&interrupted),
13225            "the restored runtime is the runtime that crashed",
13226        );
13227
13228        drive(&mut restored, &envelopes[4..]);
13229        assert_eq!(
13230            digests(&restored.journal),
13231            digests(&uninterrupted.journal[4..]),
13232            "every post-restore record is byte-identical to the uninterrupted one",
13233        );
13234        assert_eq!(
13235            surface(&restored),
13236            surface(&uninterrupted),
13237            "and so is the state they end in",
13238        );
13239    }
13240
13241    /// **Verification 1 (rebase form)** · the same differential over a checkpoint whose logical
13242    /// state sits *below* its covered head.
13243    ///
13244    /// The restore therefore has real work to do before it touches the journal: it replays the
13245    /// bounded tail, verifies each replayed record against the digest the checkpoint recorded, and
13246    /// only then continues.
13247    #[test]
13248    fn an_uninterrupted_run_and_a_rebased_restore_are_byte_identical() {
13249        let envelopes = turn_envelopes();
13250
13251        let mut uninterrupted = Runtime::new();
13252        drive(&mut uninterrupted, &envelopes);
13253
13254        let mut interrupted = Runtime::new();
13255        drive(&mut interrupted, &envelopes[..2]);
13256        let base = interrupted
13257            .checkpoint()
13258            .decode()
13259            .expect("the base candidate verifies");
13260        drive(&mut interrupted, &envelopes[2..4]);
13261
13262        let rebased = interrupted
13263            .tx
13264            .checkpoint_rebase(
13265                &CheckpointBoundary {
13266                    through_step_seq: base.through_step_seq(),
13267                    covered_head: base.covered_transaction_head_digest().clone(),
13268                },
13269                base.logical_state().clone(),
13270            )
13271            .expect("a rebase over (1, 3] is assemblable")
13272            .decode()
13273            .expect("the rebase blob verifies");
13274        assert!(
13275            rebased.base_step_seq() < rebased.through_step_seq(),
13276            "this half of the differential exercises the rebase form",
13277        );
13278        assert_eq!(rebased.tail_inputs().len(), 2);
13279
13280        let mut restored = interrupted.restore(&rebased);
13281        let cost = restored.restore_cost.unwrap();
13282        assert_eq!(cost.records_before_checkpoint, 0);
13283        assert_eq!(
13284            cost.tail_inputs_replayed, 2,
13285            "the tail was actually replayed"
13286        );
13287        assert_eq!(
13288            surface(&restored),
13289            surface(&interrupted),
13290            "replaying the bounded tail lands on the state the run was in",
13291        );
13292
13293        // The rebase form is what Task 16 adds to the checkpoint contract, so its five candidate
13294        // values and the state a restore lands on are frozen together.
13295        let produced = json!({
13296            "description":
13297                "Spec 12.2 / 12.3 rule 11 · the rebase form end to end. `logical_state` is the \
13298                 state after `base_step_seq`, `tail_inputs` covers (base, through] exactly, and \
13299                 `base_record_digest` is the chain anchor the tail replays from — the record an \
13300                 acked checkpoint is allowed to have reclaimed. Restoring the blob replays that \
13301                 tail, verifies each replayed record against the digest the checkpoint carries, and \
13302                 lands on the head the run was at.",
13303            "base_step_seq": rebased.base_step_seq().to_string(),
13304            "base_record_digest": rebased.base_record_digest().as_str(),
13305            "through_step_seq": rebased.through_step_seq().to_string(),
13306            "covered_head": rebased.covered_transaction_head_digest().as_str(),
13307            "state_digest": rebased.state_digest().as_str(),
13308            "tail_steps": rebased
13309                .tail_inputs()
13310                .iter()
13311                .map(|entry| entry.step_seq.to_string())
13312                .collect::<Vec<_>>(),
13313            "restored_head": restored.tx.head().unwrap().digest.as_str(),
13314            "restore_cost": {
13315                "records_before_checkpoint": cost.records_before_checkpoint,
13316                "tail_inputs_replayed": cost.tail_inputs_replayed,
13317                "records_after_checkpoint": cost.records_after_checkpoint,
13318            },
13319        });
13320        let expected = golden("golden_checkpoint_rebase_restore.json", &produced);
13321        assert_eq!(produced, expected, "the rebase restore drifted");
13322        assert_eq!(expected["base_step_seq"], json!("1"));
13323        assert_eq!(expected["through_step_seq"], json!("3"));
13324        assert_eq!(
13325            expected["restore_cost"]["records_before_checkpoint"],
13326            json!(0),
13327            "the whole point: nothing below the base is read",
13328        );
13329
13330        drive(&mut restored, &envelopes[4..]);
13331        assert_eq!(
13332            digests(&restored.journal),
13333            digests(&uninterrupted.journal[4..]),
13334        );
13335        assert_eq!(surface(&restored), surface(&uninterrupted));
13336    }
13337
13338    /// §7.10 · a body that lives with the host travels as a reference, and comes back as one.
13339    ///
13340    /// The half of §5q-2 that keeps the checkpoint from re-inlining what §7.10 spent an effect kind
13341    /// keeping out: the message carries the preview, the handle carries the locator and the digest,
13342    /// and the restore rebuilds exactly that pairing.
13343    #[test]
13344    fn an_external_body_is_checkpointed_by_reference_and_restored_as_one() {
13345        let (mut runtime, effect) = agent_awaiting_tool_results();
13346        let body_digest =
13347            super::super::record::canonical_digest(b"a body far over the inline threshold");
13348        runtime.submit(&payloads_resolved(
13349            "in-results",
13350            1_700_000_003_000,
13351            &effect,
13352            vec![external_payload(
13353                "call-1",
13354                body_digest.clone(),
13355                64 * 1024,
13356                "the first 2 KiB of it",
13357            )],
13358        ));
13359
13360        let context_vm = runtime.driver.project_logical_state().context_vm;
13361        let referenced: Vec<&StoredMessageState> = context_vm
13362            .messages
13363            .iter()
13364            .filter(|message| matches!(message.body, StoredMessageBody::Reference(_)))
13365            .collect();
13366        assert_eq!(referenced.len(), 1, "exactly the external result");
13367        let StoredMessageBody::Reference(reference) = &referenced[0].body else {
13368            unreachable!()
13369        };
13370        assert_eq!(reference.digest, body_digest.as_str());
13371        assert_eq!(reference.tool_call_id.as_deref(), Some("call-1"));
13372        assert_eq!(reference.preview, "the first 2 KiB of it");
13373        assert!(
13374            !serde_json::to_string(&context_vm)
13375                .unwrap()
13376                .contains("a body far over the inline threshold"),
13377            "the body itself is nowhere in the checkpoint",
13378        );
13379
13380        let checkpoint = runtime.checkpoint().decode().expect("verifies");
13381        let restored = Runtime::restore_with(Some(&checkpoint), &[]);
13382        assert_eq!(surface(&restored), surface(&runtime));
13383        assert_eq!(
13384            restored
13385                .driver
13386                .project_logical_state()
13387                .context_vm
13388                .handles
13389                .iter()
13390                .filter_map(|handle| handle.digest.clone())
13391                .collect::<Vec<_>>(),
13392            vec![body_digest.to_string()],
13393            "the handle that addresses the body is restored with its verification digest",
13394        );
13395    }
13396
13397    /// §12.3 rule 11 · the two candidate forms agree on `state_digest` for the same logical state.
13398    ///
13399    /// This is what makes them interchangeable rather than merely both legal: a host that switches
13400    /// from full-state to rebase does not change what its checkpoints *mean*, only how much they
13401    /// re-serialise.
13402    #[test]
13403    fn a_rebase_and_a_full_state_candidate_agree_on_the_state_digest() {
13404        let envelopes = turn_envelopes();
13405        let mut runtime = Runtime::new();
13406        drive(&mut runtime, &envelopes[..2]);
13407        let base = runtime
13408            .checkpoint()
13409            .decode()
13410            .expect("the base candidate verifies");
13411        drive(&mut runtime, &envelopes[2..4]);
13412
13413        let full_state = runtime.checkpoint();
13414        let rebase = runtime
13415            .tx
13416            .checkpoint_rebase(
13417                &CheckpointBoundary {
13418                    through_step_seq: base.through_step_seq(),
13419                    covered_head: base.covered_transaction_head_digest().clone(),
13420                },
13421                base.logical_state().clone(),
13422            )
13423            .expect("a rebase is assemblable");
13424
13425        assert_eq!(
13426            full_state.through_step_seq, rebase.through_step_seq,
13427            "both cover the same prefix",
13428        );
13429        assert_eq!(full_state.covered_head, rebase.covered_head);
13430        assert_ne!(
13431            full_state.state_digest, rebase.state_digest,
13432            "they carry *different* logical states — one at the head, one at the base",
13433        );
13434        assert_eq!(
13435            rebase.state_digest,
13436            *base.state_digest(),
13437            "and a rebase carries its base's state forward untouched, byte for byte",
13438        );
13439
13440        // Restoring either one lands on the same place, which is the property the digests are
13441        // evidence *for*.
13442        let from_full = runtime.restore(&full_state.decode().unwrap());
13443        let from_rebase = runtime.restore(&rebase.decode().unwrap());
13444        assert_eq!(surface(&from_full), surface(&from_rebase));
13445        assert_eq!(surface(&from_full), surface(&runtime));
13446    }
13447
13448    /// **Verification 2** · restore cost is bounded by the tail, not by how long the run is.
13449    ///
13450    /// Deterministic counters, not a timer: the claim is about how much history is read, and that is
13451    /// a number the restore can report exactly. The run is driven to three different lengths and the
13452    /// cost is asserted *equal* across all three — not merely "small".
13453    #[test]
13454    fn long_run_restore_cost_is_bounded_by_the_tail_not_the_run() {
13455        fn cost_after(turns: usize) -> RestoreCost {
13456            let mut runtime = Runtime::new();
13457            runtime.submit(&syscall_config_with(|config| {
13458                config.execution_policy = Some(ExecutionPolicy {
13459                    max_turns: Some(10_000),
13460                    ..ExecutionPolicy::default()
13461                });
13462            }));
13463            let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
13464            let mut effect = effect_id(started.step_seq);
13465            let mut at = 1_700_000_002_000;
13466            for turn in 0..turns {
13467                let acted = runtime.submit(&provider_result(
13468                    &format!("in-acted-{turn}"),
13469                    at,
13470                    &effect,
13471                    vec![tool_call(
13472                        &format!("call-{turn}"),
13473                        "search",
13474                        json!({ "q": turn }),
13475                    )],
13476                ));
13477                at += 1_000;
13478                let results = runtime.submit(&tools_resolved(
13479                    &format!("in-results-{turn}"),
13480                    at,
13481                    &effect_id(acted.step_seq),
13482                    &[(&format!("call-{turn}"), "a source", false)],
13483                ));
13484                at += 1_000;
13485                effect = effect_id(results.step_seq);
13486            }
13487
13488            // The host checkpoints at the head and hands the restore only what is above it.
13489            let checkpoint = runtime.checkpoint().decode().expect("verifies");
13490            let after = runtime.journal_from(&checkpoint);
13491            assert!(after.is_empty(), "the checkpoint covers the whole journal");
13492            Runtime::restore_with(Some(&checkpoint), &after)
13493                .restore_cost
13494                .expect("a restored runtime reports its cost")
13495        }
13496
13497        let short = cost_after(2);
13498        let medium = cost_after(8);
13499        let long = cost_after(32);
13500
13501        assert_eq!(short.total_transitions(), 0, "nothing is replayed at all");
13502        assert_eq!(
13503            (short, medium),
13504            (medium, long),
13505            "restore cost does not grow with the length of the run",
13506        );
13507
13508        // And the contrast that makes the number mean something: without a checkpoint the same
13509        // restore reads the whole journal.
13510        let mut runtime = Runtime::new();
13511        drive(&mut runtime, &turn_envelopes());
13512        let journal = runtime.journal.clone();
13513        let from_genesis = Runtime::restore_with(None, &journal);
13514        assert_eq!(
13515            from_genesis.restore_cost.unwrap().records_before_checkpoint,
13516            journal.len() as u64,
13517            "the no-checkpoint arm is O(run), which is exactly what §12 replaces",
13518        );
13519        assert_eq!(surface(&from_genesis), surface(&runtime));
13520    }
13521
13522    /// §12.2 last line · with no checkpoint the fold starts at genesis and runs the **same** path.
13523    #[test]
13524    fn a_restore_without_a_checkpoint_uses_the_same_transaction_fold() {
13525        let mut runtime = Runtime::new();
13526        drive(&mut runtime, &turn_envelopes()[..4]);
13527        let from_genesis = Runtime::restore_with(None, &runtime.journal);
13528        assert_eq!(surface(&from_genesis), surface(&runtime));
13529        assert_eq!(from_genesis.restore_cost.unwrap().tail_inputs_replayed, 0);
13530    }
13531
13532    /// **Verification 3, row 1** · §12.3 rule 5: a crash between install and ack still restores
13533    /// from the installed checkpoint.
13534    ///
13535    /// The ack is a *retention* signal, not a durability one. Nothing about the checkpoint's
13536    /// validity depends on it, which is why this row is a test rather than a caveat.
13537    #[test]
13538    fn crash_matrix_install_then_crash_before_ack_restores_from_the_installed_checkpoint() {
13539        let envelopes = turn_envelopes();
13540        let mut runtime = Runtime::new();
13541        drive(&mut runtime, &envelopes[..4]);
13542
13543        let candidate = runtime.checkpoint();
13544        let checkpoint = candidate.decode().expect("the host installed this blob");
13545        // ... and then the process dies. `note_checkpoint_acked` was never called.
13546        let restored = runtime.restore(&checkpoint);
13547        assert_eq!(surface(&restored), surface(&runtime));
13548        assert_eq!(restored.restore_cost.unwrap().records_before_checkpoint, 0);
13549
13550        // §12.2 line 8 · what the host is handed back: the effects to (re-)execute, or a terminal.
13551        let recovered = restore_operation(
13552            Some(&checkpoint),
13553            &[],
13554            ConfigDefaults::default(),
13555            InMemoryRecordIndex::new(),
13556        )
13557        .expect("the ladder runs");
13558        assert_eq!(
13559            recovered
13560                .pending_effects()
13561                .iter()
13562                .map(|effect| effect.tag())
13563                .collect::<Vec<_>>(),
13564            vec![EffectKindTag::CallProvider],
13565            "§5g-1 · the effect the operation is waiting on is exposed again",
13566        );
13567        assert!(recovered.terminal().is_none(), "the run had not ended");
13568    }
13569
13570    /// **Verification 3, row 2** · §12.3 rules 1 and 3: records appended after the candidate are
13571    /// kept as tail, and a restore replays them from the journal.
13572    #[test]
13573    fn crash_matrix_appends_after_a_candidate_are_restored_from_the_journal() {
13574        let envelopes = turn_envelopes();
13575        let mut runtime = Runtime::new();
13576        drive(&mut runtime, &envelopes[..4]);
13577
13578        let checkpoint = runtime.checkpoint().decode().expect("verifies");
13579        // The candidate is a read: the operation keeps going while the host persists the blob.
13580        drive(&mut runtime, &envelopes[4..6]);
13581
13582        let restored = runtime.restore(&checkpoint);
13583        let cost = restored.restore_cost.unwrap();
13584        assert_eq!(
13585            cost.records_after_checkpoint, 2,
13586            "the two records appended after the candidate are replayed from the journal",
13587        );
13588        assert_eq!(cost.records_before_checkpoint, 0);
13589        assert_eq!(surface(&restored), surface(&runtime));
13590    }
13591
13592    /// **Verification 3, row 3** · §12.3 rule 2: install does not require the covered head to still
13593    /// be the current head.
13594    #[test]
13595    fn crash_matrix_install_when_the_covered_head_has_moved_on() {
13596        let envelopes = turn_envelopes();
13597        let mut runtime = Runtime::new();
13598        drive(&mut runtime, &envelopes[..4]);
13599        let candidate = runtime.checkpoint();
13600        let covered = candidate.through_step_seq;
13601
13602        drive(&mut runtime, &envelopes[4..]);
13603        assert_ne!(
13604            runtime.tx.head().unwrap().step_seq,
13605            covered,
13606            "the journal has moved past the covered head",
13607        );
13608
13609        // The ack still names a prefix of *this* journal, which is the only precondition rule 2
13610        // leaves standing.
13611        let mut acked = Runtime::new();
13612        drive(&mut acked, &envelopes);
13613        let usage = acked
13614            .tx
13615            .note_checkpoint_acked(&candidate.boundary())
13616            .expect("a checkpoint that covers a prefix is ackable after the head moved");
13617        assert_eq!(
13618            usage.records, 3,
13619            "acking reclaims the covered prefix and keeps the rest as tail",
13620        );
13621
13622        // And the blob still restores to the prefix it was taken over.
13623        let restored = runtime.restore(&candidate.decode().unwrap());
13624        assert_eq!(surface(&restored), surface(&runtime));
13625    }
13626
13627    /// **Verification 3, row 4** · §12.3 rules 6, 7 and 10: after an ack the prefix may be
13628    /// reclaimed, and a redelivery from down there is still answered — by reference.
13629    ///
13630    /// This is the row that would have been silently wrong without the ledger: with the record
13631    /// gone, a `prepare` that consulted only the journal would accept the input a **second** time.
13632    #[test]
13633    fn crash_matrix_ack_then_prune_then_restore_still_answers_a_redelivery() {
13634        let envelopes = turn_envelopes();
13635        let mut runtime = Runtime::new();
13636        drive(&mut runtime, &envelopes[..4]);
13637
13638        let checkpoint = runtime.checkpoint().decode().expect("verifies");
13639        runtime
13640            .tx
13641            .note_checkpoint_acked(&checkpoint.boundary())
13642            .expect("the boundary names a prefix of this journal");
13643
13644        // Retention reclaims everything the checkpoint covers: the restore gets the blob and an
13645        // empty journal, exactly as a pruned host would hand it over.
13646        let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13647        assert_eq!(surface(&restored), surface(&runtime));
13648
13649        // A redelivery of an input from the reclaimed prefix is acknowledged, not re-accepted.
13650        let redelivered = restored.prepare(&envelopes[3]);
13651        let RecordPreparation::Replayed(replay) = redelivered else {
13652            panic!("a redelivery below the checkpoint base must not be accepted again");
13653        };
13654        assert_eq!(replay.step_seq, WireU64::new(3));
13655        assert_eq!(
13656            replay.record_digest,
13657            *runtime.journal[3].record_digest(),
13658            "§12.3 rule 10 · the answer is the original step and record digest",
13659        );
13660        assert!(
13661            replay.committed_step.is_none() && replay.record.is_none(),
13662            "and it carries no step payload — the guarantee down there is idempotent \
13663             acknowledgement, not step reproduction",
13664        );
13665
13666        // The operation still runs forward from where it was.
13667        drive(&mut restored, &envelopes[4..]);
13668        let mut uninterrupted = Runtime::new();
13669        drive(&mut uninterrupted, &envelopes);
13670        assert_eq!(surface(&restored), surface(&uninterrupted));
13671    }
13672
13673    /// §12.3 rule 8 / Task 16 acceptance · identity does not move across a restore.
13674    #[test]
13675    fn a_restore_preserves_effect_terminal_attempt_and_handle_identity() {
13676        let envelopes = turn_envelopes();
13677        let mut runtime = Runtime::new();
13678        drive(&mut runtime, &envelopes[..4]);
13679
13680        let before_effects: Vec<String> = runtime
13681            .tx
13682            .pending_effects()
13683            .map(|effect| effect.effect_id.to_string())
13684            .collect();
13685        let before_handles =
13686            serde_json::to_value(runtime.driver.project_logical_state().context_vm.handles)
13687                .unwrap();
13688        let before_attempts =
13689            serde_json::to_value(runtime.driver.project_logical_state().scheduler.attempts)
13690                .unwrap();
13691
13692        let checkpoint = runtime.checkpoint().decode().expect("verifies");
13693        let mut restored = Runtime::restore_with(Some(&checkpoint), &[]);
13694
13695        assert!(!before_effects.is_empty(), "the fixture has live identity");
13696        assert_eq!(
13697            restored
13698                .tx
13699                .pending_effects()
13700                .map(|effect| effect.effect_id.to_string())
13701                .collect::<Vec<_>>(),
13702            before_effects,
13703            "§5g-1 · appended-but-unpublished effects are re-exposed under their own ids",
13704        );
13705        assert_eq!(
13706            serde_json::to_value(restored.driver.project_logical_state().context_vm.handles)
13707                .unwrap(),
13708            before_handles,
13709            "handle identity survives",
13710        );
13711        assert_eq!(
13712            serde_json::to_value(restored.driver.project_logical_state().scheduler.attempts)
13713                .unwrap(),
13714            before_attempts,
13715            "task attempt identity survives",
13716        );
13717
13718        // A terminal survives too, and closes the restored operation to the same inputs.
13719        drive(&mut restored, &envelopes[4..]);
13720        let terminal = restored.tx.terminal().cloned().expect("the run ended");
13721        let after_terminal =
13722            Runtime::restore_with(Some(&restored.checkpoint().decode().unwrap()), &[]);
13723        assert_eq!(
13724            after_terminal.tx.terminal(),
13725            Some(&terminal),
13726            "the terminal is restored as the same terminal, not re-derived",
13727        );
13728    }
13729
13730    /// §12.3 · the hard tail limit is a retryable `CheckpointRequired`, and there is no latch.
13731    ///
13732    /// The whole arc, because the latch this replaces was only visibly wrong at the *end* of it:
13733    /// refuse → checkpoint → ack → the same envelope, unchanged, succeeds.
13734    #[test]
13735    fn a_full_tail_refuses_retryably_and_the_same_envelope_succeeds_after_an_ack() {
13736        let mut runtime = Runtime::new();
13737        runtime.submit(&syscall_config_with(|config| {
13738            config.recovery_policy = Some(super::super::command::RecoveryPolicy {
13739                provider_recovery_attempts: None,
13740                output_recovery_attempts: None,
13741                tail_bounds: Some(super::super::command::TailBoundsPolicy {
13742                    soft_records: Some(WireU64::new(2)),
13743                    hard_records: Some(WireU64::new(3)),
13744                    soft_bytes: Some(WireU64::new(64 * 1024)),
13745                    hard_bytes: Some(WireU64::new(1024 * 1024)),
13746                }),
13747            });
13748        }));
13749        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
13750        let acted = runtime.submit(&provider_result(
13751            "in-acted",
13752            1_700_000_002_000,
13753            &effect_id(started.step_seq),
13754            vec![tool_call("call-1", "search", json!({"q": "sources"}))],
13755        ));
13756
13757        let next = tools_resolved(
13758            "in-results",
13759            1_700_000_003_000,
13760            &effect_id(acted.step_seq),
13761            &[("call-1", "three sources found", false)],
13762        );
13763        let fault = runtime.reject(&next);
13764        assert_eq!(fault.code, KernelFaultCode::CheckpointRequired);
13765        assert!(fault.is_retryable(), "exactly one code says retry");
13766
13767        // The refusal's shape is a contract in its own right — it is what tells four hosts to take
13768        // a checkpoint rather than to give up — so it is frozen as a fixture.
13769        let produced = json!({
13770            "expect": "checkpoint_required",
13771            "description":
13772                "Spec 12.3 · the next transaction would carry the journal tail past its hard limit, \
13773                 so `prepare` refuses with the one retryable fault code and zero mutation. The \
13774                 input was never accepted: after a checkpoint candidate is installed and acked, the \
13775                 *same* envelope — same input id, same clock, same payload — is submitted again and \
13776                 commits. There is no permanent overflow latch: an acked checkpoint takes the tail \
13777                 pressure straight back to nominal.",
13778            "tail_bounds": {
13779                "soft_records": "2",
13780                "hard_records": "3",
13781                "soft_bytes": "65536",
13782                "hard_bytes": "1048576",
13783            },
13784            "tail_usage_at_refusal": {
13785                "records": runtime.tx.tail_usage().records.to_string(),
13786            },
13787            "refused_envelope": serde_json::to_value(&next).unwrap(),
13788            "fault": serde_json::to_value(&fault).unwrap(),
13789            "retryable": fault.is_retryable(),
13790        });
13791        let expected = golden(
13792            "reject_transaction_checkpoint_required_tail_full.json",
13793            &produced,
13794        );
13795        assert_eq!(produced, expected, "the CheckpointRequired refusal drifted");
13796
13797        // Zero mutation: the refusal moved nothing, so the checkpoint below covers the same prefix
13798        // the refusal saw.
13799        let head_before = runtime.tx.head().expect("a head");
13800        let candidate = runtime.checkpoint();
13801        assert_eq!(candidate.through_step_seq, head_before.step_seq);
13802
13803        runtime
13804            .tx
13805            .note_checkpoint_acked(&candidate.boundary())
13806            .expect("the ack names this journal's head");
13807
13808        // The *same* envelope, byte for byte — §5e-3 forbids re-stamping the clock — now commits.
13809        let committed = runtime.submit(&next);
13810        assert_eq!(committed.step_seq, WireU64::new(3));
13811        assert_eq!(
13812            runtime.tx.tail_pressure(),
13813            TailPressure::Nominal,
13814            "an acked checkpoint moves the pressure straight back — there is no latch",
13815        );
13816    }
13817
13818    /// §12.3 · crossing the soft watermark is advice, delivered once, on the commit that crossed it.
13819    #[test]
13820    fn the_soft_watermark_is_advice_delivered_once_on_the_crossing() {
13821        let mut runtime = Runtime::new();
13822        let genesis = runtime.submit(&syscall_config_with(|config| {
13823            config.recovery_policy = Some(super::super::command::RecoveryPolicy {
13824                provider_recovery_attempts: None,
13825                output_recovery_attempts: None,
13826                tail_bounds: Some(super::super::command::TailBoundsPolicy {
13827                    soft_records: Some(WireU64::new(2)),
13828                    hard_records: Some(WireU64::new(8)),
13829                    soft_bytes: Some(WireU64::new(64 * 1024)),
13830                    hard_bytes: Some(WireU64::new(1024 * 1024)),
13831                }),
13832            });
13833        }));
13834        assert!(
13835            genesis.checkpoint_advice.is_none(),
13836            "one record is not a watermark crossing",
13837        );
13838
13839        let started = runtime.submit(&agent_start("in-start", 1_700_000_001_000));
13840        let advice = started
13841            .checkpoint_advice
13842            .expect("the second record takes the tail to the soft watermark");
13843        assert_eq!(advice.through_step_seq, started.step_seq);
13844        assert_eq!(advice.usage.records, 2);
13845        assert_eq!(advice.bounds.soft_records, WireU64::new(2));
13846
13847        let acted = runtime.submit(&provider_result(
13848            "in-acted",
13849            1_700_000_002_000,
13850            &effect_id(started.step_seq),
13851            vec![tool_call("call-1", "search", json!({"q": "sources"}))],
13852        ));
13853        assert!(
13854            acted.checkpoint_advice.is_none(),
13855            "advice is edge-triggered: staying over the watermark is not news",
13856        );
13857    }
13858
13859    /// §12.3 rule 9 · rebuilding after a poisoned transaction costs O(tail) when a checkpoint
13860    /// exists.
13861    ///
13862    /// The failure this replaces re-executed every accepted input of the run, so the cost of one
13863    /// CAS conflict grew with the age of the operation. Here the recovery path *is* the restore
13864    /// path, and the counter says so.
13865    #[test]
13866    fn rebuilding_after_a_conflict_costs_the_tail_not_the_run() {
13867        let envelopes = turn_envelopes();
13868        let mut runtime = Runtime::new();
13869        drive(&mut runtime, &envelopes[..6]);
13870        let checkpoint = runtime.checkpoint().decode().expect("verifies");
13871
13872        // A CAS conflict poisons the transaction: the journal moved under this runtime and §8.3
13873        // leaves exactly one way forward.
13874        let preparation = runtime.prepare(&envelopes[6]);
13875        let token = preparation.token().expect("prepared").clone();
13876        let fault = runtime.tx.note_append_conflict(&token, None);
13877        assert_eq!(fault.code, KernelFaultCode::TransactionConflict);
13878        assert!(runtime.tx.is_poisoned());
13879
13880        let rebuilt = Runtime::restore_with(Some(&checkpoint), &[]);
13881        let cost = rebuilt.restore_cost.unwrap();
13882        assert_eq!(
13883            cost.total_transitions(),
13884            0,
13885            "the rebuild reads the checkpoint and nothing else — O(tail), not O(run)",
13886        );
13887        assert!(!rebuilt.tx.is_poisoned());
13888    }
13889
13890    /// A checkpoint whose logical state was edited fails the restore's own re-projection check.
13891    ///
13892    /// The point of the check is that it catches a *hydration* gap as well as a tampered blob: both
13893    /// present as "the state that came back is not the state that was captured".
13894    #[test]
13895    fn a_restore_that_does_not_reproduce_the_captured_state_fails_closed() {
13896        let envelopes = turn_envelopes();
13897        let mut runtime = Runtime::new();
13898        drive(&mut runtime, &envelopes[..4]);
13899        let checkpoint = runtime.checkpoint().decode().expect("verifies");
13900
13901        // Re-assemble the same header over a *different* logical state, so every digest is
13902        // internally consistent and only the state is wrong. A blob edited in storage would be
13903        // caught by `verify()`; this one gets past it and has to be caught by the re-projection.
13904        // A *derived* field is the honest probe here: the partition token counters come back from
13905        // re-pushing the messages, so a forged counter is exactly what a hydration gap would look
13906        // like from the outside — the state that came back is not the state that was captured.
13907        let mut state = checkpoint.logical_state().clone();
13908        state.context_vm.partition_tokens.history += 7;
13909        let forged = KernelCheckpoint::assemble(CheckpointDraft {
13910            operation_id: operation(),
13911            genesis_digest: checkpoint.genesis_digest().clone(),
13912            base_step_seq: checkpoint.base_step_seq(),
13913            base_record_digest: checkpoint.base_record_digest().clone(),
13914            through_step_seq: checkpoint.through_step_seq(),
13915            covered_transaction_head_digest: checkpoint.covered_transaction_head_digest().clone(),
13916            logical_state: state,
13917            tail_inputs: Vec::new(),
13918        })
13919        .expect("a self-consistent checkpoint over a different state");
13920
13921        let error = restore_operation(
13922            Some(&forged),
13923            &[],
13924            ConfigDefaults::default(),
13925            InMemoryRecordIndex::new(),
13926        )
13927        .expect_err("a state that does not come back is a refusal, not a warning");
13928        assert_eq!(error.code, KernelFaultCode::CheckpointCorrupted);
13929        assert!(error.message.contains("restored logical state hashes to"));
13930    }
13931
13932    /// §5e-5 · the tail bound the transaction enforces is the one the genesis record froze, not
13933    /// the binary's baseline.
13934    #[test]
13935    fn the_tail_bound_comes_from_the_genesis_configuration() {
13936        let mut runtime = Runtime::new();
13937        assert_eq!(
13938            runtime.tx.bounds(),
13939            TailBounds::DEFAULT,
13940            "before genesis the transaction runs on the bootstrap baseline",
13941        );
13942
13943        runtime.submit(&syscall_config_with(|config| {
13944            config.recovery_policy = Some(super::super::command::RecoveryPolicy {
13945                provider_recovery_attempts: None,
13946                output_recovery_attempts: None,
13947                tail_bounds: Some(super::super::command::TailBoundsPolicy {
13948                    soft_records: Some(WireU64::new(4)),
13949                    hard_records: Some(WireU64::new(8)),
13950                    soft_bytes: Some(WireU64::new(64 * 1024)),
13951                    hard_bytes: Some(WireU64::new(256 * 1024)),
13952                }),
13953            });
13954        }));
13955
13956        assert_eq!(
13957            runtime.tx.bounds(),
13958            TailBounds::new(4, 8, 64 * 1024, 256 * 1024).unwrap(),
13959            "the genesis record's resolved configuration is what bounds the tail",
13960        );
13961        assert_eq!(
13962            runtime
13963                .tx
13964                .config()
13965                .expect("configured")
13966                .recovery_policy
13967                .tail_bounds,
13968            runtime.tx.bounds(),
13969            "and it is frozen in the record, so a rebuild re-derives the same bound",
13970        );
13971    }
13972}