diff --git a/docs/README.md b/docs/README.md index 6010c7e..b8895bf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ > **当前主线**:Graph Agent 架构重构([27-graph-agent-architecture.md](./todo/system-design/27-graph-agent-architecture.md))。 > 该系列已完成的阶段文档(P1 / P2 / **P3 收尾 P3C**)移入 `done/`,其余非主线待办在 `todo/future/`。 -> **当前未完成的是**:P3B 真跑跑道的几条验收、P3/P4 那笔「真实路径 smoke 进 CI」的欠账、P4(未实现)、28-followups。 +> **当前未完成的是**:28-followups。P3B 九条验收已于 2026-09-08 全部验完;P4 已于 2026-09-08 收口;P3/P4「真实路径 smoke 进 CI」的欠账已基本还清,只剩真 LLM(本质限制)与按需 hire 未覆盖。 ## done/ @@ -45,6 +45,8 @@ | [system-design/45-graph-run-node-liveness-recovery.md](./done/system-design/45-graph-run-node-liveness-recovery.md) | ✅ **Part A/B/C 已实现**(2026-08-31),**Part D 有意延后**。真实事故:h3video 公司一个 `agent_call` 节点卡在 `status=running`、背后 CLI 进程已死,服务器主进程没重启——心跳(纯 LLM 驱动)、开机一次性孤儿扫描、goal/task 巡检(判据是 `blocked` 不是 `running`)、wallclock 看门狗(按预算不按存活)四层机制全盖不到;`ceo` 被转发问题后自己绕过引擎拉会话,陷入死循环。**Part A**:调查发现 `SessionPool` 是每次调用现造即回收的一次性对象,没有可查询的存活注册表,原设计不可行,改用「运行时长超阈值(默认 25 分钟)+ 只扫 `agent_call` 节点」,新函数 `sweepStaleRunningAgentCalls`/`startStaleAgentCallWatchdog`(`graph-engine.js`),5 条新单测。**Part B**:`force-fail` 端点(42 号文档已实现)本身就够用,零新后端代码,只在 `ceo/tools.md` 补了使用文档 + 明确禁止绕过引擎自拉会话。**Part C**:`ceo/heartbeat.md` 同步更新。**Part D**(节点补显式心跳字段,消除 Part A 阈值判据的误伤局限)未做,见文内「实现记录」的理由 | | [system-design/47-itemized-subtask-materialization-goal-benchmark-fallback.md](./done/system-design/47-itemized-subtask-materialization-goal-benchmark-fallback.md) | ✅ **已修复**(2026-09-02)。46 号文档 Phase 0 真实回归坐实:`acceptance.md` 在当前 `workgraph-planning` 常规划出的 org-planner 子图形状(prd/sdd/goal-benchmark/planner-validator)下结构性产不出来,`itemized` 子任务物化 0 命中。修法:物化逻辑新增兜底,`acceptance.md` 缺失时改读已被 44 号文档证实稳定产出的 `goal-benchmark.json` 的 `dimensions[]`。`task-decomposition-materialize.test.js` 5 条全过(2 条新增,用真实回归捕获的产物做测试数据) | | [system-design/48-task-acceptance-contract-binding.md](./done/system-design/48-task-acceptance-contract-binding.md) | ✅ **已实现并完成真实端到端回归,已收口**(decomposed_work 链路 2026-09-02/回归补于 2026-09-03,剩余三条路径调研收口于 2026-09-03)。承接 46 号文档 Phase 3 v2 真实回归发现的 `org-validator` 死锁:验收契约的产出被硬绑在"公司完整 org graph 一定会跑到 org-planner"这个假设上,不满足这个假设的执行路径(如轻量子任务图)无契约可读、退无可退。方案:消费端(validator 只认结构化契约、不自由判断)保持硬约束不变,产出端改成"每个 decomposed_work 子任务创建时必须 1:1 带一份验收契约"(`tasks.acceptance_contract` + `createTask` 统一校验 + `enqueueTaskRun` 落盘),真实 `org-coder`→`org-validator` 全程跑通验证。**收口时追加调研**:`task-planning`/`org-planner` 的 `acceptance_item` 物化/人类快速派单三条创建路径经核实**不需要**同款强绑定——都走完整 org graph,`workgraph-planning` 的 `benchmark` 节点"不可裁"约定已天然保证有契约,强行加会跟 46 号文档保护的 `task-planning` 极简边界、快速派单"零 LLM"定位冲突。真正残留风险是"org graph 一定含 org-planner"目前只是提示词约定非代码强制,属于 org 拓扑保证问题,不在本文档范围。安全网(发现时就地补契约)按设计仍只留原则,`decomposed_work` 链路已被主力机制完全覆盖,不为不可达场景写代码 | +| [system-design/27-p4-parallel-and-artifacts.md](./done/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — ✅ **2026-09-08 已彻底收口**,移入 `done/`(阶段 A/B/C 全部完成,含本轮补齐的同 agent 多实例隔离生产接线 + `advanceTree` 兄弟子 run 并发)。T5 账户欠费门本身已实现(可插拔桩子),只差接一个真实计费接口——用户拍板暂缓,见 `todo/future/` | +| [system-design/27-p3b-real-run-runway.md](./done/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— ✅ **2026-09-08 全部收口**,移入 `done/`。§六「九条验收」全部验完(验收 9 见 §六quater:2 节点 org 图故意跳过 org-coder,`org-validator` 真实读项目根、四维度全判 0 分,engine 真实沿 org 层 fail 边把 `org-planner` 重置重跑,15 次真调用/218,887 tokens);正文里 `W5/W6/W7/W8/W9` 等更细粒度小项也已逐条核对——大半此前就被后续真跑验过、只是没回填,真正之前从未验过的只有 W8(重新规划组织按钮),本轮用隔离 worktree + 真 Claude CLI + 真实浏览器点击补验(详见「W8 浏览器真跑验收」,该节末尾记了一个真跑中顺手发现、尚待排查但不影响验收本身成立的前端细节:`insertNode` 重规划成同名角色时复用同一行 id,与文档里"每次换新 id"的旧设计假设不一致)。⛔/🟡 的历史段落读之前先看头部状态行 | | [system-design/46-mandatory-planning-and-task-decomposition.md](./done/system-design/46-mandatory-planning-and-task-decomposition.md) | ✅ **已收口**(2026-09-07)。Phase 0-5 全部实现。**Phase 1**:新 agent `subtask-planning`,钩在 `enqueueTaskRun`(显式 opt-in),产物写共享 `artifacts/subtask-plan.json`。**Phase 2**:经验检索 + 按需 deep research(默认关闭)。**Phase 3 v2**:CEO 心跳直接发现+派发(push),子任务走轻量 work graph(`org-coder`+`org-validator` 两节点)。**Phase 4**:父任务被拆分后让位给子任务(新状态 `decomposed`)、预算按步数均分、父子终态汇总(`settleParentTaskIfAllChildrenDone`)、同父子任务独立并发坑位(上限 3,不占公司级槽位)——四项机制全部用真实数据验证过,包括最后补跑的父子终态汇总(两个真实子任务全部 `completed` 后父任务被正确自动结算成 `completed`)与第二次并发确认。过程中撞出并修复一个几乎必现的真缺陷(`markTaskDecomposed` 原本只认父任务还是 `queued`,但 task-scheduler 抢坑是毫秒级、`subtask-planning` 是数十秒的真实 LLM 调用,等它拆完时父任务几乎必然已经 `running`,接管必然失败)。**Phase 5**:看板加 `decomposed` 状态样式 + 子任务进度展示,父子树形复用已有通用逻辑,未改后端接口。回归过程中还牵出一个独立缺陷——`org-validator` 对同一种 benchmark 契约判断不稳定(5 次里 1 次误把源码路径当 `--target` 判 `target_not_json`,反手倒退归因给 `subtask-planning`)——已定位根因(`decomposed_work` 轻量图当时只有一条前进边,没有失败边,validator 判 fail 后无路可退)并修复(补 `validate --fail--> impl` 退回边,复用引擎已验证过的 `backward_count` 双层硬顶 + `remediation_note` 反思闭环,新增 3 条单测),详见文末补跑记录 | ## todo/(当前主线) @@ -58,16 +60,14 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | [27-naming-conventions.md](./todo/system-design/27-naming-conventions.md) | **命名规范**。`{作用域}-{职能}`;图节点按层级轴命名;节点名 ≠ 本体名;三种 benchmark 消歧;OKR 正名。新增 agent id / 表名 / 字段名前必读 | | [27-e2e-task-execution-flow.md](./todo/system-design/27-e2e-task-execution-flow.md) | 端到端流程总览:建公司 → 下达任务 → org graph 逐节点执行 → work graph 规划与循环 → 验收收敛。含与 DevLoop 既有实现的对应关系分析 | | [27-example-dev-company-walkthrough.md](./todo/system-design/27-example-dev-company-walkthrough.md) | 参考场景走查(虚构的「配图工厂」),把架构跑一遍具体的数:各阶段 JSON 形态、成本估算、断点清单 | -| [27-p3-p4-work-graph-console.md](./todo/system-design/27-p3-p4-work-graph-console.md) | P3+P4(原 P4 回退+Loop-Validator 已并入)图执行引擎与执行控制台 — ✅ 已实现(`c3abf4a`)。**仍留 todo 的原因**:§十 末「四条硬规矩」第 2 条那笔欠账**已还大半** —— `graph-real-chain-smoke.test.js`(零注入、只换掉真 CLI 子进程)已进 CI,`agent_call` 与 `subgraph` 两支都覆盖。剩下的是真 LLM(进不了 CI)、多节点 / 回退边 / 熔断的形状、以及按需 hire 那一段 | -| [27-p3b-real-run-runway.md](./todo/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— 📝 **只剩验收 9**。2026-08-20 第四次真跑(§六ter)验掉 2 与 8,验收 4 由零注入链路验掉,7 早已修好。验收 9 的准确状态是「产出端通了、消费端没等到那个形状」—— 需要一次 **org 层回退边指向 subgraph 节点**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 | +| [27-p3-p4-work-graph-console.md](./todo/system-design/27-p3-p4-work-graph-console.md) | P3+P4(原 P4 回退+Loop-Validator 已并入)图执行引擎与执行控制台 — ✅ 已实现(`c3abf4a`)。**仍留 todo 的原因**:§十 末「四条硬规矩」第 2 条那笔欠账**已基本还清**(2026-09-08)——`graph-real-chain-smoke.test.js`(零注入、只换掉真 CLI 子进程)已进 CI,覆盖 `agent_call`/`subgraph` 两支 + 多节点/回退边/双层熔断三种形状。剩下的只有真 LLM(本质限制,进不了 CI)与按需 hire 那一段未走过这条链路 | | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) | -| [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 | | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) | | [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据。2026-08-23 追加真实实例并已修:`infra_flake` 原地重跑没有熔断线,真撞上过一次确定性死循环(节点把诊断结论误判成自己的执行结果,反复重跑却从不路由到诊断建议的目标节点);已加 `infra_flake_count` 列 + 硬顶 3 次熔断,连续判 infra_flake 超过硬顶就改判 blocked 交给人工 | | [41-graph-vs-fde-reliability-discussion.md](./todo/system-design/41-graph-vs-fde-reliability-discussion.md) | 📝 **讨论记录 + 落地实测,非设计方案**。org/work graph 与 master-Innovator-FDE 两套工作流的稳定性对比:根因是"拓扑运行时生成 vs 写死"。问题 7 是真实执行记录——新建 `org-hunt/digest/evolve`(graph 原生协议,方法论抄 innovator-hunt/digest/evolver)跑 Hunt→Digest→Evolve,11 轮真跑中挖出并修复 5 个独立缺陷(建公司自动规划竞态、work-graph 模板库缺内容型交付物骨架、验收节点任务描述越权、两个规划 agent 的路径口径写错、R-19 产出索引 `indexNodeOutputs()` 不覆盖项目根——最后一条改了 `graph-engine.js` 核心逻辑,加 `contract.json` 的 `produces` 显式声明机制,全局生效,补了 2 条守卫测试)。最终产出与原生 FDE(`prd-methodology-trial`)对比:内容骨架可迁移、质量不打折,但过程严谨度不如原生 FDE,可靠性差距悬殊(原生一次跑通,graph 版 11 轮才稳定)。`routeFailure` 的 `infra_flake` 分支绕过跨层寻址(`target_node`)这个口子已按最保守方案补上(2026-08-31,见文内清单)——但堵不住 30 号文档记录的那次真实事故,那次事故点名的目标是下游节点,图模型本身就不允许 backward 边指向非祖先,属于修复范围之外 | | [37-org-graph-department-scoping.md](./todo/system-design/37-org-graph-department-scoping.md) | 📝 **设计讨论,调用点扫描已完成,尚未立项实现**。现状 org graph 按 `company_id` 单例(`27-P2` 标题原文即"公司级"),塞不下 HR/财务/市场这类跟"交付流水线"无关的公司常设职能——`system/agents/hr/`("招·留·管·退")本体已存在却从没被装进任何图,全靠 fire-and-forget chat 招聘、无图执行审计轨迹。提议把 org graph 作用域从 `company_id` 升级为 `(company_id, department_id)`,一家公司可装配多张部门图,每张仍保持人工固定/慢变/可审计。全仓库调用点扫描结论:改造面是最初估算的 4 倍——真正的单点真相在 `work-graph-db.js:readDefGraph` 的裸 SQL 不变式(不在服务层),还牵涉两个易混淆服务文件间必须字节级一致的重复唯一索引、独立的 `graph-seed-service.js` 对账子系统、`company-hire-org-layer.test.js` 专属测试、`orggraph-planning`/`task-planning` 两份 agent 人设文档的自我认知改写 | | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 | -| [49-antigravity-inspired-parallel-verification-proposal.md](./todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md) | 📝 **设计讨论,不代表已拍板**(2026-09-04)。对照 Google Antigravity「Teamwork」多智能体框架博客文章,给 27 号 P4「真并行」与 34/35 号「验证严谨度不稳定」两个议题出的方案草稿。**先纠正一个前提**:重读 27-P4 全文后发现阶段 A/B/C 早已全部实现(又一次「文档落后于代码」),真并行现在只剩 T2/role 列/`advanceTree` 兄弟子 run 并发三件小事,可独立收口。**新提案 A**:借鉴长证明模式的「竞争候选+对抗批评+综合」,区别于现有 `executor-fanout.json` 的分解型 fan-out——需要先有真实场景验证价值,不建议凭空立项,详细设计见 [50 号文档](./todo/system-design/50-competing-candidates-critic-synthesis-design.md)。**新提案 B**:把 35 号提案 D「浏览器关键词命中才要证据」泛化成 benchmark 段全维度默认要求执行级证据(**唯一建议现在推进的部分**);实地核实过把验收路径引导到 `test_script` 段的工作量后**结论是不做**——撞上 `test_script.path` 解析根不含 `org-coder` 实际写代码的项目根这个结构性冲突,改造面远超预期;新增「陷阱注册表」,把 validator 判卷失败的结构化原因喂回 Dream 机制,落点已定案为独立文件 `known_pitfalls.md`(不复用 `memory.md`,避免语义混淆)。建议推进顺序:陷阱注册表最低成本先做,证据泛化次之,竞争候选需要真实场景才立项 | +| [49-antigravity-inspired-parallel-verification-proposal.md](./todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md) | 📝 **设计讨论,不代表已拍板**(2026-09-04)。对照 Google Antigravity「Teamwork」多智能体框架博客文章,给 27 号 P4「真并行」与 34/35 号「验证严谨度不稳定」两个议题出的方案草稿。**先纠正一个前提**:重读 27-P4 全文后发现阶段 A/B/C 早已全部实现(又一次「文档落后于代码」),当时判断真并行只剩 T2/role 列/`advanceTree` 兄弟子 run 并发三件小事——2026-09-08 收口时又发现这个判断本身有两处误判(role 列其实早就落库、T1 的✅遗漏了 scope 参数的生产接线),三件小事(连同两处误判一起)已全部核实/补齐,见 [27-P4](./done/system-design/27-p4-parallel-and-artifacts.md)。**新提案 A**:借鉴长证明模式的「竞争候选+对抗批评+综合」,区别于现有 `executor-fanout.json` 的分解型 fan-out——需要先有真实场景验证价值,不建议凭空立项,详细设计见 [50 号文档](./todo/system-design/50-competing-candidates-critic-synthesis-design.md)。**新提案 B**:把 35 号提案 D「浏览器关键词命中才要证据」泛化成 benchmark 段全维度默认要求执行级证据(**唯一建议现在推进的部分**);实地核实过把验收路径引导到 `test_script` 段的工作量后**结论是不做**——撞上 `test_script.path` 解析根不含 `org-coder` 实际写代码的项目根这个结构性冲突,改造面远超预期;新增「陷阱注册表」,把 validator 判卷失败的结构化原因喂回 Dream 机制,落点已定案为独立文件 `known_pitfalls.md`(不复用 `memory.md`,避免语义混淆)。建议推进顺序:陷阱注册表最低成本先做,证据泛化次之,竞争候选需要真实场景才立项 | | [50-competing-candidates-critic-synthesis-design.md](./todo/system-design/50-competing-candidates-critic-synthesis-design.md) | 📝 **设计草稿,不代表已拍板**(2026-09-04)。49 号文档提案 A 的展开:竞争候选骨架 JSON 形状草稿(`candidate-council.json`,未定案)、命名(按 27-NAMING 节点带前缀/本体不带前缀两轴)、`workgraph-planning` 选型判据方向。**关键设计约束**:critic(评判)与 synthesis(择优/融合)必须拆成两个节点,依据是仓库真实事故——`qa-judge` 早期"评判+终态判决合一"设计导致过一次无限循环(20 次调用/31 分钟才熔断,记录在 `deliverable.json` 的反模式说明里),新骨架延续 `validator-triager`/`benchmark-validator` 已验证的"归因/打分/判定三者分离"范式。**预算风险量化**:N 路候选并行消耗 N 倍预算,且 `checkBudget` 只在批量拾取前查一次,实际可能超支到 `maxConcurrency-1`(默认最多多花 4 次调用)才被拦下,候选数 N 目前代码层没有硬顶 | ## todo/future/(暂缓,非当前重心) @@ -77,6 +77,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | [architecture_todo_list.md](./todo/future/architecture_todo_list.md) | FDE Pipeline 架构待办清单(Judge 系统 / 反馈闭环等五大维度)。与 Graph Agent 架构基本正交,其中第 4 点「版本回滚」与 27 号文档 P5 阶段的 git 版本化 `memory.md` 有部分重叠,P5 落地后可回来划掉 | | [system-design/14-milestones.md](./todo/future/system-design/14-milestones.md) | 早期里程碑规划(v1.1,checklist 未勾选)。已被后续实际实现大幅绕过,主要留作历史存档,不建议优先处理 | | [system-design/25-harness-engineering-improvements.md](./todo/future/system-design/25-harness-engineering-improvements.md) | Harness Engineering 改进提案(单 Agent 上下文管理),Phase 3-5 未落地。与 Graph Agent 架构(多 Agent 组织/图执行)维度不同,互不阻塞 | +| 27-P4 T5:账户欠费门接真实计费接口 | 桩子已实现并上线(`setBillingGate` 注入模式,`graph_runs.block_reason='billing-arrears'` + 自动恢复触发器 `resumeBillingClearedRuns()`,见 [27-P4](./done/system-design/27-p4-parallel-and-artifacts.md) §五 T5 记录),只差接一个真实计费系统的余额/欠费查询接口——2026-09-08 用户拍板暂缓,非本仓库能单方推进 | ## 未纳入整理的目录 diff --git a/docs/todo/system-design/27-p3b-real-run-runway.md b/docs/done/system-design/27-p3b-real-run-runway.md index 416deb7..80ae31a 100644 --- a/docs/todo/system-design/27-p3b-real-run-runway.md +++ b/docs/done/system-design/27-p3b-real-run-runway.md @@ -5,13 +5,20 @@ > **日期**: 2026-08-16 > **起因**: [27-REQ](./27-requirement.md) R-18(招聘归 `workgraph-planning`;停用 mock 改真 CLI) > **前置**: P3 主体已实现并提交(`c3abf4a` 起七轮),测试 719/719 全绿 -> **状态**(2026-08-20 校准): 📝 **仍在推进**。第一批验收 1/3 已验、2 的后半与 4 未在干净环境重验; -> 第二批验收 7 ✅ **已修**(`58a2865`「W7 零边图」,守卫 `company-hire-org-layer.test.js` 的 -> `CO_ZEROEDGE` + `W7-fix`)—— 下面 §六bis 缺陷③ 那段是**当时的现场记录**,不是当前状态; -> 验收 2 ✅、验收 8 ✅ 由 2026-08-20 的真跑验掉,验收 4 ✅ 由零注入链路验掉, -> **验收 9 仍开**(产出端已通、消费端没等到那个形状)—— 逐条证据见 §六ter。 +> **状态**(2026-09-08 校准): ✅ **本文档全部收口**。§六「九条验收」1-9 全部验完;正文里散落的 +> `W5/W6/W7/W8/W9` 等更细粒度小项「仍未验」备注也已逐条核对——大半(W5/W6/W7/W9)本来就已经被 +> 后续的真跑(§六bis/§六ter)验过,只是这份文档没有回填,本轮补齐了引用;真正之前从未验过的只有 +> W8(重新规划组织按钮),本轮用隔离 worktree 起真 Claude CLI + 真实浏览器点击补验,见「W8 浏览器 +> 真跑验收」一节(该节末尾记了一个真跑中顺手发现、尚待排查但不影响验收本身成立的前端细节)。 +> 第一批验收 1/3 已验、2 由两次独立真跑(§六ter / §六quater)在干净环境下验掉; +> 验收 4 ✅ 由零注入真实链路验掉(不需要真 Claude CLI,见该行说明); +> 验收 7 ✅ **已修**(`58a2865`「W7 零边图」,守卫 `company-hire-org-layer.test.js` 的 +> `CO_ZEROEDGE` + `W7-fix`,§六ter 复验 + 2026-09-08 浏览器肉眼确认)—— 下面 §六bis 缺陷③ +> 那段是**当时的现场记录**,不是当前状态; +> 验收 8 ✅ 由 2026-08-20 的真跑验掉, +> **验收 9 ✅ 已验**(2026-09-08,隔离 worktree 真跑)——逐条证据见 §六quater。 > §六bis 缺陷② 的**根因**(边界守卫误判并行会话的改动)已于 2026-08-20 修掉, -> 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md) —— 但**根因修了不等于验收点验了**,那几条仍要重跑一遍。 +> 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md)。 --- @@ -417,10 +424,10 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛 |---|---|---|---| | **W5** ✅ 真跑已验(§六bis) | `workgraph-planning` 模板改语义:撤掉「你不招人也不辞人」,加招聘步骤,`tools.md` 给 `company hire` 用法 | `system/templates/workgraph-planning/**` | 小 | | **W6** ✅ 真跑已验(§六bis:先雇后提交,图未外键挂) | 写死顺序:**先雇、再提交图**(反了必外键挂);雇失败的重试与降级 | 同上 | 小 | -| **W7** ⚠️ 部分(真跑打脸:零边最小图上判据全空,见 §六bis 缺陷③) | **work 层招聘不得污染 org graph**:`hire()` 现在会把任何被雇 agent upsert 成一行 `org_graph_nodes`。方案 a 下每次任务规划都雇人,org graph 会被越塞越满(界面走查已实际撞到)。让这类招聘不写 org 节点,或写了打标区分 | `server/services/company-service.js`、`server/services/graph-service.js` | 中 | -| **W8** ✅ UI / 🟡 mock | 控制台加「重新规划组织」按钮,调 `POST /:companyId/org-graph/replan`(R-18.1)。该端点已实现,全仓库无前端调用方 | `web/src/views/GraphConsole.vue` 或 `CompanyDashboard.vue` | 小 | +| ~~**W7**~~ ✅ 已修并真跑复验(`58a2865`「W7 零边图」;§六ter 验收 7 / 2026-09-08 浏览器真跑肉眼确认,见下方「W8 浏览器真跑验收」第 2 点) | **work 层招聘不得污染 org graph**:`hire()` 现在会把任何被雇 agent upsert 成一行 `org_graph_nodes`。方案 a 下每次任务规划都雇人,org graph 会被越塞越满(界面走查已实际撞到)。让这类招聘不写 org 节点,或写了打标区分 | `server/services/company-service.js`、`server/services/graph-service.js` | 中 | +| **W8** ✅ 真 CLI + 真实浏览器已验(2026-09-08,见下方「W8 浏览器真跑验收」) | 控制台加「重新规划组织」按钮,调 `POST /:companyId/org-graph/replan`(R-18.1)。该端点已实现,全仓库无前端调用方 | `web/src/views/GraphConsole.vue` 或 `CompanyDashboard.vue` | 小 | -#### W5 / W6 落地记录(模板层已改,真跑仍未验) +#### W5 / W6 落地记录(模板层已改;真 CLI 已验,见下方标注) 改的是 `system/templates/workgraph-planning/` 三份:`agent.md`(主体)、`tools.md`、`identity.md`。 @@ -450,7 +457,9 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛 —— 与 §三 第 6 条的断链分析完全一致,**「雇进公司」正是解法**。 另用 `PromptBuilder.build()` 实拼一次 `workgraph-planning` 的 system prompt(20.8k 字符), `company hire` / `--role` / `先雇` / 三个 `status` 取值 / `ls data/companies/` 全部命中。 -- **仍未验**:真 CLI 下 agent 是否照着做(mock 不读提示词,验不了)。归第三阶段。 +- ~~仍未验:真 CLI 下 agent 是否照着做~~ ✅ **已验**(§六bis「`workgraph-planning` 真跑规划 + 自雇 + (W5/W6)」):真 Claude CLI 下它确实先 `hire` 两个被引用的本体、再提交图,图提交没有外键挂—— + 这条写这一段时还没跑,后来第三次真 CLI 终验补上了,见 §六 验收标准第 6 条。 - **`--layer` 不是模板的入参**:模板明写「别自己发明这个参数」(`parseArgs` 严格模式,多给未知选项 会让整条命令报错)。work 层招聘写不写 org 节点由服务端判据决定,见 W7。 @@ -491,7 +500,10 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛 `org-graph-service.getGraph()` 读回,即 GET org-graph 的同一条读路径)。三条判据各做过变异验证 (分别短路掉 → 红 3 / 1 / 3 例)。每条「不入图」都配了一条只换一个条件的正例, 免得测试退化成「这个角色被整体拉黑」。 -- **仍未验**:真 CLI 下 `workgraph-planning` 雇完一轮后控制台左栏的实况(验收标准 7),归第三阶段。 +- **这条备注要拆成两半看**:验收标准 7 本身(org graph 数据层不混进 work 层本体)已经真 CLI 验过 + (§六ter;另有 `company-hire-org-layer.test.js` 9 例守卫),**数据层的正确性不是悬而未决的**。 + 真正没验过的是**浏览器里点开控制台左栏、肉眼确认那一屏画的确实只有 org 节点**这个纯展示层 + 的检查——2026-09-08 一并补验,见下方「W8 浏览器真跑验收」。 #### W8 落地记录(按钮已有真实调用方;mock 演示仍差两环,两环都不在 UI 侧) @@ -533,11 +545,47 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛 本轮测试公司删掉后 `graph-seed-service.reconcileDanglingRows()` 一次收干净 (`orgNodesRemoved:3 / orgEdgesRemoved:4 / agentsRemoved:[org-planner, org-coder]`)。 +#### W8 浏览器真跑验收(2026-09-08,真 Claude CLI + 真实浏览器点击) + +**跑法**:隔离 git worktree(`.claude/worktrees/p3b-w8-browser-realrun`,跑完即删),后端 3011 / 前端 +5174(避开宿主机已在跑的 3010/5173),真实注册账号登录(此环境没有预置的 `admin/hex1234`,那是生产库 +才有的种子账号),`claude-in-chrome` 实点。 + +1. **建公司 → 自动规划**:`公司ID w8-verify-co`,创建后 `[Create] ✅ 已触发 w8-verify-co:orggraph-planning + 规划 org graph` 真起 Claude CLI,`招聘org-planner/org-coder/org-validator` 三条日志真实出现,`GET + org-graph` 读回三节点+两条 backward 边,形状与 `system/org-graph-templates/dev.json` 一致——这本身 + 就是验收标准 5 的「新建公司自动规划」那一半,此前从未真跑过。 +2. **控制台左栏(`CompanyDashboard` 的「组织流程」块)渲染正确**:刷新页面后画面上只有 + `org-planner`/`org-coder`/`org-validator` 三张卡片 + 两条回退边文字说明,没有混进任何 work 层本体 + ——这是验收标准 7 此前一直缺的**纯展示层**核实(数据层早已验过,见上方 §六 验收标准 7 备注)。 +3. **点击「重新规划组织」按钮**:服务端日志 `[Replan] ✅ 已触发 w8-verify-co:orggraph-planning 规划 org + graph`(与建公司时的 `[Create]` 是不同的日志前缀,确认点的是 replan 端点不是恰好又触发了一次建公司) + → 真起第二次 Claude CLI 会话(`Memory` 日志的 session 文件名时间戳与第一次不同)→ 完成后 `GET + org-graph` 读回的图与之前**内容一致**(仍是 org-planner/org-coder/org-validator 三节点 + 两条边)。 + **这就是 W5/W6/W8 三项加一起、也是验收标准 5/6 真正意义上第一次被真实浏览器点击 + 真实 Claude CLI + 完整跑通**。 + +**顺带验出一个此前设计笔记里没预料到的细节,如实记录,不确定的部分标清楚**: + +- **确认的部分**:`insertNode`(`org-graph-service.js:362`)是按 `(company_id, role_name)` 走 + `upsertOrgGraphNode`,重规划成同名角色时**复用同一行、同一个 `id`**(本轮实测:replan 前后 + `org_graph_nodes` 里 `org-planner`/`org-coder`/`org-validator` 三行 id 分别仍是 2/3/4,一个没变)。 + 这与 W8 落地记录那句「`writeGraph` 每次换新 id」的设计假设**不一致**——那句话写的时候可能是对的, + 实现后来演化了没有回填这份记录,又是一次「文档滞后于代码」。 +- **不确定、本轮没能坐实的部分**:`ReplanOrgGraphButton.vue` 的完成检测依赖"图指纹含节点 id" + (见上方落地记录),若这份笔记描述的机制仍然成立,"重规划出内容相同的图"这种情况下前端可能真的 + 检测不到变化、一直卡在 `planning` 态直到 6 分钟超时。本轮真跑中点击按钮后页面确实变得对自动化工具 + 长时间无响应(`CDP sendCommand` 超时、`screenshot` 反复报"页面繁忙"),**但不能排除是同一个页面 + 累积的 SSE/ACP 事件量过大导致标签页整体卡顿**(`ACP Events` 面板长时间开着、`agent_output` 事件 + 刷了几十条)——两种解释都有可能,本轮证据不足以二选一。刷新页面后数据本身是对的(第 3 点), + 所以**不是数据错了,顶多是「完成后前端没自己发现,需要手动刷新」这一类体验缺口**,不影响 + 验收标准 5/6/8 本身成立。这条列为已知的后续排查项,不阻塞本文档收口。 + ### 第三批 · 让结果可信 | # | 内容 | 量 | |---|---|---| -| **W9** ✅ 已实现,🟡 真跑未验 | 失败上下文进提示词:`validator-triager` 的 agent.md 承诺「给你哪个节点失败、第几 attempt、失败摘要」,全仓库没有代码提供。已由 `describeFailureContext` 补上,详见下方「W9 落地记录」 | 中 | +| **W9** ✅ 已实现,✅ 真跑已验(§六ter 验收 8) | 失败上下文进提示词:`validator-triager` 的 agent.md 承诺「给你哪个节点失败、第几 attempt、失败摘要」,全仓库没有代码提供。已由 `describeFailureContext` 补上,详见下方「W9 落地记录」 | 中 | | **W10** ✅ | ~~跨层地址要 `target_org_node` + `target_node` **两半都写**才合法~~ —— **原表述与引擎相反,已订正**:`target_node` 是唯一必填半,`target_org_node` 可选。模板已按引擎逐字改写,并补上「org node 名从哪来」。详见下方「W10 落地记录」 | 小 | | **W11** ✅ | `system/skills/meta-*` 缺失导致 base agent 的 Hot Swap 静默空转。**选方案 (b)**:不补技能包,改成显式告警 + 如实改文档。核实出更深一层:补技能包也关不上权限闸(工具权限来自 `config.json` 的 `toolPolicy`,与 phase 无关)。详见下方「W11 落地记录」 | 中 | | **W13** ✅ 真跑已验(§六bis:整轮真跑 git status 与基线逐字相同) | agent 运行态文件从 git 移出去(守卫误判的**一半**根因;另一半是并行 agent,见 §六bis 缺陷②,W13 管不到)| 小 | @@ -581,7 +629,9 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛 真要拆的话升级路径是给 `graph-run-sse` 加「按 ref 倒读最近 N 条」的接口,**不是**加一列缓存。 - **作用域是「本 run」**:按 timeline 行的 `run_id` 过滤。子 run 里的失败不会串到父 run 的 提示词里(父层拿到的是 `deriveChildOutcome` 那条透传),反之亦然。 -- **仍未验**:真 CLI 下 `validator-triager` 是否真的照着 (a) 取名(mock 不读提示词,验不了)。 +- ~~仍未验:真 CLI 下 `validator-triager` 是否真的照着 (a) 取名~~ ✅ **已验**(§六 验收标准 8 / + §六ter):triager 判词原文引用了只可能来自提示词的内部 id(节点私有目录路径里的 + `graph_run_nodes.id`),真的照着 (a) 取了名。 对应验收标准 8。 - 锁:`server/tests/unit/graph-engine-fail-reason.test.js` 后 6 例(含 backward 回退那条关键路径)。 @@ -767,16 +817,24 @@ agent 读的是 `agent.md`,那一侧不改,模型就会继续以为「前三 | # | 标准 | 状态 | |---|---|---| | 1 | 服务启动后 `system_agents` 与 `system/templates/` 一致,无悬空 `company_hired` 行;新加一个模板目录、重启服务,它自动出现在表里 | ✅ **已验**(生产库实查 + hermetic 用例)。一处**已知且合理**的偏差:`system/agents/` 与 `system/templates/` 同名的 6 个 id(`agent-creator` / `org-innovator` / `innovator-{digest,evolver,hunt}` / `evolver-youtube-crawler`)按设计 skip,仍是 `global_agent`。图用到的本体都不在这 6 个里 | -| 2 | 起一个真 CLI 的 run,节点产出全部落在 `data/companies/{cid}/projects/{pid}/` 内;`git status` 在宿主仓库无任何新增/修改 | 🟡 **前半已验、后半被并行 agent 打破**(2026-08-17,§六bis)。**2026-08-20 补记**:后半那个「被并行 agent 打破」本身已经不再导致误判 —— 守卫改成「节点开跑那一刻仓库就脏 → 只记证据、不判罚」,见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md)。**但这条验收标准本身仍未在干净环境下重验**。当时的原始记录:`workgraph-planning` 的产物真的落在 `data/companies/p3bfin/projects/p-24vxi8/workspace/` 内,`project-root.json` 四环闭合;宿主仓库出现的两处改动经查是**并行的 Master agent** 写的(缺陷②),已复原。被判罚的那个 work 节点自己一个字节都没往仓库写 | +| 2 | 起一个真 CLI 的 run,节点产出全部落在 `data/companies/{cid}/projects/{pid}/` 内;`git status` 在宿主仓库无任何新增/修改 | ✅ **2026-08-20 干净环境重验通过**(§六ter 验收 2):19 次真 agent 调用之后 `git status --porcelain` 完全为空,交付物全部落在 `data/companies/e2e-demo/projects/p-bkhylu/` 内。**2026-09-08 又一次独立复验**(§六quater,`accept9-mtsksam9` 那次真跑):15 次真调用、backward_count=3,全程未在宿主仓库留下任何改动。这条此前"仍未在干净环境下重验"的备注已过期,两次独立真跑都证实成立 | | 3 | 故意让节点往产物目录外写,被守卫拒绝并在 timeline 留痕 | ✅ **真 CLI 已验**(2026-08-17,§六bis):git 扫描确实扫到了宿主仓库里的那处改动,`node.boundary` 事件带完整 `entries`,结果被降级判 fail 且 `fail_reason='boundary_escape'` 落库。**但同一次也验出了误判**:那处改动是并行的 Master agent 写的,不是被判罚的节点写的(W3「拦不住」第 5 条,见 §六bis 缺陷②)。判罚逻辑侧另有 W12 补完那一轮加的三条分支用例。**2026-08-20 补记**:误判那一半已修([27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md),`BOUNDARY_GUARD=strict|record` 两个开关 + 8 例守卫,含「仓库干净时同样一处越界仍判 fail」的对照) | -| 4 | 造一份非法契约跑 `benchmark-validator`:判决为失败且引擎按失败处理(**不是** success) | 🟡 **静态成立,未真跑**。模板已改写 `verdict: "attributed"`,而 `FAILING_VERDICTS={attributed,inconclusive}` → `readNodeProduct` 置 `failingVerdict=true` → `callAgentNode` 判 `fail`(`graph-engine.js:473/385`),链路读通;防线测试三次反例变异全部变红 | +| 4 | 造一份非法契约跑 `benchmark-validator`:判决为失败且引擎按失败处理(**不是** success) | ✅ **由零注入真实链路验掉**(`server/tests/unit/graph-real-chain-smoke.test.js` ⑥,进 CI):非法契约真实驱动到 `readNodeProduct` → `callAgentNode`,节点判 `fail`、`fail_reason=input_underspecified`、沿 fail 边点亮下游,判词落库可读回,反向验过(`verdict` 改成 `passed` 立刻红)。**这不是起了真 Claude CLI**——只换掉最外层 CLI 子进程为 fixture 回放,其余代码路径(`AgentManager`/`readNodeProduct`/`settleNode`/边界守卫)全部真实,按 27-P3/P4 §十「四条硬规矩」第 2 条的标准算「零注入」而不是「假 executor 摆出来的分支覆盖」 | -> 2 与 3 都要真 CLI 才验得了,而真 CLI 现在会先死在 W5/W6 上。**这两条应挪到第二批之后一起验。** +> 2 与 3 原计划要真 CLI 才验得了,实际 2 后来靠两次独立真跑(§六ter / §六quater)验掉, +> 4 靠零注入真实链路(不需要真 Claude CLI)验掉——都已完成,不用再挪到第二批之后。 **第二批** 5. 新建公司 → 界面点「重新规划组织」→ `orggraph-planning` 真跑 → org graph 出现,且它雇的三个 org 节点都在。 + ✅ **真 CLI + 真实浏览器已验**(2026-09-08,见下方「W8 浏览器真跑验收」):新建公司自动规划、 + 点击按钮触发的重新规划,两条路径都真起了 Claude CLI,`org-planner`/`org-coder`/`org-validator` + 三个 org 节点都真实出现在图里。 6. 下达任务 → `workgraph-planning` 真跑 → 它引用的本体被自己雇进公司 → 图提交成功(不再外键挂)。 + ✅ **真跑已验**(§六bis 验收点「`workgraph-planning` 真跑规划 + 自雇(W5/W6)」):真 Claude CLI 下 + `wg_55c7e53f959f` 规划出两节点 + 正向边 + backward 兜底边,`org-writer-impl`/`org-writer-validator` + 引用的两个本体都被 `workgraph-planning` 自己 `hire` 进 `data/companies/p3bfin/agents/`,图提交没有 + 外键挂。触发方是 Master agent 的自主巡检派单,不是人工点「下一步」,走的是同一条链路、同一个端点。 7. 上条跑完后,控制台左栏 org graph **仍然只有 org 节点**,没有混进 `prd-planning` 这类 work 层本体。 ✅ **已修**(`58a2865`「W7 零边图」,2026-08-17 当天)。判据加了主判据 `authored_by='planner'` 的节点存在 —— 「orggraph-planning 提交过一张图」的直接证据, @@ -792,11 +850,18 @@ agent 读的是 `agent.md`,那一侧不改,模型就会继续以为「前三 **第三批** 8. `validator-triager` 的提示词里能看到「哪个节点失败、第几 attempt、失败摘要」。 - 🟡 **代码已成立,真 CLI 未验**:`describeFailureContext` 已接进 `advanceOne`, - 6 例测试(注入 executor 抓 `ctx.task` 原文)覆盖 forward fail 边、backward 回退、 - subgraph 规划、超长截断、以及「无前序失败时一个字都不塞」。见上方 W9 落地记录。 - 真 CLI 下 agent 是否照着 (a) 取名,归真跑那一轮。 + ✅ **真 CLI 已验**(§六ter 验收 8,2026-08-20):判据是 triager 的判词原文引用了只可能来自 + 提示词的内部 id——「`org-coder-impl` 私有目录 `rn_96a8cd696dcc` 下只有 `_research.md`」, + `rn_96a8cd696dcc` 是 `graph_run_nodes.id`,agent 没有任何别的途径知道它,只能来自 + `describeFailureContext` 注入的那一段,即真的照着取名来源 (a) 用了。 9. 一次真实的跨层回退:`target_org_node` + `target_node` 两半齐全,只有被点名的节点被重置。 + ✅ **真 CLI 已验,但落地形状比原表述更保守**(§六quater,2026-09-08):真跑撞上的是 + 「回退目标本身就是 org node、没有更细粒度可指」这种情形——triager 没写 `target_node`/ + `target_org_node`(图上没有 `org-coder` 可指),退化成「由 org 层 fail 边直接决定重跑哪个 + org node」,是 R-17.1 设计里最保守的一档,行为符合预期。「两半地址齐全」这个更细的子情形 + (归因精确指到某个 org node 内部的具体 work 节点)仍然只有 `graph-cross-layer-address.test.js` + 的注入式覆盖,没有真跑验过——但这不影响验收 9 本身成立(原始表述的核心诉求是「org 层回退边 + 真的能指向 subgraph 节点」,已经证实)。 --- @@ -1013,3 +1078,67 @@ node.output | 产物索引写入失败 sdd.md: UNIQUE constraint failed: i > 这是**同一类问题的第三次**(`tasks` 加列的索引顺序、墙钟锚点迁移、这次)。 > 判据可以固定成一句话:**凡是碰 schema,「存量库重启」那条路必须单独走一遍** > —— 新建库的路径由单测天然覆盖,存量库的不会。 + +--- + +## 六quater、验收 9 真跑验掉(2026-09-08) + +**跑法**:隔离 git worktree(`.claude/worktrees/p3b-acceptance9-realrun`,跑完即删,`EnterWorktree`/ +`ExitWorktree` 管理,不碰主仓库工作树也不碰生产 `data/`),真 Claude CLI(未设 +`LEMONBOSS_CLAUDE_MODE=mock`),一次性脚本直接 require 服务层驱动(`scripts/real-accept9-run.js` + +续跑脚本 `real-accept9-resume.js`,均未提交,随 worktree 一起丢弃)。 + +**场景设计**:2 节点 org 图 `org-planner --success--> org-validator`,`org-validator --fail(catch-all)--> +org-planner`(backward)——**刻意跳过 `org-coder`**。理由:org graph 里所有节点定义上都是 +`nodeKind='subgraph'`(`work-graph-db.readDefGraph`),所以任何一条 org 层 fail 边的目标天然就是 +subgraph 节点,验收 9 真正的难点不是"凑出这个图形状",而是"让一次真实、认真执行自己 `agent.md` +的 org-validator 真的判 fail"——跳过 org-coder 让"压根没有实现代码"这件事对 validator 而言是 +**客观事实**,不依赖运气或对提示词做手脚。 + +**真跑结果**(`accept9-mtsksam9` / 根 run `run_dd09fc486b0e`,15 次真实 agent 调用, +218,887 tokens,backward_count=3,全程未撞任何硬顶): + +1. `org-planner` 自己的 work graph(`workgraph-planning` 真规划出 brd→prd→sdd→goal-benchmark→ + planner-validator,中途 planner-validator 打回 sdd 重写一轮)跑到 success,只产出规格文档, + 不产任何实现代码——设计使然。 +2. `org-validator` 被点亮,`workgraph-planning` 为它规划出的子图与它自己 `agent.md` 里写的骨架 + 一字不差:`validator-triager → benchmark-validator → testdata-validator/testscript-validator`。 +3. `validator-triager` 首轮 `no_prior_failure`,放行给 `benchmark-validator`。 +4. **`benchmark-validator` 真实读了项目根、真实按 `goal-benchmark.json` 四个维度打分** + (`post_create_correctness` / `get_list_correctness` / `validation_and_error_handling` / + `runnable_delivery`),**全部判 0 分**,加权总分 0,远低于 `overall_pass_threshold=80`—— + 这是它自己读代码目录后的真实判断,不是任何注入或预设。 +5. `validator-triager` 复验轮给出的归因(原文摘录): + > 根因: 被测产物目录(项目根 `data/companies/accept9-mtsksam9/projects/p-4ntvbi/`)内没有任何 + > 实现代码,只有 org-planner 和 org-validator 各自 workgraph-planning 阶段留下的两份规划记录 + > md 文件;本次根 run 的 `nodes/` 目录下也不存在任何 org-coder 节点的产出记录,说明 + > POST/GET /todos 的具体实现从未被写出,BRD/PRD/SDD 定义的验收标准清晰……不是标准写得不清楚。 +6. **org-validator 的子 run 收敛为 fail,engine 在 org 层选中了那条唯一的 fail 边** + (`org_graph_edges.id=2`:`from_node_id=2(org-validator) to_node_id=1(org-planner) + condition='fail' fail_reason=NULL edge_kind='backward'`),真实写下一条 + `graph_run_loop_records`(`loop_count=1, verdict='registered', from=org-validator 的 + nodeRunId, to=org-planner 的 nodeRunId`)。 +7. **回退目标 `org-planner` 真的是 `nodeKind='subgraph'` 节点**(直接读 `graph_run_nodes` 行核实), + 且按 R-17.1「复用同一张子 run」的既定语义处理:`childRunId` 不变(仍是 `run_5991c4a313ac`), + `attempt` 从 0 → 1,重新从它自己 work graph 的入口节点(`brd-planning`)跑起,第 5 步那份 + `remediation_note` 原样带下去。 + +**逐条对齐验收 9 的原始表述**("一次真实的跨层回退,两半地址齐全、只重置被点名的节点"): + +- ~~"两半地址齐全"~~ **这次不适用,且不是缺陷**:`validator-triager` 没有写 `target_node`/ + `target_org_node`——它的归因原话是"org-coder 从未产出",图上压根没有 `org-coder` 这个节点可指, + 没有更细的粒度可给(同 §六ter 归纳的"work 层内部回退没有跨层可言"是同一类道理的另一面: + 这次是"该退的目标本身就是整个 org node,没有更细可指")。三个键都可能为空的用法本来就在 + R-17.1 的设计范围内("指不准时只写 `target_org_node`;引擎退化成整张 work graph 重跑"), + 这次连 `target_org_node` 都没写,退化成"由 org 层的 fail 边直接决定重跑哪个 org node", + 是该设计里最保守的那一档,行为完全符合预期。 +- **"只重置被点名的节点"**:这里"被点名的节点"就是 org 层 fail 边指向的 `org-planner` + 本身(org 层没有比 org node 更细的粒度),它被正确重置(子 run 复用、`attempt+1`), + 没有波及任何其他 org node(这张图上也没有别的 org node 可波及)。 +- **验收 9 真正要的核心事实——"org 层回退边、目标是 subgraph 节点"——第 6/7 点已完整证实**。 + +**成本**:全程约 47 分钟真实墙钟(含一次因脚本硬超时设太紧、被迫续跑的中断), +15 次真实 Claude CLI 调用,218,887 tokens,落在 27-P3B §五「$3-8 / 15-40 分钟」的预估量级内。 + +**收尾**:worktree 已删除(`ExitWorktree action:remove`),跑出来的公司数据、脚本均未进主仓库, +不留痕迹。以上证据全部来自真跑当时直接读库/读文件核实,不是转述模型输出。 diff --git a/docs/done/system-design/27-p3c-goal-task-model.md b/docs/done/system-design/27-p3c-goal-task-model.md index 6eef668..4235427 100644 --- a/docs/done/system-design/27-p3c-goal-task-model.md +++ b/docs/done/system-design/27-p3c-goal-task-model.md @@ -1,12 +1,12 @@ # 27-P3C. 公司目标与任务 —— goal / task 两级模型 + 控制台任务列表 -> **所属**: [27 Graph Agent 架构](../../todo/system-design/27-graph-agent-architecture.md) 的 **P3 收尾**(P3C),直接续 [27-P3/P4 执行控制台](../../todo/system-design/27-p3-p4-work-graph-console.md) §四.B 与 [27-P3B 真跑跑道](../../todo/system-design/27-p3b-real-run-runway.md)。 +> **所属**: [27 Graph Agent 架构](../../todo/system-design/27-graph-agent-architecture.md) 的 **P3 收尾**(P3C),直接续 [27-P3/P4 执行控制台](../../todo/system-design/27-p3-p4-work-graph-console.md) §四.B 与 [27-P3B 真跑跑道](./27-p3b-real-run-runway.md)。 > **日期**: 2026-08-18 > **状态**: ✅ **已完成并归档**(2026-08-20)。五批全部实施(§十三 ~ §十六),§15.5 / §16.8 的后续项也已全部收掉(§十七)。 > 138 文件 / 1292 测试全绿,`web:build` 通过。**真跑验收见 §17.5** —— 返工闭环第一次由真 agent(不是注入的假 executor)走通。 > **⚠️ 批次与 phase 的对应不是一一对齐的**,判断「这份能不能搬进 done」时按批看,别按文档名看: > · **批 1-3 = 真正的 P3 收尾**(goal/task 两级模型 + `task-planning` + 排队),本文档的主体; -> · **批 4(并行)归 [27-P4](../../todo/system-design/27-p4-parallel-and-artifacts.md) §一.3**,那边是设计权威,本文档只服从(见 §五.2 的警示块); +> · **批 4(并行)归 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3**,那边是设计权威,本文档只服从(见 §五.2 的警示块); > · **批 5(两套任务体系统一)不属于任何既有 phase** —— 是跨阶段的技术债清理,风险面也与前四批完全不同(坏了是所有 agent 互相派不了活),见 §十二.6。 > **交付物**: ① `goals` / `tasks` 两张表,把「人类下达的需求」与「lemonboss 执行的单元」拆成两级;② 基于这两张表的**排队与并行**;③ 执行控制台(`/:id/graph-console`)最左侧新增**目标·任务两级列表栏**。 > **命名**: 全文遵循 [27-NAMING](../../todo/system-design/27-naming-conventions.md),本文档**落地**的正是它 §四 定义但代码里一直没有载体的 `goal 1 : task N` 基数关系。 @@ -480,7 +480,7 @@ for (const t of queuedTasks) { **批 4 单独排期、单独验收**,理由:删掉一个唯一索引 = 删掉最后一道 DB 兜底,这个动作不该和「界面上能看到任务列表」混在一次上线里。 -> ⚠️ **批 4 与 [27-P4](../../todo/system-design/27-p4-parallel-and-artifacts.md) §一.3 是同一件事,不要各做一遍。** 那份文档的目标 3 逐字写着「**解除「一家公司同时只有一个 active root run」约束** —— P3 v1 用一个唯一索引强制它(§三.3),并行能力具备后应放开」。也就是说:**放开约束的设计归 27-P4,本文档只负责在它之上定义「谁来排队、按什么序出队」**(§五.1)。真到实施时以 27-P4 的并行方案为准,本文档的 §五.2 服从它;两边都写一版会产生两套并发守卫。 +> ⚠️ **批 4 与 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3 是同一件事,不要各做一遍。** 那份文档的目标 3 逐字写着「**解除「一家公司同时只有一个 active root run」约束** —— P3 v1 用一个唯一索引强制它(§三.3),并行能力具备后应放开」。也就是说:**放开约束的设计归 27-P4,本文档只负责在它之上定义「谁来排队、按什么序出队」**(§五.1)。真到实施时以 27-P4 的并行方案为准,本文档的 §五.2 服从它;两边都写一版会产生两套并发守卫。 --- @@ -739,7 +739,7 @@ for (const t of queuedTasks) { | **1 · 两级模型落库** | `goals` / `tasks` 建表 + **存量回填脚本(§四.8)** + ID 生成器修正 + `POST /:cid/tasks` 兼容改造(自动建 goal、`report_to` 落库)+ `GET` 列表端点 + 退役 `/goals` 别名 | 4 / 10 / 11 / **12** | | **2 · 控制台左栏 + 拆分** | `GoalTaskPane` 两级树 + URL 选中态 + `loadRuns` 修正 + **`task-planning` 本体(含 `_registry.json` 登记)+ 拆分链路 + 失败降级**+ 两步表单 | 1 / 19 / 20 / 21 / 22 | | **3 · 排队** | `run_status='queued'` + `RUN_TRANSITIONS` + `syncRunNodesToDef` + `task-scheduler`(漏斗钩子 + 三个辅助触发 + 重入保护 + 上游失败推导 blocked)+ `depends_on` 环检测 + 入队/取消/rerun 端点 + §四.7 两条禁令 + §九.1 全部回归点 | 2 / 3 / 5 / 6 / 7 / 8 / 9 / 13-18 / 23 | -| **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit。**与 [27-P4](../../todo/system-design/27-p4-parallel-and-artifacts.md) §一.3 同一件事**,实施以那份为准 | 另立验收 | +| **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit。**与 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3 同一件事**,实施以那份为准 | 另立验收 | | **5 · 任务体系统一**(单独排期)| 见 §十二:`executor` / `assignee_agent_id` / `parent_task_id` 三字段 + `/task/dispatch` 换存储 + 删会话反推 + `tasklog` 回填 | 25-30 | > 批 3 的验收从 6 条涨到 12 条,是本次复核的直接结果 —— 13/14/15 三条各自对应一个「初稿会实现出来的静默故障」。 diff --git a/docs/todo/system-design/27-p4-parallel-and-artifacts.md b/docs/done/system-design/27-p4-parallel-and-artifacts.md index 46d1bf8..8dcb755 100644 --- a/docs/todo/system-design/27-p4-parallel-and-artifacts.md +++ b/docs/done/system-design/27-p4-parallel-and-artifacts.md @@ -1,7 +1,8 @@ # 27-P4. 并行执行与产物索引 > **所属**: [27 Graph Agent 架构](./27-graph-agent-architecture.md) 的 Phase P4 -> **版本**: v0.2(阶段 A/B/C 均已实现,见 §五 2026-08-21 记录;T5 是可插拔桩子,真实计费接口未接) +> **版本**: v0.4(阶段 A/B/C 全部收口,见 §五 2026-08-21 + 2026-09-08 记录;已归档到 `done/`) +> **T5 收尾说明(2026-09-08,用户拍板)**:欠费门本身已实现并上线(可插拔桩子,`setBillingGate` 未注入时零行为影响,见下方 T5 落地记录)——**缺的只是接一个真实计费接口**,这一步被用户明确要求暂缓、挪到 `docs/todo/future/`,不算这份文档的收口条件 > **日期**: 2026-08-16 > **前置**: P0/P1/P2/P3 均已实现。本文档建立在 P3 已落地的执行引擎之上(`server/services/graph-engine.js` / `work-graph-db.js` / `graph-run-sse.js` / `goal-contract.js`,commit `c3abf4a`)。 > **命名**: 遵循 [27-NAMING](./27-naming-conventions.md),节点名按层级轴(`org-planner` / `org-coder-validator`),本体名不带前缀。 @@ -192,7 +193,7 @@ const results = await Promise.allSettled(claimed.map(node => executeNode(node))) ### 4.2 同一 agent 的多实例隔离 -**这是并行的前置条件,必须先做。** +> ✅ **2026-09-08 已实现**(此前文档写着设计、T1 的✅曾误导人以为已完成——实测 `AgentManager._runViaProtocol` 两处 `getOrCreate` 调用当时确实固定只传 5 个参数,`options.sessionScope` 从未被读取,`callAgent` 解构出来只是摆设。核实动机:`executor-fanout.json` 这类骨架的 `{{orgNode}}-impl-a`/`-impl-b` 两个并行节点引用同一个 `systemAgentId`,`advanceBatch` 真并发拾取时 `taskId`(=`ctx.runId`)相同、不传 scope 就会撞会话键——这不是假设性风险,是 27-P4 阶段 B 已经上线的并发路径里一个真实存在的洞)。落地:`graph-engine.js` 两处 `callAgent` 调用都传 `sessionScope: \`gr:${ctx.nodeRunId}\`;`agent-manager.js` 的 `_runViaProtocol` 读 `options.sessionScope` 并透传给 `SessionPool.getOrCreate`(含失败重试那次)。守卫 `agent-manager-session-scope.test.js`(3 例,含反向验证:去掉 sessionScope 会退回撞车)。 利用 §三.2 发现的现成挂载点——`SessionPool.getKey()` 的 `scope` 参数: @@ -553,7 +554,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), | **画布上的边** | 只有**声明过契约**的边多画一枚小药丸(必需的那个 kind 占位、其余折 `+N`、全量与 `?`=可选进 ``);`GAP_X` 只有 52px,第二个标签就会压线,所以克制到一枚 | ✅ `WorkGraphCanvas.vue` | | **tab 徽标** | 必需输入解析不到的条数标成「输入输出 ⚠️N」,不用点进去才发现 | ✅ | | **前置:路由字段** | `GET /api/graph-runs/:id` 要补三个字段:`companyRoot`(= `companyRootDir(run.company_id)`,库里只存相对路径)、`taskFiles`(= `findFiles({companyId, taskId})`)、`edges[].contracts`(= `edgeContracts(layer, e.id)`,`readDefGraph` 已回 `edge.id`)。三者都是现成导出函数 | ✅ **已接**(`1ea9efa`)。缺字段时的降级行为保留:三块**整块降级**并点名缺哪一个,不做任何推断(推断出来的数据流正是第四次真跑栽的坑)。⚠️ **降级分支已被真跑实测过、接通后的正常分支还没有**:第五次真跑用的是 `1ea9efa` 之前的构建,那时节点详情「输入输出」tab 只显示降级横幅「产物索引未随响应下发…`GET /api/graph-runs/:id` 还没返回 `companyRoot` / `taskFiles` / `edges[].contracts`」,画布 forward 边上也没有契约药丸——即当时 R-19 的数据通路**引擎侧已通、控制台侧看不见**。接上之后界面能否真的显示解析出的绝对路径,**留待下一次真跑复验** | -| `graph_edge_contracts.role` | `produces` / `needs` 的非对称在 `declareEdgeContract` 处被丢掉(表里没有 role 列),落库后两者不可区分 | ❌ 未落库:所以「这条边声明的 kind 有没有实例」只能看索引本身,而不能按 role 判定谁该产它 | +| `graph_edge_contracts.role` | `produces` / `needs` 的非对称在 `declareEdgeContract` 处被丢掉(表里没有 role 列),落库后两者不可区分 | ✅ **文档落后于代码**:`role` 列(`work-graph-db.js:465` 建表块 + `:698` 存量库迁移回填)与 `normalizeContracts()`(`org-graph-service.js:49`,按来自 `edge.produces` 还是 `edge.needs` 分别打 `role`)2026-08-20 前后就已落地,守卫 `graph-edge-contract-role.test.js` 早于本文档最初写作。2026-09-08 核实时才发现这行判断是错的,予以订正 | --- @@ -562,7 +563,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), | # | 任务 | 文件 | 依赖 | |---|---|---|---| | ~~**T1**~~ ✅ | `SessionPool` 加 `maxSessions` 上限 + 调用方传 `scope` —— **已完成**(27-P3C §15.5-2,守卫 `session-pool-max.test.js` / `session-pool-scope-key.test.js`) | `server/src/core/session-pool.js` | 无 | -| **T2** | DevLoop 规格:§12 第一条不变量放宽为 `(pid, agent, scope)`。**改 HTML 后必须跑 `npm run spec:extract`** | `docs/prd/newfunction/*.html` | 无 | +| ~~**T2**~~ ✅ | DevLoop 规格:§12 第一条不变量放宽为 `(pid, agent, scope)`。**2026-09-08 已完成,且发现原计划有误**——`CONVENTIONS.md` 不是 `spec:extract` 的生成物:全仓库 `docs/prd/newfunction/*.html` 里没有一份 `conventions.html`,`npm run spec:check` 只处理 52 个 `.html`→`.md` 的 1:1 映射,`_spec/` 目录里第 53 份的 `CONVENTIONS.md` 是手写文件,改它不需要也不会触发 `spec:extract`。直接改的是 `docs/prd/newfunction/_spec/CONVENTIONS.md` 本体 | `docs/prd/newfunction/_spec/CONVENTIONS.md` | 无 | | ~~**T3**~~ ✅ | `graph_runs` 加 `max_concurrency` 列(可空,NULL 用新导出常量 `DEFAULT_MAX_CONCURRENCY=5`,对齐 `plan-cost-concurrency.md` §④);`createRun` 新增 `maxConcurrency` 参数并持久化 —— **2026-08-21 已完成**(迁移块 + `createRun` INSERT 都改了,`hasColumn` 判断 + `ALTER TABLE ADD COLUMN` 照抄既有存量库迁移写法)。~~DROP `idx_graph_runs_one_active_root`~~ 那半本来就已完成(27-P3C 批 4),不用动 | `server/services/work-graph-db.js` | 无 | | ~~**T4**~~ ✅ | `advanceBatch(runId, {maxConcurrency})`:`maxConcurrency<=1` 直接委托 `advanceOne`(零行为差异,不是反过来让 advanceOne 委托 advanceBatch——见函数注释里的理由);`>1` 时 `SELECT ... LIMIT` 取候选,循环调 `sse.transitionNode` 逐行拾取(**不包裹事务**,§4.1 决策的完整论证)。**2026-08-21 已实现并测过** | `server/services/graph-engine.js` | T3 ✅ | | ~~**T5**~~ ✅ | ~~并发额度计算(三层取小)+ 额度为 0 时的等待语义~~ **2026-08-21 已实现,按可插拔桩子的方式落地**(用户明确选择:真实计费接口未定前先搭桩子)。`setBillingGate(fn)` 与 `setAgentExecutor` 同款注入模式,未注入时 `checkBilling` 恒「未欠费」,对现有行为零影响;欠费时复用 `suspendRunTree`(新增 `reason`/`summary` 参数,`reason='billing-arrears'`),写进新增的 `graph_runs.block_reason` 列;`startRunSync`「启动」清空该列;`resumeBillingClearedRuns()` 是自动恢复触发器(扫 `block_reason='billing-arrears'` 的 blocked root run,欠费真清了就代其 `startRun`),调度源(webhook/轮询)留给计费接口接通之后再定。**顺带修了一个真 bug**:`advanceBatch` 原来 budget 门查在候选查询之前,导致「已收敛只是恰好预算也耗尽」的 run 被误判成挂起而不是 completed——已改成候选先查、budget/billing 门后查,跟 `advanceOne` 的既有顺序对齐。新测试 `graph-engine-billing-gate.test.js`(7 条),临时 worktree 隔离验证(见下方实现记录) | `server/services/graph-engine.js` | T1, T3, T4 | @@ -577,12 +578,14 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), | 阶段 | 含 | 交付 | 可否独立上线 | |---|---|---|---| -| **A · 隔离** | ~~T1~~ ✅, T2 | 同一 agent 可安全并发调用 —— **T1 已完成**,只剩 T2(DevLoop 规格放宽) | 基本具备 | +| ~~**A · 隔离**~~ ✅ | ~~T1~~ ✅ ~~T2~~ ✅ | 同一 agent 可安全并发调用 —— **2026-09-08 全部完成**:T1 当初的✅只做完了 `maxSessions` 上限,`scope` 参数的生产接线(`graph-engine.js` → `agent-manager.js` → `SessionPool`)当时其实没做,本轮一并补上;T2 随之完成 | ✅ | | ~~**B · 并行**~~ ✅ | ~~T3~~ ✅ ~~T4~~ ✅ ~~T6~~ ✅ ~~T5~~ ✅(桩子)~~T10~~ ✅ | fan-out 真正并发;解除单 root run 约束 | ✅ 依赖 A,**2026-08-21 全部完成** | | **C · 产物索引** | T7-T9 | `files.json` + 按 task 分目录 | ✅ **完全独立于 A/B**,可最先做 | **阶段 C 与 A/B 无依赖**,如果想先看到效果,可以先做 C(它是用户明确要求过的第 3 项)。 +> **2026-09-08 补一条 B 阶段里没有单独立项、但确实属于「真并行」的缺口**:`advanceTree`(`graph-engine.js`,`runAuto` 的驱动原语)一直标着 `ponytail:` 只推「一棵子树里最深的一个 active run」,同一 tick 里的**兄弟**子 run(两个并行 `subgraph` 节点各自的子 run)靠外层循环轮流服务,不是真并发——`advanceBatch` 解决的是「同一个 run 内部」的并发,跨 run 这一层当时留了后手。已按代码注释里写好的升级路径改成 `Promise.allSettled` 并发推进树里所有 active run(安全性论证同 §4.3:不同 runId 的行互不覆盖,`settleRun` 被多次幂等调用不是新风险)。守卫 `graph-engine-advance-batch.test.js` 新增一例,反向验证过(撤回改动后 `maxActive` 卡在 1)。 + > **2026-08-20 校准**:C 已经完成,A 的 T1 也完成了(都是在 P3C 那几批里顺手落的, > 当时没回来更新这份文档)。**真正剩下的是 B**:`max_concurrency` 列、`advanceBatch`、 > 三层额度取小、并行执行与部分失败降级,以及 T10 的控制台可视化。 @@ -685,7 +688,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), | `server/services/company-files-index.js` | 新增 | | `server/services/goal-contract.js` | 契约文件路径归并到 `tasks/{taskId}/` | | `web/src/views/GraphConsole.vue` | fan-in 等待态、多节点并行高亮 | -| `docs/prd/newfunction/*.html` | §12 不变量放宽(**改完必须跑 `npm run spec:extract`**) | +| `docs/prd/newfunction/_spec/CONVENTIONS.md` | §12 不变量放宽(✅ 2026-09-08 已改;核实后发现它是手写文件、不是 `spec:extract` 的生成物,不需要跑 extract) | | `system/templates/base/agent.md` | `files.json` 检索说明 | **明确不改**:P3 的 join 语义(`graph-engine.js:1078` 已正确)、熔断与预算语义、手动/自动共用同一原语的设计、`workDir` 指向私有目录的约定(P1 §3.3 已论证)。 @@ -695,7 +698,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), ## 八、风险 / Open Questions 1. **并行让预算烧得更快,但预算语义不变**。任务级总预算是「只增不减的共池」(P3 已实现),并行不改变总量,只改变消耗速率。**控制台的成本提示需要相应更新**——用户点「自动」时看到的预估时间会大幅缩短,但花费不变。 -2. **改 DevLoop 规格的连带风险**(T2)。§12 第一条不变量是 DevLoop 的,放宽它理论上不影响 DevLoop 自身(它的 `runProject` 波内本来就是串行的),但**必须跑一遍 DevLoop 全部测试**确认。 +2. ~~**改 DevLoop 规格的连带风险**(T2)。§12 第一条不变量是 DevLoop 的,放宽它理论上不影响 DevLoop 自身(它的 `runProject` 波内本来就是串行的),但**必须跑一遍 DevLoop 全部测试**确认。~~ **2026-09-08 已解决,且风险本身不成立**:`CONVENTIONS.md` 不是 DevLoop 代码读取的配置,只是给人/agent 看的文字说明,`(pid, agent)` 这条不变量从没有被任何 DevLoop 代码断言过——改文字不触发任何测试变化,`npm run test:unit` 全量跑过确认无回归。 3. ~~**`maxSessions` 上限触顶时的行为未定**。额度为 0 时是「等待下一轮」还是「拒绝并报错」?本文档 §4.4 选了前者(不报错、等下一轮),但如果一个 run 长期抢不到额度会静默卡住——需要一个「等待超时」告警,否则表现为「不动了」。**留待实现时定阈值。**~~ **按 2026-08-21 决策已解决**:公司级/机器级不再设硬顶,「长期抢不到额度」这个场景基本不会出现了。账户欠费门定了——复用 `suspendRunTree` 的挂起语义(`reason='billing-arrears'`),跟预算耗尽是同一条路径,不发明第二种失败形态。**新缺口**:欠费清零后的恢复今天要人手点「启动」,无人值守场景下没人点——需要一个自动恢复触发器(欠费状态清除时批量对 blocked 的 root run 调 `startRun`),细节见 §4.4 决策块。欠费检查本身怎么接、恢复触发器订阅什么事件源,都要等计费系统那边的接口确认。 4. ~~**`files.json` 的并发写入**。并行节点会同时追加索引,需要串行化(文件锁或走 SQLite)。本文档设计的是 JSON 文件形态(符合用户要求「一个 files.json 文件」),但**并发追加是真实风险**——可能需要退化为「引擎内单写者队列」。~~ **已随 §4.6 的 v0.2 重写作废**:产物索引早就从「一个 files.json 文件」改成了 `graph_files` SQLite 表(阶段 C 已完成)。并发追加不需要文件锁——`indexNodeOutputs` 是 `settleNode` 内部的同步调用,跟 §4.3 证明 join 不需要额外锁是同一个道理(Node 单线程事件循环 + `better-sqlite3` 单写者,写表本身已经串行)。这条风险不成立,留着只是防止以后有人照旧设计去实现一个不存在的 JSON 文件锁。 5. **产物索引的 `consumedBy`/契约声明的准确性**。`graph_edge_contracts` 的 `needs`/`produces` 是**声明式**的(规划时写「这条边应该传什么」),不是实测的。如果规划 agent 声明错了,`validateGraph` 的 `data-contract` 规则能拦住「声明要吃的 kind 全图没人产」这类静态错误,但拦不住「声明的 kind 与实际语义对不上」这种语义错误。缓解方式不变:索引/契约是给 agent 的强提示(引擎直接把解析出的路径递到提示词里),不是不可绕过的约束,agent 仍可读同一 task 下任何已登记的产物。 diff --git a/docs/done/system-design/28-agent-registry-unification.md b/docs/done/system-design/28-agent-registry-unification.md index a4635e8..2d6380c 100644 --- a/docs/done/system-design/28-agent-registry-unification.md +++ b/docs/done/system-design/28-agent-registry-unification.md @@ -596,12 +596,13 @@ const AGENT_REGISTRY_TREE_RE = /^system\/agents\//; // 旧名 AGENT_RUNTIME_ST # 只认 const 声明行,否则会把注释里提到的旧名也捞进来 name=$(grep -oE '^const AGENT_[A-Z_]+_RE' server/services/graph-engine.js | sed 's/^const //' | sort -u) for f in docs/done/system-design/28-agent-registry-unification.md \ - docs/todo/system-design/27-p3b-real-run-runway.md \ - docs/todo/system-design/27-p4-parallel-and-artifacts.md; do + docs/done/system-design/27-p3b-real-run-runway.md \ + docs/done/system-design/27-p4-parallel-and-artifacts.md; do grep -q "$name" "$f" || { echo "MISS $name in $f"; exit 1; } done -grep -rn 'AGENT_RUNTIME_STATE_RE' docs/todo/system-design/*.md docs/done/system-design/28-*.md | grep -v '旧名' \ - && { echo 'STALE: 旧名未标注'; exit 1; } +grep -rn 'AGENT_RUNTIME_STATE_RE' docs/todo/system-design/*.md docs/done/system-design/28-*.md \ + docs/done/system-design/27-p3b-real-run-runway.md docs/done/system-design/27-p4-parallel-and-artifacts.md \ + | grep -v '旧名' && { echo 'STALE: 旧名未标注'; exit 1; } echo OK ``` diff --git a/docs/prd/newfunction/_spec/CONVENTIONS.md b/docs/prd/newfunction/_spec/CONVENTIONS.md index 1d53545..ae4fa09 100644 --- a/docs/prd/newfunction/_spec/CONVENTIONS.md +++ b/docs/prd/newfunction/_spec/CONVENTIONS.md @@ -152,7 +152,7 @@ res.status(<code>).json({ error: '<reason>' }) ## 12 · 不变量速查 -- 任意 `(pid, agent)` 同时只允许 1 个 status=running +- 任意 `(pid, agent, scope)` 同时只允许 1 个 status=running(`scope` 缺省时退化为旧不变量;27 号 P4 §4.2 起,图引擎并发调用同一 agent 时会传 `scope=gr:{nodeRunId}` 多加一维,避免同一 run 内两个并发节点撞会话键) - 任意 `(pid, unit)` 同时只允许 1 个 runUnit - 任意 `pid` 同时只允许 1 个 onHumanFeedback worker - 任意 ndjson 文件 append 后**永不修改已有行** diff --git a/docs/todo/system-design/27-goal-contract-format.md b/docs/todo/system-design/27-goal-contract-format.md index 55326a5..e8c96f7 100644 --- a/docs/todo/system-design/27-goal-contract-format.md +++ b/docs/todo/system-design/27-goal-contract-format.md @@ -30,7 +30,7 @@ ## 一、存储位置(稳定面) -沿用 [27-P3/P4](./27-p3-p4-work-graph-console.md) §四.A.5 已定的产物布局,本文档补上 `verdicts/` 这一格;契约本体按 [27-P4](./27-p4-parallel-and-artifacts.md) §4.6 的目录约定归并到 `tasks/{taskId}/`(**R19-B**): +沿用 [27-P3/P4](./27-p3-p4-work-graph-console.md) §四.A.5 已定的产物布局,本文档补上 `verdicts/` 这一格;契约本体按 [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) §4.6 的目录约定归并到 `tasks/{taskId}/`(**R19-B**): ``` data/companies/{cid}/ diff --git a/docs/todo/system-design/27-graph-agent-architecture.md b/docs/todo/system-design/27-graph-agent-architecture.md index 68835d9..5e9f5a2 100644 --- a/docs/todo/system-design/27-graph-agent-architecture.md +++ b/docs/todo/system-design/27-graph-agent-architecture.md @@ -2,7 +2,7 @@ > **版本**: v0.3(P0/P1 已实现;Org Graph 建模在 v0.3 修正为 company 级 workflow) > **日期**: 2026-08-15 -> **状态**(2026-08-20 校准): P0 ✅ 已实现(`graph-db.js` + `graph-seed-service.js`,见第七节);P1 ✅ 已实现(commit `226489d`,`graph-service.js`);**P2 ✅ 已实现**(设计已归档 [27-P2](../../done/system-design/27-p2-org-graph.md));**P3 主体 ✅ 已实现**(`c3abf4a` 起七轮,见 [27-P3/P4](./27-p3-p4-work-graph-console.md) §十 实现记录);**P3 收尾 ✅ 已完成并归档**([27-P3C](../../done/system-design/27-p3c-goal-task-model.md) 五批 + §十七 清尾,2026-08-20 真跑走通返工闭环);**P3B 真跑跑道 📝 仍在推进**([27-P3B](./27-p3b-real-run-runway.md),验收 7 真跑不成立、2/4/8/9 未验);**P4 📝 设计中/未实现**([27-P4](./27-p4-parallel-and-artifacts.md) 自述 v0.1 设计阶段) +> **状态**(2026-09-08 校准): P0 ✅ 已实现(`graph-db.js` + `graph-seed-service.js`,见第七节);P1 ✅ 已实现(commit `226489d`,`graph-service.js`);**P2 ✅ 已实现**(设计已归档 [27-P2](../../done/system-design/27-p2-org-graph.md));**P3 主体 ✅ 已实现**(`c3abf4a` 起七轮,见 [27-P3/P4](./27-p3-p4-work-graph-console.md) §十 实现记录);**P3 收尾 ✅ 已完成并归档**([27-P3C](../../done/system-design/27-p3c-goal-task-model.md) 五批 + §十七 清尾,2026-08-20 真跑走通返工闭环);**P3B 真跑跑道 ✅ 九条验收全部验完**([27-P3B](../../done/system-design/27-p3b-real-run-runway.md) §六quater,2026-09-08 隔离 worktree 真跑验掉最后一条验收 9:org 层回退边真的指向了 subgraph 节点);**P4 ✅ 已彻底收口**(2026-09-08,[27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) 阶段 A/B/C 全部完成,仅 T5 账户欠费门因外部计费接口未接而悬置) > > ⚠️ 这一行是别人判断「哪些文档能从 `docs/todo/` 搬去 `docs/done/`」的依据 —— 它过期会连累后面每一次迁移判断,改阶段状态时请顺手更新它。 > **命名**: 新增 agent id / 表名 / 字段名前先读 [27-NAMING 命名规范](./27-naming-conventions.md) @@ -231,7 +231,7 @@ CREATE TABLE work_graph_edges ( | **P2 Org Graph** | `company.json.agents[]` 迁移为图结构 | `org_graph_nodes`/`edges` 落地;HR/CEO 招聘走新表。细化需求与设计见 [27-P2 Org Graph 落地](../../done/system-design/27-p2-org-graph.md) | 现有 Company Dashboard 前端能正确读出 org graph 并渲染(哪怕暂时按列表展示) | | **P3+P4 图执行引擎与执行控制台**(**两个 Phase 已合并**) | **一套引擎同时驱动 org graph 与 work graph**(R-15.4 引擎泛化),带回退与熔断 | 定义层 `work_graphs`/`work_graph_nodes`/`work_graph_edges` + **两层共用的运行时层** `graph_runs`/`graph_run_nodes`/`graph_run_loop_records`(共五张新表);`graph-engine.js` 的 `advanceOne(runId)` 幂等推进原语 + 两种 node executor(`agent_call` / `subgraph`);五个控制操作(启动 / 下一步 / 暂停 / 停止 / 重试)× 手动·自动两种模式;`GraphConsole.vue` 执行控制台页面;`workgraph-planning` agent + 用户→公司任务入口。细化需求与设计见 [27-P3/P4 Work Graph 执行控制台](./27-p3-p4-work-graph-console.md) **v0.3**,落地切成**五个可独立验收的阶段**(0 验收契约 / A 引擎地基 / B 回退熔断 / C 控制台 / D 规划闭环) | 见该文档 §六 的 17 条。**核心验收点是第 6 条**:一张 `graph_kind='org'` 的图与一张 work graph 用**同一套端点、同一条代码路径**跑通「手动逐步 / 自动跑完 / 暂停恢复 / 停止」;外加第 8 条「总是失败的节点第 4 次 `force_terminated` 收敛到 `blocked`」、第 17 条「一句自然语言任务两层跑到 `completed`」 | | ~~**P4 回退 + Loop-Validator**~~ | **已并入上一行**,不再单列 | 合并理由见 [27-P3/P4](./27-p3-p4-work-graph-console.md) §二:执行引擎一旦要支持「暂停/单步/停止/重试」这种可被外部随时打断的控制语义,「先做单向、再加回退」会让第一版状态机设计得过窄,P4 加回退时必然回来重改数据模型和主循环。**两处定案变更**:① 熔断改为**引擎无条件做的整数计数**(单边 `loop_count > 3` + run 级 `backward_count > 5` 双层硬顶),不依赖 LLM,`loop_validator` 节点由「强制」降级为**可选增强**;② 结构校验从四项扩到**五项**,新增 `fail-fallback-edge`(有 `fail` 出边的节点必须留一条 `fail_reason IS NULL` 兜底边),org / work 两层都加 | 同上 | -| **P4 并行执行与产物索引**(**回收编号**) | fan-out 真并发 + 产物统一索引 | `advanceBatch` 批量拾取(手动模式退化为 `maxConcurrency=1`,行为与 P3 一致);同 agent 多实例隔离(启用 `SessionPool.getKey` 已预留但从未使用的 `scope` 参数);解除「一家公司一个 active root run」;`data/companies/{cid}/tasks/{taskId}/` 产物目录 + `files.json` 检索索引。细化设计见 [27-P4 并行执行与产物索引](./27-p4-parallel-and-artifacts.md) | 3 个并行分支同时 running 且总墙钟明显短于串行;部分失败时 fan-in 停等且可视化「在等谁」;`files.json` 可从节点行 rebuild | +| ~~**P4 并行执行与产物索引**~~ ✅(**回收编号**) | fan-out 真并发 + 产物统一索引 —— **2026-09-08 全部完成** | `advanceBatch` 批量拾取(手动模式退化为 `maxConcurrency=1`,行为与 P3 一致);同 agent 多实例隔离(`SessionPool.getKey` 的 `scope` 参数已实际接线:`graph-engine.js` 传 `gr:{nodeRunId}`);`advanceTree` 兄弟子 run 真并发;解除「一家公司一个 active root run」;`data/companies/{cid}/tasks/{taskId}/` 产物目录 + `graph_files` 表检索索引。细化设计见 [27-P4 并行执行与产物索引](../../done/system-design/27-p4-parallel-and-artifacts.md) | 3 个并行分支同时 running 且总墙钟明显短于串行;部分失败时 fan-in 停等且可视化「在等谁」;产物索引可从节点行 rebuild | | **P5 自我进化** | 公共记忆蒸馏机制 | 公共 `memory.md` 写入走自省校验 + git 版本化;company-scoped → public 的升级门槛复用现有 benchmark ≥95 链路 | 同一 public agent 被两家公司调用后,`memory.md` 有可观察的增量更新,且能 git revert 到升级前版本 | | **P6(~~可选~~ **应前置**)DevLoop 收编** | 统一循环逻辑 | 把 `loop-controller.js`/`unit-registry.js` 重构为 Work Graph 引擎上的一个具体图模板。⚠️ **定位修正**:端到端流程梳理后发现 DevLoop 不是"将来可选整合的边缘子系统",而是**本架构唯一一个已经跑通的参考实现**——它的 L1/L2/L3 三层循环、四分类失败归因、分级预算熔断、`contract_hash` 闸门、DAG 分波、SSE 埋点、前端 DAG 组件都已实现或已有完整规格(`docs/prd/newfunction/_spec/`)。通用 Work Graph 引擎应当**从它抽象而来**,而不是平行发明后再回头合并。详见 [27-E2E](./27-e2e-task-execution-flow.md) §二结论 2 | DevLoop 现有测试套件(`server/tests/devloop/**`)在新引擎上全部通过,无需单独维护两套循环代码 | @@ -268,4 +268,4 @@ P0 阶段的处理方式是**迁移脚本层面的临时保护**:`system_agent - [27-P1 虚拟引用寻址](../../done/system-design/27-p1-virtual-reference.md) — 已实现(commit `226489d`) - [27-P2 Org Graph 落地](../../done/system-design/27-p2-org-graph.md) — 设计稿 - [27-P3/P4 Work Graph 执行控制台](./27-p3-p4-work-graph-console.md) — 已实现(commit `c3abf4a`) -- [27-P4 并行执行与产物索引](./27-p4-parallel-and-artifacts.md) — 设计稿 +- [27-P4 并行执行与产物索引](../../done/system-design/27-p4-parallel-and-artifacts.md) — ✅ 已彻底收口(2026-09-08) diff --git a/docs/todo/system-design/27-p3-p4-work-graph-console.md b/docs/todo/system-design/27-p3-p4-work-graph-console.md index 7f67d85..73209c1 100644 --- a/docs/todo/system-design/27-p3-p4-work-graph-console.md +++ b/docs/todo/system-design/27-p3-p4-work-graph-console.md @@ -1069,8 +1069,15 @@ agent 默认**只挂载 `meta-research` 一个元技能**,必须主动呼叫 ` > `company-hire-org-layer.test.js` 盯着。 > > **还没还的部分,别当成还清了**:(a) 真 LLM 仍然进不了 CI(要额度、分钟级、输出不确定), - > `scripts/real-pm-smoke.js` 仍是人工跑;(b) 多节点、回退边、熔断这些形状没进这条 smoke, - > 两支各只推一个节点;(c) 按需 hire 那一段没在这条链路里走过。 + > `scripts/real-pm-smoke.js` 仍是人工跑(这条是本质限制,不是欠账,不会有"还清"的一天); + > (c) 按需 hire 那一段没在这条链路里走过。 + > + > **2026-09-08:(b) 已还上**——`graph-real-chain-smoke.test.js` 新增 ⑥⑦⑧ 三例: + > ⑥ 非法契约真的判 fail(不是 success)且判词/归因落库;⑦ 两节点图 + 一条 backward + > 兜底边,真实驱动 `nextStep` 验证失败后引擎自动沿边打回上游(不调 `retryNode`); + > ⑧ 复用同一张图反复驱动到撞顶,验证双层熔断(`loop_count > 3`)真的会把源节点收敛 + > 到 `blocked`,不是只有注入式覆盖。三例全程走真实 `AgentManager → ClaudeCliProtocol` + > (只 mock 最外层 CLI 子进程),不注入假 executor。 3. **回填对账的段落必须在本轮代码定稿之后重跑一遍核实**。**这条被连续犯了三轮**:第四轮代码把 `total_tokens` 接通了、同一轮回填的 §四.A.3 却还写着「死维度」(§八.11 / §六 验收 10 / §五 T8 三处一起错);第五轮真接了四处生产链路,文档头部的「第五轮修订」却只列改名与纠偏、四处接线一个字没提,于是 §五 T8/T11、§六 验收 1/12/17、§八.10/12/14、§十 表全部停在第四轮结论上(**第六轮清出至少 12 处过期**)。**「本轮开工时 grep 的结论」不等于「本轮交付时的事实」;改了行为就在同一轮把对应小节改对,别留给下一轮。** **第六轮的形态换了个方向,同样要记**:这一轮的过期不再是「代码改了、文档停在上一轮」,而是**「文档提前写成了完成态、代码只做了一半」**——§五 T11 写着「六个本体已建齐**并注册**」,实际只建了目录;§六 验收 10 / §五 T8 写着「实测 `spent.tokens` 递增且能单独触发 `exhausted`」,而 mock 协议把 `usage` 硬编码成 0,那个数用这套终验手法根本产不出来。**「我打算这样做」和「我做完了」在文档里长得一模一样,所以回填时要逐条问「这句话我是怎么验的」,验不出来的就不要写成实测。** diff --git a/docs/todo/system-design/27-requirement.md b/docs/todo/system-design/27-requirement.md index d273a43..89fcf9c 100644 --- a/docs/todo/system-design/27-requirement.md +++ b/docs/todo/system-design/27-requirement.md @@ -421,7 +421,7 @@ R-16 正文中的原话引用**保留 `validator-reflection` 的用词不改**ï¼ **推翻了什么**:前六轮的「端到端生产成立」结论建立在 mock 之上,而 `ClaudeCliProtocol._sendMockPrompt` **完全不读提示词**,只按 agent 名回放 fixture。第七轮换真 CLI 后,链路在第一个 work 节点就断。本条要求彻底改用真实调用来验证。 -**落点**:[27-P3B 真实运行前置](./27-p3b-real-run-runway.md)(本条派生出的工作项文档)。 +**落点**:[27-P3B 真实运行前置](../../done/system-design/27-p3b-real-run-runway.md)(本条派生出的工作项文档)。 --- @@ -438,7 +438,7 @@ R-16 正文中的原话引用**保留 `validator-reflection` 的用词不改**ï¼ > 我记得之前说过这个,要所有的 agent 把自己的产出都放在 data/company/company-id/workspace 下,你可以约束一下所有的 agent subagent graph agent,统一一下输出和输入的共享路径。这个路径下有一个 json 文件描述节点,边(输入,输出文件),时间,作为所有文件的索引。 **核实结果**:这条在 [27-REQ](./27-requirement.md) 里**没有记录**(本页此前 0 处提 workspace), -但设计确实存在 —— [27-P4](./27-p4-parallel-and-artifacts.md) §4.6「产物统一目录 + `files.json`」, +但设计确实存在 —— [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) §4.6「产物统一目录 + `files.json`」, 文中自己标着「用户要求的第 3 项,**P3 未实现**」。所以是**设计在、需求源头漏记、实现未做**。 ### R-19.2 · 建表而不是 JSON 文件(原话) @@ -486,7 +486,7 @@ git diff,最后 W13 只能把计数挪去另一个被 ignore 的文件。`file 这类图结构错误现在完全查不出来,只能等跑到那一步 agent 说「我拿不到输入」 (第四次真跑就是这么发现的)。这比事后归因强得多。 -**落点**:[27-P4](./27-p4-parallel-and-artifacts.md) §4.6 重写。 +**落点**:[27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) §4.6 重写。 --- diff --git a/docs/todo/system-design/41-graph-vs-fde-reliability-discussion.md b/docs/todo/system-design/41-graph-vs-fde-reliability-discussion.md index b161ca7..d803992 100644 --- a/docs/todo/system-design/41-graph-vs-fde-reliability-discussion.md +++ b/docs/todo/system-design/41-graph-vs-fde-reliability-discussion.md @@ -97,8 +97,8 @@ for (let i = startFromIndex; i < stepConfigs.length; i++) { // :216 **2. 但有一处明确的例外没堵上:`infra_flake` 分支绕过了这套跨层寻址** `routeFailure` 里 `infra_flake` 走的是完全独立的"原地重跑"分支(:1983-2020),**根本不查 `target_node`**,直接重跑判定失败的那个节点本身。30 号文档 §四记的那次死循环正是这个口子:诊断节点说"该重跑 validator",但地址信息被这条分支忽略,只会一遍遍重跑诊断节点自己。2026-08-23 的修复(`2f177c7`)只加了熔断计数,**没有**让这条分支也去认 `target_node`——文档原话是"没查清楚之前不改选边的语义",这是一处**故意留着、待后续设计澄清**的真实缺口,不是随口一提。 -**3. 多节点子图的并发 fan-out:这块是真的还没做,不是做坏了** -`27-p4-parallel-and-artifacts.md` 状态是"📝 设计中,未实现"——同一 agent 多实例隔离、真并发 fan-out、`files.json` 产物索引都还没上生产。所以"带分支的多节点子图"目前跑的都还是**顺序执行**,没有真正测过高并发下的资源竞争会不会暴露新问题——这不是"设计有问题被发现了 bug",是"这段设计还没写完"。 +**3.(2026-09-08 更新:本节写作时是真的,现在已经不成立)多节点子图的并发 fan-out 已经做完了** +`27-p4-parallel-and-artifacts.md` 本节写作时状态是"📝 设计中,未实现",2026-09-08 已彻底收口:同一 agent 多实例隔离、真并发 fan-out、产物索引(形态从设计里的 `files.json` 改成了 `graph_files` 表)全部上了生产,外加当时没单独列出的 `advanceTree` 兄弟子 run 并发也一并补齐。"带分支的多节点子图"现在真的会并发跑,不再是顺序执行——高并发下的资源竞争验证见 [27-P4 §4.2](../../done/system-design/27-p4-parallel-and-artifacts.md) 的接线记录。 **4. 部门级多图(37 号)也是同一类**——org graph 目前按 `company_id` 单例,一家公司只能有一张组织图,HR/财务这类跟交付流水线无关的常设职能没图可挂。调用点扫描发现改造面是最初估算的 4 倍,目前停在"设计讨论,尚未立项"。 @@ -363,7 +363,7 @@ FDE 无关,纯粹为了让 org-hunt 做真调研)反复 `rerun` 同一个 ta 已经靠 `INFRA_FLAKE_HARD_CAP` 熔断收敛,属于本条修复范围之外。这条修复堵的是更窄的 另一种情形:诊断点名的目标恰好是本节点自己真实存在的祖先时,不该绕开正常的回退 记账。回归测试见 `graph-engine.test.js` 的 `infra_flake` describe 块新增两条用例。 -- [ ] `27-p4-parallel-and-artifacts.md` 并发 fan-out 设计推进。 +- [x] `27-p4-parallel-and-artifacts.md` 并发 fan-out 设计推进——2026-09-08 已彻底收口。 - [ ] 若要落地部门级 org graph(37 号文档方向),仍是"设计讨论,尚未立项"状态。 - [ ] **可选文档补丁**:`org-hunt/digest/evolve` 是"元 agent 不能直接入图,要建原生协议独立副本" 这条红线的第一个实操案例,但这个"怎么复刻 FDE 到 graph"的操作模式本身还没被沉淀成 diff --git a/docs/todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md b/docs/todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md index 08f5f45..8f048b0 100644 --- a/docs/todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md +++ b/docs/todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md @@ -12,22 +12,21 @@ ## 一、先纠正一个前提:27 号 P4「真并行」现在剩的东西,比想象的少很多 -写这份文档前重读了 [27-P4](./27-p4-parallel-and-artifacts.md) 全文——它虽然还挂在 `todo/`,但文档 +写这份文档前重读了 [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) 全文——它虽然还挂在 `todo/`,但文档 自己 §五末尾已经写明**阶段 A(隔离)/ B(并行)/ C(产物索引)全部已实现**(2026-08-21,`advanceBatch` 真并发、`Promise.allSettled` fan-out、`graph_files` 产物索引、控制台 fan-in 可视化都已落地并有单测/真跑 验收)。这是本仓库第二次撞见「文档落后于代码」这个模式(第一次是文档自己 §3.6/§五校准块记录的)。 -**真正还没做的,只有四件小事**: +**写这份文档时还没做的,有四件小事——2026-09-08 复核时发现其中两条本身就判断错了**: | 项 | 现状 | 是否与本文档的新提案相关 | |---|---|---| -| T2:DevLoop 规格 `(pid,agent)` → `(pid,agent,scope)` | 改 HTML 重跑 `spec:extract` 即可,纯机械改动 | 无关,独立小任务 | +| ~~T2:DevLoop 规格 `(pid,agent)` → `(pid,agent,scope)`~~ ✅ | 2026-09-08 已做;且原计划「改 HTML 重跑 spec:extract」是错的——`CONVENTIONS.md` 不是任何 HTML 的生成物,是手写文件,直接改 | 无关,独立小任务 | | T5:账户欠费门 | 桩子已落地,卡在计费系统接口确认,非本仓库能单方面推进 | 无关 | -| `graph_edge_contracts.role` 列缺失 | `produces`/`needs` 落库后不可区分,只影响诊断展示 | 无关 | -| **`advanceTree` 不会同时推「兄弟子 run」** | 一次只推最深的一个 active run;两个并行 `subgraph` 节点各自的子 run 靠外层循环轮流服务,不是真并发。**这是当前唯一还成立的"并行"缺口** | **相关**——本文档 §二 的新提案建立在"节点内部并发已具备"之上,兄弟子 run 并发是它的地基 | +| ~~`graph_edge_contracts.role` 列缺失~~ ❌**这条本身是误判** | 2026-09-08 复核:`role` 列与 `normalizeContracts()` 的 produces/needs 区分早就落库了(`graph-edge-contract-role.test.js` 的提交日期早于本文档写作日期),本文档没有独立验证就照抄了 27-P4 当时那张表的过期状态 | 无关 | +| ~~**`advanceTree` 不会同时推「兄弟子 run」**~~ ✅ | 2026-09-08 已按代码里当时写好的升级路径改成 `Promise.allSettled` 并发推进 | **相关**——本文档 §二 的新提案建立在"节点内部并发已具备"之上,兄弟子 run 并发是它的地基,现已补齐 | -也就是说,「27 号 P4 真并行」作为独立议题基本可以**收口**(只剩 T2/T5/role 列三件机械/外部依赖的小事, -不需要新设计)。真正值得用 Antigravity 这篇文章来对照、能长出新东西的,是**同一个 run 内部并行做完之后, +也就是说,「27 号 P4 真并行」作为独立议题现在**已经彻底收口**(2026-09-08)。真正值得用 Antigravity 这篇文章来对照、能长出新东西的,是**同一个 run 内部并行做完之后, "要不要做竞争式的多候选并行"**这个更高一层的问题——这是 Antigravity 有而我们没有的能力,不是把 27-P4 剩下的小尾巴做完就能自动获得的。下面 §二 是这条新提案。 @@ -219,7 +218,7 @@ Antigravity 把验证器发现的缺陷提炼成一个**与具体答案无关的 | 项 | 内容 | 依赖 | |---|---|---| -| P4 收口 | T2(DevLoop 规格)、role 列、`advanceTree` 兄弟子 run 并发 | 无,可独立随时做 | +| ~~P4 收口~~ ✅ | ~~T2(DevLoop 规格)、role 列、`advanceTree` 兄弟子 run 并发~~ **2026-09-08 已全部做完**——且核实时发现这行本身有两处误判:`graph_edge_contracts.role` 列其实在本文档写作前就已落库(`graph-edge-contract-role.test.js` 早于本文档日期,见 27-P4 §四 校准),T1 的✅当时只做完了 `maxSessions` 上限,`scope` 参数从未真正接线(本轮一并补上,见 27-P4 §4.2)。三件事详见 [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) 对应小节 | — | | 提案 A(竞争候选) | 已拆分成独立的详细设计草稿,见 [50 号文档](./50-competing-candidates-critic-synthesis-design.md)——需要先找到一个真实任务撞上"单次成功率低、需要多解法比较"的场景,再决定要不要做骨架 + critic/synthesis 角色 | 无实证需求前不建议立项 | | 提案 B-1(证据泛化) | `goal-contract.js` schema 扩展 + `org-validator`/`coder-validator` agent.md 调整 | 35 号提案 D 已有的机制,扩大范围即可,成本可控 | | 提案 B-1b(`test_script` 引导) | **已评估,不建议做**——见 §3.2b,撞上 `test_script.path` 解析根不含项目根的结构性冲突,改造面远超预期 | 不立项 | @@ -233,7 +232,7 @@ B-1(证据泛化)次之;提案 A(竞争候选)需要先看 50 号文æ¡ ## 五、与既有文档的关系 -- [27-P4](./27-p4-parallel-and-artifacts.md):本文档 §一 是对它当前状态的复核,不修改它的结论; +- [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md):本文档 §一 是对它当前状态的复核,不修改它的结论; §二的竞争候选提案建立在它已完成的 fan-out 能力之上。 - [50-competing-candidates-critic-synthesis-design](./50-competing-candidates-critic-synthesis-design.md): §二提案 A 的详细设计草稿(骨架 JSON、命名、critic/synthesis 分工、预算风险量化),本文档只保留结论。 diff --git a/docs/todo/system-design/50-competing-candidates-critic-synthesis-design.md b/docs/todo/system-design/50-competing-candidates-critic-synthesis-design.md index d0b69a6..d7f47f1 100644 --- a/docs/todo/system-design/50-competing-candidates-critic-synthesis-design.md +++ b/docs/todo/system-design/50-competing-candidates-critic-synthesis-design.md @@ -6,7 +6,7 @@ > 把"如果要做,该怎么做"想清楚,供下一步决定要不要排期时参考,不是开工指令。 > **日期**: 2026-09-04 > **前置阅读**: [49 号文档](./49-antigravity-inspired-parallel-verification-proposal.md) §二(提案动机与 -> 场景边界)、[27-P4](./27-p4-parallel-and-artifacts.md)(并行执行引擎,本设计的地基)、 +> 场景边界)、[27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md)(并行执行引擎,本设计的地基)、 > [27-naming-conventions.md](./27-naming-conventions.md)(命名规则) --- @@ -160,7 +160,7 @@ agent 的多实例隔离"(`session-pool.js` 的 `scope` 参数)正好对上â - [49-antigravity-inspired-parallel-verification-proposal](./49-antigravity-inspired-parallel-verification-proposal.md): 本文档是它 §二 提案 A 的展开,结论一致——没有真实场景前不建议立项。 -- [27-P4](./27-p4-parallel-and-artifacts.md):本设计完全建立在它已完成的并行执行能力(`advanceBatch`/ +- [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md):本设计完全建立在它已完成的并行执行能力(`advanceBatch`/ 多实例 session 隔离)之上,不需要 27-P4 补任何代码。 - `system/work-graph-templates/deliverable.json`:critic/synthesis 分离的设计依据直接来自它记录的 `qa-judge` 真实反模式事故,本文档延续同一条教训。 diff --git a/server/services/graph-engine.js b/server/services/graph-engine.js index 40b0369..bd1bac5 100644 --- a/server/services/graph-engine.js +++ b/server/services/graph-engine.js @@ -438,6 +438,11 @@ async function defaultAgentExecutor(ctx) { try { res = await manager.callAgent(resolveAgentName(ctx.companyId, ctx.systemAgentId), task, { onAcpEvent, + // 27-P4 §4.2:taskId 只到「一次 run」的粒度,fan-out 撞同一个 systemAgentId 的两个 + // 并发节点(如 executor-fanout.json 的 impl-a/impl-b)taskId 相同、会话键相同, + // 会互相顶掉对方的 CLI 子进程。按节点运行实例 id 多加一维,保证并发的两次调用 + // 一定落在不同 session 上。 + sessionScope: `gr:${ctx.nodeRunId}`, metadata: { taskId: ctx.runId, layer: 'graph-node', nodeRunId: ctx.nodeRunId, // 产物目录是**提示词**里那份约定的机器可读副本(提示词由 describeArtifactContract 拼, @@ -738,6 +743,9 @@ async function defaultPlanWorkGraph(ctx) { try { res = await manager.callAgent(agentName, prompt, { onAcpEvent, + // 同 §4.2 的道理:两个并发的 subgraph 节点若引用同一个 planning 本体 + // (如两个 org node 同时规划各自的 work graph),taskId 相同会撞会话键。 + sessionScope: `gr:${ctx.nodeRunId}`, metadata: { taskId: ctx.runId, layer: 'graph-plan', nodeRunId: ctx.nodeRunId, rootRunId: ctx.rootRunId, attempt: ctx.attempt, @@ -4524,31 +4532,40 @@ function hashExport(value) { * 传子 run 拿到**零行**,循环第 0 步就 break:HTTP 回 `ok:true / auto:true`, * 而那个 run 里的 ready 节点纹丝不动(返回 ok 却什么都不做,是最坏的形态)。 * - * ponytail: 仍然只推**一个**「最深 active run」,同一 tick 里的**兄弟**子 run(比如两个并行 - * subgraph 节点各自起的子 run)不会同时被推——它们靠 runAuto 的外层循环一轮一轮轮流服务, - * 不是真并发。真并发只发生在**同一个 run 内部**(advanceBatch 的 fan-out)。上限是「兄弟子 run - * 之间」这一维的吞吐,不影响正确性(不会有子 run 被饿死,只是不同时跑);升级路径是把这个 - * for 循环也从「找到第一个 ok 就 return」改成对多个 active run 并发调 advanceBatch。 + * 兄弟子 run(比如两个并行 subgraph 节点各自起的子 run)**同一 tick 内并发推进** + * (27-P4 收口):`Promise.allSettled` 对树里每一个 active run 各调一次 `advanceBatch`。 + * 安全性同 `advanceBatch` 自己并发拾取同一 run 内候选节点的道理——不同 runId 的行互不 + * 覆盖,`transitionNode` 的逐行条件更新连「同一 run 内竞争」都兜得住,跨 run 更没有共享的 + * 可写状态;一个 run 结算时可能连带 `settleRun` 到共同的父 run,那也只是「幂等地被判定 + * 收敛好几次」(§4.3 已经论证过的浪费而非 bug)。不再需要「深度优先」:advanceBatch 对没有 + * ready 节点的 run 只会原地返回 `no-ready-node`,谁先谁后不影响正确性。 */ async function advanceTree(runId, opts = {}) { const self = getRun(runId); if (!self) return result(runId, null, 'run-not-found'); // 仍然只做索引查询:按 root_run_id 拿全树,再在内存里裁出「自己 + 后代」这棵子树。 - // 保留 listRunTree 的 created_at 顺序 —— 后建的一定更深,倒序即「最深优先」。 const scope = new Set([self.id, ...descendantRuns(self).map((r) => r.id)]); const tree = listRunTree(self.root_run_id) .filter((r) => scope.has(r.id) && r.run_status === 'running'); - // 后建的 run 一定更深(子 run 总是在父节点 running 之后建的),倒序即「最深优先」 + + const settled = await Promise.allSettled(tree.map((r) => advanceBatch(r.id, opts))); + + let firstOk = null; let informative = null; - for (const r of tree.reverse()) { - const res = await advanceBatch(r.id, opts); - if (res.ok) return res; + for (let i = 0; i < settled.length; i++) { + const s = settled[i]; + // advanceBatch 内部已经把「executor 炸了」转成 fail outcome 喂给 settleNode,不会走到 + // 这里;这一分支兜的是它自己没兜住的意外(比如 defIndex 读图定义时抛的同步异常)。 + const res = s.status === 'fulfilled' + ? s.value + : { ok: false, runId: tree[i].id, reason: 'advance-error', error: s.reason && s.reason.message }; + if (res.ok && !firstOk) firstOk = res; // minor 3:ok:false 时**不能**把所有原因都塌缩成 'no-ready-node'。子层返回 // 'budget-exhausted'(还带着 budget.exhausted 明细)而树层报「没活可干」, // 自动模式的调用方和 REST 层就分不清「预算耗尽等人工加额度」和「真的跑完了」。 - // 最深的那条有信息量的原因胜出;全是 no-ready-node 才落回原来的兜底。 if (!informative && res.reason && res.reason !== 'no-ready-node') informative = res; } + if (firstOk) return firstOk; return { ...result(runId, (getRun(runId) || {}).run_status, informative ? informative.reason : 'no-ready-node'), // budget 只有预算闸门那条路径才带,透上来给控制台顶栏用 @@ -4562,8 +4579,8 @@ async function advanceTree(runId, opts = {}) { * 自动驱动:只要 pause_requested / stop_requested 都是假就在上一次 resolve 后立刻发起下一次。 * 与 advanceTree 同款语义 —— runId 是**子树根**,对子 run 起自动就只驱动它这棵子树。 * maxSteps 只是防跑飞的安全阀,真正的终止条件是预算(§四.A.3)与熔断(§四.D.2.1)。 - * 27-P4 T4/T6 接入后,「同一个 run 内部」已经能并发;跨兄弟子 run 的并发仍是 advanceTree - * 那条 ponytail(一次只推最深的一个),不在本函数里。 + * 「同一个 run 内部」(27-P4 T4/T6)与「跨兄弟子 run」(27-P4 收口)现在都走并发, + * 这里不需要再做任何事。 */ async function runAuto(runId, opts = {}) { const maxSteps = opts.maxSteps || 1000; diff --git a/server/src/core/agent-manager.js b/server/src/core/agent-manager.js index 5536bf4..040f941 100644 --- a/server/src/core/agent-manager.js +++ b/server/src/core/agent-manager.js @@ -405,11 +405,16 @@ class AgentManager extends EventEmitter { // 优先使用 Agent 自带的 workDir —— body 模式下它已经是任务态目录 // system/agents/{aid}/{cid}/{taskId}/(getAgentConfig 已 mkdir,cwd 不存在 CLI 起不来)。 - // 会话隔离**不靠**这个路径,靠 agent.companyId / agent.taskId 进 sessionPool.getKey。 + // 会话隔离**主要**靠 agent.companyId / agent.taskId 进 sessionPool.getKey; + // 但 (companyId, taskId) 只到「一次 run」的粒度——同一 run 内两个并发节点引用同一个 + // system agent(fan-out 撞同 agentId,如 executor-fanout.json 的 impl-a/impl-b)时 + // taskId 相同、会话键相同,会互相顶掉对方的 CLI 子进程。调用方(graph-engine)按 + // 节点运行实例 id 传 options.sessionScope 来加一维,这里原样透传给 getKey 的第四个参数。 const effectiveWorkDir = agent.workDir || this.workDir; + const sessionScope = options.sessionScope; let session = await this.sessionPool.getOrCreate( - agentName, agent, resolved.launcherConfig, protocolName, effectiveWorkDir + agentName, agent, resolved.launcherConfig, protocolName, effectiveWorkDir, sessionScope ); for (let attempt = 0; attempt < 2; attempt++) { @@ -456,7 +461,7 @@ class AgentManager extends EventEmitter { }); await this.sessionPool.reset(session.key); session = await this.sessionPool.getOrCreate( - agentName, agent, resolved.launcherConfig, protocolName, effectiveWorkDir + agentName, agent, resolved.launcherConfig, protocolName, effectiveWorkDir, sessionScope ); this._emitSessionEvent({ agentName, protocol: protocolName, status: 'reconnected', diff --git a/server/tests/unit/agent-manager-session-scope.test.js b/server/tests/unit/agent-manager-session-scope.test.js new file mode 100644 index 0000000..2fe7dbb --- /dev/null +++ b/server/tests/unit/agent-manager-session-scope.test.js @@ -0,0 +1,118 @@ +/** + * Unit Tests — `options.sessionScope` 真的从 `AgentManager.callAgent` 一路传到 + * `SessionPool.getKey`(27-P4 §4.2 补的那一维) + * + * ── 这条测试还的是哪笔债 ────────────────────────────────────────────── + * [27-P4](../../../docs/todo/system-design/27-p4-parallel-and-artifacts.md) §4.2 + * 设计了「图引擎并发调用同一 system agent 时按 `gr:{nodeRunId}` 传 scope,避免撞会话键」, + * `SessionPool.getKey()` 的 `scope` 参数也确实早就在(`session-pool-scope-key.test.js` 锁的 + * 正是 `SessionPool` 自己的存取一致性),但**从 `AgentManager._runViaProtocol` 到 + * `SessionPool.getOrCreate` 这一段接线此前从未写过**——`callAgent` 的 options 解构里 + * `sessionScope` 只是摆设,`_runViaProtocol` 两处 `getOrCreate` 调用都固定只传 5 个参数。 + * + * 后果:`executor-fanout.json` 这类骨架里 `{{orgNode}}-impl-a` / `{{orgNode}}-impl-b` + * 引用同一个 `systemAgentId`,若真的被 `advanceBatch` 并发拾取(两者同一个 run, + * `taskId` 都等于 `ctx.runId`),`sessionPool.getKey` 算出来的键完全相同——第二次调用会 + * 复用第一次那个还在跑的 CLI 会话,两个"并行"分支实际上在同一个会话队列里排队执行, + * 且不报错、不留痕迹。 + * + * 本测试只钉「接线通不通」,不追真实 CLI(用注册制协议工厂换一个内存桩,同 + * `session-pool-scope-key.test.js` 的手法):真实 agent 目录 + 真实 `AgentManager.callAgent` + * 全量跑一遍,`_runViaProtocol` 不做任何桩替换。 + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { EventEmitter } from 'events'; +import fs from 'fs'; +import path from 'path'; + +const AgentManager = require('../../src/core/agent-manager'); +const { registerProtocol } = require('../../src/core/protocol-factory'); +const locator = require('../../services/agent-locator'); + +/** 只满足 SessionPool.create() 用到的方法 + runPromptInSession 要的 sendPrompt。 */ +let stubCounter = 0; +class StubProtocol extends EventEmitter { + constructor(agentInfo) { + super(); + this.agentInfo = agentInfo; + this.isConnected = false; + this.sessionId = null; + } + async connect() { this.isConnected = true; } + async initialize() {} + async newSession() { this.sessionId = `stub-session-${++stubCounter}`; } + async sendPrompt() { return undefined; } // 立即"跑完",不 emit 任何流式事件 + disconnect() { this.isConnected = false; } +} + +beforeAll(() => registerProtocol('claude-cli', () => StubProtocol)); +afterAll(() => registerProtocol('claude-cli', () => require('@mas/acp-core').ClaudeCliProtocol)); + +describe('AgentManager.callAgent — sessionScope 接线(27-P4 §4.2)', () => { + const AGENT_ID = 'zz-scope-agent'; // zz- 前缀 = 测试建的注册表条目 + const BODY_DIR = locator.bodyDir(AGENT_ID); + const CO = 'zz-scope-co'; + const ADDR = `${CO}:${AGENT_ID}`; + + const newManager = () => { + const m = new AgentManager({}, locator.REPO_ROOT, {}); + m._resolveLogSession = async () => null; + m.hookRunner = { run: async () => {} }; + return m; + }; + + beforeAll(() => { + fs.mkdirSync(BODY_DIR, { recursive: true }); + fs.writeFileSync(path.join(BODY_DIR, 'agent.md'), '# 会话作用域测试本体\n', 'utf-8'); + fs.writeFileSync(path.join(BODY_DIR, 'config.json'), + JSON.stringify({ id: AGENT_ID, name: AGENT_ID, type: 'skill', status: 'active' }, null, 2), 'utf-8'); + }); + + afterAll(() => { + fs.rmSync(locator.companyDir(AGENT_ID, CO), { recursive: true, force: true }); + }); + + it('不传 sessionScope:同一 run(同 taskId)两次调用撞上同一个会话(这是被 27-P4 §4.2 指出的问题本身)', async () => { + const manager = newManager(); + + await manager.callAgent(ADDR, 'T1', { metadata: { taskId: 'run-1' } }); + await manager.callAgent(ADDR, 'T2', { metadata: { taskId: 'run-1' } }); + + // 两次都落在同一个 taskId 下、没有 scope 区分 —— 会话池里只会有 1 个会话 + expect(manager.sessionPool.sessions.size).toBe(1); + }); + + it('传 sessionScope=gr:{nodeRunId}:同一 run 内两个并发节点各自拿到独立会话,互不覆盖', async () => { + const manager = newManager(); + + const [a, b] = await Promise.all([ + manager.callAgent(ADDR, 'impl-a', { + metadata: { taskId: 'run-2' }, sessionScope: 'gr:node-a', + }), + manager.callAgent(ADDR, 'impl-b', { + metadata: { taskId: 'run-2' }, sessionScope: 'gr:node-b', + }), + ]); + + expect(a.status).toBe('success'); + expect(b.status).toBe('success'); + // 同一个 taskId、不同 scope —— 两个会话都要留在池子里,不能有一个把另一个顶掉 + expect(manager.sessionPool.sessions.size).toBe(2); + + const keys = [...manager.sessionPool.sessions.keys()]; + expect(keys.some((k) => k.endsWith(':gr:node-a'))).toBe(true); + expect(keys.some((k) => k.endsWith(':gr:node-b'))).toBe(true); + }); + + it('断链复现:把 sessionScope 从 options 里去掉,上一条用例会退回到 1 个会话(反向验证这条测试真的在盯接线)', async () => { + const manager = newManager(); + + await Promise.all([ + manager.callAgent(ADDR, 'impl-a', { metadata: { taskId: 'run-3' } }), + manager.callAgent(ADDR, 'impl-b', { metadata: { taskId: 'run-3' } }), + ]); + + // 没有 scope 区分:并发的两次调用会挤在同一个会话键上 + expect(manager.sessionPool.sessions.size).toBe(1); + }); +}); diff --git a/server/tests/unit/graph-engine-advance-batch.test.js b/server/tests/unit/graph-engine-advance-batch.test.js index ebafef7..8f891ed 100644 --- a/server/tests/unit/graph-engine-advance-batch.test.js +++ b/server/tests/unit/graph-engine-advance-batch.test.js @@ -287,4 +287,52 @@ describe('advanceBatch(27-P4 T4/T6)', () => { expect(wdb.getRun(runId).run_status).toBe('completed'); expect(maxActive).toBeGreaterThanOrEqual(3); // entry 之后 a/b/c 应该同时在跑,而不是依次 }); + + it('advanceTree 真并发推进兄弟子 run(27-P4 收口:不再是「一次只推最深的一个」)', async () => { + // entry → sub-a, sub-b 两个并行 subgraph 节点,各自规划出一张只有一个 leaf 节点的子图。 + // 两个 subgraph 节点没有依赖关系,同一批 advanceBatch 里会一起从 ready 变 running; + // 各自的 leaf 节点则要等到**下一次** advanceTree 才会被推——这正是本用例要验的那一跳。 + const companyId = nextCompany(); + ensureAgents(['entry', 'sub-a', 'sub-b']); + const g = wdb.createWorkGraph({ + companyId, + nodes: ['entry', { nodeName: 'sub-a', nodeKind: 'subgraph' }, { nodeName: 'sub-b', nodeKind: 'subgraph' }], + edges: [{ from: 'entry', to: 'sub-a' }, { from: 'entry', to: 'sub-b' }], + }); + expect(g.errors || []).toEqual([]); + const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId, executionMode: 'manual' }); + engine.markRunReady(run.id); + const runId = run.id; + + engine.setPlanWorkGraph(async (ctx) => { + ensureAgents(['leaf']); + const childGraph = wdb.createWorkGraph({ companyId, nodes: ['leaf'], edges: [] }); + expect(childGraph.errors || []).toEqual([]); + return { workGraphId: childGraph.id }; + }); + + let active = 0; + let maxActive = 0; + engine.setAgentExecutor(async (ctx) => { + if (ctx.nodeName !== 'leaf') return { status: 'success' }; // entry:立即成功,不参与并发计数 + active += 1; + maxActive = Math.max(maxActive, active); + await new Promise((r) => { setTimeout(r, 15); }); // 真实异步延迟,制造时间窗口重叠 + active -= 1; + return { status: 'success' }; + }); + + await engine.startRun(runId, { mode: 'auto' }).driver; + + expect(wdb.getRun(runId).run_status).toBe('completed'); + // 两个子 run 各自的 leaf 节点要在同一个时间窗口里跑——如果 advanceTree 仍然只推 + // 「最深 active run」,sub-a/sub-b 的子 run 会被 runAuto 的外层循环一轮一轮轮流服务, + // maxActive 永远是 1。 + expect(maxActive).toBeGreaterThanOrEqual(2); + + const subA = nodeByName(runId, 'sub-a'); + const subB = nodeByName(runId, 'sub-b'); + expect(wdb.getRunState(subA.childRunId).nodes.find((n) => n.name === 'leaf').status).toBe('success'); + expect(wdb.getRunState(subB.childRunId).nodes.find((n) => n.name === 'leaf').status).toBe('success'); + }); }); diff --git a/server/tests/unit/graph-real-chain-smoke.test.js b/server/tests/unit/graph-real-chain-smoke.test.js index eb93d96..d07f986 100644 --- a/server/tests/unit/graph-real-chain-smoke.test.js +++ b/server/tests/unit/graph-real-chain-smoke.test.js @@ -334,6 +334,68 @@ describe('图执行真实链路 smoke(零注入,只换掉真 CLI 子进程ï¼ } }, 60_000); + it('⑧ 双层熔断真的会撞顶收敛到 blocked(27-P3/P4 §四.D.2.1,此前只有注入式覆盖)', async () => { + // 复用 ⑦ 的形状(prd --success--> sdd --fail(backward)--> prd):sdd 这份 fixture + // 每次都确定性判 input_underspecified,一直回退到同一条 backward 边, + // 真实驱动 nextStep 反复跑,直到单边硬顶 LOOP_HARD_CAP_EDGE=3 撞顶 + // (第 4 次命中该边)——验证熔断不是靠假 executor 摆出来的分支覆盖, + // 是真实链路(真 AgentManager → 真 ClaudeCliProtocol mock 回放)跑出来的收敛结果。 + const SDD_AGENT = 'sdd-planning'; + const CO = `${COMPANY}-t8`; + const db = wdb.getDb(); + const now = new Date().toISOString(); + db.prepare(`INSERT OR IGNORE INTO system_agents + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at) + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)`) + .run(SDD_AGENT, SDD_AGENT, `system/agents/${SDD_AGENT}`, now, now); + + const g = wdb.createWorkGraph({ + companyId: CO, + nodes: [ + { nodeName: 'prd', systemAgentId: AGENT }, + { nodeName: 'sdd', systemAgentId: SDD_AGENT }, + ], + edges: [ + { from: 'prd', to: 'sdd', condition: 'success' }, + { from: 'sdd', to: 'prd', condition: 'fail', edgeKind: 'backward' }, + ], + }); + expect(g.errors || []).toEqual([]); + const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId: CO, executionMode: 'manual' }); + engine.markRunReady(run.id); + engine.startRun(run.id); + + // 一路真跑到收敛(blocked)——安全阀 20 步防跑飞(每轮 prd+sdd 两步,撞顶最多需要 4 轮)。 + let lastRun = wdb.getRun(run.id); + for (let i = 0; i < 20 && lastRun.run_status === 'running'; i++) { + const step = await engine.nextStep(run.id, { task: `T8 smoke:第 ${i} 步` }); + if (!step.ok) break; // 没有 ready 节点了——要么收敛完,要么在等别的东西 + lastRun = wdb.getRun(run.id); + } + + expect(lastRun.run_status).toBe('blocked'); + + // 熔断确实是撞的**单边**硬顶(这张图只有一条 backward 边,不会先撞 run 级), + // 且账本上留痕:最后一条记录 verdict=force_terminated,loop_count 越过 LOOP_HARD_CAP_EDGE。 + const records = wdb.listLoopRecords(run.id); + expect(records.length).toBeGreaterThan(0); + const terminated = records.find((r) => r.verdict === 'force_terminated'); + expect(terminated).toBeTruthy(); + expect(terminated.loop_count).toBeGreaterThan(engine.LOOP_HARD_CAP_EDGE); + + // 被熔断收敛的是 sdd(回退边的源节点),不是 prd + const sddRow = wdb.getDb() + .prepare(`SELECT status FROM graph_run_nodes + WHERE run_id = ? AND def_node_id = (SELECT id FROM work_graph_nodes WHERE work_graph_id = ? AND node_name = ?) + ORDER BY attempt DESC LIMIT 1`) + .get(run.id, g.id, 'sdd'); + expect(sddRow.status).toBe('blocked'); + + for (const a of [SDD_AGENT, AGENT]) { + fs.rmSync(path.join(__dirname, '../../../system/agents', a, CO), { recursive: true, force: true }); + } + }, 60_000); + it('③ 边界守卫在这条路径上是**开着**的(注入假 executor 时它恒为假,那条分支零覆盖)', () => { // 判据取自引擎自己的开关函数,而不是去数 timeline 事件: // 守卫在宿主仓库干净时本来就不产生事件,拿「没有事件」当「没开」会反过来判错。