# ADR-0229:Local Workflow Task 单 cadence 运行生命周期 - 状态:Proposed - 日期:2026-07-30 - 关联 RFC:QL-RFC-0001 D-18、D-19、D-71、D-115、D-175、D-207、D-212、D-213 - 关联 ADR:ADR-0066—0072、ADR-0223—0228 ## 背景 Workflow admission、frontier、Task Attempt admission、cancellation 和 recovery 已经在共享领域层及 SQLite/PostgreSQL adapter 中存在,但 Local application 仍只有 普通 Run-centric scheduler/execution lifecycle。直接把 Workflow aggregate Run 交给 普通 dispatcher 会把 Workflow ID/publication digest 当成 Task identity;普通 completion/control/recovery 又会提前终结父 Run。 Local application 已经拥有 scheduler cadence、execution control cadence、单 SQLite operation authority、completion receipt 与受审 POSIX launcher。新增 Workflow timer、 数据库连接、表或 workspace package 会增加路由设备空闲成本,也会制造第二套停止顺序。 ## 决策 ### 1. 一个 scheduler cadence 承载完整 Workflow 前进顺序 `LocalWorkflowSchedulerCoordinator` 是无 timer 的组合器,每个 cycle 固定执行: 1. 整体 Workflow cancellation convergence; 2. 普通 schedule; 3. Workflow frontier; 4. generation-bound Task Attempt admission; 5. 有界 local dispatch。 它复用既有 `LocalSchedulerLifecycle`。Edge 每轮 cancellation 4×1、frontier 1×1、 Task admission 1×1、dispatch 1;Standalone 分别为 32×4、16×4、16×4、dispatch 4。任何阶段失败都由原 lifecycle 隔离并在下一 cadence 重试,不增加 per-Workflow timer、watcher、listener 或后台扫描。 ### 2. Workflow Task 执行只推进 Attempt/StepRun Local dispatch candidate 可以携带 `stepRunId`。只有 immutable Task admission ledger 与 exact ready StepRun 同时存在时,父 Run 为 `running` 的 Task Attempt 才可派发。 prepare、running、start-failure、completion、timeout 和 cancellation 都经窄 `LocalWorkflowTaskExecutionRepository` 在一个 `BEGIN IMMEDIATE` 中推进 Attempt、 StepRun、Run counter 与 Event/mutation;父 Workflow Run 保持 `running`,最终状态只 由 frontier 汇总。 普通 Run 仍要求 latest Attempt,并保持既有 `queued→dispatching→running→terminal` 语义。Workflow 分支不能改变普通 Run 的 retry、completion 或 cancellation contract。 ### 3. 启动恢复按聚合类型分流 普通 startup recovery 明确排除存在 immutable Workflow admission 的父 Run。专用 Workflow Task recovery 以 Attempt 为单位: - orphaned claimed Attempt 写 `lost`,并把 StepRun 刷新为新的 ready epoch; - starting/running 先读取 authenticated completion receipt; - 没有 receipt 时只接受 exact persisted process identity 的两次一致 live inspection, 或可信 not-running evidence; - unknown、缺 handle、身份变化和平台不可验证一律失败关闭。 恢复不终结父 Workflow,也不拥有 timer。 ### 4. completion identity 与 Workflow portable identity 统一 Workflow plan 已明确使用最长 36 字节的 portable Run identity,Task admission 生成 `wta:<32 hex>` deterministic Attempt identity。Local completion receipt/journal 过去 只接受 UUIDv7,导致 adapter 单测通过而真实 launcher 永远在请求校验处失败。 receipt、journal 和文件分片统一接受 `[A-Za-z0-9][A-Za-z0-9._:-]{0,35}`。UUIDv7 是该集合的子集;空值、前导点、斜杠、 反斜杠、空白、NUL 和超长 identity 继续拒绝,因此没有扩大目录穿越 authority。 ### 5. startedAt 绑定 spawn 前 durable barrier launcher 在 spawn 前登记 journal,并把同一个 `handle.startedAtMs` 写进不可覆盖 receipt。coordinator 必须把 Attempt/StepRun 的 `startedAtMs` 保存为该 handle fact, 不能在 spawn 后用新的 wall-clock observation 覆盖。否则合法 receipt 会因早于 Attempt start 而被 quarantine。 Event 时间仍必须不早于 Run/Attempt creation;时间回退继续失败关闭。 ### 6. 停机先冻结 admission,再 drain execution application stop 顺序保持: 1. `LocalSchedulerLifecycle.stopAndDrain()`,冻结新 Workflow/Task admission; 2. `LocalExecutionControlLifecycle.stopAndDrain()`,对在途进程提交 shutdown intent、 exact stop 与 Step-scoped terminal/recovery; 3. 关闭共享 SQLite storage 和 adoption fence。 并发 stop 复用同一个 promise,不产生第二轮 scheduler 或重复 Attempt。 ## 不采用的方案 ### 新建 `local-workflow` workspace package 拒绝。该能力没有独立部署或依赖边界;组合器、执行 port、SQLite adapter 分别属于既有 local-execution/local-sqlite/local-application。继续拆包会产生更多单文件 package。 ### 为每个 Workflow 创建 timer 或 child Run 拒绝。前者增加路由器唤醒/RSS,后者复制 Run/取消/recovery authority,并破坏 same-Run StepRun fence。 ### 保留 receipt UUIDv7-only 并把 deterministic Attempt 强制改为随机 UUID 拒绝。会破坏 admission 的可重算 exact replay,也与已经受审的双方言 portable identity contract 冲突。严格 portable 字符集在保持路径安全的同时覆盖两类 Run。 ## 当前验证 1. `runtime-core` 419/419、`local-process` 18/18、`local-execution` 30/30、 `local-sqlite` 189/189; 2. macOS application 全量 41 项为 38 pass、3 条 Linux 条件 skip、0 fail; 3. 本机已有 `node:24-bookworm-slim` 镜像在 `--pull never --network none` 下执行真实 Linux product vertical:两步 Workflow 全部成功,每个 Step 恰好一个 Attempt, 两条 admission,无重复 dispatch; 4. 启动前用户取消的 Workflow 在 application 首轮 cadence 原子收敛为 cancelled, 普通 Run recovery 扫描为 0,且没有启动 Attempt; Linux 产品门还覆盖在途 Local Task:等待真实 PID/Attempt/StepRun running, 持久化父 Workflow 取消意图,先停止进程,再将 Attempt/全部 StepRun/父 Workflow 精确收敛为 cancelled; 5. PostgreSQL 18.4 arm64 physical HA 新增独立 `pluginPackageWorkflowTaskAttempt*` 五项 gate:原子提交、exact replay、 `remote_apply` 晋升前复制、runtime-only ACL、timeline 1→2 后存活全部为 true; 6. HA 继续完成旧主 fencing、`pg_rewind` 只读同步重入、两个 fresh control replica, `gates.passed=true`,并使用 `QL3_HA_SKIP_IMAGE_PULL=true`; 7. 真实 Local Workflow 已进入既有 Linux resource gate,没有新增 workspace package、依赖或常驻 cadence。arm64 Node 24.18.0 的 128 MiB/0.5 CPU 档 `memory.peak=120217600`、Workflow process peak RSS `87449600`、RSS delta `16908288`,16 次 admission write-lock p95 `4.053 ms`;256 MiB/1 CPU 档 `memory.peak=121344000`、Workflow process peak RSS `87949312`、RSS delta `17408000`,32 次 write-lock p95 `3.317 ms`。两档 `memory.events` 的 max/OOM 增量为零,并完成 Edge/Standalone admission 与 conclusive-stop/control-terminal 两组各 16 个 `SIGKILL → reopen → exact replay` 场景。 8. PostgreSQL 18.4 arm64 HA 已完成 Remote Workflow `stop_requested → completion response loss/exact replay → parent convergence response loss/exact replay → promotion 后再次 replay`;Run/Attempt/StepRun cancelled、Lease completed@v6,独立四项 gate 和总 `passed` 均为 true。 ## 尚未关闭 1. 固定 Edge/Standalone 物理路由设备报告、闪存/FTL 写放大、真实 ENOSPC 与受控 突发断电门;CI `SIGKILL` crash matrix 明确不等同于物理断电; 2. Linux x64/arm64 的长期 CI 成功记录和实际低配设备基线。