kernel/event/record/run.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! What a dispatch, a fork and a freeze record about a run.
7
8use serde::{Deserialize, Serialize};
9
10use crate::completion::Completion;
11use crate::event::identity::{RunId, Seq};
12use crate::event::kind::EventKind;
13use crate::event::who::Who;
14use crate::locator::{B3Hash, Locator};
15use crate::model::{Effort, RunPolicy};
16use crate::origin::Origin;
17
18/// One skill a run was dispatched with, pinned to the bytes it read.
19///
20/// The hash is the point: a page answers "which runs read these exact
21/// bytes" from it, and a pin naming a name alone cannot answer that.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
24pub struct SkillPin {
25 pub name: String,
26 #[cfg_attr(feature = "schema", schemars(with = "String"))]
27 pub hash: B3Hash,
28}
29
30/// `run_started`: the work a run was given, and where it came from.
31///
32/// Every field defaults, because the golden fixtures hold a
33/// `run_started` whose payload is `{}` and a projection that refused it
34/// would refuse a ledger this repository ships.
35#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37pub struct RunStarted {
38 /// What the person asked for, in their words.
39 #[serde(default)]
40 pub task: String,
41 /// What finishing looks like, in their words.
42 #[serde(default)]
43 pub goal: String,
44 /// The job this dispatch came out of. Absent only in a record
45 /// written before the key existed.
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
48 pub job: Option<Locator>,
49 /// The run that forked this one, when one did.
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub parent: Option<RunId>,
52 /// The run this one replaced, when it is a successor. Spelled the
53 /// same way here and on `checkpoint_committed`, because it is one
54 /// fact with one spelling.
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub predecessor: Option<RunId>,
57 /// Written even when empty, and deliberately: a key that comes and
58 /// goes is a shape a reader has to guess at, and "this building
59 /// admits no skills" is a fact worth recording rather than an
60 /// absence to infer.
61 #[serde(default)]
62 pub skills: Vec<SkillPin>,
63 /// Who dispatched this run: the person, the city's own desk (a
64 /// plan, the schedule, an arrival), or the resident that delegated,
65 /// succeeded itself or knocked. The line's author cannot say it,
66 /// because the city's desk writes every `run_started`. Absent only
67 /// in a record written before the key existed.
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
70 pub dispatched_by: Option<Who>,
71 /// The run policy this run was dispatched under (`crates/kernel/spec/Model.lean` §8-77).
72 /// Absent in a record written before the policy was recorded; the
73 /// six mode words before it never reached a typed payload, so
74 /// there is no older shape to read.
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub policy: Option<RunPolicy>,
77 /// The identity version this run's session froze: the key a page
78 /// reads the names the request carried back by, through the content
79 /// store (`crates/kernel/spec/Event/Record.lean` §8-79). Absent in a record written before the
80 /// key existed, which a page answers with the address rather than
81 /// with today's names.
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 #[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
84 pub naming: Option<B3Hash>,
85 /// How the run's first user message was written (`crates/kernel/spec/Event/Record.lean` §8-82-1):
86 /// what a fork reads to rebuild that message the way the mother sent
87 /// it. Absent in a record written before the key existed, and on a
88 /// harness run, whose first words are the harness's own prompt.
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub opening: Option<Opening>,
91 /// The effort the run's requests froze (`crates/kernel/spec/Event/Record.lean`
92 /// §8-85): the session's, which no turn may change. Absent when the
93 /// dispatch stated none and the provider chooses - which is not
94 /// [`Effort::None`], asking it not to think - on a harness run, and
95 /// in a record written before the key existed.
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub effort: Option<Effort>,
98}
99
100/// How a run's first user message opens.
101///
102/// Exhaustive, and the choice is made once by the city that wrote (or
103/// did not write) the job file. It is not a formatting preference: a
104/// session working from an assignment and a session talking with the
105/// person want different first words, and inferring which from an empty
106/// string would make the emptiness of a goal mean two things.
107///
108/// It lives here rather than beside the conversation that writes it
109/// because `run_started` records it, and the ledger's payloads are this
110/// crate's; `runtime::conversation` re-exports it.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
114pub enum Opening {
115 /// Somebody wrote the task down; the job file's text is in the prefix.
116 FromJob,
117 /// Somebody wrote the task down for a mother, and a branch is
118 /// rebuilding her conversation: her job file lives in her room, not
119 /// in the branch's prefix, so the task travels in the message.
120 Inherited,
121 /// Nobody did; the person is on the other side of this message.
122 WithPerson,
123}
124
125/// `run_frozen`: how a run ended, and what it cites for having ended
126/// that way.
127///
128/// The three endings are spelled by [`Completion::name`], which stays
129/// the authority for the words: this struct carries whichever word that
130/// method printed rather than an enum that would have to agree with it.
131/// A run that ended any other way cites nothing, and the key is absent
132/// rather than empty, exactly as the hand-written writer left it.
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
135pub struct RunFrozen {
136 pub completion: String,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub evidence: Option<Vec<EvidenceCite>>,
139 /// Why the line was written by something other than the run's own
140 /// driver (`crates/kernel/spec/Event/Record.lean` §8-82-2). Absent on every freeze a run wrote
141 /// for itself, so those lines keep the bytes they always had.
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub cause: Option<FreezeCause>,
144}
145
146/// Why a run was frozen by something other than its own driver.
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
150pub enum FreezeCause {
151 /// The process driving it died; the next `resume` froze it.
152 ProcessDied,
153}
154
155impl RunFrozen {
156 /// The one projection from a verdict to the line that records it.
157 ///
158 /// Reading is deliberately not the inverse: a [`Completion::Done`]
159 /// holds [`EventRef`](crate::EventRef)s, and minting those from a
160 /// line would forge the evidence the type exists to guarantee.
161 pub fn of(completion: &Completion) -> RunFrozen {
162 let evidence = match completion {
163 Completion::Done(evidence) => Some(
164 evidence
165 .refs()
166 .iter()
167 .map(|cited| EvidenceCite {
168 seq: cited.seq(),
169 kind: cited.kind(),
170 })
171 .collect(),
172 ),
173 Completion::Limit | Completion::Cancelled => None,
174 };
175 RunFrozen {
176 completion: completion.name().to_owned(),
177 evidence,
178 cause: None,
179 }
180 }
181
182 /// The freeze of a run whose process died before it froze itself:
183 /// the one place that says which ending such a run has.
184 ///
185 /// `cancelled`, because the run stopped without finishing - `done`
186 /// needs evidence the city recorded and it has none - and no ceiling
187 /// cut it, which is what `limit` says. The cause tells it apart from
188 /// a cancel somebody asked for (kernel D14).
189 pub fn lost() -> RunFrozen {
190 RunFrozen {
191 completion: Completion::Cancelled.name().to_owned(),
192 evidence: None,
193 cause: Some(FreezeCause::ProcessDied),
194 }
195 }
196}
197
198/// One line a finished run cites as the evidence it finished.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201pub struct EvidenceCite {
202 pub seq: Seq,
203 pub kind: EventKind,
204}
205
206/// `session_opened`: a new session began at this address.
207///
208/// The room is the subject rather than the run, because what changed is
209/// what the *next* run there is governed by: the model and the effort
210/// the previous session froze are gone, and whether the summary it left
211/// travels with this one is the one thing the caller chose. A replay
212/// reads the answer to "why did this run start fresh" from here;
213/// neither fact is visible in the room's files afterwards, because both
214/// were removals.
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
217pub struct SessionOpened {
218 /// What the session branched from, when it is a branch.
219 ///
220 /// `#[serde(default)]` and absent when there is none, so a line
221 /// written before this key existed reads as a session that began
222 /// without one - which is what it was.
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub from: Option<Origin>,
225 /// Whether the previous session's handoff travelled with it.
226 ///
227 /// **The room itself is the record's own `addr` field**, not a copy
228 /// here: a line that belongs to an address says so in the envelope,
229 /// and a payload copy would be a second place the same address is
230 /// spelled (`crates/kernel/spec/Event.lean` §8-4).
231 pub carried: bool,
232}
233
234/// `run_forked`: which run this one continues, and from where in it.
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
237pub struct RunForked {
238 /// The run whose history this one resumes.
239 pub from: RunId,
240 /// The last sequence number of `from` that this run inherits.
241 pub at_seq: Seq,
242}
243
244/// `eval_run`: the handoff probe asked of a successor, compared with
245/// what its predecessor answered.
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
248pub struct EvalRun {
249 /// The probe's name and version, which together fix its questions.
250 pub probe: String,
251 pub version: u32,
252 /// The run whose answers are the reference.
253 pub predecessor: RunId,
254 /// How many answers the successor kept.
255 pub kept: u32,
256 /// The question indices whose answers differ, in order.
257 pub lost: Vec<u32>,
258 /// The predecessor's answers and the successor's, one per question.
259 pub before: Vec<String>,
260 pub after: Vec<String>,
261}
262
263#[cfg(test)]
264#[allow(
265 clippy::unwrap_used,
266 clippy::expect_used,
267 clippy::panic,
268 clippy::indexing_slicing,
269 reason = "test code"
270)]
271mod tests;