feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,228 @@
# ADR-0021Worker Execution Offer Inbox 与启动崩溃恢复
- 状态:Proposed
- 日期:2026-07-18
- 关联:QL-RFC-0001、ADR-0003、ADR-0012、ADR-0013、ADR-0014
## 上下文
ADR-0014 已让控制面从 Run Lease 恢复稳定 `offerId`,但“控制面可以重复投递”不等于“Worker 可以安全重复接收”。如果 Worker 只用内存 Map 去重,进程重启后仍会出现以下风险:
1. offer 已收包,但 starting ACK 尚未提交;
2. starting ACK 已提交,但 Worker 尚未调用 Executor
3. Worker 正在调用 Executor,无法确认 spawn 是否发生;
4. Executor 已返回稳定 handle,但 running ACK 丢失;
5. 相同 `offerId` 被错误地复用到不同 `ExecutionSpec` 或 Worker Session。
低配路由设备不能为每个任务增加 Node sidecar,也不应为了 inbox 强制部署 PostgreSQL、Redis 或另一个 SQLite 连接;集群 Worker 又必须与 edge 使用相同的幂等和 fencing 语义。因此接收状态机需要与 transport、数据库和部署规模解耦。
## 决策
### 1. Worker 在任何 spawn 前持久化 offer 状态
Worker 的处理顺序固定为:
1. 认证 transport 将消息交给已绑定的 Worker runtime
2. 接收器重新验证 `offerId`、规范化 `ExecutionSpec` digest、Run/Attempt/Project/Task/revision、Worker ID/session/generation、Run Lease 和当前 Worker Session
3. 将完整 offer 以 `accepted` 写入 Worker 私有 journal
4. 调用 ADR-0013 `acknowledgeStarting`,成功后写入 `starting_acknowledged`
5. 把最新 Run Lease 交给单 timer lifecycle
6. `WorkerExecutionContextFactory.prepare()` 只准备可回收的输出/回调能力,不得 spawn 或产生不可逆业务副作用;completion token 必须是每 Attempt 独立的高熵随机 capability
7. 校验 callback token/sequence,只把 sequence 与 token SHA-256 写入 journal,并在调用 `Executor.start()` **之前**原子写入 `launching`;原 token 只保留在内存 `ExecutionContext` 并交给 launcher
8. Executor 返回稳定 handle 后写入 `started`
9. 使用 lifecycle 中最新 lease version 提交 `acknowledgeRunning`
10. running ACK 成功或幂等重放后写入 `running_acknowledged`
启动准备或 `Executor.start()` 明确拒绝时,先持久化 `start_failed`,再重放 `failStart`,最后写入 `start_failure_acknowledged`。同一进程内的并发重复 delivery 合并成一个 operation。
### 2. `launching` 是禁止盲目重试的 crash barrier
无法把文件写入与 OS spawn 做成一个跨资源原子事务。因此 Worker 重启后若看到 `launching`,不能再次调用 Executor。当前实现把它推进为 `recovery_required: launch_outcome_unknown`,等待后续 Reconciler 结合 durable process identity、completion receipt 和控制面事实判断。
该策略偏向 at-most-once:在无法证明 ownership 时允许任务被标记 lost 并由控制面创建新 Attempt,但不允许同一 Attempt 静默双跑。不得用“超时后直接再 spawn”替代恢复判断。
其他窗口具有确定重放语义:
- `accepted`:可重放 starting ACK
- `starting_acknowledged`:可继续首次启动;
- `started`:只能重放 running ACK,不能再次启动;
- `running_acknowledged`:返回幂等成功;
- `start_failed`:只能重放失败 ACK
- 控制面已 running 但本地没有匹配 handle,或控制面意外 terminal:进入 `recovery_required`
### 3. 本地 journal 是 delivery 状态,不是新的执行 authority
Run Lease 仍是唯一远程执行 authorityRun/Attempt 仍是控制面事实源。Worker journal 只保存:
- 完整且经校验的 offer,包括 Worker 所拥有的不透明 lease capability
- 状态、单调 revision、接收/更新时间;
- `started` 后的稳定 executor handle、start time 和可选 log artifact
- `launching` barrier 后可选的 completion callback sequence 与 64 字符小写 SHA-256;不得保存原 completion token
- 需要人工或 Reconciler 处理的低敏恢复原因。
journal 不得自行延长 Lease、改派 Worker、创建新 Attempt 或宣称 Run 已 running。相同 `offerId` 的新 delivery 只允许携带同一 authority、candidate 和 digestlease version 可以单调推进。不同 digest、Session、generation、token 或 Task revision 必须作为冲突拒绝。
### 4. edge 默认采用私有原子文件 adapter
首个 adapter 每个 offer 使用一个 JSON 文件:
- root 必须是绝对路径并由单个 Worker runtime 独占;目录权限 `0700`,文件权限 `0600`
- 临时文件写入后 `fsync`,再以 hard-link 原子首次发布;状态更新使用同文件系统临时文件加 atomic rename
- 默认最多 64 条,硬上限 1024 条;单条最多 192 KiB;扫描一页最多 64 条并使用稳定 `offerId` cursor
- journal 满时 fail closed,不能驱逐 active/unknown offer 给新任务让路;
- journal 本身不创建 watcher、扫描 timer、额外数据库连接或每任务 sidecarownership lease 只使用一个 `unref` 刷新 timer
- 同一 owner 内的 `create/replace/remove` 经过一个 Promise mutation queue 串行化,避免不同 offer 并发容量检查越界,也让 revision-conditional remove 与 replace 具有确定顺序;读取仍可并发;
- 不支持目录 `fsync` 的文件系统只保证重启一致性,断电持久性为 best effort,必须在设备矩阵实测。
adapter 必须在任何 list/read/create/replace/remove 前显式取得 root ownership:使用 root 内私有 lock directory 原子竞争,默认 stale window 为 30 秒,可配置范围为 5 秒至 5 分钟,并按一半窗口刷新。第二个进程不能读取或写入;lock 被删除、mtime 被替换或刷新超过窗口时,旧 owner 立即进入 `compromised`,此后所有 journal 操作 fail closed,不能自动重新 acquire。释放成功后另一个进程才能接管。
多 Worker 进程共享同一 root、NFS/对象存储目录或不支持同目录原子 rename/lock directory 的文件系统不在支持范围内。cluster Worker 可以提供等价的本地 SQLite/系统服务 adapter,但必须通过同一 port contract,不能把网络队列 ACK 当作 journal。
### 5. 启动审计有界且不执行恢复副作用
取得 ownership 后先运行只读 startup auditor。edge 默认每页 16 条、最多 4 页;硬上限为每页 64、16 页和总计 1024 条。cursor 重复、倒退、跳页、超页或预算耗尽必须显式 fail closed。
`WorkerExecutionOfferInboxLifecycle` 固定执行 acquire → audit → optional bounded recovery → re-audit → hold:未注入 recovery 时保持 acquire → audit → hold;完整审计为 `ready``reconciliation_required` 时持续持锁,直到显式 stop。扫描预算、recovery action 预算耗尽、journal 损坏或审计异常时先释放 ownership,再拒绝启动。该 lifecycle 不创建自己的 timer,也不启动 delivery transport。
审计结果只包含 offer ID、Attempt ID、journal state 和低敏分类,不返回 lease token、命令或 `ExecutionSpec`
- `accepted/starting_acknowledged/start_failed`:没有已知本地执行,可等待认证 transport 重投或 Lease 过期;
- `start_failure_acknowledged`:启动失败 delivery 已收敛;
- `completion_acknowledged`:控制面完成已成功或精确幂等重放,本地可进入回执清理与后续 retention;
- `launching/recovery_required`:launch 结果未知,必须进入 launch Reconciler
- `started/running_acknowledged`:已经或可能建立本地执行 ownership,必须进入 execution Reconciler。
`running_acknowledged` 只说明控制面持久化了 running,并不是执行终态,也不能按 terminal retention 清理。`completion_acknowledged` 单独归类为 `settled_completion`,不会阻塞 Worker 启动;startup auditor 本身仍不得重放 ACK、调用 Executor、停止进程或删除记录。
#### 5.1 恢复先建立可信证据,不从“不确定”推导“已退出”
`next` 已增加 transport-neutral 的 `WorkerExecutionOfferRecoveryReconciler` 作为 startup auditor 后的证据层。单条恢复顺序固定为:
1. 先按 Attempt ID 读取 immutable completion receipt
2. 同时校验 Run/Attempt、已持久化 start time 和 Worker 本地 completion capability
3. 只有 receipt 缺失时,才用 `PersistedExecutionInspector` 检查 durable handle
4. 若 handle 已退出,立即重读 receipt,并可配置最多 5 秒的有界发布宽限后再读一次;
5. receipt 仍缺失、handle 无效/identity mismatch、平台不支持或探测失败时保留明确 unknown,不据此重启、signal 或宣称 lost。
receipt authenticator 是必需 port,不能把“私有目录里存在一个 JSON 文件”等同于可信完成事实。Receiver 在 `launching` barrier 之前从 ephemeral callback 派生 SHA-256journal 只允许 sequence/digest 成对出现,且 `accepted/starting_acknowledged` 不得提前携带。`Sha256WorkerExecutionCompletionReceiptAuthenticator` 同时比较 callback sequence,并对 32-byte digest 做 constant-time 比较;缺失元数据的旧记录仍可读取,但不能认证 receipt。Reconciler 的结果只返回 callback sequence、时间、exit code 和 outcome,不返回 receipt token、Run Lease token、命令或 ExecutionSpec。receipt 冲突、认证器不可用和读取 I/O 错误都会在 process probe 前 fail closed。
每次 pass 使用一个固定 observation time,区分 `current``session_fenced``worker_offline``worker_session_expired``run_lease_expired`。即使旧 Attempt 的 receipt 可以证明完成,新 Worker Session 也不能代替旧 Session 提交 Lease completion;控制面已经 terminal 时同样只保留证据,不重复提交。该层不依赖 Executor 的进程内 `WeakMap`,也不注入 `start/stop/ACK/remove` port,因此无法产生二次 spawn 或 PID 误杀。
Reconciler 只恢复可信 ownership/completion **事实**,不执行控制面 mutation。`WorkerExecutionOfferRecoveryCoordinator` 在同一 journal ownership 下重新读取 record,且只对 `completion_observed + completionSubmission: ready` 提交完成;不得把 `ready` 当作已提交。
#### 5.2 恢复副作用按不可逆顺序收敛
完成恢复固定为:
1. 使用 journal 中原 Worker Session、Run Lease generation/token/version 和可信 receipt 的 callback sequence/result 提交 `RemoteRunCompletion`
2. 控制面首次完成或返回语义完全一致的 `already_terminal` 后,以 journal revision CAS 写入 `completion_acknowledged``completionAcknowledgedAtMs`
3. 只有终态 journal 已持久化,才按 Attempt ID 删除本地 receipt。
控制面完成成功但 journal 写失败时,receipt 必须保留,下一次以相同命令幂等重放;journal 已终态但 receipt 删除失败时,终态不能回退,后续 pass 只重试清理。并发恢复按 offer ID 在进程内合并,journal revision 仍是最终本地 CAS。结果只暴露低敏状态和清理结果,不返回任一 capability。
控制面的 `completeWithLease` 只对“相同 Run/Attempt、Worker/session/generation、lease generation/token,且当前 lease 已 `completed`、version 恰为请求版本 + 1”的精确重放绕过 Worker 当前在线性与观测时间单调检查;事务内 CompletionService 仍校验 callback sequence 和终态 outcome。首次完成、改变 callback/outcome 或改变任一 fence 都继续要求当前有效 Worker/Lease 或直接拒绝。这关闭了“控制面已提交,Worker 在本地终态写入前重启/被替换”的收敛窗口,而没有让旧 Session 获得新的完成权。
running ACK 恢复只在 receipt 缺失、durable identity 明确 running、authority 为 current 且 journal 为 `started` 时执行;成功后写 `running_acknowledged`,控制面已经 terminal 时只写 `recovery_required: control_plane_terminal`,不能伪造完成。恢复使用 journal 已持久化的 lease version;若 renewal version 更新但尚未通过 recovery delivery 合并回 journalversion mismatch 必须保留原状态并等待新 authority,不允许猜测或无限重试。
#### 5.3 启动恢复与终态保留都必须有硬预算
`WorkerExecutionOfferStartupRecoverySupervisor` 只消费一次完整 startup audit,按 audit 的稳定顺序串行驱动 `settled_completion`、launch recovery 和 execution recovery;默认最多 64 个 action,硬上限 1024。scan 未完成或 action 超预算时在任何副作用前 fail closed;单项失败不暴露异常文本,并继续处理同一有界批次的其他记录,但整体保持 `reconciliation_required`。它不创建 timer,生产 headless lifecycle 仍需显式决定何时重审并开放 delivery transport。
`WorkerExecutionOfferJournalRetentionService` 每次只读取一页,默认 16 条、最多 64 条,默认最多执行 8 次删除、硬上限 64;调用方持有并持久化 cursor 与 cadence。它只处理超过显式 retention 的 `completion_acknowledged``start_failure_acknowledged`:前者必须先幂等清理 receipt,再以 record revision 条件删除 journalreceipt 清理失败、revision 变化或文件删除失败都保留 journal。最小 retention 为 1 分钟、最大 30 天,生产值必须覆盖 transport redelivery/诊断窗口。该服务无 watcher、无 timer、无目录递归和额外数据库连接。
#### 5.4 运行中失租先停止旧本地身份,不赋予旧 Worker lost 写权
`WorkerRunLeaseLifecycle.onLost` 只是同步失权通知,不能直接承担异步 stop 或控制面状态转换。`WorkerExecutionLeaseLossCoordinator` 接收其中的 lease snapshot,并重新按该 authority 计算稳定 offer ID、读取 journal;只有 Run/Attempt、Worker/session/generation、lease generation/token 全部与记录一致,且记录仍可能拥有本地执行时,才调用 `PersistedExecutionController.stop({ durableHandle, reason: reconcile })`。journal 中的旧 lease version 可以落后于 lifecycle 的 renewal version,但 authority capability 不允许变化。
controller 必须自行解析和复验 durable identity`termination_requested/already_exited` 写为 `recovery_required: lease_lost_local_execution_stopped`handle 缺失、identity/pid mismatch、unsupported 或 invalid 写为 `lease_lost_local_execution_unverified`。controller 抛错时不写“已停止”,以便显式重试;同 offer 的进程内并发通知合并,journal revision CAS 仍裁决与 completion coordinator 的竞态。`completion_acknowledged` 永远优先,两个失租原因也保持幂等。它们不是可 retention 的业务终态,transport redelivery 不得再次 spawn。
该协调器刻意不持有 Remote completion、RunRepository 或 RunCommandService,旧 Worker 因此无法在 fence 失效后把 Run/Attempt 标为 lost。控制面使用独立的 server-owned `expireWithLease`:事务内锁定当前 Leaselive/completed/其他 release 不改写,确已过期时写入 `released: lease_expired`。未启动的 claimed Attempt 只释放以便 Dispatcher 后续生成新 generation;运行中的 starting/running Attempt 与 Run 在同一事务转为 lost 并写两个 reconciler Event;已有取消/超时意图时只撤销旧执行 authority,把终态留给取消链。renew、completion 与 expiry 的竞争由同一 Lease 行事务串行化,任一步异常都回滚 lease、Run、Attempt 和 Event。
SQLite `RunDispatchLeaseExpirySource` 使用现有 `(status, expires_at_ms, attempt_id)` 索引,只返回 runtime-owned active Run/Attempt`RunDispatchLeaseExpiryScanner` 固定一次 observation,每页默认 16、硬上限 64,按 `(expiresAtMs, attemptId)` 稳定 cursor,逐条串行且隔离单项失败。失败时停止当前页并只返回连续成功前缀的 cursor,失败项留在下一轮,禁止为了继续批次而跳过它。它无 timer、无多页递归,调用方负责 cadence 和 cursor 持久化,适合 edge 小批次运行;cluster-control 必须提供等价 PostgreSQL adapter。当前 `next` 的 Worker 本地协调器与控制面 expiry scanner 都保持生产不可达;lifecycle 装配和 PostgreSQL adapter 仍是 Gatelost retry/new Attempt 由下一节的独立控制面协调器处理。
#### 5.5 lost 后只按 admission 策略创建新的 Attempt
`next` 新增一对一的 `RunRetryPolicies`,由 Primary 建单事务在可信 admission provider 返回策略时与 Run、Attempt 1 一起提交。没有策略记录是安全默认值:不自动重试。策略固定最大 Attempt 数、是否处理 lost、`unknown/idempotent/deduplicated` 安全等级、退避上下限、下一次尝试时间和独立 version。Primary 只向 provider 发送冻结的 Task revision 身份子集;Run 命令自带策略会在任何写库或 spawn 前拒绝。启用自动 lost retry 必须由固定 Task revision 证明幂等,或绑定已经强制执行的业务去重契约;策略不从 Worker 请求、失租回调或运行中可变 payload 推导。
控制面 Reconciler 先将符合条件的 lost Run 转为 `retry_wait` 并原子保存退避时间。到期事务重新验证 runtime ownership、取消意图、最新 Attempt 仍为 lost、策略 version 与次数预算,然后创建 Attempt N+1Run queued、policy cursor 清理、Attempt claimed 和两个 Event 要么全部提交,要么全部回滚。新 Attempt 不继承旧 Worker/Session、Run Lease、handle/PID、callback capability 或 completion sequence。旧 Attempt 永远保持 lost`retry_of_run_id` 也不参与自动重试。
本地 Primary 的 claimed activation seam 可读取并复验这个 Attempt N+1,要求它是未取消 queued Run 的最新 claimed Attempt、属于 runtime 且 executor type 一致,再复用既有的 starting/spawn/running/completion 协议。Run CAS 在 spawn 前关闭同机并发激活窗口;旧 Attempt、非最新 Attempt 和不同 Executor ownership 均在副作用前拒绝。其外层本地 Dispatcher 每周期最多激活一个 Attempt,默认只扫 1×8,硬上限 16×64;它固定 observation、验证稳定 cursor、跳过其他 Executor,且必须在激活前规范化并深拷贝 pinned `ExecutionSpec`。两者都不自行解析任务内容,也不构造 Secret、日志或回执 context。
SQLite source/scanner 每次默认 16、最多 64 条,无 timer、无递归分页;异常候选保留并上报,其他同页候选仍可收敛。外层 profile-aware lifecycle 使用一个 `unref` timer、禁止重叠、停机等待有上限,并固定每 tick 一页:edge 30 秒×8standalone 5 秒×32。它与 expiry scanner 一样仍是单控制面 adapterPostgreSQL 多副本实现是生产 Gate。ADR-0022 已提供 pinned revision 与动态 context 的组合 materializer contract。当前实现仍保持 production unreachabledefault manual Primary 未配置可信 provider,也没有 Task revision persistence、SecretStore、Artifact/output adapter;有界本地 Dispatcher 无 lifecycle/startup 装配。只有 Task/API 安全声明与去重契约、具体 materializer adapters、Dispatcher cadence、startup 装配、指标告警、PostgreSQL contract 和 edge 写放大验证完成后才能启用。
### 6. Session、draining 和 Lease 继续 fail closed
- delivery 必须精确命中当前 Worker ID/session/generation
- offline、Worker Session Lease 过期或 Run Lease 过期时,在写 journal 前拒绝;
- `draining` 拒绝 `new_claim`,但允许同一 active Lease 的 `lease_recovery`,以便完成已有 ownership
- starting 后才开始跟踪 Run Leaserunning/fail ACK 从 lifecycle 读取最新 versionversion mismatch 只允许小次数有界重试,不能循环忙等;
- journal 中原始 token 不进入普通日志、诊断、Event 或 API,备份与本地加密策略仍需安全 ADR。
### 7. 当前仍不开放生产 transport
当前实现提供 domain、port、原子文件 adapter、绑定的 activation/completion client、transport-neutral receiver、证据 Reconciler 和 side-effect coordinator,并保持旧 Controller、HTTP、gRPC 与 headless bootstrap 不可达。进入生产还必须完成:
- 控制面身份认证、enrollment、双向 TLS/token rotation 和消息大小/速率限制;
- 生产 `WorkerExecutionContextFactory` 的高熵 capability 生成与安全轮换;
- Worker 本地失租 coordinator、控制面 expiry scanner 与 lost retry lifecycle 的 startup 装配,以及消费重试 queued Attempt 的 Dispatcher
- Artifact 获取、校验、日志上传和 completion transport
- terminal retention cadence/cursor 的 headless 装配、敏感备份和安全擦除策略;
- PostgreSQL 多控制面 contract 与固定 edge/多架构资源门禁。
## 影响
正面影响:
- 控制面重投不再依赖 Worker 进程内存;
- ACK 丢失只会重放 ACK,不会重放 spawn;
- edge 无新增常驻服务,集群仍能替换 adapter;
- Worker Session replacement、digest drift 和过期 Lease 在执行前被拒绝;
- crash window 被显式持久化,后续 Reconciler 有可审计输入。
- 完成提交、journal 终态和 receipt 清理之间的两个 crash window 都可幂等收敛。
代价与风险:
- `launching` 无法自动区分“尚未 spawn”和“已 spawn”,需要 durable identity/receipt 恢复;
- durable handle 只能证明已记录的进程 identity;`launching` 在没有 receipt/handle 时仍必须保持 unknown
- 文件 journal 持有 lease capability,部署必须保护目录、备份和诊断输出;
- completion 前清理会丢失去重与执行 ownership,清理过晚会占用容量;
- ownership lease 依赖本地文件系统 mtime/atomic mkdir 语义,设备 suspend 和异常时钟跳变必须实测。
## 未选择的方案
1. **只用内存 LRU 去重**Worker 重启后会重复执行。
2. **先 spawn,再写 journal**:最危险的重复执行窗口没有 barrier。
3. **`launching` 超时后自动重试**:无法证明旧进程不存在。
4. **为每个任务启动 Node guardian**:不满足 edge RSS/PID 预算。
5. **新增控制面 DispatchOffers authority 表**:与 Run Lease 形成双重真相;Worker 本地 delivery journal 也不能取代 Lease。
6. **让消息队列 exactly-once 保证执行**delivery exactly-once 不等于外部进程副作用 exactly-once。
## 验证要求
- journal 原子首次发布、revision replacement、重启读取、权限、容量和分页通过测试;
- 未 acquire 时拒绝所有操作;双 owner 竞争只有一个成功,释放后可接管,lock compromise 后旧 owner 全面 fail closed
- startup audit 对所有状态稳定分类,输出不含 token/命令,预算耗尽和非法 cursor 显式拒绝;
- 相同 offer 并发/串行重放只调用一次 Executor;
- 相同 ID 不同 digest/Task revision/Session authority 在 spawn 前拒绝;
- 过期 Lease、替换 Session、draining 新 claim 在写 journal 前拒绝;draining recovery 可继续;
- `launching` 重启只进入 `recovery_required`,调用 Executor 次数为 0
- `started` 重启只重放 running ACK
- `running_acknowledged` 仍被视为待恢复的本地执行 ownership,不得作为清理终态;
- 恢复必须 receipt-first;可信 receipt 不再探测进程,进程 exited 后在有界 grace 内重读 receipt
- receipt Run/Attempt/start time/capability 冲突或读取失败时不得继续 process probe;恢复结果不得包含 receipt/lease token
- callback sequence/token digest 必须在 `launching` 前成对落盘,原 callback token 不得进入 journal;旧记录无 digest 时必须拒绝 receipt 认证;
- durable identity 的 running/exited/invalid/mismatch/unsupported 和探测异常均稳定分类,任何分支都不调用 start、stop、ACK 或 remove
- 相同完成事实遇到替换 Session、offline、Worker Session Lease 过期、Run Lease 过期或控制面 terminal 时只报告 blocked,不借用新 authority 提交;
- 可信完成必须先提交控制面,再写 `completion_acknowledged`,最后清理 receipt;journal 写失败不得清理,清理失败不得回退终态;
- 控制面完成后的精确 fence/callback/outcome 重放可在 Worker 被替换后返回 `already_terminal`,任何变体继续拒绝;
- running ACK 恢复只允许 current `started` + running identity,控制面 terminal 只能进入恢复状态;
- 失租 stop 只能命中同 offer authority 的 durable handle;完成已确认时不得 stopidentity 无法复验时必须写 unverified 而不能声称已停止;
- 失租本地记录不得被 terminal retention 清理或被 transport redelivery 再次 spawn,旧 Worker 不得直接写控制面 lost
- lost 自动重试必须在 admission 拒绝 unknown safety;无策略、禁用和次数耗尽分别稳定收敛,取消意图必须先于新 Attempt 生效;
- retry_wait 到期只能创建 Attempt N+1Run queued、policy cursor 清理、Attempt claimed 与有序 Event 任一步失败必须整事务回滚,重放不得创建 Attempt N+2
- lost retry source/scanner 必须限制为一页最多 64 条,不启动 timer 或递归翻页,未来到期项不得提前返回;
- startup recovery action 超预算时不得执行部分副作用;单项异常只能返回低敏失败结果;
- terminal retention 必须先清理 completion receipt,再按 journal revision 删除;并发 replace、清理失败和超预算均不得误删 active/新版本记录;
- Executor 明确拒绝后 failure ACK 可重放且不二次启动;
- 控制面已 running/terminal 但 journal 缺少一致 ownership 时不得 spawn
- Node 22、Node 24 全量测试通过,并在真实 edge 文件系统验证断电、ENOSPC、inode 耗尽和权限模型。