Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
537 changes: 537 additions & 0 deletions docs/audit-dag-memory-goal-2026-08-18.md

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions docs/findings/dag-batch-findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# DAG 批次 Findings Register

- 验收 primary source:`docs/audit-dag-memory-goal-2026-08-18.md`(DAG 章节)
- 分支:`fix/dag-batch` → PR `dev`
- 收敛判据:连续两轮独立审阅(Spec 镜 + Standards 镜)零 findings + 模块门禁全绿
- 规格:`workflows/audit-fix-loop.md`
- 触发:用户指示在 GOAL run(PR #334)之后立即开工,不等合入

## 审计缺陷切片(输入项,非审阅 finding)

| ID | 严重性 | 切片顺序 | 状态 | 提交 |
|---|---|---|---|---|
| DAG-01 + DAG-02 | High | A(P0,审计明确要求一并修) | 完成(红-绿-变异×3 通过) | 71ab1bdf6 |
| DAG-03 | Medium | B (P1) | 完成(红-绿-变异×2 通过) | 1c4f1ad7a |
| DAG-04 | Medium | C (P1,#316 机制部分;触发源不追查,按审计记录缺口) | 完成(红-绿-变异×2 通过) | db626d4ba |

## 切片设计要点(实现后回填)

- **A(DAG-01+02)**:
- 运行时:spawnReady 条件求值前对字符串依赖输出做 `parseJsonOption` 归一化(与 replan-verdict 门同源);非 JSON 回退原串(整串等值可用、字段路径仍 false、数值比较仍 loudly-fail)。
- authoring:`checkpointGateDiagnostics` 追加「被门控 checkpoint 必须声明 output_schema」(authoring 期错误;运行时路径不要求——runtime-created 图按 CONTEXT.md 有意豁免 authoring 校验,`requireOutputSchema:false`)。
- 门禁接线:`validatePostCompile` 的 checkpoint 门不再随 `structural:false` 对 replan/extend 关闭(fragment 内对生效);`replanStructuralDiagnostics` 对 merged 图补跑 checkpoint 门(覆盖 fragment 挂到既有 checkpoint 的场景),**豁免持久图中已终态的 checkpoint**(裁决已交付,加波/重开是受 sanction 的模式——reopenDenial 加性重开的既有语义)。
- 波及适配 2 个既有 harness(blanket `report_to_parent:true` 的 rev-view / stale-nodefailed 形状按门禁语义补 condition);dag-wake-integration 的加波/重开场景经终态豁免自然兼容,无需改动。
- **B(DAG-03)**:pause 终态失败 fail-closed(恒 hold),`Effect.catch` → `Effect.catchCause`(hasInterrupts 再抛)折叠 defect;logWarning → logError(含 durableStatus)。
- **C(DAG-04)**:publisher 外层 catchCause 依 F1 模式 `hasInterrupts` 再抛;`disposeAllInstancesAndEmitGlobalDisposed` 加 10s 有界超时(`timeoutOption`——超时即放弃且不产生错误,保住 HttpApi dispose endpoint 的 `never` 错误通道),去掉 uninterruptible 包裹;Disposed 事件在超时/吞错后仍必落地;真实处置失败在非 swallow 路径仍传播。
- 中断测试注入方式:scope-disposal 杀 fiber 的 cause 实测为 Die 而非 Interrupt(已实证),改用 `Effect.failCause(Cause.interrupt(0))` 在 store 边界直接注入 interrupt cause(goal e2e 既有模式),精确命中被修复的 catchCause 判别线。

## 模块门禁
- 切片级:每切片 dag 目标测试簇绿 + 包内 typecheck 绿 + 变异翻红验证(见上表)
- 全量套件:进行中

## 审阅轮次

(每轮审阅结果记账于此;全部关闭后才具备发 PR 资格)

### Round 1
- Spec 镜:**PASS**,3 条 Low INFO;Standards 镜:**PASS**,6 条 findings(F1-F6)。处置:
- F1 + INFO-2(Medium):ADR-0003 与新 enforcement 矛盾。→ **已同步**:Decision/Consequences/Deferred 改写为「validatePostCompile 全动作 + replanStructuralDiagnostics merged 图(终态豁免)+ create 刻意不动 + output_schema authoring 义务」,Deferred 首项标记 resolved。
- F2 + INFO-1(Medium,需主裁决):fail-closed 保持会被后续 NodeCompleted/NodeSkipped/stepped 的 durable-row re-sync 解除;且 Replanned 处理器从不重同步 paused(hold 也会闷死 corrective 派发)。用户裁决「解决所有已知问题」。→ **已实装**:`WorkflowEntry.vetoHold`——门设置、两处 re-sync 点(node 终态序言 + refreshControlFlags)尊重保持、三个父控制事件(Replanned/Resumed/Stepped)释放并重同步(Replanned 补上从未有过的 flag 重同步);新增 2 条红绿测试(re-sync 存活 + replan 释放)+ 双向变异验证。
- F3(Low):global-lifecycle.test.ts 未用 `Option` 导入。→ 已删。
- F4(Low):loop.ts「normalization above」方向失准。→ 已改为指向 NodeCompleted 处理器。
- F5(Low):终态豁免措辞「delivered its verdict」对 failed/aborted/skipped 不真。→ 三处改为「settled and immutable」(CONTEXT.md/validation.ts×2)。
- F6(Low, informational):无 uninterruptible 的取舍记录。→ 无需动作(reviewer 确认 trade 正确)。
- INFO-3(Low):register 误记「3 个 harness 适配」。→ 已更正为 2(wake-integration 经终态豁免免改)。
- 结论:非干净轮。修复后进入 Round 2。

### Round 2
- Spec 镜:**PASS**,3 条 INFO;Standards 镜:**PASS**,1 条 INFO。处置:
- R2-1(INFO):vetoHold 注释宣称「replan/resume/step 均可释放」不精确——hold 态持久行是 running,resume 对 running 是非法迁移(InvalidTransitionError),resume 释放仅在 stepping/pending 态可达。→ 已改写字段注释:replan/step 任何 hold 态可达;resume 仅 stepping/pending;control(replan) 正是 verdict 所求的处置路径。
- R2-2(INFO):hold 为进程内状态,重启后从持久行重建(审计 DAG-03 的范围就是进程内 fail-open)。→ 已在字段注释记录该边界。
- R2-3(INFO):timeoutOption 的切断发生在第一个可中断点——uninterruptible finalizer 区域内的 wedge 可越过上限(硬切断需 Effect.disconnect,刻意不取,与 exerciser 的 Promise.race 同残差)。→ 已在 global-lifecycle 注释精确化。
- Standards INFO-1:dag-loop-guards.test.ts 未用的 `probe` 变量。→ 已删。
- 结论:非干净轮(4 INFO)。修复后进入 Round 3。

### Round 3
- Spec 镜:**PASS,no findings**(干净轮候选)。
- Standards 镜:**PASS**,1 条 INFO:R3-1——vetoHold 注释的 resume 枚举「stepping/pending」不完整:hold 之后父层仍可先持久 pause 再 resume(该路径释放有效),且 pending 对已启动工作流不可达。→ 已改为「resume only when the durable row is not running (paused/stepping)」并说明直至持久 pause 落地。
- 结论:非干净轮。修复后进入 Round 4。

### Round 4
- Spec 镜:**PASS,no findings**。
- Standards 镜:**PASS**,1 条 INFO:R4-1——ungated 诊断的 hint 把「declare output_schema」列为独立替代项,但单独声明 schema 不能解除 ungated 错误(须与 gating 条件组合)。→ 已改写为「Gate … with condition … and declare output_schema …」组合句式。
- 结论:非干净轮。修复后进入 Round 5。

### Round 5
- Spec 镜:**PASS,no findings**;Standards 镜:**PASS,no findings**(R4 hint 修复逐条复核为真)。
- 结论:**干净轮 1/2**。

### Round 6
- Spec 镜:**PASS,no findings**(四缺陷 + F2 + 文档同步独立复核;附 process note:PR 前回填 R5 结果——本条即回填)。
- Standards 镜:**PASS,no findings**(Effect v4 API 对照 effect-smol 源码逐一验证;注释真实性对照 transition table/replan.ts/spawn.ts 复核)。
- 结论:**干净轮 2/2**。连续两轮零 findings → **DAG 模块收敛**。

## 收敛结论

R1(Spec 3 Low + Standards 6)→ R2(Spec 3 + Standards 1)→ R3(Spec 0 + Standards 1)→ R4(Spec 0 + Standards 1)→ **R5+R6 连续两轮双镜零 findings**。全部 findings 关闭,findings 衰减轨迹清晰(Medium 实装 → Low 措辞 → 零)。

## 模块门禁(终态)

- 切片级:dag 测试簇绿(579/579 含新增 19 条回归)+ 包内 `bun typecheck` 绿 + 每切片变异翻红验证。
- 全量套件:4157 tests / 342 files,仅 2 失败均为 GOAL run 期间已在干净基线 detached 复跑证实的 darwin 环境既有失败(help-snapshots、project-copy;pty 本轮通过),与本批无因果;DAG/GOAL 相关零失败。

## 交付

- 分支:`fix/dag-batch`(基于 origin/dev 1d087ffe9)
- 提交链:43fd72bbd(audit 文档)→ 71ab1bdf6(DAG-01/02)→ 1c4f1ad7a(DAG-03)→ db626d4ba(DAG-04)→ 337814648 / ff1f8dab5 / 85df04311 / 5128f0958(审阅轮修复)
- PR → dev(Typecheck 门禁),合入由用户授权执行
2 changes: 1 addition & 1 deletion packages/opencode/src/dag/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Workflow Orchestration turns one user objective into one durable DAG. Its model-
- Model-facing graph actions expose only `spec_path`; graph fields live in YAML so provider tool-call serialization cannot turn a nested graph into a string.
- Legacy YAML may be adapted at the file boundary without making legacy fields valid inline input.
- Runtime Admission and Workflow Authoring Check have separate names, state, and responsibilities.
- Dependents of a reporting checkpoint must be gated on its output; authoring rejects ungated shapes at start/validate (enforcement point: authoring boundary only, runtime create deliberately unchanged).
- Dependents of a reporting checkpoint must be gated on its output; authoring rejects ungated shapes at start/validate AND at replan/extend fragment actions, and the runtime replan/extend mutation seam re-checks the merged graph (exempting checkpoints already terminal in the durable graph — they are settled and immutable, the spawn-before-verdict race is past; runtime create remains deliberately unchanged). A gated checkpoint must declare `output_schema` (authoring obligation).

## Boundaries

Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/src/dag/dag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,9 @@ export const layer = Layer.effect(
addCount: plan.add.length,
merged: wfConfig ? computeMergedConfig(wfConfig, normalizedFragment, plan) : { nodes: normalizedFragment.nodes },
config: { mode: wfConfig?.mode, max_total_nodes: wfConfig?.max_total_nodes },
terminalNodeIds: new Set(
nodes.filter((n) => isNodeTerminalStatus(n.status as NodeStatus)).map((n) => n.id),
),
})
const replanErrors = DagValidation.sortLegacyStructural(replanDiagnostics.filter((d) => d.severity === "error"))
for (const warning of replanDiagnostics.filter((d) => d.severity === "warning")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,44 @@ checkpoint must be a reporting leaf, or the node must drop `report_to_parent`.
`node_defaults.report_to_parent` is honored: a node inheriting the default
reports the same way.

Enforcement lives in `checkpointGateDiagnostics`, wired only into
`validatePostCompile`'s structural branch — the authoring start/validate path.
Every ungated dependent emits one error-severity `dag.invalid` diagnostic in
both `portable` and `environment` profiles, so `start` and `validate` reject
the shape before any durable graph exists.

Enforcement is authoring-only by design. `Dag.create` and the replan/extend
fragment paths stay untouched: the verdict vocabulary is open, the ACCEPT path
must not wait for the parent, and runtime enforcement would change the
semantics of every existing graph, including issue #294's wake-chain and
reopen-extend behavior.
Enforcement lives in `checkpointGateDiagnostics`. It is wired into
`validatePostCompile` for every action — `start`/`validate` AND the
replan/extend fragment authoring paths (DAG-02, 2026-08-18: pre-fix it hid
behind the `structural === start-only` branch, so a fragment could attach an
ungated dependent to a reporting checkpoint and the engine spawned it before
the parent read the verdict) — and into `replanStructuralDiagnostics`, which
re-checks the MERGED graph at the runtime replan/extend mutation seam, so a
fragment dependent on an EXISTING checkpoint cannot escape either. Every
ungated dependent emits one error-severity `dag.invalid` diagnostic in both
`portable` and `environment` profiles.

Two deliberate carve-outs:

- Checkpoints already terminal in the durable graph are exempt at the runtime
merged-graph check: they are settled and immutable, the
spawn-before-verdict race is past, and the sanctioned additive/reopen waves
(`node("repair", ["checkpoint"])` after a completed reporting leaf) keep
working.
- A gated checkpoint must additionally declare `output_schema` (DAG-01,
authoring obligation): without a schema the child may complete with prose
that resolves no fields, leaving the gate permanently false and silently
skipping the gated subtree. The runtime path does not impose the schema
obligation (`requireOutputSchema: false`) because runtime-created graphs
deliberately bypass authoring validation.

`Dag.create` itself stays untouched by design: the verdict vocabulary is
open, the ACCEPT path must not wait for the parent, and runtime create-level
enforcement would change the semantics of every existing graph, including
issue #294's wake-chain and reopen-extend behavior.

## Consequences

- Ungated reporting checkpoints fail fast at start/validate with a diagnostic
naming the checkpoint, the dependent, and the three legal fixes.
- Ungated reporting checkpoints fail fast at start/validate — and since
DAG-02 also at replan/extend fragment authoring and at the runtime
replan/extend mutation seam — with a diagnostic naming the checkpoint, the
dependent, and the legal fixes.
- Runtime create, wake chains, and reopen-extend semantics are unchanged;
trusted internal callers retain full runtime flexibility.
trusted internal callers retain full runtime flexibility at create time.
- Saved and curated workflows were audited: 14 curated block workflows are
unaffected; only `ultra-flow-route.yaml` and `release-route.yaml` trip the
new check and are tracked in opencode-dag-config#14.
Expand All @@ -62,9 +82,8 @@ reopen-extend behavior.

## Deferred

- Replan/extend fragments are not checkpoint-gate-checked (coverage gap; no
date). Runtime flexibility was prioritized; fragment authoring remains
advisory.
- ~~Replan/extend fragments are not checkpoint-gate-checked~~ — resolved
2026-08-18 (DAG-02): fragment authoring and the runtime merged-graph seam
both enforce the gate, with the terminal-checkpoint carve-out above.
- Deprecation of advisory wake chains (no date): `report_to_parent` without
gated dependents stays legal but is a smell worth revisiting once fragment
coverage exists.
gated dependents stays legal but is a smell worth revisiting.
Loading
Loading