Skip to main content

deepstrike_core/runtime/kernel/wire/
event.rs

1//! External events — facts the host observed (spec §7.7).
2
3use serde::{Deserialize, Serialize};
4
5use super::scalar::{AttemptId, BoundedJson, DeliveryId, FiniteF64, SignalId, TaskId, WireU64};
6use super::syscall::SyscallRequest;
7
8/// An external event is a **fact**, not an effect result: a signal can arrive with no pending
9/// effect, and a child completes long after its spawn was acknowledged.
10///
11/// No variant carries a host wall clock. The envelope's `observed_at_ms` is the only clock the
12/// kernel admits — a result that carries its own `Date.now()` produces different bytes for the
13/// same intent, which turns idempotent replay into a conflict fault.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum ExternalEvent {
17    DeliverSignal(DeliverSignal),
18    ChildCompleted(ChildCompleted),
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[serde(deny_unknown_fields)]
23pub struct DeliverSignal {
24    /// Host delivery identity — distinct from the logical signal identity, so a redelivery is
25    /// recognisable as the same signal.
26    pub delivery_id: DeliveryId,
27    pub attempt: u32,
28    pub signal: LogicalSignal,
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct LogicalSignal {
34    pub signal_id: SignalId,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub source: Option<SignalSourceKind>,
37    #[serde(default)]
38    pub target: SignalTarget,
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub urgency: Option<SignalUrgency>,
41    #[serde(default, skip_serializing_if = "BoundedJson::is_null")]
42    pub payload: BoundedJson,
43    /// Metadata only. TTL, deadlines and admission all use the envelope's accepted time.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub source_timestamp_ms: Option<WireU64>,
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub dedupe_key: Option<String>,
48    /// How long this signal may wait before its urgency is raised one tier — a **duration**, not
49    /// an instant.
50    ///
51    /// A duration is the only shape that can be a canonical input. The deadline it implies is
52    /// anchored to the envelope's accepted time, which the kernel already owns, so a redelivery of
53    /// the same bytes anchors identically; an absolute `deadline_ms` would be a second host clock
54    /// on the wire (DEC-2, §11.2) and would make the same intent decode to a different deadline on
55    /// every retry.
56    ///
57    /// Inert unless `signal_policy.deadline_escalation` is on — that switch is the operation's
58    /// statement that it wants waiting to change priority at all. `Some(0)` is a legitimate value:
59    /// escalate on admission.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub escalate_after_ms: Option<WireU64>,
62}
63
64impl LogicalSignal {
65    pub fn new(signal_id: SignalId) -> Self {
66        Self {
67            signal_id,
68            source: None,
69            target: SignalTarget::default(),
70            urgency: None,
71            payload: BoundedJson::null(),
72            source_timestamp_ms: None,
73            dedupe_key: None,
74            escalate_after_ms: None,
75        }
76    }
77
78    /// The urgency this delivery reaches at admission, once a due `escalate_after_ms` is applied.
79    ///
80    /// The kernel has to know this *before* the router moves: the one effect a delivery can
81    /// publish is `PreemptTasks`, and DEC-8 says an undeclared effect is refused with nothing
82    /// mutated. Reading the wire urgency alone would let an escalated-to-critical signal reach the
83    /// router and only then discover the host cannot stop its children.
84    pub fn effective_urgency(&self, escalation_enabled: bool) -> SignalUrgency {
85        let urgency = self.urgency.unwrap_or(SignalUrgency::Normal);
86        let due_on_admission = self.escalate_after_ms.is_some_and(|after| after.get() == 0);
87        if escalation_enabled && due_on_admission {
88            urgency.escalated()
89        } else {
90            urgency
91        }
92    }
93}
94
95/// A signal targets the operation or one logical task. Host session ids are not a target.
96///
97/// Both variants are newtypes over their own struct rather than inline struct variants: serde
98/// cannot apply `deny_unknown_fields` to an inline variant of an internally tagged enum, so an
99/// inline shape would silently accept `{"kind":"task","task_id":"t","session_id":"s"}`.
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101#[serde(tag = "kind", rename_all = "snake_case")]
102pub enum SignalTarget {
103    Operation(OperationTarget),
104    Task(TaskTarget),
105}
106
107impl Default for SignalTarget {
108    fn default() -> Self {
109        Self::Operation(OperationTarget {})
110    }
111}
112
113#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
114#[serde(deny_unknown_fields)]
115pub struct OperationTarget {}
116
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118#[serde(deny_unknown_fields)]
119pub struct TaskTarget {
120    pub task_id: TaskId,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum SignalSourceKind {
126    Cron,
127    Gateway,
128    Heartbeat,
129    Custom,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
133#[serde(rename_all = "snake_case")]
134pub enum SignalUrgency {
135    Low,
136    Normal,
137    High,
138    Critical,
139}
140
141impl SignalUrgency {
142    /// One tier up, saturating at `Critical`. Mirrors the router's own escalation so the
143    /// pre-admission check and the router cannot disagree about what a due deadline produces.
144    pub fn escalated(self) -> Self {
145        match self {
146            Self::Low => Self::Normal,
147            Self::Normal => Self::High,
148            Self::High | Self::Critical => Self::Critical,
149        }
150    }
151}
152
153/// A child task attempt finished.
154///
155/// `parent_requests` is the **only** legal child→parent request channel: the requests enter P1
156/// with `ChildAttempt` causation inside this same transition. GAP-4: the completion itself is a
157/// fact and commits unconditionally, while each request is adjudicated independently — a denied
158/// request produces a structured rejection observation and changes neither the completion nor
159/// its siblings.
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161#[serde(deny_unknown_fields)]
162pub struct ChildCompleted {
163    pub task_id: TaskId,
164    pub attempt_id: AttemptId,
165    pub result: ChildResult,
166    #[serde(default, skip_serializing_if = "Vec::is_empty")]
167    pub parent_requests: Vec<SyscallRequest>,
168}
169
170#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
171#[serde(deny_unknown_fields)]
172pub struct ChildResult {
173    #[serde(default)]
174    pub status: ChildStatus,
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub output: Option<String>,
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub error: Option<String>,
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub usage: Option<UsageFacts>,
181    /// Observation-only quality score (verifier/judge). Finite by construction, and never a
182    /// branch input — thresholds that gate kernel decisions use fixed-point `Ppm`.
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub score: Option<FiniteF64>,
185}
186
187#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "snake_case")]
189pub enum ChildStatus {
190    #[default]
191    Completed,
192    Failed,
193    Cancelled,
194}
195
196/// Host-observed resource facts for one child attempt.
197#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
198#[serde(deny_unknown_fields)]
199pub struct UsageFacts {
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub input_tokens: Option<WireU64>,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    pub output_tokens: Option<WireU64>,
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub turns: Option<u32>,
206}