Commit 9cab41fd7ebafc6d69fe0f5640d7b5e2a2a31253
1 parent
91b39388
docs(31/33): 记录 sdd 兜底边修复的真实 E2E 回归证据 + 新发现的 needs 契约误判 bug
用真实 LLM 起了一条新任务(G-mt2r62ia-73af/T-mt2r6fz0-125b)回归验证 83afecb 的修 复:org-planner-sdd 自判失败后沿新补的兜底边自动回退 3 次、零人工介入,最终收敛到 completed,补上 31 号文档 §六此前用确定性测试代替的那步真实验收证据。 顺带在 org-coder 子图撞见一个独立的新 bug:inboundContracts()/requiredInputsMissing() 不按 role 过滤 graph_edge_contracts,把同一条边上声明的 produces(源节点自己产出什么) 也当成目标节点的必需输入,导致 org-coder-validator 被 missing_required_input 反复假阳 性 blocked(agent 自己判定 passed,引擎却强改成 fail)。已定位根因、写清复现证据与建议 修法,记在 33-needs-gate-role-filter-bug.md,留给下次实施。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
3 changed files
with
141 additions
and
0 deletions
docs/README.md
| @@ -48,6 +48,8 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | @@ -48,6 +48,8 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | ||
| 48 | | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) | | 48 | | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) | |
| 49 | | [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) | | 49 | | [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) | |
| 50 | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据 | | 50 | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据 | |
| 51 | +| [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 | | ||
| 52 | +| [33-needs-gate-role-filter-bug.md](./todo/system-design/33-needs-gate-role-filter-bug.md) | 🔴 **待修**。`inboundContracts`/`requiredInputsMissing` 不按 `role` 过滤,把同一条边上的 `produces` 声明也当成下游节点的必需输入——发现于上面 31 号文档的真实回归验证过程中,`org-coder-validator` 被 `missing_required_input` 反复误判 blocked,与兜底边问题无关,是独立新 bug | | ||
| 51 | 53 | ||
| 52 | ## todo/future/(暂缓,非当前重心) | 54 | ## todo/future/(暂缓,非当前重心) |
| 53 | 55 |
docs/todo/system-design/31-work-graph-fail-fallback-coverage.md
| @@ -7,6 +7,15 @@ | @@ -7,6 +7,15 @@ | ||
| 7 | > 改成用真实引擎代码 + 真实 DB + 只换掉最外层 Claude CLI 子进程的确定性回归测试代替—— | 7 | > 改成用真实引擎代码 + 真实 DB + 只换掉最外层 Claude CLI 子进程的确定性回归测试代替—— |
| 8 | > 比一次性真跑更严格(可重复验证),且零真实 LLM 花费。 | 8 | > 比一次性真跑更严格(可重复验证),且零真实 LLM 花费。 |
| 9 | > **触发**: `e2e-demo` 公司真实任务 `T-mt1qx0c4-95bf`(goal `G-mt1qwgt5-6b77`)卡住排障,见 §一 | 9 | > **触发**: `e2e-demo` 公司真实任务 `T-mt1qx0c4-95bf`(goal `G-mt1qwgt5-6b77`)卡住排障,见 §一 |
| 10 | +> | ||
| 11 | +> **2026-08-21 追加:真实 E2E 回归验证补上了**。§六「尚未做」的那一步(真跑证明「下次系统自己会 | ||
| 12 | +> 补这条边」)当时因为原任务状态不干净作罢,改用确定性测试代替(§7.3)。同一天晚些时候重启服务 | ||
| 13 | +> 加载了本次修复后,起了一条新任务(goal `G-mt2r62ia-73af` / task `T-mt2r6fz0-125b` / root run | ||
| 14 | +> `run_0e9074815130`)走真实 LLM:`org-planner-sdd` 真实自判失败,沿新补的兜底边**自动**回退 3 次 | ||
| 15 | +> (零 `retryNode` 调用),最终 planner 子图收敛到 `completed`。**修复确认生效,有真实证据**。 | ||
| 16 | +> 同一次验证顺带在 `org-coder` 子图撞见一个**独立的新 bug**(`inboundContracts` 未按 `role` | ||
| 17 | +> 过滤,把 `produces` 声明误当 `needs`,导致 `missing_required_input` 假阳性),与本文档无关, | ||
| 18 | +> 已记录在 [33-needs-gate-role-filter-bug.md](./33-needs-gate-role-filter-bug.md)。 | ||
| 10 | 19 | ||
| 11 | --- | 20 | --- |
| 12 | 21 |
docs/todo/system-design/33-needs-gate-role-filter-bug.md
0 → 100644
| 1 | +# 33-needs-gate-role-filter-bug. `requiredInputsMissing` 未按 role 过滤 —— produces 声明被误判成 needs | ||
| 2 | + | ||
| 3 | +> **日期**: 2026-08-21 | ||
| 4 | +> **状态**: 🔴 **待修**(发现于 31 号文档修复的真实 E2E 回归验证过程中,是一个独立的新 bug, | ||
| 5 | +> 与 31 号文档的兜底边问题无关) | ||
| 6 | +> **触发**: `e2e-demo` 公司真实回归任务 `T-mt2r6fz0-125b`(goal `G-mt2r62ia-73af`, | ||
| 7 | +> root run `run_0e9074815130`)——为验证 [31 号文档](./31-work-graph-fail-fallback-coverage.md) | ||
| 8 | +> 的兜底边修复而起的新任务,链路走到 `org-coder-validator` 时意外反复 blocked | ||
| 9 | + | ||
| 10 | +--- | ||
| 11 | + | ||
| 12 | +## 一、先说 31 号文档的验证结果:修复生效,有真实证据 | ||
| 13 | + | ||
| 14 | +这条任务本身是为了拿到 31 号文档 §六「尚未做」的那一步真实验收证据(当时因为原任务 | ||
| 15 | +`T-mt1qx0c4-95bf` 状态不干净,改用确定性回归测试代替,见 31 号文档 §7.3)。这次补上了: | ||
| 16 | + | ||
| 17 | +- 新起的 `org-planner` 子图(`run_38b3df8fd228`)里,`org-planner-sdd` 真实自判失败 | ||
| 18 | + (下游信息不足),沿新补的兜底边(`org-planner-sdd --fail--> org-planner-prd`, | ||
| 19 | + edge `wge_ce0f7f05f6dc`)**自动回退了 3 次**(`graph_run_loop_records`: | ||
| 20 | + `verdict=registered`,`loop_count` 1→2→3),全程**零次 `retryNode` 调用**,最终 | ||
| 21 | + brd→prd→sdd→benchmark→validator 整条链路收敛到 `run_status=completed`。 | ||
| 22 | +- 31 号文档的决策 A(除入口节点外强制兜底边)在真实 LLM 驱动下按设计工作,**结论确认**。 | ||
| 23 | + | ||
| 24 | +--- | ||
| 25 | + | ||
| 26 | +## 二、新发现:`org-coder-validator` 被 `missing_required_input` 反复误判 | ||
| 27 | + | ||
| 28 | +### 2.1 现象 | ||
| 29 | + | ||
| 30 | +同一条任务往下走到 `org-coder`(`run_21fd814c2f56`,用的是 `executor.json` 骨架)时, | ||
| 31 | +`org-coder-validator` 连续两轮都以 `fail_reason=missing_required_input` 收场: | ||
| 32 | + | ||
| 33 | +- 第一轮(attempt 1,10:01-10:11):沿兜底边 `validator --fail--> impl` 回退两次 | ||
| 34 | + (`wge_1bdb181b14ef` 一次、`wge_aa5bad61fe9a` 一次),第三次撞上 run 树级回退硬顶 | ||
| 35 | + (`backward_count` 全树累计 >5,`LOOP_HARD_CAP_RUN=5`),`force_terminated`, | ||
| 36 | + 节点 `blocked`。**这一段是硬顶熔断本身在正常工作**,不是这次要报的 bug。 | ||
| 37 | +- 人工 `POST /nodes/:id/retry` 把 `org-coder-validator` 打回 `ready` 后, | ||
| 38 | + **第二轮**(attempt 2,10:42:48-10:44:01)**又**判了一次 `missing_required_input`, | ||
| 39 | + 这次连新的 `loop_record` 都没写(回退硬顶已越顶,直接静默 blocked)。 | ||
| 40 | +- 节点自己的 `remediationNote` 写的是「无需整改。……本轮重新独立跑了一遍测试脚本与 | ||
| 41 | + goal-contract,结果与 attempt-1 完全一致,判定 attempt-1 的 passed 结论有效」—— | ||
| 42 | + **agent 自己认为通过了**,是引擎的产出/输入契约闸门(T-mt25ecbd 那次加的机制, | ||
| 43 | + commit `aecf5c9`)把 success 强行改判成 fail。 | ||
| 44 | + | ||
| 45 | +### 2.2 复现证据:`graph_files` 里明明有 `artifact` 却仍报缺 | ||
| 46 | + | ||
| 47 | +查 `T-mt2r6fz0-125b` 的产物索引(`data/graph.sqlite` 的 `graph_files` 表), | ||
| 48 | +`org-coder-impl`(`rn_e71be3641174`,10:06:51 完成)与 `org-coder-test` | ||
| 49 | +(`rn_cd3e3a1c51e2`,10:08:52 完成)都成功登记了当前(`superseded_by IS NULL`) | ||
| 50 | +`kind='artifact'` 的产物行(`_plan.md` / `_research.md` / `coder.log`)。 | ||
| 51 | +`org-coder-validator` 的 attempt 2 在 10:42:48 才跑,**比这些产物晚了半小时以上**, | ||
| 52 | +不是时序竞争,是稳定复现。 | ||
| 53 | + | ||
| 54 | +### 2.3 根因:`inboundContracts` 不按 `role` 过滤,把 `produces` 声明也当成 `needs` | ||
| 55 | + | ||
| 56 | +`executor.json` 里 `test → validator` 这条边同时声明了两条契约(`work-graph-db.js` | ||
| 57 | +`declareEdgeContract`): | ||
| 58 | + | ||
| 59 | +```json | ||
| 60 | +{ "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", | ||
| 61 | + "produces": ["log"], "needs": ["artifact"] } | ||
| 62 | +``` | ||
| 63 | + | ||
| 64 | +即:`kind=log, role=produces`(test 自己产出的是 log)+ `kind=artifact, role=needs` | ||
| 65 | +(validator 要吃的是 artifact)。这两条都落进同一张 `graph_edge_contracts` 表, | ||
| 66 | +`required=1`。 | ||
| 67 | + | ||
| 68 | +问题出在 `work-graph-db.js:2542` 的 `inboundContracts()`: | ||
| 69 | + | ||
| 70 | +```js | ||
| 71 | +function inboundContracts(edgeLayer, edges, defNodeId) { | ||
| 72 | + const inbound = (edges || []).filter( | ||
| 73 | + (e) => String(e.to) === String(defNodeId) && (e.edgeKind || 'forward') !== 'backward' | ||
| 74 | + ); | ||
| 75 | + const out = []; | ||
| 76 | + for (const e of inbound) { | ||
| 77 | + for (const c of edgeContracts(edgeLayer, e.id)) out.push({ ...c, fromNodeId: e.from }); | ||
| 78 | + } | ||
| 79 | + return out; | ||
| 80 | +} | ||
| 81 | +``` | ||
| 82 | + | ||
| 83 | +它把入边上**所有**契约行原样吐出来,不管 `role` 是 `produces` 还是 `needs`。 | ||
| 84 | +下游 `graph-engine.js:3945` 的 `requiredInputsMissing()` 接着只按 `required` 过滤: | ||
| 85 | + | ||
| 86 | +```js | ||
| 87 | +const requiredKinds = [...new Set(contracts.filter((c) => c.required).map((c) => c.kind))]; | ||
| 88 | +``` | ||
| 89 | + | ||
| 90 | +于是 `validator` 的「必须在索引里找到实例」的 kind 集合变成了 `{log, artifact}`, | ||
| 91 | +而不是本该只有的 `{artifact}`——`log` 是 test 声明**自己产出**的 kind(描述 test 的 | ||
| 92 | +输出形态),不是 validator 要读的输入。这个 task 的 `graph_files` 里从来没有任何 | ||
| 93 | +`kind='log'` 的行(`org-coder-test` 实际写的是 `coder.log` 但被索引成 | ||
| 94 | +`kind='artifact'`,不是 `kind='log'`),于是 `requiredKinds - haveKinds = ['log']`, | ||
| 95 | +`missing.length > 0`,success 被强改成 fail。 | ||
| 96 | + | ||
| 97 | +**对照**:同一份代码里至少三处其他函数在读同一张 `graph_edge_contracts` 表时都记得 | ||
| 98 | +按 `role` 过滤(`(c.role || 'produces') !== 'needs'` 或 `=== 'produces'`, | ||
| 99 | +见 `graph-engine.js:968` / `:3811` / `:3866`),唯独 `inboundContracts` / | ||
| 100 | +`requiredInputsMissing` 这一条链路漏了。判据不一致,不是设计如此。 | ||
| 101 | + | ||
| 102 | +### 2.4 影响面推测(未逐一核实,留给修的时候一并查) | ||
| 103 | + | ||
| 104 | +- 任何在同一条边上**同时声明了 `produces` 和 `needs`** 的节点,只要 `produces` | ||
| 105 | + 的那个 kind 从未被单独索引过(大多数「描述性」kind,比如这里的 `log`,本来就不 | ||
| 106 | + 一定会被单独记录成同名 kind 的产物行),下游节点的 success 就会被这条误判的 | ||
| 107 | + `missing_required_input` 拦下来,且**每次重试都会复现**(不是一次性的)。 | ||
| 108 | +- `executor.json` / `executor-fanout.json` / `validator.json` 里凡是 `test→validator` | ||
| 109 | + 这种「同边混写 produces+needs」的写法都要复查一遍(§一 T3/T4 审计脚本可以扩展 | ||
| 110 | + 一个「edge 同时有 produces 与 needs 时,produces 的 kind 是否真的会被索引」的检查项)。 | ||
| 111 | + | ||
| 112 | +### 2.5 建议修法(留给实施时定案,这里只记方向) | ||
| 113 | + | ||
| 114 | +`inboundContracts()` 应该只返回 `role='needs'`(含历史 NULL 按 `produces` 处理的 | ||
| 115 | +存量行需要保留原有兼容语义,但 NULL 不该被当成 `needs`)的契约行,或者 | ||
| 116 | +`requiredInputsMissing()` 在 `contracts.filter(...)` 那一步同时按 | ||
| 117 | +`(c.role || 'produces') === 'needs'` 过滤——两者选一,不建议两处都改(避免出现 | ||
| 118 | +「谁的语义才是权威」的第二份判据)。改完要跑一遍 §一 提到的真实回归任务,确认 | ||
| 119 | +`org-coder-validator` 不再假阳性 blocked。 | ||
| 120 | + | ||
| 121 | +--- | ||
| 122 | + | ||
| 123 | +## 三、下一步 | ||
| 124 | + | ||
| 125 | +- [ ] 定案 §2.5 的修法,改 `inboundContracts` 或 `requiredInputsMissing` 之一 | ||
| 126 | +- [ ] 新增单测:`test→validator` 同边声明 `produces:log` + `needs:artifact`, | ||
| 127 | + 只有 `artifact` 有索引实例、`log` 没有,validator 的 success 判定不应被拦 | ||
| 128 | +- [ ] 跑一遍 `graph-real-chain-smoke.test.js` 同款「真实引擎代码 + 真实 DB + 换掉最外层 | ||
| 129 | + CLI 子进程」的确定性回归,复现本次撞见的具体场景 | ||
| 130 | +- [ ] 复查 `system/work-graph-templates/*.json` 里其他「同边混写 produces+needs」的边 |