Files
qinglong/docs/adr/ADR-0156-durable-step-run-aggregate-and-run-fence.md
T

5.6 KiB
Raw Blame History

ADR-0156:耐久 StepRun Aggregate 与 Run Fence

  • 状态:Accepted
  • 日期:2026-07-26
  • 关联:ADR-0001、ADR-0133、ADR-0154、ADR-0155RFC D-03/D-131/D-148/D-149

背景

ADR-0155 已经要求任何 Tool adapter 在产生外部副作用前绑定独立的 StepRun、Trace 和 Audit evidence,但当时只有 evidence receipt 的纯契约。RunEvent.step_run_idRunAttempt.step_run_id 仍是 nullable 占位列,不能证明:

  • 工作流中的逻辑步骤具有独立、可恢复的状态;
  • 同一次 mutation 没有在响应丢失后被重复应用;
  • StepRun 变更和所属 Run 的 version/Event sequence 属于同一个提交;
  • Attempt/Event 引用的 StepRun 确实属于同一个 Run。

直接为 StepRun 新建 workspace package 也没有独立部署、权限或版本生命周期依据,会让 低配路由器承担不必要的 importer 和依赖闭包。

决策

1. 领域契约留在 runtime-core 的显式 subpath

@qinglong/runtime-core/step-run 提供 profile-neutral 的 exact contract,不新增 workspace package 或第三方依赖:

  • immutable qinglong/step-run@v1 record
  • task|tool|model|agent|condition|approval|subworkflow 七种 kind
  • pending → ready → waiting_approval/running → lost/terminal 的封闭状态机;
  • 每个 Run 最多 128 个 StepRun、每个 StepRun 最多 64 次 attempt
  • create/transition mutation 同时绑定预期 Run version/Event sequence、旧 StepRun version/digest/status 和 canonical RunEvent
  • repository 只公开 exact replay、按 identity 查询和稳定 keyset pagination。

StepRun 不保存 handler、input 明文、Secret、任意异常或 execute seam。inputRefoutputRef 只能指向后续受控 Artifact 边界。

2. StepRun mutation 是 Run aggregate 的一部分

双方言 repository 必须在一个短事务中:

  1. 串行化同一 Run
  2. 优先识别 exact mutation replay
  3. 校验 Run version/Event sequence 且拒绝 terminal Run
  4. create 或 compare-and-set StepRun
  5. 同时推进 Run version/Event sequence
  6. 插入 canonical RunEvent
  7. 插入 immutable StepRun mutation ledger
  8. 提交后才返回 applied。

历史 mutation 的 exact replay 必须返回当时的 StepRun、Run version 和 Event sequence 即使当前 StepRun 已继续推进。相同 mutation identity 绑定不同内容必须失败关闭。

3. SQLite 与 PostgreSQL 保持语义对等

SQLite capability v26 使用 0051-step-runs0052-capability-v26

  • StepRuns 保存完整镜像列和 canonical JSON,并用完整 CHECK 双向绑定;
  • StepRunMutations 保存历史结果和 Event/Run fence
  • 四个 exact readiness-audited trigger 限制 RunAttempt/RunEvent 只能引用同 Run StepRun
  • repository 复用单个 LocalSqliteOperationAuthorityBEGIN IMMEDIATE

PostgreSQL capability v27 使用 pg-0028-step-runs

  • ql3.step_runsql3.step_run_mutations 采用同一记录和 ledger 语义;
  • 复合 foreign key 在数据库层绑定 (run_id, step_run_id)
  • repository 使用 SERIALIZABLE、固定 statement/lock/idle timeout 和最多三次 serialization/deadlock retry
  • 只有 ql3_runtime 具有 StepRun 的 SELECT/INSERT/UPDATE 和 mutation ledger 的 SELECT/INSERT,其他产品角色保持零表权限。

两种方言允许使用不同数据库机制,但状态机、digest、fence、重放和上限必须一致。

4. 继续保持 production execution 不可达

显式 local-sqlite/step-runcluster-postgres/step-run subpath 只提供存储能力; root、通用 runtime、admin、worker ingress 不重新导出该 authority。当前不把仓储接入 任何 Tool adapter,也不提供 execute API。

只有后续同一 Tool start-barrier transaction 能够精确证明 plan/dispatch、StepRun、 Trace 和 Audit 全部耐久化,才允许产品 composition 取得 adapter authority。

被否决方案

  1. 每种 Step kind 一张表:放大 migration、查询与恢复复杂度,拒绝。
  2. 只写 RunEvent,不建 StepRun aggregate:没有独立状态和 CAS fence,拒绝。
  3. StepRun 独立提交后再更新 Run/Event:崩溃窗口会产生不可解释的半提交,拒绝。
  4. (run_id, step_key) 作为唯一 identity:重命名和子工作流展开会混淆稳定 identity,拒绝。
  5. 为 StepRun 新增 workspace package:没有独立部署/权限生命周期,拒绝。
  6. PG 只用应用层检查同 Run 引用:旁路 SQL 可以制造跨 Run 引用,拒绝。
  7. 旧 mutation 只返回当前 StepRun:不能证明响应丢失前实际提交的结果,拒绝。

验证

  • runtime-core StepRun 定向领域测试:10/10,完整 runtime-core297/297
  • local-sqlite 完整测试:100/100
  • cluster-postgres package 测试:153 pass、1 条件 skip
  • PostgreSQL 18.4 arm64 六角色真实 integration39 pass、1 条件 skip,覆盖 StepRun create、transition、历史 replay、查询和同 Run foreign key
  • PostgreSQL 18.4 arm64 physical HA:主备复制、fencing、promotion、pg_rewind、 双 control replica 与总 gate passed=true
  • 未新增 workspace package、第三方依赖、timer、watcher、socket 或常驻进程。

后续门禁

  1. 实现有界、append-only 的 Tool Trace span 与 Audit evidence receipt
  2. 实现 opaque/encrypted plan 和 redacted preview Artifact
  3. 把 plan/dispatch、StepRun、Trace、Audit 在同一 start barrier 中提交或精确重放;
  4. 为 built-in、isolated process、MCP、HTTP 和 Worker adapter 分别定义启动后恢复证据;
  5. 完成双方言故障注入、物理 edge 和 PostgreSQL HA 后,才解除 production unreachable。