sprawling-kernel 0.0.12

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
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
-- 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`。

这一分部只有文字:它是说明文档,不是形式规格,这里没有一句是被证明的;它写下的接口形状与取舍由 Rust 的类型与 `kernel::event::record::tool::tests`、`kernel::event::record::turn::tests`、`kernel::event::record::run::tests`等 守住。
-/

/-!
**`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 agent: Option<AgentRunIdentity>, // §8-87:harness run 由哪一个 ACP agent 接手
}
pub struct AgentRunIdentity { pub id: String, pub version: Option<String>, pub launch_digest: B3Hash }
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 window_from: Option<String>,    // person|upstream|registered|preset;窗口未知时缺席,缺席即省略
                           pub ceiling_from: Option<String>,   // person|upstream|registered|preset|policy|provider;缺席即省略
                           #[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 facts_blob: Option<B3Hash> }      // §8-88:登记时读到的 ModelFacts 在 CAS 里的那一份;缺席即省略
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 max_in_flight: Option<u32>,        // 没人定过就省略;1 到 256 之外读作未设
                            pub account_retries: Option<AccountRetries>, // 同一账号上再发几次(one|two,§8-86);没人定过就省略
                            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 thinking: Option<ThinkingStatement>, // 缺席即省略:列表没说思考档
                        pub canonical: Option<String>,           // 上游自己的规范 id;缺席即省略
                        pub listing: Listing }                   // 这一行从哪里来(D63);listed 即省略
pub enum Listing { Listed, Named }   // 线上 snake_case;缺键读作 listed
pub struct ThinkingStatement { pub levels: Vec<Effort>,         // 列表点名的档,城的升序;恒不含 none;空即省略
                               pub default: Option<Effort>,     // 列表说的默认档
                               pub on: Option<bool>,            // 列表说能否要求它「开启思考」
                               pub default_on: Option<bool> }   // 列表说什么都不发时想不想;三者缺席即省略
// 这两个类型的「缺席即省略」只在人读的格式里成立(D58):端点簿把 ModelFacts 带进 views 与 standing
// 的 postcard 快照,二进制格式里每一格照写,缺席写 Option 的 0 标签。Serialize 因此手写,问 is_human_readable()。
// 两个新键由 gateway::endpoint::models 读出、gateway::provider::thinking 与 provider::identity 读(gateway §8-39、§8-38);
// 早于它们的行读作没说,所以 0.0.10 写下的 endpoint_probed 照原样读回。
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 },
                       Switch { to: ServerLabel, code: String, subject: String },   // 换到名册里的另一个账号,不等(§8-86)
                       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> },
}
impl CacheRenewed { pub fn outcome(&self) -> RenewalOutcome; }
/// 一次续期对它要保住的缓存说明了什么:Read=读到了;Late=usage 报出非零的 cache_write_tokens,
/// 前缀被重写(到得太晚);Unreported=回答了但没报写入数;Refused=provider 拒了。
pub enum RenewalOutcome { Read, Late, Unreported, Refused }
```

结构按族住 `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-87 `run_started` 记下接手这次 harness run 的 ACP agent(`kernel::event::record::run`,形状 2 值类型)

```rust
pub struct RunStarted {
    // …既有字段…
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub agent: Option<AgentRunIdentity>,
}
pub struct AgentRunIdentity {
    pub id: String,                       // agent 条目的 id:`[[agent]]` 一行的 `id`,或内置条目的 id
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,          // agent 在 `initialize` 里报出的版本;没报就缺席
    pub launch_digest: B3Hash,            // 人同意的那份启动说明的摘要
}
```

- **只记身份,不记命令行**:命令行与路径是一台机器的事实,可能带着用户名与目录(`crates/kernel/src/config.rs` 对 `[[mcp]]` 的同一条规矩),账本可以被任何人重放与导出。`id` 说是哪一个条目,`version` 说 agent 自己报的版本,`launch_digest` 说人同意的是哪一份启动说明;三者合起来足以回答「这次 run 是谁跑的、跑的是不是人同意的那一份」,而不带出任何路径。
- **摘要的来源只有一个**:人同意时(`AddAgent` 的 `spec_digest`)城重算的那一份,同意之后启动说明改了,摘要就不同,读账的人能看出这次 run 用的不是当初同意的那一份。
- **缺席读作「不是 ACP agent 接手的 run」**:本城自己调模型的 run 与早于这个键的行都没有它,按 `default` 加、缺席不写,旧账本照读;0.0.10 读到带这个键的行按未知键拒(§8-40 的方向门照旧)。
-/

/-!
### 8-88 `endpoint_attached` 带上登记时读到的模型事实(`kernel::event::record::endpoint`,形状 2 值类型)

```rust
pub struct EndpointAttached {
    // …既有字段…
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub facts_blob: Option<B3Hash>,       // CAS 里一份 `Vec<ModelFacts>` 的摘要
}
```

- **上游说过的话跨过重启**:登记时或人点「刷新模型」重新探测时读到的 `ModelFacts`(只含城读的那几格)写进 CAS,这一行只记它的摘要。replay 由摘要取回事实,于是上游说出的思考档位、窗口与价格在重启之后仍在,而不是只剩模型 id。
- **缺席读作「这一行没带事实」**:早于这个键的行与没有读到列表的登记都没有它;读者照旧只按 `models` 的 id 重建,不编造历史里没有的事实。
- **warm-up 不读任何东西**:开城时不带凭据重新拉列表(gateway D26);事实只在人发起的登记或刷新时更新,写下新的一行。
-/

/-! D57 登记时的模型事实进 CAS,账本行只记摘要

**决定**:`endpoint_attached` 加一个可缺的 `facts_blob: Option<B3Hash>`,指向 CAS 里那一份事实;不把事实平铺进行里。

**理由**:OpenRouter 一次列出四百多个模型,连同推理陈述的事实有上百 KB,平铺进行里会让每次 replay 与每个读这一种行的视图都搬运它,而读它的只有端点簿的重建。CAS 已经是城存大块内容的地方(`kernel::locator` 的 `cas:`),摘要让一行保持几十字节,事实只在需要时取回一次。

**被否**:①在行里平铺 `facts`:行变大,每次 replay 都付出代价;②开城时带凭据重新拉列表:每次开城多一次带凭据的网络调用,与隐私优先相悖,也破坏 warm-up 不带密钥的性质(gateway D26)。

**重开参数**:CAS 的读取成为 replay 的瓶颈,或事实缩到几百字节以内。
-/

/-! D58 快照也带的记录类型:缺席的键只在人读的格式里省略

**决定**:`ModelFacts` 与 `ThinkingStatement` 的 `Serialize` 手写,按 `is_human_readable()` 分两臂,与 §8-84 的两种摘要同一个问题。人读的格式(账本行、CAS 里那份 JSON 事实、`serde_json::Value`)照旧省略缺席的 `Option` 与空的 `levels`,写出的字节与省略规则落地时逐字相同,不多一个 `null`;二进制格式(views 与 standing 快照的 postcard)每一格都写,缺席写 `Option` 的 0 标签、空列表写长度 0。`Deserialize` 仍是派生的:人读的格式缺键读作 `default`,二进制格式按声明序逐格读回。

**理由**:postcard 不自描述,读方按声明序数格子。`skip_serializing_if` 在二进制格式里省掉一格,后面每一格都错位一格,读方把下一个值的首字节当作 `Option` 的标签,报 `Found an Option discriminant that wasn't 0 or 1`;端点簿只要登记过一个列出模型的端点就带着 `ModelFacts`,于是每次开城都报 `E_CAS_CORRUPT` 并从 genesis 重折。`skip_serializing_if` 只看值,看不到格式,所以省略与否要在 `Serialize` 里问格式。

**被否**:①去掉 `skip_serializing_if`:账本行与 CAS 事实里多出 `"thinking":null` 一类的键,0.0.11 已写下的字节变了;②gateway 里给快照另设一个行类型:同一行的形状有两处定义,加一格时只改一处的那一次就是下一次错位;③快照改用自描述格式:views 快照的大小与解码时间都要重新量过,而错的只是两个类型。

**重开参数**:`record::probe` 之外又有一个带 `skip_serializing_if` 的类型进了 postcard 快照(那时把 `Absence` 从 `record::probe` 移到两处共用的地方),或快照换成自描述格式。
-/

/-! D63 模型行记下它是端点列出的,还是人点名而端点没列出的

**决定**:`ModelFacts` 多一格 `listing: Listing`,两个值:`listed` 是端点自己的 `/models` 列出了这一行,`named` 是人在登记时点名了这个 id、端点这一次没有列出它(列表里没有,或列表没读到),城按人的话登记它,其余事实都是缺席。人读的格式里 `listed` 省略,缺键读作 `listed`,所以 0.0.11 写下的 `endpoint_probed` 行与 CAS 事实逐字不变;postcard 快照照 D58 每格都写。写 `named` 的只有 `accounting` 的登记(gateway D47),探测的行恒是 `listed`。

**理由**:一个人点名的 id 与一行什么都没说的列表行,事实格子一模一样,页面与读这本书的人都分不出「端点列过它」和「只有人说过它」,而前者的缺席是 provider 没说,后者的缺席是没人问过 provider。记在行上而不记在 `endpoint_attached` 行里:事实已经整份进 CAS(D57),书按 id 把它们 `learn` 回来,标记跟着它说明的那一行走,不必在两处按 id 对齐。

**被否**:①`endpoint_attached` 加一张 `named` 的 id 表:同一个 id 的来历分在行与 CAS 两处,`learn` 之后还要再对一次;②用 `probed` 一位代表整张单子:一张单子里可以既有列出的也有点名的,一位说不清是哪几行。

**重开参数**:除了「列出」与「点名」之外出现第三种来历(例如从另一个端点抄来的行)时,再加一个值。
-/

/-!
### 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` 的写者。
-/

/-! D20 耗时记整数微秒,作为可缺席的键挂在既有的行上,不另立种类,账本版本不进位

**决定**:`tool_result` 多一把 `took_us: Option<u64>`,是这次工具执行从开始到给出答案的整数微秒;`model_returned` 多两把 `first_us: Option<u64>` 与 `took_us: Option<u64>`,是从请求发出到首个非空内容、到回复收齐的整数微秒。三把键都是 `#[serde(default, skip_serializing_if = "Option::is_none")]`。读数由注入的时钟给出:`bin::assembly` 仍是唯一的采样点,它交给运行层的时钟同时带墙钟毫秒与单调计时,量程两端都在同一个时钟上读;kernel 不采样。读者显示耗时时先读微秒键,缺席时退回两个信封时刻 `t` 之差(毫秒),并照实标出单位。

**理由**:账本的时刻是整数毫秒(ARCHITECTURE §10 规则 6),毫秒时刻相减分不出 1 ms 以内的差别,所以耗时要一个单独的量;记成时长而不是第二个更细的时刻,是因为墙钟会被系统调整,单调计时的差才是耗时,而单调计时的绝对值在两次开城之间没有意义,不能入账。挂在既有的行上与 §8-75 的 `first_at` 同一个理由:旧行与没量到的行字节不变,读宽即可重放,`EVENT_LOG_V` 不为一个可缺席的键进位。载荷不放浮点(确定性七条之 6),故用整数微秒而不是小数毫秒。

**被否**:①新种类 `tool_timed`:每次工具调用多一行,读者要把两行配对,而这个数只属于那一个结果;②把信封的 `t` 改成微秒:全账本换单位,旧行与新行的 `t` 不可比,等于换账本版本;③记纳秒:工具与模型的耗时在微秒以上,纳秒只让数字变长。

**重开参数**:出现一个读者需要工具内部分段(门、路径解析、IO、秘密扫描、检查点)的入账读数时,重议是否在 `tool_result` 里记分段,或只在仪表里记(今天只在仪表里记)。

写方:`runtime::turn` 从 `RunHooks.monotonic_us` 读三次单调读数(工具开始与答复、尝试发出、首个内容、回复收齐),差即这三把键;它与线上的 `Call`、`Used` 帧同一次 `WIRE_V` 进位(`crates/wire/Spec.lean` D22)。
-/

/-! D21 会话中改运行策略是一个入窗的种类 `run_policy_changed`,在下一个 `SafePoint::BeforeWave` 生效

**决定**:加一个种类 `run_policy_changed`(`EventKind::RunPolicyChanged`,追加在 `ALL` 末尾,不写 `ig`),载荷 `RunPolicyChanged { policy: RunPolicy, by: Who }`,`addr` 是房间,`run` 恒为 `RunId::CITY`:这一行是人经城做的事,由 accounting 的 `record_at` 这一扇城自己的门写下;正在跑的 run 按 `addr` 认出它,不按 `run`。写方是城:收到线上的 `Command::ChangeRunPolicy { room, policy, idem }` 时写这一行;正在跑的 run 经 `Interrupt::Policy` 在任一安全点收到它(与 Steer 同一扇门),在下一个 `SafePoint::BeforeWave` 生效(runtime D28,`crates/runtime/spec/PolicyTake.lean`):从那一波起按新策略过门,并在那一波工具结果之后追加一句说明,冻结的前缀不动。此后人派进这个房间的 run,`run_started.policy` 取房间这一 session 里最后一次改过的策略,帧里带的策略只管没有改过的 session;新的 `session_opened` 让房间回到帧里的策略,因为会话菜单只把 session 开始之后的改动当作房间的状态(`crates/sprawling/spec/Accounting/Worker.lean` §8-133)。窗类:入窗,因为那一句说明决定下一次模型请求的字节。模型与思考强度在会话中不变。

**理由**:改策略是城里发生过的事,重放要能说出某一步是在哪个策略下过的门;把它记在 `run_started` 里只够说一次 run 开头的策略。生效点放在安全点而不是立即,是因为一个工具波已经按旧策略过了门,半途换门会让同一波的两次调用被不同的规则判。工具定义不随策略变:会话开始时工具清单定成各模式的并集(`crates/runtime/spec/Catalog.lean` D25),所以改策略只动门与那一句说明,提示缓存不失效。

**被否**:①复用 `autonomy_changed`:它回答的是「谁答设计问题」,作用域是城或楼,与一次 run 的纪律是两件事;②只改偏好文件、不入账:重放读不到,门的判决就没有来历;③立即生效:同一波工具被两套规则判。

**重开参数**:出现会话中改模型或思考强度的需求时,重议是否把它们并进同一个种类(今天它们在会话中固定,因为换模型使缓存整段失效)。

载荷在 `kernel::event::record::run`(`RunPolicyChanged`,`by` 经 `Who` 的拼写);窗类在 `spec/Event/Kind.lean` 记作 `InWindow`。门一侧的取用(`Interrupt::Policy`、`runtime::PolicyCell` 在 `BeforeWave` 取用、追加那一句)在 `crates/runtime/src/run/lifecycle.rs`;尚未落地的是把这一行投进正在跑的 run 的信箱,以及 `edit` 与 `exec` 改读 `PolicyReader`,所以今天的写门仍按 `run_started.policy` 判。
-/

/-! D22 会话的显示名是一个 record-only 种类 `session_named`;地址仍是身份,标签仍在偏好文件里

**决定**:加一个种类 `session_named`(`EventKind::SessionNamed`,record-only,追加在 `ALL` 末尾),载荷 `SessionNamed { began: Seq, name: String }`:`addr` 是房间,`began` 指认这段 session(与 `wire::SessionLine.began` 同一个值),`name` 是人给的显示名,空串即撤回显示名、退回地址。写方是城,收到 `Command::NameSession { room, began, name, idem }` 时写,名字经 `crates/wire/spec/CarriedName.lean` 的规则拒空白与控制字符。同一段 session 以最后一行为准。标签不入账:它们仍是偏好文件里的 `tags`(`crates/wire/spec/Preference.lean` D21);没有标签时读者按工作区给一个默认标签,这是读法,不是记录。

**理由**:显示名是这段 session 在城里叫什么,换一台设备、导出这座城、让居民在对话里提到它,读到的都该是同一个名字,所以它属于城的历史;标签是人怎么归类自己的工作,D21 已经给了它一个家。显示名不改地址:地址决定读写域与账本身份,改它等于搬家。

**被否**:①把显示名也放进偏好文件:导出的城与远程设备看不到它;②改房间地址:读写域、历史与引用全部要迁移;③一个 `session_labelled` 种类同时记名字与标签:与 D21 冲突,人的分类会随导出给下一个人。

**重开参数**:居民需要按显示名找一段 session 时,重议名字的唯一性(今天同一房间两段可以同名)。
-/

/-! D23 skill 上架时的审核是一个 record-only 种类 `skill_audited`;调用记录不加种类,从已有的行折出

**决定**:加一个种类 `skill_audited`(`EventKind::SkillAudited`,record-only,追加在 `ALL` 末尾),由城在一个 skill 落位之后、以及扫架时看见它此刻的整包摘要没有审核时写(每个 `(skill, digest)` 在一个城进程里只发起一次;何时发起、问谁、怎样读回答由 `crates/city/spec/Library/Audit.lean` city D19 规定),`run` 为 `RunId::CITY`,`addr` 是书架所在的 scope。载荷:

```rust
pub struct SkillAudited {
    pub local_only_reason: Option<String>, // 缺省缺席;当前摘要未登记来源时说明跳过远端的理由
    pub skill: String,                 // skill 名
    pub digest: B3Hash,                // 被审的那份内容的摘要;内容变了摘要就变,旧审核不再算数
    pub source: AuditSource,           // SkillsSh | SkillSpector
    pub scanner: String,               // 审核方报出的版本或名字;SkillsSh 时是合作方名,如 "socket"
    pub verdict: AuditVerdict,         // Pass | Warn | Fail | Unreachable
    pub risk: Option<String>,          // 审核方给的风险等级原词
    pub audited_at: Option<String>,    // 审核方给的审核时刻原文
    pub link: Option<String>,          // 取不到时给 skills.sh 上这个 skill 安全页的链接
}
```

`Unreachable` 是「这次问了、没审成」:skills.sh 不答、答 401/403、超时或答复读不出,SkillSpector 的退出码既不是 0 也不是 1;不拦上架(D89:skills.sh 默认、SkillSpector 可选)。没有一个审核方适用时(本地路径或自带的 skill,且 PATH 上没有 SkillSpector)不写行:没人问过就不该有一行说问过。skills.sh 每个合作方一行,`scanner` 是合作方名。使用记录不入新行:`describe` 取指南与读 skill 目录下的文件都已在 `tool_called` 里,按 skill 折叠的视图由 accounting 从这些行折出;MCP 的使用同样从 `call` 与 MCP 工具调用的 `tool_called` 折出。

**内容版本另有一行**:skill 页的「内容版本(摘要、时刻、谁改的)」读 record-only 种类 `skill_shelved`(`EventKind::SkillShelved`,追加在 `ALL` 末尾):载荷 `SkillShelved { skill: String, digest: B3Hash, source: ShelvedFrom }`,`ShelvedFrom { Path, Git { url, rev }, SkillsSh { name }, Shipped, Page }`(线上以 `kind` 键区分五臂),`run` 为 `RunId::CITY`,`addr` 是书架所在的 scope:城库缺席(与 `autonomy_changed` 的 `Scope::City` 同一读法),楼架是那栋楼的地址。经 §8-28 的门(`library::install`)每次真正落位(`Placed::Fresh`)时由城写一行,答 `AlreadyShelved` 时一个字节没变、不写,`digest` 是 `Installed::hash`。它取代此前为 `PutShelved` 计划的 `shelved_document_written`:两条上架的路是同一件事,一个种类。User 在架外直接改文件没有行,由 `run_started` 钉住的摘要与这件 skill 的任何一行 `skill_shelved` 都不同读出,页面把这一版的「谁改的」显示为「在架外」(wire D33)。

**现状**:种类、载荷(`kernel::event::record::skill`)与表的行都在;今天唯一的写方是新城成形(`accounting::worker::genesis::form`):自带的 skill 在第零行之前落位,成形在第零行之后为每件 `Fresh` 的写一行 `source = Shipped`,因为账本在第零行之前不存在。`InstallSkill` 还不是线上的动词(wire D32 定了形,wire 的文法里没有这一臂),`PutShelved` 仍答 `not_built`;两者的执行者落地时各写一行,`source` 分别是来源的那一臂与 `Page`。

**理由**:审核的结论要和它审的那份内容绑定,才能说「内容变了要重审」,所以摘要必须在同一行;审核来自城外,结论是城收到的事实,重放不能再去问一次外网,所以入账。使用早已入账,再记一行只是同一件事的第二份。

**被否**:①每次读 skill 记一行 `skill_used`:与 `tool_called` 是两份定义;②审核结果只缓存在保留子树的文件里:重放与导出读不到,「谁在什么时候审过哪一版」没有历史;③取不到审核就拦上架:skills.sh 的无令牌接口随时可能关,拦住会让书架整个不能用。

**重开参数**:审核来源超过两个、或要按 skill 记多个合作方的分项结论时,重议 `scanner` 与 `verdict` 是否改成一张表。
-/

/-! D32 同步 `send` 的一次等待是一对种类 `signal_wait_started`/`signal_wait_ended`,都入窗,种类表的行与实现同一次改动落地

**决定**:两个种类(追加在 `ALL` 末尾,入窗:模型下一次调用要读到「等过、为什么停」),写方都是 collab 的发信门(collab D9),`run` 是等待的那个 run,`addr` 是它的房间:

```rust
pub struct SignalWaitStarted { pub on: Address, pub signal: SignalId, pub deadline_ms: u64 }   // 注入时钟上的 deadline
pub struct SignalWaitEnded { pub signal: SignalId, pub by: WaitEnd }
pub enum WaitEnd { Reply { reply: SignalId }, Timeout, Left }   // 回信到了|到了 deadline|run 离开房间
```

- `signal_wait_started` 紧跟在它那一封信的 `signal_enqueued` 之后写,`signal` 指那封信;一个 run 同一时刻至多一个没配对的 `started`(模型 `Collab.Delivery` 的 `waits` 每个 run 一条)。
- 每一行 `started` 恰有一行 `ended` 与它配对:回信到了写 `Reply`(`reply` 是那封回信),注入的时钟过了 deadline 写 `Timeout`,run 在等待中离开房间(取消、失败、进程死后冻结)写 `Left`;`waits_end` 保证没有第四种。进程死后的那一行由冻结它的那一处补写(D14 同一口径),所以重放不会留下一个永远在等的 run。
- `deadline_ms` 是注入时钟上的读数,不是墙钟:重放按 seq 读次序,不按时刻;线上把它换成墙钟时刻(wire D34)。
- 等待期间 run 不调模型、不写 `model_returned`,watchdog 读到一个未配对的 `started` 就知道这个 run 是停着而不是卡住,不对它退避或报警。

**现状**:两个种类已在 `crates/kernel/spec/Event/Kind.lean` 的表里与 `EventKind` 里(`ALL` 末尾,入窗),上面三个载荷类型在 `kernel::event::record`(`crates/kernel/src/event/record/collaboration.rs`;`WaitEnd` 线上以 `end` 键区分三臂),还没有写方;写方是 collab 的发信门(collab D9),读者在那之前把两个种类当作没有内容的行跳过。

**理由**:一个停着的 run 必须在账本里看得出来,否则 `status`、页面、watchdog 与重放都分不清它在等还是卡住(`crates/collab/spec/Delivery.lean` 的 `wait_bounded`、`waits_end` 说的是模型,账本要能把同一件事说给读者)。开始与结束分两行,是因为等待跨越安全点、可能跨越进程死亡;只记一行「等过多久」要等结束才写,中间那段时间什么都看不见。入窗,是因为下一次模型调用要知道它是被回信还是被超时叫醒的。

**被否**:①不入账、只在内存里停:重放与重开的页面看不到一个正在等的 run;②在 `signal_enqueued` 的载荷上加 `wait` 字段:说得出开始,说不出结束;③用 `run_policy_changed` 之类的已有种类表示停住:一个种类两种意思。

**重开参数**:一个 run 可以同时等两封回信时,重议「至多一个未配对的 `started`」。
-/

/-! D61 一次端点门前的等待是一行 record-only 的 `prefix_held`,种类表的行与实现同一次改动落地

**决定**:一个种类(追加在 `ALL` 末尾,只入账),写方是 runtime 的回合层:一次尝试返回之后(成功与失败两臂),它问模型端口的 `prefix_held`(`crates/kernel/spec/Model.lean` D60),有就写这一行,`run` 是那次尝试的 run,`addr` 缺席(与 `model_called` 相同:回合层的行由 run 说明是谁的),位置紧跟那次尝试的 `model_called`、在 `model_returned` 或那次失败之前:

```rust
pub struct PrefixHeld {
    pub waited_us: u64,              // 从到门前到被放行,注入的单调时钟上的整数微秒
    pub ended: PrewarmEnd,           // 等的那次 pre-warm 怎样结束
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sent: Option<PrewarmSent>,   // 只在发 pre-warm 的那次调用的行上
}
#[serde(rename_all = "snake_case")]
pub enum PrewarmEnd { Answered, Refused, TimedOut, Abandoned }
#[serde(untagged)]
pub enum PrewarmSent {               // 先试 Refused,同 CacheRenewed:Answered 的字段全可缺
    Refused { refused: AxError },
    Answered { usage: Option<ModelUsage>, billed_usd_micros: Option<UsdMicros> },
}
```

载荷住 `crates/kernel/src/event/record/prefix_held.rs`,在 `renewal.rs` 旁,与 `CacheRenewed` 同族;窗类是 `RecordOnly`,因为等待不改任何一次请求的字节。`model_returned.took_us` 从尝试发出算起,包含这段等待。

**理由**:人看见一个子 run 晚出发几秒,要能在它自己的历史里读到为什么;pre-warm 是一次真请求,它的写价要进城的历史。花费只在发它的那一行,一笔钱只入一次账。

**被否**:①挂在 `model_returned` 上作一个可缺席的键(D20 的做法):调用失败时没有那一行,等待与花费却发生了;②复用 `cache_renewed`:续期重发一段已缓存的前缀、不让它过期,pre-warm 在调用之前写一段冷前缀,一个种类两种意思;③每个跟随者的行都带花费:同一笔钱记 N 次。

**重开参数**:一次调用能等两次 pre-warm(例如共享部分分两层各自 pre-warm)时,重议一次尝试一行。
-/

/-! D38 一封信落在哪里是一行 record-only 的 `signal_landed`,紧跟在它的 `signal_enqueued` 之后

**决定**:一个种类(追加在 `ALL` 末尾,只入账),写方是记账线程把信投进房间的每一处,三处读同一个判定(`accounting::worker::waking::landing`):lane 经 relay 发出的信(`waking` 的 `deliver_sent`),子房间交接回父房间的信(`dispatching::handback` 经 `settling::landing::discharging`),以及一次落定里投递的信(`settling::landing` 的 `Then::Deliver`,例如计划节点变红时的阻塞通知)。它在投递、决定敲不敲门之后写;`run` 与 `who` 同那一行 `signal_enqueued`,`addr` 是收信房间:

```rust
pub struct SignalLanded { pub signal: SignalId, pub landing: Landing }
pub enum Landing { Delivered, Queued, Knocked }   // 进了正在跑的 run 的信槽|进队列等一次已在等的敲门或等人派活|为它敲门开一个新 run
```

- 每一行 `signal_enqueued` 恰配一行 `signal_landed`,按 `signal` 配(模型 `Collab.Delivery` 的 `one_landing_per_signal`、`landings_once`);`Knocked` 当且仅当这次投递为收信房间排了一次敲门(`knocked_is_the_knock`,与 `knock_once` 同一个敲门),`Delivered` 当且仅当收信房间此刻借给了一个 run(`delivered_is_running`)。
- 敲门之后被推迟或被链长拒绝,落点仍是 `Knocked`:这一行记的是投递那一刻的判定,敲门的去向由 `run_started` 或那一条拒绝说。
- 这一行之前写下的账本没有它;页面配不上就画工具自己答的东西,不猜。
- 每条路径上的发信 run,就是那一行 `signal_enqueued` 的 `run`:relay 路径是正在跑、调用了 `send` 的那个 run;交接路径是落地的子 run(交接信以它的名义写);落定路径是正在落定的那个 run(阻塞通知以放下红节点的 run 的名义写)。
- 三条路径都写这一行,所以性质 8 的「每封信恰一行」在整座城成立(`crates/collab/spec/Delivery.lean` 的 `send` 不分路径)。一次落定投递到一半被房间拒绝时,已写下的落点行留在账本上,敲门按 `settling::landing` 的口径退回标记:那几行记的仍是投递那一刻的判定,与被推迟的敲门同一口径。
- 判定只读进程里的房间表与敲门队列,Windows、macOS、Linux 上同一段 trace 得同一行。

**理由**:工具答复时还不知道信落在哪里(collab D17),页面要把三种结果画在发信那一行上,就要一行在判定的那一刻把它记下来;只入账,因为发信的模型已经读过工具的答复,落点不改变它下一次请求的字节。

**被否**:①在 `signal_enqueued` 的载荷上加 `landing`:那一行在投递之前已由发信 run 经 relay 写下,判定还没发生;②由页面从 `run_started` 的 `dispatched_by` 反推:推迟的敲门与链长拒绝让反推在投递之后很久才成立或永不成立;③每次敲门再写一行:一封信两行落点,页面又要决定读哪一行。

**重开参数**:一封信可以同时落进两个房间(广播)时,重议「每封信恰一行」。
-/

/-! D24 `asset_archived` 在 `archive record` 被调用时写,不等 run 冻结

**决定**:`asset_archived` 的写方从 run 收尾挪到工具波:`archive record` 执行时在同一个波里写这一行,同一个 run 的 `archive recall` 就读得到它。载荷与种类不变。`recall` 答复里的 `searched` 是这次真正比对过的条目数。

**理由**:测试城里同一个 run 的 `record` 在 seq 251、276,`asset_archived` 在 seq 324–325,晚于 `run_frozen`(seq 321),于是 recall 答 `searched: 0`。工具说「已记下」时事情就该已经发生(ARCHITECTURE §5 第 4 步:效果先成为事件)。

**被否**:保留冻结时写、让 recall 另读本 run 的待写表:同一件事在两处有状态,重开的城读不到待写表。

**重开参数**:归档要与工作树检查点同进退(例如一次被回滚的 run 不该留下归档)时,重议写入时刻。
-/

/-! Provider accounts 的声明由 `ProviderAccount` 持有:id 为 ServerLabel,reference
为可缺席 SecretRef,header 为可缺席字符串;引用缺席代表匿名账号。
`validate_provider_accounts` 是非空、id/reference 唯一及 header 合法性的唯一判定。
AttachedTuning.accounts 缺席保留旧登记;显式列表解析失败必须拒绝,不能抹掉整张
tuning 后回退 auth。顺序与准入模型见 `crates/gateway/spec/Router.lean`。
-/

/-! ModelCalled.provider_account 是可缺席的 ProviderAccountBinding,含 provider 与
ServerLabel account,不含凭据或 reference;旧记录缺席时不会建立亲和。
ModelCalled 的 model 与 segments 在旧的不完整调用记录中可缺席,读为空;
此兼容性只允许重放记录,生产写者仍写完整请求身份。 -/