# ADR-0002：以事件溯源作为 Run 的唯一真相

- 日期：2026-07-27
- 状态：已接受

## 背景

产品要交付的三件事——**统一时间线**、**审计**、**自动复现**——都需要回答同一个问题：
"当时到底按什么顺序发生了什么"。

同时，观测是**双通道**的：Agent 报一套，带外控制器报一套，两者时钟不同步，
主机断电时 Agent 那一路会直接消失。

## 决定

`run_events` 是 append-only 的唯一真相。`runs` / `run_steps` / `recovery_actions`
是可以从事件重放重建的**读模型**。

序号 `seq` 由控制平面单点分配（不是时间戳排序），Agent 事件与带外事件进同一条序列。

仓储层**不提供** `update_run_state()` 类 API。改状态的唯一入口是
`events/recorder.py::record()`，它写事件并同步更新投影。

## 理由

- 主机断电时，Agent 的最后几条日志可能永远到不了。带外控制器的观测
（掉电时刻、画面指纹）是那段时间唯一的证据。两路事件必须在同一条时间线上
才能交叉校验——"Agent 最后一条日志 seq=812，带外报掉电 seq=813"这种判断
只有单点序号能给。
- 审计要求记录"操作者、审批者、命令模板版本、输入参数、结果、撤销动作"
（方案 §6.6）。这些天然是事件，硬塞进当前状态表会做成一堆冗余字段。
- 自动复现（§8.4）需要"失败前后的最小步骤区间"。有完整事件流才能裁剪。

## 代价

- 写路径变长，每次状态变化多一次插入。
- `run_events` 会很大：100 循环 × 每轮 ~30 事件 = 3000 条/Run。
  缓解：事件按 run_id 分区友好，日志正文不进事件表（只存对象存储 URI）。
- 开发者容易忍不住直接 UPDATE。缓解：仓储层不给这个 API，code review 卡这一条。

## 什么时候推翻

不推翻。真要优化，是加快照（每 N 个事件存一次状态快照加速重放），
不是放弃事件溯源。
