33-needs-gate-role-filter-bug. requiredInputsMissing 未按 role 过滤 —— produces 声明被误判成 needs
日期: 2026-08-21 状态: 🔴 待修(发现于 31 号文档修复的真实 E2E 回归验证过程中,是一个独立的新 bug, 与 31 号文档的兜底边问题无关) 触发:
e2e-demo公司真实回归任务T-mt2r6fz0-125b(goalG-mt2r62ia-73af, root runrun_0e9074815130)——为验证 31 号文档 的兜底边修复而起的新任务,链路走到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, edgewge_ce0f7f05f6dc)自动回退了 3 次(graph_run_loop_records:verdict=registered,loop_count1→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 那次加的机制, commitaecf5c9)把 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):
{ "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():
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 过滤:
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」的边