Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f8c9d2c
fix(test): out-of-process watchdog for the httpapi exerciser
LeXwDeX Aug 5, 2026
9105f37
Merge pull request #177 from LeXwDeX/fix/httpapi-exerciser-watchdog
LeXwDeX Aug 5, 2026
d329bb1
feat(dag): node timeout escalation — signal, adjudication, cap backstop
LeXwDeX Aug 6, 2026
0226006
Merge pull request #178 from LeXwDeX/feat/dag-timeout-escalation
LeXwDeX Aug 7, 2026
8ad5918
feat(core): batch durable publish + listener fan-out contract + fork …
LeXwDeX Aug 7, 2026
f56f2fb
Merge pull request #179 from LeXwDeX/feat/event-batch-publish
LeXwDeX Aug 7, 2026
ffe52f0
feat(llm): per-request timeout through http transport options
LeXwDeX Aug 7, 2026
eb92034
Merge pull request #180 from LeXwDeX/feat/llm-request-timeout
LeXwDeX Aug 7, 2026
98899f1
feat(core): session runner hot path — turn timeout, incremental histo…
LeXwDeX Aug 7, 2026
5e9a772
Merge pull request #181 from LeXwDeX/feat/session-runner-hotpath
LeXwDeX Aug 7, 2026
4f63d43
fix(config): degrade gracefully when remote config source is unreachable
LeXwDeX Aug 7, 2026
83d0053
Merge pull request #182 from LeXwDeX/fix/config-offline-degrade
LeXwDeX Aug 7, 2026
17675a3
feat(goal): stall-resistant pause/resume with full branch coverage
LeXwDeX Aug 7, 2026
4c6a707
Merge pull request #183 from LeXwDeX/feat/goal-pause-resume
LeXwDeX Aug 7, 2026
cb9fba4
fix(core): deep-review findings — hasInterrupts interrupt check, ratc…
LeXwDeX Aug 7, 2026
5330b15
Merge pull request #184 from LeXwDeX/fix/deep-review-fixes
LeXwDeX Aug 7, 2026
20ee51e
fix(dag): acceptance-time template binding validation removes spawn-d…
LeXwDeX Aug 7, 2026
5cc7156
test(dag): drop unsafe type assertions in acceptance-validation tests
LeXwDeX Aug 7, 2026
16b39a9
docs(batch-a): freeze grill docs, transition table v2, implementation…
LeXwDeX Aug 7, 2026
17f10f0
feat(dag): batch A — Q1 flag lifecycle, Q2 delivery-gated re-time, Q3…
LeXwDeX Aug 7, 2026
7215162
fix(dag): C1 — three-valued nodeExtendTimeout reject contract (Q2-rej…
LeXwDeX Aug 7, 2026
01a903c
test(dag): drop 6 redundant Effect<never> casts to reconcile CI lint …
LeXwDeX Aug 8, 2026
c7defea
Merge pull request #185 from LeXwDeX/fix/accept-template-binding
LeXwDeX Aug 8, 2026
4ddeaf2
Merge pull request #186 from LeXwDeX/feat/batch-a
LeXwDeX Aug 8, 2026
456425a
docs: record batch-A backlog findings (phantom cancelled node state, …
LeXwDeX Aug 8, 2026
0c08d63
Merge pull request #187 from LeXwDeX/docs/backlog-tickets
LeXwDeX Aug 8, 2026
9ba5048
Potential fix for pull request finding 'CodeQL / Incomplete URL subst…
LeXwDeX Aug 8, 2026
444f6c6
Potential fix for pull request finding 'CodeQL / Incomplete URL subst…
LeXwDeX Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ jobs:
# default 6h. The full gate baseline is ~6m in CI, so 15m is generous;
# test:httpapi:ci adds --progress/--trace to effect mode so the log
# names the exact scenario and phase if it ever hangs again.
# 2026-08-05: a native-level freeze survived every in-process guard
# (timers die with the event loop) and burned the full 15m silently.
# --progress now also arms an out-of-process watchdog that SIGKILLs
# the runner after 120s without progress, naming the last scenario/
# phase — the step timeout stays as the final backstop.
timeout-minutes: 15
run: bun run test:httpapi:ci

Expand Down
57 changes: 57 additions & 0 deletions .opencode/batch-a-implement-manifest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Batch A Implement — 图编排 Manifest

## reference_template
`parallel-development-loop`(global,13 节点)——保护脊柱保留:
audit-module-wave(模块波本地审查,PASS|LOOP|BLOCKED)→ wire-modules(单一集成/提交所有者)→ simulate-wired-system(reasoner 纯逻辑推演)→ verify-wired-system(确定性验证)→ 三路 fresh review → arbitrate-final-review(唯一终审)→ finalize-delivery(仅 PASS 条件放行)。

## 任务注入
`.scratch/batch-a/issues/01-08`(09 为 dev→main 收束票,CI 全绿后在图外执行)。
规格依据:`.opencode/grill-batch-a/`(CONTEXT.md + ADR-0001~0004 + node-lifecycle-transitions.md v2,Round 2 doc 审核 PASS 冻结版,基线 dev@5330b15a9)。

## 展开(expand)
参考图的 develop-core / develop-adapters / develop-tests 三个泛模块槽替换为 8 个票据实现节点:

| 节点 | 票据 | 依赖 |
|---|---|---|
| impl-q1 | 01 裁决旗清旗 | freeze-contract |
| impl-q2 | 02 送达门控 re-time | freeze-contract |
| impl-s5 | 05 锁一行超时 | freeze-contract |
| impl-flaky-stdout | 06 stdout 污染族 | freeze-contract |
| impl-flaky-ws | 08 workspace 计时 | freeze-contract |
| impl-q3 | 03 事件+guard 前移 | impl-q1(projector 写集串行) |
| impl-sdk | 04 SDK 再生 | impl-q3 |
| impl-flaky-share | 07 ShareNext 计时 | impl-flaky-stdout(同文件串行) |

## 剪裁(prune_decisions)
| node | prune_reason | replacement_coverage |
|---|---|---|
| develop-core | 泛槽与已审计的票据分解不匹配 | 8 个票据节点按审计后写集分工,含测试切片(各票 TDD 自带) |
| develop-adapters | 同上 | 同上(03/04 覆盖 schema/SDK 适配面) |
| develop-tests | 测试切片并入各票 TDD | 每票先写失败测试再实现;review-tests 终审覆盖矩阵 |
| freeze-design | 设计已在图外冻结(两轮 doc 审核 PASS) | 改为 freeze-contract:只核验票据写集互斥并产出结构化契约,不重做设计 |

## 写集互斥表
- q1:projector 折叠侧 + dag 测试(清旗族)
- q2:runtime/loop.ts re-time 发起点 + 门控测试
- q3:schema 事件定义 + EventManifest + dag.ts 命令路径 + projector handler + 测试
- sdk:packages/sdk/js 生成物 + 消费者类型对齐
- s5:dag.ts withWorkflowLock 包装层(唯一区域)+ 测试
- flaky-stdout:test/cli/run + src/share/share-next.ts + 相关 fixture
- flaky-share:test/share 计时部分(06 已合入其依赖报告)
- flaky-ws:workspace sync 测试(+ 根因所需最小 src,须记录)
已知同文件异区:s5 与 q3 同 dag.ts(区域互斥:锁包装层 vs 命令路径);q1 与 q3 同 projector(已串行)。

## Git 纪律
- 所有 impl 节点禁止任何 git 操作(add/commit/stash/branch/push)
- wire-modules 是唯一提交所有者(typecheck + 套件 + lint 全绿后单 commit)
- PR 由父会话在终审 PASS 后开(分支 feat/batch-a → dev)

## 审查门禁义务
arbitrate-final-review 必须审计本 manifest:每个 prune 有 prune_reason + replacement_coverage,缺任一禁止 PASS(fail-closed)。

## 续作记录(Continuation)
原图 dag_024a09546ffevmMvS3M6v0I1Av 于 audit PASS、wire-modules 提交 17f10f0ce 之后 terminal failed——两个 spawn 期配置错误:impl-q3({{freeze-contract}} 非直接依赖,已由 impl-q3b 替换并完成)与 verify-wired-system(replan 片段遗漏 input: repo)。
终态不可逆 → 按续作合约起新图 batch-a-continue:
- reused_nodes:freeze-contract、impl-q1/q2/q3b/s5/flaky-stdout/flaky-ws/flaky-share/sdk、audit-module-wave(PASS)、wire-modules(提交 17f10f0ce)——全部完成且经审计,不重跑
- 续跑尾部:verify-wired(修复 input 绑定)→ simulate-wired + 三路 review → arbitrate-final → finalize
- 尾部节点一律从真实仓库状态(git show HEAD + 票据 + grill 文档)取证,不注入可能为空的旧输出(fail-closed)
50 changes: 50 additions & 0 deletions .opencode/grill-batch-a/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# CONTEXT — 批次 A 设计 grilling(D2/D3 + escalation_pending 生命周期 / S5 锁超时)

状态记录文件:术语表 + 决策树 + 已定/未定。随 grilling 更新。

## 术语表(domain glossary)

| 术语 | 当前定义(代码事实) | 问题 |
|---|---|---|
| `escalation_pending` | node 列。escalate projector 置 true;NodeStarted/NodeRestarted 清 false;updateNodeDeadline 清 false;**终态不清** | 语义未定:是"有未送达的 wake"还是"等待裁决"?(Q1) |
| `wake_reported` | node 列。wake 送达后 true;escalate 时 re-arm false | 与 escalation_pending 职责边界模糊(D2 根因) |
| adjudication(裁决) | 主 agent 对升级节点的处置:extend(replan 带新 timeout)/ restart / cancel | extend 写入在事件日志之外(D3) |
| re-time | extend 落地动作:nodeExtendTimeout 重算死线(now + new timeout) | 门控见 loop.ts:800(A1 cap gate) |
| delivery boundary(交付边界) | wake 投递条件:`escalationPending ∨ (timeoutExtensions>0 ∧ terminal)` | 依赖 escalation_pending 语义(Q1 决定后复查) |
| 升级循环 | watchdog 超时 → nodeTimeoutEscalated(count+1, pending=true, wake re-arm)→ 主 agent 裁决 | 每轮消耗一个 count,21×cap 兜底 |
| workflow lock | KeyedMutex per dagID,单许可、不可重入、**无超时**(S5) | 静默死锁风险 |

## 设计公理(用户宏观原则,2026-08-07 确立,永久约束)

1. **状态流转优先**:节点生命周期以显式状态机为真相源;轮询/watchdog 只能是「转移提议者」,不得充当监督权威或直接写状态。
2. **错误即状态**:错误类别(error_class/trigger)是状态机的输入,由状态决定后续动作与 agent 的判断/处置依据(wake 文案承载)。
3. **奥卡姆剃刀**:解法需要复杂策略(新错误类族、per-caller 语义分支、特殊化处理)= 重新思考的信号;优先砍机制而非加机制。
4. **单一写权威**:节点状态一切变更走「dag 命令 → durable 事件 → projector」;直写行 = 破窗(现存唯一破窗 updateNodeDeadline,Q3 已决废除)。

## 决策树

- **Q1(根):escalation_pending 的生命周期契约** → ✅ **已定:(b) 裁决状态旗**。「节点正在等待主 agent 裁决」;由裁决写动作清(extend / restart / cancel)**或**由终态清(NodeCompleted/NodeFailed 清旗——死掉的节点无需裁决,结果走终态交付臂)。投递是 wake_reported 的本职,两旗职责正交(D2 病灶即职责混用)。→ 落 ADR-0001
- **Q2:wake 未送达时 re-time 放不放行(D2)** → ✅ **已定:(a) 送达门控**(Round 2 机制修正:skip 合取项,非放行析取项)。`loop.ts:800` A1 skip 之外新增 Q2 skip 合取项 `(escalationPending && !wakeReported)`——已升级但未送达一律跳过 re-time(节点保留过期死线,watchdog `spawn.ts:111` 自续再升级,wake 照常投递,裁决必发生在送达之后)。放行条件等价于 `deadline≤now ∨ deadline=null ∨ (escalationPending ∧ wakeReported)`;不可写成放行析取项(会被 deadlineElapsed 析取吞没,对公共路径无效,cons-F1 旧病)。updateNodeDeadline 的 wake_reported:true 退化为无害 no-op。restart/cancel 不加门控(不改死线,不受 D2 威胁)。→ 落 ADR-0002(v Round 2)
- **Q3:deadline 变更入事件日志(D3)** → ✅ **已定:(a) 全量入日志**(Round 2 机制修正:guard 前移到命令层)。新增 durable 事件 `NodeDeadlineExtended`(nodeID + 新死线 + 裁决时 extension 计数),workflow 锁内发布,projector 幂等投影(event id 去重、replay-safe);`nodeExtendTimeout` 改为标准「命令 → 事件 → 投影」形态,直写废除。guard(running-guard + Q2 送达门控)在命令层、`events.publish` 之前持锁同步判,`0/1` 是命令同步 Effect 返回(不经 publish 链——projector 返回值在 `event.ts:256-258` 被丢弃,imp-F1);编排器经状态(终态 / 持续 escalation_pending)+ wake 观察拒绝(公理 ②)。成本:schema dag-event + DurableDefinitions + projector handler + dag.ts + 测试 + SDK event union 再生;无路由变化。→ 落 ADR-0003(v Round 2)
- **Q4+Q5(S5):锁超时语义与参数** → ✅ **已定:被 Q6 奥卡姆重构收编**。原案(类型化错误类 + per-caller 语义)被否——违反公理 3;最终形态:withLock 外层一行 `Effect.timeout("30 seconds")`,复用 TimeoutException,零新错误类、零 per-caller 改动、watchdog 零特殊化(自续间隔天然重试,计数只在成功时 +1)。→ 并入 ADR-0004
- **Q6(收口):批次 A 最终采纳范围** → ✅ **已定:(A)**。Q1-Q3 照旧 + S5 一行超时 + **节点生命周期转移表**作为权威审查基准(此后引擎改版先对照表审)。入表既有小疵:watchdog 强杀时 promptSvc.cancel 先于 nodeFailed 事件(应改为事件后处置,不另开工单)。→ 落 ADR-0004/0005

**决策树状态:全部闭合(Q1✅ Q2✅ Q3✅ Q4/Q5→Q6 收编✅ Q6✅)。grilling 完成,共识达成。**

## 交付物清单

- ADR-0001 escalation_pending 裁决状态旗契约
- ADR-0002 送达门控 re-time(D2)—— **Round 2 修正**:skip 合取项(`loop.ts:800`),语义保留
- ADR-0003 NodeDeadlineExtended 入事件日志(D3)—— **Round 2 修正**:guard 前移到命令层(非 projector 返回值),保留 durable 事件/schema/SDK 再生/回放一致性
- ADR-0004 S5 奥卡姆版:一行超时(收编 Q4/Q5)
- ADR-0005 转移表基准(node-lifecycle-transitions.md **v2**:G1 skip 合取项 + T9 命令层 guard + guard 拒绝非转移说明)
- 实施顺序建议:Q1+Q2+Q3 一个 PR(escalation 生命周期闭环);**Q3 的 SDK event union 再生(`./packages/sdk/js/script/build.ts`)为强制伴随步骤**;S5 一行超时并入或单独小 PR;转移表随实施落地后从 grill-batch-a 晋级至 .dag-specs

## 证据锚点

- D2:store.ts updateNodeDeadline(set escalation_pending:false + wake_reported:true);loop.ts:800 re-time gate
- D3:dag.ts nodeExtendTimeout 无 events.publish / seq bump;投影重建恢复旧死线
- 终态不清:projector.ts escalate 置 true / NodeStarted:243、NodeRestarted:362 清 / NodeCompleted、NodeFailed 无清理
- 边界谓词:节点级 `loop.ts:949-953`(`escalationPending ∨ (timeoutExtensions>0 ∧ terminal)`)/ 工作流级决策 `loop.ts:960-967`;summary 谓词:`store.ts:245-260`(`escalatedRows`:`escalation_pending=true ∧ status='running'`)。注:`loop.ts:906-912` 实为 workflow-terminal 清理,非交付边界谓词;DAG 运行时无 3 分钟阈值(watchdog 自续间隔 = `Math.max(1_000, timeoutMs)`,`spawn.ts:111`)
- S5:dag.ts:298-305(KeyedMutex 注释:单许可不可重入,持锁者崩溃/挂起 = 静默死锁);历史发现 S7 recovery INVENTED 推断同域
- 五轮 review 归档:.opencode/promotion-review-round1/arbitrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ADR-0001: escalation_pending 是裁决状态旗(Q1)

- 状态:已接受(批次 A grilling,2026-08-07,决策 (b))
- 上游公理:设计公理 ①②④(CONTEXT.md)

## 背景

`escalation_pending` 曾是无契约的混合旗:escalate 置 true、NodeStarted/Restarted 清、updateNodeDeadline 清、终态不清。被三个消费者依赖(交付边界、summary escalatedNodes、re-time 门控)。D2(wake 被 extend 偷吃)与「终态永挂旗」陷阱同源于语义未定义——投递状态与裁决状态共用一旗。

## 决策

`escalation_pending` 的唯一语义:**该节点正在等待主 agent 裁决**。

清除时机(仅两种):
1. **裁决写动作**:extend(NodeDeadlineExtended 投影)/ restart(NodeRestarted)/ cancel(NodeCancelled)
2. **终态**:NodeCompleted / NodeFailed 清旗——死掉的节点无裁决对象,其结果由交付边界终态臂 `(extensions>0 ∧ terminal)` 保证送达

投递(送达)是 `wake_reported` 的专职,两旗职责正交。

## 后果

- projector:NodeCompleted/NodeFailed handler 增补清旗(修复终态挂旗)
- NodeCancelled handler 清旗(cancel 即裁决)
- summary 谓词与边界谓词不变(语义对齐后自然正确)
- 测试:终态清旗回归测试 + cancel 清旗测试
69 changes: 69 additions & 0 deletions .opencode/grill-batch-a/adr/ADR-0002-delivery-gated-retime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# ADR-0002: 送达门控的 re-time(Q2,D2 的修复)

- 状态:已接受(批次 A grilling,2026-08-07,决策 (a);Round 2 机制修正,语义保留)
- 依赖:ADR-0001(旗子语义)

## 背景

re-time 门控(loop.ts:800 的 A1 cap gate)的 pending 臂只认 `escalationPending`,不认送达状态。主 agent 因不相干原因 replan 时若修改了升级节点的 timeout,会在 agent **从未见过该次升级**的情况下完成裁决并消费未送达的 wake(`store.updateNodeDeadline` 置 `wake_reported:true`,`store.ts:331`)——违反 §5-3「wake 主 agent」。

## 决策(Q2,机制修正后)

re-time 门控新增一个 **skip 合取项**:已升级但 wake 未送达的节点一律跳过 re-time——**裁决必发生在送达之后**成为结构不变式(G1)。

- **未送达的升级**(`escalationPending=true ∧ wakeReported=false`)→ 跳过 re-time:节点保留过期死线,watchdog 自续下个间隔再升级(计数照常爬向 G2 cap,偏安全侧),wake 照常投递
- restart/cancel **不加**送达门控(不改死线,不受 D2 威胁)
- `store.updateNodeDeadline` 的 `wake_reported:true` 写入在门控生效后退化为无害 no-op(送达早已 true)——D2 被结构性消灭,无需改写入逻辑

## 机制(Round 2 修正:skip 合取项,非放行析取项)

### re-time 是单路径,单门控点全覆盖

re-time 在整仓只有**一条触发路径**:`loop.ts:818` replan handler → `dag.nodeExtendTimeout`(`dag.ts:869-871`,全仓唯一调用点)→ `store.updateNodeDeadline`(`store.ts:321-343`,唯一的 deadline **延长**写)。`nodeExtendTimeout` 的全仓调用点仅 `loop.ts:818`;`deadline_ms` 的延长写点仅 `store.ts:331`(外加两个初始投影 `projector.ts:211`/`235` 的初始写,非延长)。watchdog(`spawn.ts:105` `makeDeadlineWatcher`)只提议 T8 `nodeTimeoutEscalated`(`spawn.ts:181`)与 T4 `nodeFailed`(`spawn.ts:160`),**从不**调用 `nodeExtendTimeout`;wake 投递路径(`loop.ts` 8 处 `tryDeliverWake`)从不写 deadline。**re-time 是单路径,单门控点即全覆盖。**

### 为什么 Round 1 表述在公共路径无效(cons-F1)

Round 1 把送达门控写成 re-time **放行条件的一个析取项**("已升级且已送达即放行"),结果是它被公共路径的 `deadlineElapsed` 析取项淹没。真实的公共 case 是 `[escalationPending=true ∧ wakeReported=false ∧ deadline≤now]`——升级发生在死线过期之后(`NodeTimeoutEscalated` 投影 `projector.ts:382-405` 不动 `deadline_ms`,死线仍在过去)。在此 case 上 Round 1 公式不改变行为(被 `deadlineElapsed` 析取项覆盖,依旧放行 re-time,悄悄清旗消费未送达 wake——D2 病灶);Round 1 只在罕见的 `[escalationPending=true ∧ deadline>now]` 上改变行为,而该 case A1 门控本就 skip(健康未来死线)。故 Round 1 在公共路径上无效。

### 精确编辑规格(loop.ts:800)

现状(A1 cap gate,只认 `escalationPending`,不认送达):
```ts
if (!node.escalationPending && node.deadlineMs != null && node.deadlineMs > now) continue
```

修改后(A1 + 送达门控 G1,**新增 skip 合取项**):
```ts
if (
(!node.escalationPending && node.deadlineMs != null && node.deadlineMs > now) // A1: 死线健康且无待裁决 → 跳过(防循环改值绕 cap)
|| (node.escalationPending && !node.wakeReported) // Q2: 已升级但 wake 未送达 → 跳过(裁决必发生在送达之后)
) continue
```

`node.wakeReported` 已在 `NodeRow` 上(`store.ts:111`),replan handler 迭代的 `nodes` 即 `NodeRow[]`,无需新增读取。re-time 的放行条件等价于:

`re-time ⟺ (¬escalationPending ∧ (deadline≤now ∨ deadline=null)) ∨ (escalationPending ∧ wakeReported)`

两处皆为 **skip 条件的合取项**,不可改回放行析取项(会重蹈 cons-F1 旧病)。

## 验证(逐路径覆盖 + 为何解 cons-F1)

| 节点状态 | Round 1 行为 | 修改后行为 | 结论 |
|---|---|---|---|
| 已升级未送达 `escalationPending=true ∧ wakeReported=false ∧ deadline≤now`(公共路径) | 因 `escalationPending=true` 而 A1 **不** skip → re-time 触发,悄悄清旗消费未送达 wake(D2 病灶) | 新增 `(escalationPending && !wakeReported)` 命中 → **skip**。节点保留过期死线,watchdog(`spawn.ts:111`)自续再升级,wake 照常投递 | **公共路径结构性修复(cons-F1)** |
| 已升级已送达 `escalationPending=true ∧ wakeReported=true` | 放行 → re-time | 放行 → 编排器 replan 裁决落地(T9) | T9 正常 |
| 未升级死线健康 `¬escalationPending ∧ deadline>now` | A1 skip | A1 skip 不变 | 无变化 |
| wake 投递路径 / watchdog 路径 | 不延长 deadline | 不延长 deadline | 结构上无法绕过(从不调 `nodeExtendTimeout`) |

`store.updateNodeDeadline`(`store.ts:331`)的 `wake_reported:true` 写入:门控生效后只在已送达 case 触发(`escalationPending=true ∧ wakeReported=true`),此时 `wake_reported` 早已 true → no-op,ADR-0002 的「无害退化」成立。

## 后果

- `loop.ts:800` 一行条件修改(A1 + 新增 Q2 skip 合取项)
- 已知次生语义:投递滞缓时 extend 被推迟至送达后——agent 本就看不见未送达的升级,推迟即正确行为;投递有 bootstrap sweep + idle 边界兜底(G4 交付边界,节点级谓词 `loop.ts:949-953`、工作流级决策 `loop.ts:960-967`)
- 测试:未送达升级的 re-time 被拒(deadline 冻结 + watchdog 再升级);送达后同 replan 放行
- 后 ADR-0003 落地建议:把送达门控的权威副本放进 `nodeExtendTimeout` 命令本身(命令持锁读行 `dag.ts:894` withWorkflowLock、skip 即不发事件),`loop.ts:800` 只留 A1 效率预过滤——两处同一谓词,无新机制(符合公理 ③)

## 修订记录

- **Round 2(本修订,2026-08-07)**:机制重写。Round 1 把送达门控写成 re-time 放行条件的析取项,被 cons-F1 证实对公共路径 `[escalationPending=true ∧ wakeReported=false ∧ deadline≤now]` 无效(被 `deadlineElapsed` 析取项淹没)。本修订改为 `loop.ts:800` 的 **skip 合取项** `(escalationPending && !wakeReported)`,直接作用于公共路径。**语义保留**(裁决必发生在送达之后);状态保持 Accepted。伴随同步:`node-lifecycle-transitions.md` G1 重写、`CONTEXT.md` 决策树 Q2 行重写。
Loading
Loading