sprawling-kernel 0.0.8

Every decision of a sprawling city - addresses, events, gates, plans and errors - with nothing that touches a disk.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-- Copyright (c) 2026 2youg1 and the sprawling contributors

/-!
# kernel::event::record

规定 `kernel::event::record`(`crates/kernel/src/event/record.rs` 与 `crates/kernel/src/event/record/` 下按族分的文件):每个有载荷结构的 `EventKind` 一个 serde 结构,以及它们与账本之间仅有的两扇门。本文件是 `crates/kernel/Spec.lean` 的一个分部;下面每一节保留它在 kernel 规格里的标签 §8-n,别处引作 `crates/kernel/Spec.lean §8-n`。
-/

/-!
**`kernel::event::record`——每个 `EventKind` 一个 serde 结构**

载荷的键只在这里拼写一次;`Payload::of` 与 `Payload::read` 是它与账本之间仅有的两扇门,
调用点不手写 `insert("k")` 与 `get("k")`。

```rust
pub struct SkillPin { pub name: String, pub hash: B3Hash }   // hash 必填
pub struct RunStarted {                 // 字段全部 #[serde(default)]
    pub task: String, pub goal: String, pub job: Option<Locator>,
    pub parent: Option<RunId>, pub predecessor: Option<RunId>,
    pub skills: Vec<SkillPin>,          // 空亦写出
    pub dispatched_by: Option<Who>,     // 由谁派来:person/city/派活的居民地址;缺键为 None
    pub policy: Option<RunPolicy>,      // 这次 run 的运行策略(§8-77);缺键即这一行早于策略入账
    pub effort: Option<Effort>,         // §8-85
}
pub struct RunForked { pub from: RunId, pub at_seq: Seq }
pub struct PromptSource { pub addr: Address, pub kept: u64, pub marker: bool, pub dropped: u64 }
    // prompt_assembled 的一行来源;不带摘要生产者指纹:没有路径产出摘要(runtime D1)。旧行里多出的 producer 键读时忽略;本结构没有方法,读者直接读字段
pub struct EvalRun { pub probe: String, pub version: u32, pub predecessor: RunId,  // eval_run:交接探针的一次读数
                    pub kept: u32, pub lost: Vec<u32>,       // lost 是答案不同的题号
                    pub before: Vec<String>, pub after: Vec<String> }   // 全部必填,空亦写出
pub struct WentBack { pub name: String, pub point: GitOid }            // 回到过去:一棵新树起于 point
pub struct FileRestored { pub name: String, pub path: String, pub point: GitOid } // path 取 git 树的写法
pub struct CommitAttribution {          // flatten 进每一条指名提交的记录
    pub model: String, pub effort: Option<Effort>, pub predecessor: Option<RunId>,
}
pub struct Commit { pub oid: GitOid, #[serde(flatten)] pub by: CommitAttribution,
                    pub scope: Vec<String>, pub files: Vec<String> }
#[serde(untagged)]
pub enum CheckpointCommitted { JobPinned { job: Locator }, Committed(Commit) }

pub enum Scope { City, Building(Address), Workshop(Address) }   // city | building:<addr> | workshop:<addr>
impl Scope { pub fn covers(&self, addr: &Address) -> bool; pub fn parse(raw: &str) -> Result<Scope, AxError>; }

pub struct ApprovalResolved { pub id: ApprovalId, pub verdict: Ruling, pub cluster: ClusterKey }
pub struct AutonomyChanged { #[serde(default = "city_wide")] pub scope: Scope,
                             #[serde(with = "autonomy_word")] pub autonomy: Autonomy }
pub struct CityHalted { pub scope: Scope, pub state: Admittance }
#[serde(rename_all = "snake_case")] pub enum Admittance { Halted, Released }
pub mod autonomy_word {                          // owner | delegate:<resident>
    pub fn spell(autonomy: &Autonomy) -> String;
    pub fn read(word: &str) -> Result<Autonomy, AxError>;
}
pub struct GovernedDocumentWritten { pub which: String, pub bytes: usize,
                                     pub naming: Option<B3Hash> }        // §8-79
pub struct RulesChanged { pub scope: Scope, pub which: GoverningDocument,
                          pub before: Option<B3Hash>, pub after: B3Hash, pub bytes: usize }
pub enum GoverningDocument { Rules, Config }     // serde: "RULES.toml" | "CONFIG.toml"
pub struct FileDiscarded { pub paths: Vec<String>,            // `file:<path>`,git 认的名字 Address 未必认
                           pub restoration: Option<Restoration> } // 缺或方案不识:None,paths 照读
pub struct DiscardRestored { pub paths: Vec<String> }
pub struct AssetArchived { #[serde(default = "fact")] pub kind: String,
                           #[serde(default)] pub day: u64, #[serde(default)] pub subject: String }
pub struct PursuitChanged { pub step: PursuitMove,       // goal 只在 clear 之后缺席
                            pub goal: Option<String> }
#[serde(rename_all = "snake_case")] pub enum PursuitMove { Set, Pause, Resume, Clear }
pub struct GoalConflict { pub goal: GoalId, pub with: GoalId, pub level: ConflictLevel }
#[serde(rename_all = "snake_case")] pub enum ConflictLevel { Serialize, Arbitrate }
// goal_registered 的载荷就是 GoalEntry 本身,不另立 struct:目标表持有的正是它
impl PursuitChanged { pub fn held(self) -> Result<Option<(String, PursuitState)>, AxError>; }
pub struct SignalEnqueued { pub id: SignalId, pub kind: SignalKind, pub from: String, pub room: Address,
                            pub room_version: Version, pub payload: Payload, pub at: TimeMs,
                            pub lane: Option<Lane> }   // lane 写出给 collab 之外的读者,回读不采信:它由 kind 推出
pub struct RoadmapMoved { pub by: String, pub node: NodeId,   // roadmap_claimed/split/finished/released/blocked 共用
                          #[serde(flatten)] pub step: RoadmapStep }   // 五行同有 by、node、verb,差别只在 verb 之后
#[serde(tag = "verb", rename_all = "snake_case")]
pub enum RoadmapStep { Claimed { item: String }, Split { children: Vec<String> },
                       Finished { item: String, evidence: Locator },
                       Released { item: String, why: StopCause, line: String },
                       Blocked { item: String, why: StopCause, line: String } }
// NodeId 读回经 NodeId::parse:node 读不出的一行让折叠停下,而不是被跳过——跳过会让节点看似无人持有
pub struct WorktreeOpened { pub name: String, pub disk_bytes: ByteLen }   // 不携路径:路径是一台机器的事实
pub struct SignalConsumed { pub id: SignalId, pub by: String }   // 内容已在 enqueue 行里,不写第二遍
#[serde(rename_all = "snake_case")] pub enum Lane { Urgent, Ordinary }
pub struct SignalId(String);                       // 非空、无空白;serde 经 parse/as_str
pub enum SignalKind { Mention, Thread, Broadcast, Steer }   // serde 经 parse/as_str,四个线上词只一处
pub struct EmbeddingCalled { pub model: String, pub inputs: u64, pub vectors: u64,
                             pub dimensions: Option<u64>, pub prompt_tokens: Option<Tokens> }
pub struct RerankCalled { pub model: String, pub passages: u64, pub ranks: u64,
                          pub prompt_tokens: Option<Tokens> }

#[serde(rename_all = "snake_case")] pub enum AdviserAsk { Noul, Score, Choice }
pub struct AdviserAsked { pub ask: AdviserAsk, pub subject: String }
#[serde(tag = "ask", rename_all = "snake_case")]
pub enum AdviserAnswer { Noul { keep: bool, confidence_bp: u16 },
                         Score { score_bp: u16 }, Choice { chosen: String } }
pub struct AdviserAnswered { pub subject: String, #[serde(flatten)] pub answer: AdviserAnswer,
                             pub elapsed_ms: u64 }
#[serde(rename_all = "snake_case")]
pub enum AdviserFailure { Unavailable, Timeout, Unreadable }
pub struct AdviserFellBack { pub subject: String, pub reason: AdviserFailure }

// record::credential:F3 家族里凭据进出的两行。`ref` 是 SecretRef 而不是 String,
// 语法之外的引用读不成这一行(Payload::read 报 E_WIRE_MISMATCH),不再被读者各自静默跳过。
// 旧版本在订阅登录时另写过 `expires_at` 键;读入时忽略它,本版本不写。
pub struct SecretCaptured { #[serde(rename = "ref")] pub reference: SecretRef,
                            #[serde(default)] pub origin: String }  // enrolment | pasted;旧行另有 <provider>-subscription | <provider>-renewal
pub struct ToolkitLinkOpened { pub toolkit: String }

// record::endpoint:F3 家族里「哪个 model 替哪个 tag 作答」的两行。gateway 的 EndpointBook
// 只经 Payload::read 读它们;InputKinds 随之归 kernel(gateway::InputKinds 是它的再导出),
// 因为账本行的值要用 kernel 自己的类型。
#[serde(rename_all = "snake_case")] #[derive(Default)]
pub enum InputKinds { #[default] Text, TextImage }
pub struct ModelSelected { pub tag: ModelTag, pub endpoint: String, pub model: String,
                           pub context_tokens: u64,
                           #[serde(default)] pub max_output_tokens: Option<u64>, // 总是写出,缺席写 null
                           pub ceiling_from: Option<String>,   // person|upstream|preset|policy;缺席即省略
                           #[serde(default)] pub input: InputKinds, // 旧行没有这个键,读作 Text
                           pub input_price: UsdMicros, pub output_price: UsdMicros,
                           pub cache_read_price: UsdMicros, pub cache_write_price: UsdMicros }
impl ModelSelected { pub fn ceiling(&self) -> Option<Ceiling>; }   // 0 与 null 同读作「未声明」
pub struct EndpointLost { pub name: String }
pub struct EndpointAttached { pub name: String, pub base_url: String, pub dialect: DialectKind,
                              pub auth: Option<SecretRef>,          // 引用,从不是密钥;缺席即省略
                              pub auth_header: Option<String>,      // 非 bearer 时凭据所在的 header
                              pub models: Vec<String>,              // 空亦写出
                              pub connection_kind: Option<String>,  // 旧行没有,读者按 dialect 回推
                              #[serde(default = true)] pub probed: bool,
                              pub tuning: Option<AttachedTuning> }  // 什么都没设就省略;不是对象读作未设
pub struct AttachedTuning { pub label: Option<String>, pub timeout_ms: Option<u64>,
                            pub stream_idle_timeout_ms: Option<u64>, pub request_max_retries: Option<u32>,
                            pub proxying: Option<Proxying>,        // 默认值省略
                            pub extra_headers: Vec<(String, String)>, pub overrides: Vec<(String, String)> }
// AttachedTuning 的每个键缺席读作未设、在而读不懂也读作未设(行不被拒):编造一个期限比没有期限更难解释。
// EndpointAttached 顶层的键则不然:probed、auth、connection_kind 在而读不懂,整行读不成(E_WIRE_MISMATCH),
// 因为把一个没探到的端点读成探到过,是在书里放进一个没人够得着的端点。

// record::probe:endpoint_probed,一次探测在挂上任何东西之前看到的。ModelFacts 随之归 kernel
// (gateway::ModelFacts 是它的再导出,读一行 /models 的 gateway::endpoint::models::facts_of 仍归 gateway),
// 因为账本行的值要用 kernel 自己的类型。
pub struct ModelFacts { pub id: String, pub context_tokens: Option<u64>, pub max_output_tokens: Option<Ceiling>,
                        pub input_modalities: Vec<String>, pub input_price: Option<String>,
                        pub output_price: Option<String> }        // 缺席写 null,行没说就是没说
pub struct EndpointProbed { pub name: String, pub base_url: String, pub reach: Reach,
                            pub models: Vec<String>, pub facts: Vec<ModelFacts>,   // 读不到列表时两者都写空
                            pub failed: Option<ProbeFailure> }                   // 缺席即省略
pub struct ProbeFailure { pub code: String, pub subject: String }
// record::provider:provider_degraded 有两个写方、两种形状,一个 untagged enum 让读者靠读来分,
// 不靠猜键:E_PROVIDER 经 kernel::error 的 carrier 表平铺写成 AxError 本身;vault 启动探针退到
// session memory 时写 VaultFellBack。两者都不是的行读不成(E_WIRE_MISMATCH),错误里并列两种
// 形状各自缺的那个字段:Deserialize 手写,先试 AxError 再试 VaultFellBack;派生的 untagged 只会说
// 「没有一个变体匹配」,Note::Unreadable 就给不出该去看哪个字段。写出仍是 untagged。
#[serde(untagged)] pub enum ProviderDegraded { Refused(AxError), VaultFellBack(VaultFellBack) }
pub struct VaultFellBack { pub component: String,     // 探针写 vault
                           pub fallback: String,      // 探针写 session-memory
                           pub persistence: String,   // gateway::Persistence 的自有拼法
                           pub reason: String }
// wire::note_of 只把 Refused 记成回合上的 Note::Refused;VaultFellBack 没改变任何回合,不出 note。

// record::harness:官方 harness 居民一次 run 写的两种行(`crates/sprawling/Spec.lean` §8-4e)。
// 词是本城的,不是 ACP 的:账本写下的拼写不能再改,ACP 的词跟着上游走(D9)。
#[serde(tag = "report", rename_all = "snake_case")]
pub enum HarnessReported {
    Said { text: String },                                    // 回答城的一段
    Thought { text: String },                                 // 推理的一段
    ToolCall { call: String, title: String, kind: String },   // harness 开始的一次工具调用,按它自己的话
    ToolCallStatus { call: String, status: String },          // 那次调用的状态变了
    Other { variant: String },                                // 本城没有读法的一种汇报,只记它的名字
    PermissionAsked { title: String, options: Vec<HarnessPermit> },
    PermissionAnswered { chosen: Option<String> },            // 城选的 option id;None 即答 cancelled
}
pub struct HarnessPermit { pub id: String, pub name: String, pub kind: HarnessPermitKind }
#[serde(rename_all = "snake_case")]
pub enum HarnessPermitKind { AllowOnce, AllowAlways, RejectOnce, RejectAlways }
pub struct HarnessAnswered { pub stop: HarnessStop, pub text: String }   // text 空亦写出
#[serde(rename_all = "snake_case")]
pub enum HarnessStop { EndTurn, MaxTokens, MaxTurnRequests, Refusal, Cancelled }

pub struct ToolCalled { pub id: String, pub name: ToolName, pub args: Payload,
                        pub subject: Option<String>,      // 键缺席读作 None
                        pub effect: Option<Effect>,       // 调用那一刻的登记(§8-75);缺席即省略
                        pub render: Option<RenderIntent> }
pub struct ToolResult { pub tool_use_id: String, pub name: ToolName,
                        #[serde(flatten)] pub answer: ToolAnswer }
#[serde(untagged)]
pub enum ToolAnswer { Answered { result: Payload }, Failed { error: Payload } }

pub struct CityInitialized {}           // 城名在信封的 addr
pub struct BuildingCreated { pub addr: Address, pub template: String,
                             pub adopted: bool }   // false 时不写出
pub struct BuildingConfigured { pub addr: Address, pub sandbox: bool, pub mcp: bool,
                                pub desktop: bool, pub context: bool }
pub struct CancelReceived {}
pub struct HandoffWritten { pub must_read: Vec<Locator>, pub overview: String,
    pub progress: String, pub context: String, pub next_step: String }
pub struct WatchdogFired { #[serde(flatten)] pub action: FiredAction,
                           pub corrections: u32, pub provider_failures: u32 }
#[serde(tag = "action", rename_all = "snake_case")]
pub enum FiredAction { Steer { text: String },
                       BackOff { until_ms: u64, code: String, subject: String },
                       Freeze { reason: String } }
pub struct GateChecked {}               // 无生产写方:结构是决定
pub struct PolicyChanged { pub id: String }   // policy_created/policy_revoked,无写方:结构是决定
#[serde(untagged)] pub enum CacheRenewed {   // cache_renewed:先试 Refused,因为 Answered 的字段全可缺
    Refused { refused: AxError },
    Answered { usage: Option<ModelUsage>, billed_usd_micros: Option<UsdMicros> },
}
```

结构按族住 `crates/kernel/src/event/record/` 下;哪个 kind 有结构,以那里的类型为准。
`gate_denied`、`budget_limit` 的载荷是平铺的 `AxError`,`approval_requested` 的是 `ApprovalItem`,
`result_offloaded` 的是 `runtime::sieve::ResultOffloaded`(它平铺筛子的账,筛子在 runtime),不另立结构;
`watchdog_fired` 的读方 `wire::note_of` 把 `BackOff` 读成它等待的那次拒绝(码与主语照录),
`Steer`/`Freeze` 不出 note——纠偏与冻结各有自己的行;
`pr_merged` 借 `CommitAttribution` 记「谁做的这次提交」,其余键由调用点手写。
没有结构的 kind 由调用点手写读取。

- **harness 的两种行各有一个结构。** `harness_reported` 一条汇报一行,`report` 键说是哪一种;`harness_answered` 在停止原因到了时写一行,携停止原因与这一回合 harness 回答城的文字(依次拼起来的回答段)。两者都不 `default`:这两种行自诞生起就按这个形状写,缺键的行该拒而不该猜。回答「城做了什么」的折叠恒不读 `harness_reported`,只有回答「harness 说了什么」的视图读它。

- **`ToolCalled.subject` 在写记录时算定,读方读它,不从 `args` 再推一遍。**
  两个读方各按自己的 map 序挑第一个字符串时,同一次调用读出两个主语:
  `{"10":"a","2":"b"}` 在账本的 `BTreeMap` 字节序下是 `"a"`,
  在浏览器的自有属性序下是 `"b"`。这张优先键表(`path`/`addr`/`program`/`arm`,
  都不在时取载荷键序里的第一个字符串)只有一处,即 `ToolCalled::subject_of`;
  写方 `runtime::turn::wave` 调它一次,`wire` 不再持有第二份。
  `None` 是这次调用的参数没有指名任何东西,是一个真实状态而不是失败。

`ApprovalResolved.verdict` 是 `Ruling` 本身而不是 `format!("{verdict:?}")` 的小写词:
两个读方若各自与字面量比较,认不出的词就会在一处读作拒、在另一处读作准;
所以认不出的词是一次读失败,不是一个默认值。三键均不 `default`——`approval_resolved`
自诞生起就无条件写出这三键,缺 verdict 的行该拒而不该猜。
`AutonomyChanged` 与 `CityHalted` 的四个值都有类型:作用域是 [`Scope`],
「关/开」是 `Admittance`,「谁来答」是 `Autonomy`。三条小文法只在 `record` 拼写与读回,
写方与每个读方都经这里,
认不出的词是一次 `E_WIRE_MISMATCH`,不会在一处读作「开」、在另一处读作「人自己答」。**已落盘的行照常读**:三种拼法逐字未变,
而 `autonomy_changed` 的 `scope` 键在它存在之前写下的行里缺席,那样的行指的是整座城,
`city_wide` 就这样读它。

三条不变量,因为已落盘的账本不可重拼:

1. **字节不动。** 字段名即旧写方用的键;旧写方省略的键写 `skip_serializing_if`,
   旧写方无条件写出的键(含空数组)无条件写出。键序由 `serde_json` 的 BTreeMap 定,
   与此处声明序无关。每族一组逐字节对拍测试守住这条。
2. **读宽写严。** 旧构建可能不写的字段一律 `#[serde(default)]`——`tools/fixtures/golden-s1`
   里就有一条 `data` 为 `{}` 的 `run_started`;未知键忽略而不拒。
3. **值保留 kernel 类型。** run 是 `RunId` 而非 `String`,job 是 `Locator`;
   手写 `Serialize` 的类型挂 `schemars(with = "String")` 说明其 wire 形状。

一处记录在案的形状债:`checkpoint_committed` 同时承载「派工钉住的 job」与「检查点提交」
两件不同的事,`CheckpointCommitted` 把这件事说出口而不是让读方从缺键推断;拆成两个 kind
需要新 `EventKind` 与账本版本,故记录在此而不在此处做。
-/

/-!
### 8-75 回合记录多记的事:回复的首个内容几时到(形状 2 值类型)

**(a) `model_returned.first_at`**

```rust
pub struct ModelReturned {
    // …既有字段…
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub first_at: Option<TimeMs>,   // 这次回复的首个非空内容到达城的时刻
}
```

- **它是一个时刻,不是一个时长。** 首字耗时(TTFT)由读者拿它减去开这个回合的 `model_called` 的 `t`;账本不记派生值,记下的两个时刻已经够算。
- **读数来自回合的钟**:`runtime::turn` 在它包住的增量汇点里读第一段非空内容到达的那一刻(`crates/runtime/Spec.lean` §8-50)。kernel 不采样,gateway 也不采样:`kernel::Model` 的实现从不读钟。
- **缺席有三种情形,都不是零**:回复从一扇到齐之前什么也不报的门回来(阻塞门、没有流的适配器、流式解析失败之后换阻塞门重发修好的那一次);回复在流上只带工具调用,没有一段文字或推理(首个内容的定义与理由见 runtime D6);这把键出现之前写下的每一行。三种都读作「没有量到」,页面不画首字耗时,不猜。
- **字节不动**:缺席即省略(`skip_serializing_if`),旧行与今天没量到的行字节相同;读宽(`default`)。账本版本不为此进位:`v` 为 2 的行里这一格可以缺席,读者不从版本推断它在不在。

**(b) `tool_called` 记下这件工具的登记:`effect` 与 `render`**

```rust
pub struct ToolCalled {
    // …既有字段…
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub effect: Option<Effect>,          // 调用那一刻这件工具登记的效果
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub render: Option<RenderIntent>,    // 调用那一刻这件工具登记的呈现
}
// Effect 与 RenderIntent 在 feature `schema` 下派生 JsonSchema:线上 Call 直接携它们(`crates/wire/Spec.lean` §8-55)
```

- **写方只有回合的工具波**:`runtime::turn::wave` 写 `tool_called` 时,从工具面的 `ConcurrentInvoke::meta_of` 取这件工具的 `ToolMeta`,照录其中两项(`crates/runtime/Spec.lean` §8-51)。工具面不认识这个名字时两者缺席——一次调用了没登记的工具,是一个真实状态。
- **记的是调用那一刻的登记**:一件工具以后换了效果或呈现,旧行仍说它当时是什么;折叠不去问今天的登记。新加一件内置工具不需要改任何读者:它的登记随它第一次被调用写进账本。
- **`Diff.locations` 照录登记**:登记层面的声明是空表(§8-23),每次调用的位置是工具一侧由参数算出的纯函数,今天还没有这个函数,所以账上的 `locations` 恒为空;一次编辑调用改的是哪个文件,读者读 `subject`。
- **不进模型的字节**:`tool_called` 是入窗种类,但窗口从 `model_returned` 的消息重建工具调用,从不读 `tool_called` 的载荷(`runtime::fork` 的逐种类表把它列在「不是对话」一侧),所以这两个键不改变任何请求。
- **字节不动**:缺席即省略,读宽;旧行两键都缺,读作「没有记下」。
-/

/-!
### 8-82 一次 run 怎样开篇,进程死后谁冻结它(`kernel::event::record::run`,形状 2 值类型)

#### 8-82-1 `run_started.opening`

```rust
pub enum Opening { FromJob, Inherited, WithPerson }   // "from_job" | "inherited" | "with_person"
pub struct RunStarted {
    // …既有字段…
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub opening: Option<Opening>,   // 第一条 user 消息的写法;缺席读作「不知道」
}
```

- **它说什么。** 一次 run 的第一条 user 消息有三种写法:`FromJob` 是一句指向前缀 run 段里 JOB.md 的话加目标,`Inherited` 是 `Task: …\nGoal: …`,`WithPerson` 是人的原话。写法在派活时由城定一次(`accounting::worker::freezing` 按 `city::RunBrief` 选),`runtime::conversation::Conversation::push_task_lines` 按它写出字节。
- **为什么记进账本。** 分叉从账本重建母 run 的对话(`runtime::fork::fold_run`),第一条消息要照母 run 发出的写法重建,provider 的前缀缓存才从第一条消息起命中(`crates/runtime/Spec.lean` §8-58)。不记,重建只能猜;而按 `goal` 是否为空去猜,等于在分叉里再写一遍 `city::write_brief` 的「有目标才是一份 job」那条规则。
- **一个枚举,一个家。** 这个值以前只住在 `runtime::conversation`;现在账本载荷要带它,而 runtime 依赖 kernel、kernel 不依赖 runtime,所以枚举住在本模块,`runtime::Opening` 与 `runtime::conversation::Opening` 是它的再导出,调用方的路径不变(runtime D14)。
- **写者**:`runtime::run::Charter::open` 从 `Charter.opening` 照录。模型 run 的 charter 填 `Some(RunPlan.opening)`;harness run 的第一句话是交给 harness 自己会话的 prompt,城不为它写 user 消息,填 `None`。
- **缺席读作「不知道」。** 加这个键之前写下的行没有它,`fold_run` 对它沿用加键之前的读法(`crates/runtime/Spec.lean` §8-58 的表)。按 `default` 加、缺席不写,所以旧账本照读,`golden-s1` 里载荷为 `{}` 的那行 `run_started` 照旧读成 `RunStarted::default()`。
- 验收:`runtime::fork::request_tests` 的 `a_branch_first_request_opens_with_the_bytes_of_the_mothers_last`,与 `accounting::worker::freezing::tests::lineage` 的 `a_branch_first_request_carries_the_bytes_of_the_mothers_last`(`crates/sprawling/Spec.lean` §8-141):写出的键经真实的派活与分叉读回。

#### 8-82-2 进程死后冻结的那一行:`run_frozen.cause`

```rust
pub enum FreezeCause { ProcessDied }                  // "process_died"
pub struct RunFrozen {
    pub completion: String,
    pub evidence: Option<Vec<EvidenceCite>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cause: Option<FreezeCause>,  // 不是 run 自己的驱动写下这一行时,为什么写
}
impl RunFrozen {
    pub fn of(completion: &Completion) -> RunFrozen;   // cause 恒 None,字节与加键之前相同
    pub fn lost() -> RunFrozen;                        // completion "cancelled"、无 evidence、cause ProcessDied
}
```

- **它说什么。** ARCHITECTURE §13.7 画的是 `Lost --> Frozen`:进程死在一次 run 的半途,那次 run 再也不会被驱动,重开的城要把它冻结。冻结的那一行由重开时的启动扫描写(`crates/accounting/Spec.lean` §8-18),不由 run 自己写,所以载荷多一个键说明原因。
- **结局仍是三种之一。** `Completion` 的三种结局不变(§8-20),死掉的 run 冻成 `cancelled`:它没做完,`Done` 需要城自己记下的证据而它没有;也没有被上限截断,`Limit` 说的是那件事。`cancelled` 说的是「停下了,不是做完」,`cause: process_died` 把「人或 halt 叫停的」与「进程死了」分开(D14)。
- **一处构造。** `RunFrozen::lost` 是写这一行的唯一入口,「死掉的 run 冻成哪一种结局」只在这里回答。
- **缺席即 run 自己的冻结。** `RunFrozen::of` 写的行没有这个键,字节与加键之前相同;旧行照读。读结局的读者(`views` 的 `completion`、`city::resident` 的计数)照旧只读 `completion`。
- 验收:`sprawling` 的崩溃验收 `a_city_killed_while_writing_an_answer_reopens_with_the_torn_line_cut_and_the_call_unknown`(`crates/sprawling/Spec.lean` §8-127)钉住死 run 的最后一行是这一行。

### 8-79 身份的两处入账:保存的回执与一次 run 冻下的那一版(`kernel::event::record`,形状 2 值类型)

```rust
pub struct GovernedDocumentWritten {
    pub which: String,
    pub bytes: usize,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub naming: Option<B3Hash>,     // 写完之后城的身份版本;写 CLERK.md 与旧行为 None
}
pub struct RunStarted {
    // …既有字段…
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub naming: Option<B3Hash>,     // 这次 run 的 session 冻下的身份版本(`crates/city/Spec.lean` §8-33)
}
```

- **两个键都只是摘要。** 身份本身——名字与「关于你」——在两份治理文档里,冻下的那一版在内容库里,摘要是读回它的钥匙(`cas:<naming>`)。账本不抄名字:名字是人改的字,抄进不可删的历史就有了第二个家,而人删掉的一段「关于你」会永远留在那里。
- **回执。** 写 `MAYOR.md` 或 `PREFERENCES.md`(`PutDocument`、`PutIdentity`)之后的那一行带上新的身份版本,页面见到它才把卡片标为已保存(`crates/wire/Spec.lean` §8-59)。写 `CLERK.md` 不动身份,键缺席。
- **一次 run 用的是哪一版。** `run_started.naming` 由 `runtime::run::Charter::open` 从 `RunPlan.naming` 照录(`crates/runtime/Spec.lean` §8-56)。旧行没有这个键,读作「这一行早于身份入账」,页面回退到地址或角色名,不用今天的名字。
- 两个键都按 `default` 加、缺席不写,所以旧账本照读,旧构建读新行时把它们当未知键拒(§8-40 的方向门照旧)。
- 验收:`record::run` 的 `a_run_started_line_records_the_naming_it_froze`(写出、读回、缺席不写)。
-/

/-!
### 8-85 `run_started` 记下这次 run 派出时冻下的推理强度(`kernel::event::record::run`,形状 2 值类型)

```rust
pub struct RunStarted {
    // …既有字段…
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub effort: Option<Effort>,     // 这次 run 的请求冻下的强度(`runtime::CallShape.effort`);缺席即没有说,由提供方自定
}
```

- **照录冻下的那一个值。** 强度随模型一起在 session 冻结(`runtime::turn::report` 拒绝 run 中途改它),所以 run 开头记一次就是整次 run 的事实。`runtime::run::Charter::open` 从 `RunPlan.shape.effort` 抄进来,与 `policy`、`naming` 同一处写(`crates/runtime/Spec.lean` §8-56)。
- **缺席有两种来历,读法相同。** 一是这次派活没有说强度,提供方自定——这与 `Effort::None`(请它不思考)是两件事,所以不写成 `none`;二是这一行早于这个键。页面两种都不画强度,不猜。harness run 没有本城的请求,恒缺席。
- **被否:读这次 run 的第一个提交的 `effort`**(`CommitAttribution`)。还没提交过的 run 说不出强度,而提交上的那个值写的是 `Effort::None` 兼指「没说」,读回来分不清。**被否:读 `model_selected`**:那是房间的选择,后来的派活可以在帧上另带强度,房间的选择不等于这次 run 用的那一个。
- 验收:`record::run` 的 `a_run_started_line_records_the_effort_it_froze`(写出、读回、缺席不写)。
-/

/-!
### 8-81 远程门的五种事件(`kernel::event::record::remote`,形状 2 值类型)

```rust
pub struct RemoteOpened { pub closes_at: TimeMs, pub url: String }
pub struct RemoteClosed { pub why: RemoteClosing }
pub enum RemoteClosing { Console, Locked, Expired }          // 线上 "console" | "locked" | "expired"
pub struct DevicePaired { pub device: String, pub name: String, pub authority: String }
pub struct DeviceRevoked { pub device: String, pub name: String }
pub struct RemoteSessionStarted { pub device: String, pub expires: TimeMs }
```

- **五种都是 record-only,都写在城自己名下**(`run` 为 `RunId::CITY`,无 `addr`):谁能从外面够到这座城不决定任何一次模型请求的字节。写它们的是装配层的远程门(`crates/sprawling/Spec.lean` §8-139),经 worker 的 relay 进同一个写者。`who` 是 `person`,只有门到时自己关上那一条是 `city`。
- **追加在 `ALL` 末尾,次序就是一扇门一生的次序**:开、关、配对、撤销、会话开始(表按种类排,入账的次序是开、配对、会话开始、撤销、关)。不写 `ig`:0.0.7 的读者不认识这五个种类,按 D10 的规矩报 `E_LOG_VERSION_UNSUPPORTED` 而不是跳过它们。
- **没有一行携带密钥、配对码或会话 id**:账本可以被任何人重放,带上它们等于把冒充一台设备的材料交给每一个读者。设备的公钥只在设备表里(`CityLayout::devices`)。
- **设备与权限用正文写**:`device` 是设备 id 的 base32 正文,`authority` 是 `watch` 或 `act`。两者的类型与拼写归 `remote_access::door`(crates/remote_access/Spec.lean §8-11),kernel 不依赖它,所以这里存它给出的字。

### 8-83 一份文档的一次写与一处修改提案的一生(`kernel::event::record::document`,形状 2 值类型)

```rust
pub struct DocumentWritten { pub at: Address, pub baseline: B3Hash, pub version: B3Hash, pub bytes: u64 }
pub struct ProposalOffered {
    pub doc: Address,          // 被提议修改的那份文档
    pub baseline: B3Hash,      // 提案所基于的版本(documents D3)
    pub start: u64,            // 那一版里被提议替换的半开字节区间
    pub end: u64,
    pub before: String,        // 那一段的文本,按那一版的编码解出
    pub after: String,         // 提议换成的文本
}
pub struct ProposalDecided { pub proposal: B3Hash, pub verdicts: Vec<SliceVerdict> }
pub struct SliceVerdict { pub slice: u32, pub verdict: Verdict }
pub enum Verdict { Accept, Amend { text: String } }   // 线上 "accept" | {"amend":{"text":…}}
pub struct ProposalWithdrawn { pub proposal: B3Hash }
```

- **四种都是 record-only,追加在 `ALL` 末尾**,次序是 `document_written`、`proposal_offered`、`proposal_decided`、`proposal_withdrawn`(D15)。不写 `ig`:0.0.7 的读者不认识它们,按 D10 报 `E_LOG_VERSION_UNSUPPORTED`。
- **`document_written` 是保存的回执**:写在城名下,载荷不携正文;同一条命令写下的每一行都带那条命令的 `idem`(accounting 的 `commanding::entrance`),页面凭它认出「这是我那一次」,并从 `version` 读到下一次保存的基线(`crates/wire/Spec.lean` §8-72)。`bytes` 是新版本的长度,不是改了多少。
- **区间是两个整数,不是 `documents::Span`**:kernel 不依赖 `documents`,读者经 `documents::Span::new(start, end)` 把它读回来,起点在终点之后的一行在那里被拒。
- **`before` 与 `after` 各至多 `documents::WINDOW_BYTES_MAX` 字节**:一张卡是一个窗口读得下的一段(documents D18)。判定在提出提案的那一处,kernel 只携带。
- **提案的身份不在载荷里**:它是提出它的 run 与上面六个值的摘要,由 `documents::Offer::id` 一处算出(documents D13);`proposal_decided` 与 `proposal_withdrawn` 的 `proposal` 就是这个摘要。同一个 run 对同一版同一段提同一句话是同一个提案。
- **`SliceVerdict` 与 `Verdict` 住在 kernel**:账本记下的是人逐句的决定,`wire` 的决定帧与 `documents` 的合并规则都直接用这两个类型,没有第二份拼写。`slice` 是句子在这张卡上的序号(从 0 数起);只有改动过的句子可以被点名,`Amend` 只对插入的句子成立(documents D16)。
-/

/-! D9 harness 的汇报与回答按本城的词入账

**决定**:`harness_reported` 与 `harness_answered` 的载荷用 kernel 自己的类型拼写(`HarnessReported`、`HarnessPermit`、`HarnessPermitKind`、`HarnessAnswered`、`HarnessStop`)。`agent_protocols::harness` 读 ACP 用的类型(`Update`、`PermissionAsk`、`Permit`、`StopReason`)不搬进 kernel,装配层把后者逐臂映成前者。

**理由**:账本一旦写下,拼写就不能再改(§8-4 三条不变量的第一条),ACP 的词却跟着上游走,两边只是今天恰好相同。把 ACP 的类型搬进 kernel,上游改一个词时要么改坏已落盘的行,要么让协议层读不懂新版本。映射是穷尽 `match`,上游多出一种汇报时,编译停在映射处,而不是静默丢掉。

**被否**:①载荷只记变体名与一段文字(`update: String`、`text: String`):读「harness 说了什么」的视图要再解析一次字符串,工具调用的标题与状态会混成一段;②`agent_protocols` 直接改用 kernel 的类型:理由同上;③`InputKinds`、`ModelFacts` 那样把类型归 kernel、协议层再导出:那两者的值来自本城自己的读法,这里的值来自上游规格。
-/

/-! D11 一次调用的效果与呈现,在调用那一刻从登记读出、记进 `tool_called`

**决定**:`ToolCalled` 多记两个可缺席的键 `effect` 与 `render`,由回合的工具波从工具面的登记照录(§8-75(b));线上的 `Call.effect`、`Call.render` 读这两个键。

**理由**:一件工具是什么,权威是它的 `ToolMeta`;这份登记只在一次 run 的工具台上存在,折叠账本的读面够不到它,而楼的 MCP 工具与内置工具的登记每次 run 都可能不同。写在调用那一行上,读面读到的就是那一刻的事实,一件新工具进来不需要任何读者多写一臂。

**被否**:①读面按工具名穷尽匹配出呈现:每加一件工具都要改这个匹配,MCP 工具的名字读面根本不知道;②读面持一份工具登记,在折叠时查:登记随楼与 run 变,旧行会被今天的登记重新解释;③线上自定一个不含地址的投影枚举:同一规则的第二个权威(`crates/wire/Spec.lean` §8-0)。

**代价**:经工具台写下的每条 `tool_called` 多二三十个字节。

**重开参数**:有了每次调用由参数算出位置的函数时,`render` 的 `Diff.locations` 改记那次调用的位置。
-/

/-! D13 远程门的五种事件全是 record-only,载荷里不带钥匙

**决定**:远程门开、关、配对、撤销、会话开始各是一个种类(§8-81),五种都是 record-only,载荷只写时刻、地址、设备 id 的正文、名字与权限,不写公钥、配对码与会话 id;设备与权限以 `remote_access` 给出的正文存,kernel 不另立类型。

**理由**:这五件事回答的是「谁、何时、凭什么进了城」,人从历史里读的正是这个;它们不改变任何一次模型请求,所以不入窗。钥匙类的字节进了账本就随每一次导出、每一次重放流出去,而它们在设备表里已经有一个家。kernel 不依赖 `remote_access`,为权限另立一个 kernel 枚举就是同一件事的第二份定义。

**被否**:①一个 `remote_door` 种类、载荷里带一个 `what` 字段:读者要先解载荷才知道发生了什么,`EventKind` 的穷尽匹配也看不见它;②在 `device_paired` 里存设备公钥,让设备表可以从账本重建:撤销之后公钥仍留在历史里,而设备表本来就原子写盘;③在 kernel 里定义 `DeviceAuthority`:与 `remote_access::door::Authority` 两处定义同一组值。

**重开参数**:账本需要证明「这一帧是哪台设备发来的」时(例如设备发出的命令要在账本上署设备的名),重议会话 id 是否入账。
-/

/-! D14 进程死后的 run 冻成 `cancelled`,载荷记下原因,不加第四种结局

**决定**:重开时的启动扫描为每一次有 `run_started`、没有 `run_frozen` 的 run 写一行 `run_frozen`,结局是 `cancelled`,载荷带 `cause: process_died`(§8-82-2)。`Completion` 仍是三种。

**理由**:`Completion` 的三种结局是 run 驱动自己能得出的判定,读它们的地方(视图的 `completion`、居民的冻结计数、`runtime::run` 的收尾)都按三种穷尽匹配。进程死亡不是一个 run 自己得出的判定,它是城重开时发现的事实;它在三种之中最接近 `cancelled`——停下了、没做完、不是上限截的。把「谁让它停的」放在载荷的一个可缺席的键里,读结局的地方一行不改,要区分死亡的读者读这个键。

**被否**:①加第四种结局 `Lost`:每个穷尽匹配 `Completion` 的地方都要多一臂,而 `RunFrozen::of` 的「三种结局,第四种不可表示」正是要守住结局的集合由 run 的驱动定;②不写冻结、让视图把没有冻结行的 run 读作死掉的:服务中的城与重开的城读同一份账本会得出不同的答案,而 ARCHITECTURE §13.7 的 `Lost --> Frozen` 要的是账本上的一行;③冻成 `limit`:那是「被上限截断」,读者会去找一个不存在的上限。

**重开参数**:出现一种由 run 自己得出、又不属于三种的结局时,重议 `Completion` 的集合,那时 `process_died` 也一并重议它属于哪一种。
-/

/-! D15 一份文档的一次写是一个种类,修改提案的一生是三个种类,都是 record-only

**决定**:经页面写一份任意文档记 `document_written`,修改提案提出、决定、收回各记一行(§8-83)。`document_written` 与 `proposal_decided` 写在城自己名下(`run` 为 `RunId::CITY`,无 `addr`),`proposal_offered` 与 `proposal_withdrawn` 写在提出它的 run 名下(`addr` 是那次 run 的房间);四种都不入窗,追加在 `ALL` 末尾,不写 `ig`。

**理由**:现有三种写文档的事件各只说一类文件(四份 spine 文档、三份治理文档、两份规矩文件),载荷里的 `which` 是那一类文件的名字;用它们记一份任意文档的写,`which` 就得装一个路径,读者分不出哪一行在说规矩。一次保存要回答的是「哪份文档、从哪一版到哪一版、多大」,这正是 `documents` 的版本身份(documents D3)给出的三件事,所以载荷记两枚摘要而不记正文:正文在盘上,账本记的是这件事发生过。提案不同:提案的文本是 run 说出来的话,与 `tool_called` 的参数同类,文档往后怎样改都不会把它留在盘上,所以 `before` 与 `after` 都在载荷里,重开的城只读账本就能把一张没决定的卡原样折回来。四种都不决定任何一次模型请求的字节。

**被否**:①一个 `document_changed` 种类、载荷里用 `cause` 分保存与提案:决定与写是两件事,一次决定可以什么都不写(整张拒绝),一次写可以不是决定(`PutRange`);②提案的文本只存进内容库、载荷记地址:内容库没有回收前这样省下的只是账本行的长度,而折叠就要多读一次内容库,账本也不再自足;③提案身份用随机 id 或账本 `seq`:kernel 恒不生成随机值,而 `seq` 要等落账之后才知道,写者在落账之前就要把它交给 run。

**重开参数**:账本行的长度成为开城时间里看得见的一段时,提案文本改存内容库;出现第二种写文档的入口(例如居民的 `edit` 也要记版本)时,重议 `document_written` 的写者。
-/