diff --git a/docs/README.md b/docs/README.md index 1f7060c..bdb2650 100644 --- a/docs/README.md +++ b/docs/README.md @@ -48,6 +48,8 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) | | [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) | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据 | +| [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 次,零人工介入),修复确认生效 | +| [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 | ## todo/future/(暂缓,非当前重心) diff --git a/docs/todo/system-design/31-work-graph-fail-fallback-coverage.md b/docs/todo/system-design/31-work-graph-fail-fallback-coverage.md index 94769cb..313e58a 100644 --- a/docs/todo/system-design/31-work-graph-fail-fallback-coverage.md +++ b/docs/todo/system-design/31-work-graph-fail-fallback-coverage.md @@ -7,6 +7,15 @@ > 改成用真实引擎代码 + 真实 DB + 只换掉最外层 Claude CLI 子进程的确定性回归测试代替—— > 比一次性真跑更严格(可重复验证),且零真实 LLM 花费。 > **触发**: `e2e-demo` 公司真实任务 `T-mt1qx0c4-95bf`(goal `G-mt1qwgt5-6b77`)卡住排障,见 §一 +> +> **2026-08-21 追加:真实 E2E 回归验证补上了**。§六「尚未做」的那一步(真跑证明「下次系统自己会 +> 补这条边」)当时因为原任务状态不干净作罢,改用确定性测试代替(§7.3)。同一天晚些时候重启服务 +> 加载了本次修复后,起了一条新任务(goal `G-mt2r62ia-73af` / task `T-mt2r6fz0-125b` / root run +> `run_0e9074815130`)走真实 LLM:`org-planner-sdd` 真实自判失败,沿新补的兜底边**自动**回退 3 次 +> (零 `retryNode` 调用),最终 planner 子图收敛到 `completed`。**修复确认生效,有真实证据**。 +> 同一次验证顺带在 `org-coder` 子图撞见一个**独立的新 bug**(`inboundContracts` 未按 `role` +> 过滤,把 `produces` 声明误当 `needs`,导致 `missing_required_input` 假阳性),与本文档无关, +> 已记录在 [33-needs-gate-role-filter-bug.md](./33-needs-gate-role-filter-bug.md)。 --- diff --git a/docs/todo/system-design/33-needs-gate-role-filter-bug.md b/docs/todo/system-design/33-needs-gate-role-filter-bug.md new file mode 100644 index 0000000..918efc2 --- /dev/null +++ b/docs/todo/system-design/33-needs-gate-role-filter-bug.md @@ -0,0 +1,130 @@ +# 33-needs-gate-role-filter-bug. `requiredInputsMissing` 未按 role 过滤 —— produces 声明被误判成 needs + +> **日期**: 2026-08-21 +> **状态**: 🔴 **待修**(发现于 31 号文档修复的真实 E2E 回归验证过程中,是一个独立的新 bug, +> 与 31 号文档的兜底边问题无关) +> **触发**: `e2e-demo` 公司真实回归任务 `T-mt2r6fz0-125b`(goal `G-mt2r62ia-73af`, +> root run `run_0e9074815130`)——为验证 [31 号文档](./31-work-graph-fail-fallback-coverage.md) +> 的兜底边修复而起的新任务,链路走到 `org-coder-validator` 时意外反复 blocked + +--- + +## 一、先说 31 号文档的验证结果:修复生效,有真实证据 + +这条任务本身是为了拿到 31 号文档 §六「尚未做」的那一步真实验收证据(当时因为原任务 +`T-mt1qx0c4-95bf` 状态不干净,改用确定性回归测试代替,见 31 号文档 §7.3)。这次补上了: + +- 新起的 `org-planner` 子图(`run_38b3df8fd228`)里,`org-planner-sdd` 真实自判失败 + (下游信息不足),沿新补的兜底边(`org-planner-sdd --fail--> org-planner-prd`, + edge `wge_ce0f7f05f6dc`)**自动回退了 3 次**(`graph_run_loop_records`: + `verdict=registered`,`loop_count` 1→2→3),全程**零次 `retryNode` 调用**,最终 + brd→prd→sdd→benchmark→validator 整条链路收敛到 `run_status=completed`。 +- 31 号文档的决策 A(除入口节点外强制兜底边)在真实 LLM 驱动下按设计工作,**结论确认**。 + +--- + +## 二、新发现:`org-coder-validator` 被 `missing_required_input` 反复误判 + +### 2.1 现象 + +同一条任务往下走到 `org-coder`(`run_21fd814c2f56`,用的是 `executor.json` 骨架)时, +`org-coder-validator` 连续两轮都以 `fail_reason=missing_required_input` 收场: + +- 第一轮(attempt 1,10:01-10:11):沿兜底边 `validator --fail--> impl` 回退两次 + (`wge_1bdb181b14ef` 一次、`wge_aa5bad61fe9a` 一次),第三次撞上 run 树级回退硬顶 + (`backward_count` 全树累计 >5,`LOOP_HARD_CAP_RUN=5`),`force_terminated`, + 节点 `blocked`。**这一段是硬顶熔断本身在正常工作**,不是这次要报的 bug。 +- 人工 `POST /nodes/:id/retry` 把 `org-coder-validator` 打回 `ready` 后, + **第二轮**(attempt 2,10:42:48-10:44:01)**又**判了一次 `missing_required_input`, + 这次连新的 `loop_record` 都没写(回退硬顶已越顶,直接静默 blocked)。 +- 节点自己的 `remediationNote` 写的是「无需整改。……本轮重新独立跑了一遍测试脚本与 + goal-contract,结果与 attempt-1 完全一致,判定 attempt-1 的 passed 结论有效」—— + **agent 自己认为通过了**,是引擎的产出/输入契约闸门(T-mt25ecbd 那次加的机制, + commit `aecf5c9`)把 success 强行改判成 fail。 + +### 2.2 复现证据:`graph_files` 里明明有 `artifact` 却仍报缺 + +查 `T-mt2r6fz0-125b` 的产物索引(`data/graph.sqlite` 的 `graph_files` 表), +`org-coder-impl`(`rn_e71be3641174`,10:06:51 完成)与 `org-coder-test` +(`rn_cd3e3a1c51e2`,10:08:52 完成)都成功登记了当前(`superseded_by IS NULL`) +`kind='artifact'` 的产物行(`_plan.md` / `_research.md` / `coder.log`)。 +`org-coder-validator` 的 attempt 2 在 10:42:48 才跑,**比这些产物晚了半小时以上**, +不是时序竞争,是稳定复现。 + +### 2.3 根因:`inboundContracts` 不按 `role` 过滤,把 `produces` 声明也当成 `needs` + +`executor.json` 里 `test → validator` 这条边同时声明了两条契约(`work-graph-db.js` +`declareEdgeContract`): + +```json +{ "from": "{{orgNode}}-test", "to": "{{orgNode}}-validator", "condition": "success", + "produces": ["log"], "needs": ["artifact"] } +``` + +即:`kind=log, role=produces`(test 自己产出的是 log)+ `kind=artifact, role=needs` +(validator 要吃的是 artifact)。这两条都落进同一张 `graph_edge_contracts` 表, +`required=1`。 + +问题出在 `work-graph-db.js:2542` 的 `inboundContracts()`: + +```js +function inboundContracts(edgeLayer, edges, defNodeId) { + const inbound = (edges || []).filter( + (e) => String(e.to) === String(defNodeId) && (e.edgeKind || 'forward') !== 'backward' + ); + const out = []; + for (const e of inbound) { + for (const c of edgeContracts(edgeLayer, e.id)) out.push({ ...c, fromNodeId: e.from }); + } + return out; +} +``` + +它把入边上**所有**契约行原样吐出来,不管 `role` 是 `produces` 还是 `needs`。 +下游 `graph-engine.js:3945` 的 `requiredInputsMissing()` 接着只按 `required` 过滤: + +```js +const requiredKinds = [...new Set(contracts.filter((c) => c.required).map((c) => c.kind))]; +``` + +于是 `validator` 的「必须在索引里找到实例」的 kind 集合变成了 `{log, artifact}`, +而不是本该只有的 `{artifact}`——`log` 是 test 声明**自己产出**的 kind(描述 test 的 +输出形态),不是 validator 要读的输入。这个 task 的 `graph_files` 里从来没有任何 +`kind='log'` 的行(`org-coder-test` 实际写的是 `coder.log` 但被索引成 +`kind='artifact'`,不是 `kind='log'`),于是 `requiredKinds - haveKinds = ['log']`, +`missing.length > 0`,success 被强改成 fail。 + +**对照**:同一份代码里至少三处其他函数在读同一张 `graph_edge_contracts` 表时都记得 +按 `role` 过滤(`(c.role || 'produces') !== 'needs'` 或 `=== 'produces'`, +见 `graph-engine.js:968` / `:3811` / `:3866`),唯独 `inboundContracts` / +`requiredInputsMissing` 这一条链路漏了。判据不一致,不是设计如此。 + +### 2.4 影响面推测(未逐一核实,留给修的时候一并查) + +- 任何在同一条边上**同时声明了 `produces` 和 `needs`** 的节点,只要 `produces` + 的那个 kind 从未被单独索引过(大多数「描述性」kind,比如这里的 `log`,本来就不 + 一定会被单独记录成同名 kind 的产物行),下游节点的 success 就会被这条误判的 + `missing_required_input` 拦下来,且**每次重试都会复现**(不是一次性的)。 +- `executor.json` / `executor-fanout.json` / `validator.json` 里凡是 `test→validator` + 这种「同边混写 produces+needs」的写法都要复查一遍(§一 T3/T4 审计脚本可以扩展 + 一个「edge 同时有 produces 与 needs 时,produces 的 kind 是否真的会被索引」的检查项)。 + +### 2.5 建议修法(留给实施时定案,这里只记方向) + +`inboundContracts()` 应该只返回 `role='needs'`(含历史 NULL 按 `produces` 处理的 +存量行需要保留原有兼容语义,但 NULL 不该被当成 `needs`)的契约行,或者 +`requiredInputsMissing()` 在 `contracts.filter(...)` 那一步同时按 +`(c.role || 'produces') === 'needs'` 过滤——两者选一,不建议两处都改(避免出现 +「谁的语义才是权威」的第二份判据)。改完要跑一遍 §一 提到的真实回归任务,确认 +`org-coder-validator` 不再假阳性 blocked。 + +--- + +## 三、下一步 + +- [ ] 定案 §2.5 的修法,改 `inboundContracts` 或 `requiredInputsMissing` 之一 +- [ ] 新增单测:`test→validator` 同边声明 `produces:log` + `needs:artifact`, + 只有 `artifact` 有索引实例、`log` 没有,validator 的 success 判定不应被拦 +- [ ] 跑一遍 `graph-real-chain-smoke.test.js` 同款「真实引擎代码 + 真实 DB + 换掉最外层 + CLI 子进程」的确定性回归,复现本次撞见的具体场景 +- [ ] 复查 `system/work-graph-templates/*.json` 里其他「同边混写 produces+needs」的边