Files
qinglong/docs/adr/ADR-0021-worker-execution-offer-inbox-and-crash-recovery.md
T

25 KiB
Raw Blame History

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;完整审计为 readyreconciliation_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,区分 currentsession_fencedworker_offlineworker_session_expiredrun_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_acknowledgedcompletionAcknowledgedAtMs
  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_acknowledgedstart_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 identitytermination_requested/already_exited 写为 recovery_required: lease_lost_local_execution_stoppedhandle 缺失、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/AttemptRunDispatchLeaseExpiryScanner 固定一次 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 永远保持 lostretry_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 耗尽和权限模型。