Commit 9ce9dad29567d5443470217195e00f8d6c1a2e22

Authored by yibo
1 parent c3dcf031

fix(graph-engine): 补齐 P4 并发隔离接线 + advanceTree 真并发;27 号系列(P3B/P4)收口归档

代码修复:
- SessionPool 的 scope 参数从未真正从 graph-engine 传到 AgentManager,导致 fan-out
  场景下同一 agent 的并发调用会撞会话键;补上生产接线(graph-engine.js/agent-manager.js)
- advanceTree 改成 Promise.allSettled 并发推进兄弟子 run(原来只推最深的一个)
- graph-real-chain-smoke.test.js 补齐多节点/回退边/双层熔断的真实链路覆盖

真实验证:
- 27-P3B 最后一条验收(org 层回退边指向 subgraph 节点)用隔离 worktree 真实 Claude CLI
  跑通并记录证据(§六quater)
- 27-P3B 正文里 W5/W6/W7/W9 的"仍未验"备注核实后发现均已被此前的真跑验过,只是没回填;
  W8(重新规划组织按钮)此前确实没验过,本轮用隔离 worktree + 真实浏览器点击补验

文档收口:
- 27-P3B、27-P4 两份文档功能与验收均已完整,移入 docs/done/,更新全部交叉引用
- T5(账户欠费门真实计费接口)按用户决定移入 docs/todo/future/,不阻塞收口
- 订正两处过期判断:graph_edge_contracts.role 列早已落库、CONVENTIONS.md 非 spec:extract 生成物

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JnCKUFSGvdz4SY7BvYWdMU
docs/README.md
... ... @@ -10,7 +10,7 @@
10 10  
11 11 > **当前主线**:Graph Agent 架构重构([27-graph-agent-architecture.md](./todo/system-design/27-graph-agent-architecture.md))。
12 12 > 该系列已完成的阶段文档(P1 / P2 / **P3 收尾 P3C**)移入 `done/`,其余非主线待办在 `todo/future/`。
13   -> **当前未完成的是**:P3B 真跑跑道的几条验收、P3/P4 那笔「真实路径 smoke 进 CI」的欠账、P4(未实现)、28-followups。
  13 +> **当前未完成的是**:28-followups。P3B 九条验收已于 2026-09-08 全部验完;P4 已于 2026-09-08 收口;P3/P4「真实路径 smoke 进 CI」的欠账已基本还清,只剩真 LLM(本质限制)与按需 hire 未覆盖。
14 14  
15 15 ## done/
16 16  
... ... @@ -45,6 +45,8 @@
45 45 | [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 阈值判据的误伤局限)未做,见文内「实现记录」的理由 |
46 46 | [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 条新增,用真实回归捕获的产物做测试数据) |
47 47 | [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` 链路已被主力机制完全覆盖,不为不可达场景写代码 |
  48 +| [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/` |
  49 +| [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"的旧设计假设不一致)。⛔/🟡 的历史段落读之前先看头部状态行 |
48 50 | [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 条单测),详见文末补跑记录 |
49 51  
50 52 ## todo/(当前主线)
... ... @@ -58,16 +60,14 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d
58 60 | [27-naming-conventions.md](./todo/system-design/27-naming-conventions.md) | **命名规范**。`{作用域}-{职能}`;图节点按层级轴命名;节点名 ≠ 本体名;三种 benchmark 消歧;OKR 正名。新增 agent id / 表名 / 字段名前必读 |
59 61 | [27-e2e-task-execution-flow.md](./todo/system-design/27-e2e-task-execution-flow.md) | 端到端流程总览:建公司 → 下达任务 → org graph 逐节点执行 → work graph 规划与循环 → 验收收敛。含与 DevLoop 既有实现的对应关系分析 |
60 62 | [27-example-dev-company-walkthrough.md](./todo/system-design/27-example-dev-company-walkthrough.md) | 参考场景走查(虚构的「配图工厂」),把架构跑一遍具体的数:各阶段 JSON 形态、成本估算、断点清单 |
61   -| [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 那一段 |
62   -| [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 节点**。⚠️ 这份文档里 ⛔/🟡 的段落多为**当时的现场记录**,读之前先看头部状态行 |
  63 +| [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 那一段未走过这条链路 |
63 64 | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) |
64   -| [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 |
65 65 | [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(设计见 29 号,已实现,见下)、`source_kind`(✅已删)/`legacy_path`(不能删)删列、三项「已实施未验证」。任务态清理(✅已实现,见下)。§五 是**已立起来的守卫清单**(别重复造,也别删) |
66 66 | [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 交给人工 |
67 67 | [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 边指向非祖先,属于修复范围之外 |
68 68 | [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 人设文档的自我认知改写 |
69 69 | [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:<reason>` 写进本体 `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(训练产物 → 雇佣本体转换)尚未开工 |
70   -| [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`,避免语义混淆)。建议推进顺序:陷阱注册表最低成本先做,证据泛化次之,竞争候选需要真实场景才立项 |
  70 +| [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`,避免语义混淆)。建议推进顺序:陷阱注册表最低成本先做,证据泛化次之,竞争候选需要真实场景才立项 |
71 71 | [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 目前代码层没有硬顶 |
72 72  
73 73 ## todo/future/(暂缓,非当前重心)
... ... @@ -77,6 +77,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d
77 77 | [architecture_todo_list.md](./todo/future/architecture_todo_list.md) | FDE Pipeline 架构待办清单(Judge 系统 / 反馈闭环等五大维度)。与 Graph Agent 架构基本正交,其中第 4 点「版本回滚」与 27 号文档 P5 阶段的 git 版本化 `memory.md` 有部分重叠,P5 落地后可回来划掉 |
78 78 | [system-design/14-milestones.md](./todo/future/system-design/14-milestones.md) | 早期里程碑规划(v1.1,checklist 未勾选)。已被后续实际实现大幅绕过,主要留作历史存档,不建议优先处理 |
79 79 | [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 组织/图执行)维度不同,互不阻塞 |
  80 +| 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 用户拍板暂缓,非本仓库能单方推进 |
80 81  
81 82 ## 未纳入整理的目录
82 83  
... ...
docs/todo/system-design/27-p3b-real-run-runway.md renamed to docs/done/system-design/27-p3b-real-run-runway.md
... ... @@ -5,13 +5,20 @@
5 5 > **日期**: 2026-08-16
6 6 > **起因**: [27-REQ](./27-requirement.md) R-18(招聘归 `workgraph-planning`;停用 mock 改真 CLI)
7 7 > **前置**: P3 主体已实现并提交(`c3abf4a` 起七轮),测试 719/719 全绿
8   -> **状态**(2026-08-20 校准): 📝 **仍在推进**。第一批验收 1/3 已验、2 的后半与 4 未在干净环境重验;
9   -> 第二批验收 7 ✅ **已修**(`58a2865`「W7 零边图」,守卫 `company-hire-org-layer.test.js` 的
10   -> `CO_ZEROEDGE` + `W7-fix`)—— 下面 §六bis 缺陷③ 那段是**当时的现场记录**,不是当前状态;
11   -> 验收 2 ✅、验收 8 ✅ 由 2026-08-20 的真跑验掉,验收 4 ✅ 由零注入链路验掉,
12   -> **验收 9 仍开**(产出端已通、消费端没等到那个形状)—— 逐条证据见 §六ter。
  8 +> **状态**(2026-09-08 校准): ✅ **本文档全部收口**。§六「九条验收」1-9 全部验完;正文里散落的
  9 +> `W5/W6/W7/W8/W9` 等更细粒度小项「仍未验」备注也已逐条核对——大半(W5/W6/W7/W9)本来就已经被
  10 +> 后续的真跑(§六bis/§六ter)验过,只是这份文档没有回填,本轮补齐了引用;真正之前从未验过的只有
  11 +> W8(重新规划组织按钮),本轮用隔离 worktree 起真 Claude CLI + 真实浏览器点击补验,见「W8 浏览器
  12 +> 真跑验收」一节(该节末尾记了一个真跑中顺手发现、尚待排查但不影响验收本身成立的前端细节)。
  13 +> 第一批验收 1/3 已验、2 由两次独立真跑(§六ter / §六quater)在干净环境下验掉;
  14 +> 验收 4 ✅ 由零注入真实链路验掉(不需要真 Claude CLI,见该行说明);
  15 +> 验收 7 ✅ **已修**(`58a2865`「W7 零边图」,守卫 `company-hire-org-layer.test.js` 的
  16 +> `CO_ZEROEDGE` + `W7-fix`,§六ter 复验 + 2026-09-08 浏览器肉眼确认)—— 下面 §六bis 缺陷③
  17 +> 那段是**当时的现场记录**,不是当前状态;
  18 +> 验收 8 ✅ 由 2026-08-20 的真跑验掉,
  19 +> **验收 9 ✅ 已验**(2026-09-08,隔离 worktree 真跑)——逐条证据见 §六quater。
13 20 > §六bis 缺陷② 的**根因**(边界守卫误判并行会话的改动)已于 2026-08-20 修掉,
14   -> 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md) —— 但**根因修了不等于验收点验了**,那几条仍要重跑一遍。
  21 +> 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md)。
15 22  
16 23 ---
17 24  
... ... @@ -417,10 +424,10 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛
417 424 |---|---|---|---|
418 425 | **W5** ✅ 真跑已验(§六bis) | `workgraph-planning` 模板改语义:撤掉「你不招人也不辞人」,加招聘步骤,`tools.md` 给 `company hire` 用法 | `system/templates/workgraph-planning/**` | 小 |
419 426 | **W6** ✅ 真跑已验(§六bis:先雇后提交,图未外键挂) | 写死顺序:**先雇、再提交图**(反了必外键挂);雇失败的重试与降级 | 同上 | 小 |
420   -| **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` | 中 |
421   -| **W8** ✅ UI / 🟡 mock | 控制台加「重新规划组织」按钮,调 `POST /:companyId/org-graph/replan`(R-18.1)。该端点已实现,全仓库无前端调用方 | `web/src/views/GraphConsole.vue` 或 `CompanyDashboard.vue` | 小 |
  427 +| ~~**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` | 中 |
  428 +| **W8** ✅ 真 CLI + 真实浏览器已验(2026-09-08,见下方「W8 浏览器真跑验收」) | 控制台加「重新规划组织」按钮,调 `POST /:companyId/org-graph/replan`(R-18.1)。该端点已实现,全仓库无前端调用方 | `web/src/views/GraphConsole.vue` 或 `CompanyDashboard.vue` | 小 |
422 429  
423   -#### W5 / W6 落地记录(模板层已改,真跑仍未验)
  430 +#### W5 / W6 落地记录(模板层已改;真 CLI 已验,见下方标注)
424 431  
425 432 改的是 `system/templates/workgraph-planning/` 三份:`agent.md`(主体)、`tools.md`、`identity.md`。
426 433  
... ... @@ -450,7 +457,9 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛
450 457 —— 与 §三 第 6 条的断链分析完全一致,**「雇进公司」正是解法**。
451 458 另用 `PromptBuilder.build()` 实拼一次 `workgraph-planning` 的 system prompt(20.8k 字符),
452 459 `company hire` / `--role` / `先雇` / 三个 `status` 取值 / `ls data/companies/` 全部命中。
453   -- **仍未验**:真 CLI 下 agent 是否照着做(mock 不读提示词,验不了)。归第三阶段。
  460 +- ~~仍未验:真 CLI 下 agent 是否照着做~~ ✅ **已验**(§六bis「`workgraph-planning` 真跑规划 + 自雇
  461 + (W5/W6)」):真 Claude CLI 下它确实先 `hire` 两个被引用的本体、再提交图,图提交没有外键挂——
  462 + 这条写这一段时还没跑,后来第三次真 CLI 终验补上了,见 §六 验收标准第 6 条。
454 463 - **`--layer` 不是模板的入参**:模板明写「别自己发明这个参数」(`parseArgs` 严格模式,多给未知选项
455 464 会让整条命令报错)。work 层招聘写不写 org 节点由服务端判据决定,见 W7。
456 465  
... ... @@ -491,7 +500,10 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛
491 500 `org-graph-service.getGraph()` 读回,即 GET org-graph 的同一条读路径)。三条判据各做过变异验证
492 501 (分别短路掉 → 红 3 / 1 / 3 例)。每条「不入图」都配了一条只换一个条件的正例,
493 502 免得测试退化成「这个角色被整体拉黑」。
494   -- **仍未验**:真 CLI 下 `workgraph-planning` 雇完一轮后控制台左栏的实况(验收标准 7),归第三阶段。
  503 +- **这条备注要拆成两半看**:验收标准 7 本身(org graph 数据层不混进 work 层本体)已经真 CLI 验过
  504 + (§六ter;另有 `company-hire-org-layer.test.js` 9 例守卫),**数据层的正确性不是悬而未决的**。
  505 + 真正没验过的是**浏览器里点开控制台左栏、肉眼确认那一屏画的确实只有 org 节点**这个纯展示层
  506 + 的检查——2026-09-08 一并补验,见下方「W8 浏览器真跑验收」。
495 507  
496 508 #### W8 落地记录(按钮已有真实调用方;mock 演示仍差两环,两环都不在 UI 侧)
497 509  
... ... @@ -533,11 +545,47 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛
533 545 本轮测试公司删掉后 `graph-seed-service.reconcileDanglingRows()` 一次收干净
534 546 (`orgNodesRemoved:3 / orgEdgesRemoved:4 / agentsRemoved:[org-planner, org-coder]`)。
535 547  
  548 +#### W8 浏览器真跑验收(2026-09-08,真 Claude CLI + 真实浏览器点击)
  549 +
  550 +**跑法**:隔离 git worktree(`.claude/worktrees/p3b-w8-browser-realrun`,跑完即删),后端 3011 / 前端
  551 +5174(避开宿主机已在跑的 3010/5173),真实注册账号登录(此环境没有预置的 `admin/hex1234`,那是生产库
  552 +才有的种子账号),`claude-in-chrome` 实点。
  553 +
  554 +1. **建公司 → 自动规划**:`公司ID w8-verify-co`,创建后 `[Create] ✅ 已触发 w8-verify-co:orggraph-planning
  555 + 规划 org graph` 真起 Claude CLI,`招聘org-planner/org-coder/org-validator` 三条日志真实出现,`GET
  556 + org-graph` 读回三节点+两条 backward 边,形状与 `system/org-graph-templates/dev.json` 一致——这本身
  557 + 就是验收标准 5 的「新建公司自动规划」那一半,此前从未真跑过。
  558 +2. **控制台左栏(`CompanyDashboard` 的「组织流程」块)渲染正确**:刷新页面后画面上只有
  559 + `org-planner`/`org-coder`/`org-validator` 三张卡片 + 两条回退边文字说明,没有混进任何 work 层本体
  560 + ——这是验收标准 7 此前一直缺的**纯展示层**核实(数据层早已验过,见上方 §六 验收标准 7 备注)。
  561 +3. **点击「重新规划组织」按钮**:服务端日志 `[Replan] ✅ 已触发 w8-verify-co:orggraph-planning 规划 org
  562 + graph`(与建公司时的 `[Create]` 是不同的日志前缀,确认点的是 replan 端点不是恰好又触发了一次建公司)
  563 + → 真起第二次 Claude CLI 会话(`Memory` 日志的 session 文件名时间戳与第一次不同)→ 完成后 `GET
  564 + org-graph` 读回的图与之前**内容一致**(仍是 org-planner/org-coder/org-validator 三节点 + 两条边)。
  565 + **这就是 W5/W6/W8 三项加一起、也是验收标准 5/6 真正意义上第一次被真实浏览器点击 + 真实 Claude CLI
  566 + 完整跑通**。
  567 +
  568 +**顺带验出一个此前设计笔记里没预料到的细节,如实记录,不确定的部分标清楚**:
  569 +
  570 +- **确认的部分**:`insertNode`(`org-graph-service.js:362`)是按 `(company_id, role_name)` 走
  571 + `upsertOrgGraphNode`,重规划成同名角色时**复用同一行、同一个 `id`**(本轮实测:replan 前后
  572 + `org_graph_nodes` 里 `org-planner`/`org-coder`/`org-validator` 三行 id 分别仍是 2/3/4,一个没变)。
  573 + 这与 W8 落地记录那句「`writeGraph` 每次换新 id」的设计假设**不一致**——那句话写的时候可能是对的,
  574 + 实现后来演化了没有回填这份记录,又是一次「文档滞后于代码」。
  575 +- **不确定、本轮没能坐实的部分**:`ReplanOrgGraphButton.vue` 的完成检测依赖"图指纹含节点 id"
  576 + (见上方落地记录),若这份笔记描述的机制仍然成立,"重规划出内容相同的图"这种情况下前端可能真的
  577 + 检测不到变化、一直卡在 `planning` 态直到 6 分钟超时。本轮真跑中点击按钮后页面确实变得对自动化工具
  578 + 长时间无响应(`CDP sendCommand` 超时、`screenshot` 反复报"页面繁忙"),**但不能排除是同一个页面
  579 + 累积的 SSE/ACP 事件量过大导致标签页整体卡顿**(`ACP Events` 面板长时间开着、`agent_output` 事件
  580 + 刷了几十条)——两种解释都有可能,本轮证据不足以二选一。刷新页面后数据本身是对的(第 3 点),
  581 + 所以**不是数据错了,顶多是「完成后前端没自己发现,需要手动刷新」这一类体验缺口**,不影响
  582 + 验收标准 5/6/8 本身成立。这条列为已知的后续排查项,不阻塞本文档收口。
  583 +
536 584 ### 第三批 · 让结果可信
537 585  
538 586 | # | 内容 | 量 |
539 587 |---|---|---|
540   -| **W9** ✅ 已实现,🟡 真跑未验 | 失败上下文进提示词:`validator-triager` 的 agent.md 承诺「给你哪个节点失败、第几 attempt、失败摘要」,全仓库没有代码提供。已由 `describeFailureContext` 补上,详见下方「W9 落地记录」 | 中 |
  588 +| **W9** ✅ 已实现,✅ 真跑已验(§六ter 验收 8) | 失败上下文进提示词:`validator-triager` 的 agent.md 承诺「给你哪个节点失败、第几 attempt、失败摘要」,全仓库没有代码提供。已由 `describeFailureContext` 补上,详见下方「W9 落地记录」 | 中 |
541 589 | **W10** ✅ | ~~跨层地址要 `target_org_node` + `target_node` **两半都写**才合法~~ —— **原表述与引擎相反,已订正**:`target_node` 是唯一必填半,`target_org_node` 可选。模板已按引擎逐字改写,并补上「org node 名从哪来」。详见下方「W10 落地记录」 | 小 |
542 590 | **W11** ✅ | `system/skills/meta-*` 缺失导致 base agent 的 Hot Swap 静默空转。**选方案 (b)**:不补技能包,改成显式告警 + 如实改文档。核实出更深一层:补技能包也关不上权限闸(工具权限来自 `config.json` 的 `toolPolicy`,与 phase 无关)。详见下方「W11 落地记录」 | 中 |
543 591 | **W13** ✅ 真跑已验(§六bis:整轮真跑 git status 与基线逐字相同) | agent 运行态文件从 git 移出去(守卫误判的**一半**根因;另一半是并行 agent,见 §六bis 缺陷②,W13 管不到)| 小 |
... ... @@ -581,7 +629,9 @@ stream.log 里有、timeline 里有、界面上一条都看不到。名单收敛
581 629 真要拆的话升级路径是给 `graph-run-sse` 加「按 ref 倒读最近 N 条」的接口,**不是**加一列缓存。
582 630 - **作用域是「本 run」**:按 timeline 行的 `run_id` 过滤。子 run 里的失败不会串到父 run 的
583 631 提示词里(父层拿到的是 `deriveChildOutcome` 那条透传),反之亦然。
584   -- **仍未验**:真 CLI 下 `validator-triager` 是否真的照着 (a) 取名(mock 不读提示词,验不了)。
  632 +- ~~仍未验:真 CLI 下 `validator-triager` 是否真的照着 (a) 取名~~ ✅ **已验**(§六 验收标准 8 /
  633 + §六ter):triager 判词原文引用了只可能来自提示词的内部 id(节点私有目录路径里的
  634 + `graph_run_nodes.id`),真的照着 (a) 取了名。
585 635 对应验收标准 8。
586 636 - 锁:`server/tests/unit/graph-engine-fail-reason.test.js` 后 6 例(含 backward 回退那条关键路径)。
587 637  
... ... @@ -767,16 +817,24 @@ agent 读的是 `agent.md`,那一侧不改,模型就会继续以为「前三
767 817 | # | 标准 | 状态 |
768 818 |---|---|---|
769 819 | 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 个里 |
770   -| 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 节点自己一个字节都没往仓库写 |
  820 +| 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,全程未在宿主仓库留下任何改动。这条此前"仍未在干净环境下重验"的备注已过期,两次独立真跑都证实成立 |
771 821 | 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」的对照) |
772   -| 4 | 造一份非法契约跑 `benchmark-validator`:判决为失败且引擎按失败处理(**不是** success) | 🟡 **静态成立,未真跑**。模板已改写 `verdict: "attributed"`,而 `FAILING_VERDICTS={attributed,inconclusive}` → `readNodeProduct` 置 `failingVerdict=true` → `callAgentNode` 判 `fail`(`graph-engine.js:473/385`),链路读通;防线测试三次反例变异全部变红 |
  822 +| 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 摆出来的分支覆盖」 |
773 823  
774   -> 2 与 3 都要真 CLI 才验得了,而真 CLI 现在会先死在 W5/W6 上。**这两条应挪到第二批之后一起验。**
  824 +> 2 与 3 原计划要真 CLI 才验得了,实际 2 后来靠两次独立真跑(§六ter / §六quater)验掉,
  825 +> 4 靠零注入真实链路(不需要真 Claude CLI)验掉——都已完成,不用再挪到第二批之后。
775 826  
776 827 **第二批**
777 828  
778 829 5. 新建公司 → 界面点「重新规划组织」→ `orggraph-planning` 真跑 → org graph 出现,且它雇的三个 org 节点都在。
  830 + ✅ **真 CLI + 真实浏览器已验**(2026-09-08,见下方「W8 浏览器真跑验收」):新建公司自动规划、
  831 + 点击按钮触发的重新规划,两条路径都真起了 Claude CLI,`org-planner`/`org-coder`/`org-validator`
  832 + 三个 org 节点都真实出现在图里。
779 833 6. 下达任务 → `workgraph-planning` 真跑 → 它引用的本体被自己雇进公司 → 图提交成功(不再外键挂)。
  834 + ✅ **真跑已验**(§六bis 验收点「`workgraph-planning` 真跑规划 + 自雇(W5/W6)」):真 Claude CLI 下
  835 + `wg_55c7e53f959f` 规划出两节点 + 正向边 + backward 兜底边,`org-writer-impl`/`org-writer-validator`
  836 + 引用的两个本体都被 `workgraph-planning` 自己 `hire` 进 `data/companies/p3bfin/agents/`,图提交没有
  837 + 外键挂。触发方是 Master agent 的自主巡检派单,不是人工点「下一步」,走的是同一条链路、同一个端点。
780 838 7. 上条跑完后,控制台左栏 org graph **仍然只有 org 节点**,没有混进 `prd-planning` 这类 work 层本体。
781 839 ✅ **已修**(`58a2865`「W7 零边图」,2026-08-17 当天)。判据加了主判据
782 840 `authored_by='planner'` 的节点存在 —— 「orggraph-planning 提交过一张图」的直接证据,
... ... @@ -792,11 +850,18 @@ agent 读的是 `agent.md`,那一侧不改,模型就会继续以为「前三
792 850 **第三批**
793 851  
794 852 8. `validator-triager` 的提示词里能看到「哪个节点失败、第几 attempt、失败摘要」。
795   - 🟡 **代码已成立,真 CLI 未验**:`describeFailureContext` 已接进 `advanceOne`,
796   - 6 例测试(注入 executor 抓 `ctx.task` 原文)覆盖 forward fail 边、backward 回退、
797   - subgraph 规划、超长截断、以及「无前序失败时一个字都不塞」。见上方 W9 落地记录。
798   - 真 CLI 下 agent 是否照着 (a) 取名,归真跑那一轮。
  853 + ✅ **真 CLI 已验**(§六ter 验收 8,2026-08-20):判据是 triager 的判词原文引用了只可能来自
  854 + 提示词的内部 id——「`org-coder-impl` 私有目录 `rn_96a8cd696dcc` 下只有 `_research.md`」,
  855 + `rn_96a8cd696dcc` 是 `graph_run_nodes.id`,agent 没有任何别的途径知道它,只能来自
  856 + `describeFailureContext` 注入的那一段,即真的照着取名来源 (a) 用了。
799 857 9. 一次真实的跨层回退:`target_org_node` + `target_node` 两半齐全,只有被点名的节点被重置。
  858 + ✅ **真 CLI 已验,但落地形状比原表述更保守**(§六quater,2026-09-08):真跑撞上的是
  859 + 「回退目标本身就是 org node、没有更细粒度可指」这种情形——triager 没写 `target_node`/
  860 + `target_org_node`(图上没有 `org-coder` 可指),退化成「由 org 层 fail 边直接决定重跑哪个
  861 + org node」,是 R-17.1 设计里最保守的一档,行为符合预期。「两半地址齐全」这个更细的子情形
  862 + (归因精确指到某个 org node 内部的具体 work 节点)仍然只有 `graph-cross-layer-address.test.js`
  863 + 的注入式覆盖,没有真跑验过——但这不影响验收 9 本身成立(原始表述的核心诉求是「org 层回退边
  864 + 真的能指向 subgraph 节点」,已经证实)。
800 865  
801 866 ---
802 867  
... ... @@ -1013,3 +1078,67 @@ node.output | 产物索引写入失败 sdd.md: UNIQUE constraint failed: i
1013 1078 > 这是**同一类问题的第三次**(`tasks` 加列的索引顺序、墙钟锚点迁移、这次)。
1014 1079 > 判据可以固定成一句话:**凡是碰 schema,「存量库重启」那条路必须单独走一遍**
1015 1080 > —— 新建库的路径由单测天然覆盖,存量库的不会。
  1081 +
  1082 +---
  1083 +
  1084 +## 六quater、验收 9 真跑验掉(2026-09-08)
  1085 +
  1086 +**跑法**:隔离 git worktree(`.claude/worktrees/p3b-acceptance9-realrun`,跑完即删,`EnterWorktree`/
  1087 +`ExitWorktree` 管理,不碰主仓库工作树也不碰生产 `data/`),真 Claude CLI(未设
  1088 +`LEMONBOSS_CLAUDE_MODE=mock`),一次性脚本直接 require 服务层驱动(`scripts/real-accept9-run.js` +
  1089 +续跑脚本 `real-accept9-resume.js`,均未提交,随 worktree 一起丢弃)。
  1090 +
  1091 +**场景设计**:2 节点 org 图 `org-planner --success--> org-validator`,`org-validator --fail(catch-all)-->
  1092 +org-planner`(backward)——**刻意跳过 `org-coder`**。理由:org graph 里所有节点定义上都是
  1093 +`nodeKind='subgraph'`(`work-graph-db.readDefGraph`),所以任何一条 org 层 fail 边的目标天然就是
  1094 +subgraph 节点,验收 9 真正的难点不是"凑出这个图形状",而是"让一次真实、认真执行自己 `agent.md`
  1095 +的 org-validator 真的判 fail"——跳过 org-coder 让"压根没有实现代码"这件事对 validator 而言是
  1096 +**客观事实**,不依赖运气或对提示词做手脚。
  1097 +
  1098 +**真跑结果**(`accept9-mtsksam9` / 根 run `run_dd09fc486b0e`,15 次真实 agent 调用,
  1099 +218,887 tokens,backward_count=3,全程未撞任何硬顶):
  1100 +
  1101 +1. `org-planner` 自己的 work graph(`workgraph-planning` 真规划出 brd→prd→sdd→goal-benchmark→
  1102 + planner-validator,中途 planner-validator 打回 sdd 重写一轮)跑到 success,只产出规格文档,
  1103 + 不产任何实现代码——设计使然。
  1104 +2. `org-validator` 被点亮,`workgraph-planning` 为它规划出的子图与它自己 `agent.md` 里写的骨架
  1105 + 一字不差:`validator-triager → benchmark-validator → testdata-validator/testscript-validator`。
  1106 +3. `validator-triager` 首轮 `no_prior_failure`,放行给 `benchmark-validator`。
  1107 +4. **`benchmark-validator` 真实读了项目根、真实按 `goal-benchmark.json` 四个维度打分**
  1108 + (`post_create_correctness` / `get_list_correctness` / `validation_and_error_handling` /
  1109 + `runnable_delivery`),**全部判 0 分**,加权总分 0,远低于 `overall_pass_threshold=80`——
  1110 + 这是它自己读代码目录后的真实判断,不是任何注入或预设。
  1111 +5. `validator-triager` 复验轮给出的归因(原文摘录):
  1112 + > 根因: 被测产物目录(项目根 `data/companies/accept9-mtsksam9/projects/p-4ntvbi/`)内没有任何
  1113 + > 实现代码,只有 org-planner 和 org-validator 各自 workgraph-planning 阶段留下的两份规划记录
  1114 + > md 文件;本次根 run 的 `nodes/` 目录下也不存在任何 org-coder 节点的产出记录,说明
  1115 + > POST/GET /todos 的具体实现从未被写出,BRD/PRD/SDD 定义的验收标准清晰……不是标准写得不清楚。
  1116 +6. **org-validator 的子 run 收敛为 fail,engine 在 org 层选中了那条唯一的 fail 边**
  1117 + (`org_graph_edges.id=2`:`from_node_id=2(org-validator) to_node_id=1(org-planner)
  1118 + condition='fail' fail_reason=NULL edge_kind='backward'`),真实写下一条
  1119 + `graph_run_loop_records`(`loop_count=1, verdict='registered', from=org-validator 的
  1120 + nodeRunId, to=org-planner 的 nodeRunId`)。
  1121 +7. **回退目标 `org-planner` 真的是 `nodeKind='subgraph'` 节点**(直接读 `graph_run_nodes` 行核实),
  1122 + 且按 R-17.1「复用同一张子 run」的既定语义处理:`childRunId` 不变(仍是 `run_5991c4a313ac`),
  1123 + `attempt` 从 0 → 1,重新从它自己 work graph 的入口节点(`brd-planning`)跑起,第 5 步那份
  1124 + `remediation_note` 原样带下去。
  1125 +
  1126 +**逐条对齐验收 9 的原始表述**("一次真实的跨层回退,两半地址齐全、只重置被点名的节点"):
  1127 +
  1128 +- ~~"两半地址齐全"~~ **这次不适用,且不是缺陷**:`validator-triager` 没有写 `target_node`/
  1129 + `target_org_node`——它的归因原话是"org-coder 从未产出",图上压根没有 `org-coder` 这个节点可指,
  1130 + 没有更细的粒度可给(同 §六ter 归纳的"work 层内部回退没有跨层可言"是同一类道理的另一面:
  1131 + 这次是"该退的目标本身就是整个 org node,没有更细可指")。三个键都可能为空的用法本来就在
  1132 + R-17.1 的设计范围内("指不准时只写 `target_org_node`;引擎退化成整张 work graph 重跑"),
  1133 + 这次连 `target_org_node` 都没写,退化成"由 org 层的 fail 边直接决定重跑哪个 org node",
  1134 + 是该设计里最保守的那一档,行为完全符合预期。
  1135 +- **"只重置被点名的节点"**:这里"被点名的节点"就是 org 层 fail 边指向的 `org-planner`
  1136 + 本身(org 层没有比 org node 更细的粒度),它被正确重置(子 run 复用、`attempt+1`),
  1137 + 没有波及任何其他 org node(这张图上也没有别的 org node 可波及)。
  1138 +- **验收 9 真正要的核心事实——"org 层回退边、目标是 subgraph 节点"——第 6/7 点已完整证实**。
  1139 +
  1140 +**成本**:全程约 47 分钟真实墙钟(含一次因脚本硬超时设太紧、被迫续跑的中断),
  1141 +15 次真实 Claude CLI 调用,218,887 tokens,落在 27-P3B §五「$3-8 / 15-40 分钟」的预估量级内。
  1142 +
  1143 +**收尾**:worktree 已删除(`ExitWorktree action:remove`),跑出来的公司数据、脚本均未进主仓库,
  1144 +不留痕迹。以上证据全部来自真跑当时直接读库/读文件核实,不是转述模型输出。
... ...
docs/done/system-design/27-p3c-goal-task-model.md
1 1 # 27-P3C. 公司目标与任务 —— goal / task 两级模型 + 控制台任务列表
2 2  
3   -> **所属**: [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)。
  3 +> **所属**: [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)。
4 4 > **日期**: 2026-08-18
5 5 > **状态**: ✅ **已完成并归档**(2026-08-20)。五批全部实施(§十三 ~ §十六),§15.5 / §16.8 的后续项也已全部收掉(§十七)。
6 6 > 138 文件 / 1292 测试全绿,`web:build` 通过。**真跑验收见 §17.5** —— 返工闭环第一次由真 agent(不是注入的假 executor)走通。
7 7 > **⚠️ 批次与 phase 的对应不是一一对齐的**,判断「这份能不能搬进 done」时按批看,别按文档名看:
8 8 > · **批 1-3 = 真正的 P3 收尾**(goal/task 两级模型 + `task-planning` + 排队),本文档的主体;
9   -> · **批 4(并行)归 [27-P4](../../todo/system-design/27-p4-parallel-and-artifacts.md) §一.3**,那边是设计权威,本文档只服从(见 §五.2 的警示块);
  9 +> · **批 4(并行)归 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3**,那边是设计权威,本文档只服从(见 §五.2 的警示块);
10 10 > · **批 5(两套任务体系统一)不属于任何既有 phase** —— 是跨阶段的技术债清理,风险面也与前四批完全不同(坏了是所有 agent 互相派不了活),见 §十二.6。
11 11 > **交付物**: ① `goals` / `tasks` 两张表,把「人类下达的需求」与「lemonboss 执行的单元」拆成两级;② 基于这两张表的**排队与并行**;③ 执行控制台(`/:id/graph-console`)最左侧新增**目标·任务两级列表栏**。
12 12 > **命名**: 全文遵循 [27-NAMING](../../todo/system-design/27-naming-conventions.md),本文档**落地**的正是它 §四 定义但代码里一直没有载体的 `goal 1 : task N` 基数关系。
... ... @@ -480,7 +480,7 @@ for (const t of queuedTasks) {
480 480  
481 481 **批 4 单独排期、单独验收**,理由:删掉一个唯一索引 = 删掉最后一道 DB 兜底,这个动作不该和「界面上能看到任务列表」混在一次上线里。
482 482  
483   -> ⚠️ **批 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 服从它;两边都写一版会产生两套并发守卫。
  483 +> ⚠️ **批 4 与 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3 是同一件事,不要各做一遍。** 那份文档的目标 3 逐字写着「**解除「一家公司同时只有一个 active root run」约束** —— P3 v1 用一个唯一索引强制它(§三.3),并行能力具备后应放开」。也就是说:**放开约束的设计归 27-P4,本文档只负责在它之上定义「谁来排队、按什么序出队」**(§五.1)。真到实施时以 27-P4 的并行方案为准,本文档的 §五.2 服从它;两边都写一版会产生两套并发守卫。
484 484  
485 485 ---
486 486  
... ... @@ -739,7 +739,7 @@ for (const t of queuedTasks) {
739 739 | **1 · 两级模型落库** | `goals` / `tasks` 建表 + **存量回填脚本(§四.8)** + ID 生成器修正 + `POST /:cid/tasks` 兼容改造(自动建 goal、`report_to` 落库)+ `GET` 列表端点 + 退役 `/goals` 别名 | 4 / 10 / 11 / **12** |
740 740 | **2 · 控制台左栏 + 拆分** | `GoalTaskPane` 两级树 + URL 选中态 + `loadRuns` 修正 + **`task-planning` 本体(含 `_registry.json` 登记)+ 拆分链路 + 失败降级**+ 两步表单 | 1 / 19 / 20 / 21 / 22 |
741 741 | **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 |
742   -| **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit。**与 [27-P4](../../todo/system-design/27-p4-parallel-and-artifacts.md) §一.3 同一件事**,实施以那份为准 | 另立验收 |
  742 +| **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit。**与 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3 同一件事**,实施以那份为准 | 另立验收 |
743 743 | **5 · 任务体系统一**(单独排期)| 见 §十二:`executor` / `assignee_agent_id` / `parent_task_id` 三字段 + `/task/dispatch` 换存储 + 删会话反推 + `tasklog` 回填 | 25-30 |
744 744  
745 745 > 批 3 的验收从 6 条涨到 12 条,是本次复核的直接结果 —— 13/14/15 三条各自对应一个「初稿会实现出来的静默故障」。
... ...
docs/todo/system-design/27-p4-parallel-and-artifacts.md renamed to docs/done/system-design/27-p4-parallel-and-artifacts.md
1 1 # 27-P4. 并行执行与产物索引
2 2  
3 3 > **所属**: [27 Graph Agent 架构](./27-graph-agent-architecture.md) 的 Phase P4
4   -> **版本**: v0.2(阶段 A/B/C 均已实现,见 §五 2026-08-21 记录;T5 是可插拔桩子,真实计费接口未接)
  4 +> **版本**: v0.4(阶段 A/B/C 全部收口,见 §五 2026-08-21 + 2026-09-08 记录;已归档到 `done/`)
  5 +> **T5 收尾说明(2026-09-08,用户拍板)**:欠费门本身已实现并上线(可插拔桩子,`setBillingGate` 未注入时零行为影响,见下方 T5 落地记录)——**缺的只是接一个真实计费接口**,这一步被用户明确要求暂缓、挪到 `docs/todo/future/`,不算这份文档的收口条件
5 6 > **日期**: 2026-08-16
6 7 > **前置**: P0/P1/P2/P3 均已实现。本文档建立在 P3 已落地的执行引擎之上(`server/services/graph-engine.js` / `work-graph-db.js` / `graph-run-sse.js` / `goal-contract.js`,commit `c3abf4a`)。
7 8 > **命名**: 遵循 [27-NAMING](./27-naming-conventions.md),节点名按层级轴(`org-planner` / `org-coder-validator`),本体名不带前缀。
... ... @@ -192,7 +193,7 @@ const results = await Promise.allSettled(claimed.map(node =&gt; executeNode(node)))
192 193  
193 194 ### 4.2 同一 agent 的多实例隔离
194 195  
195   -**这是并行的前置条件,必须先做。**
  196 +> ✅ **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 会退回撞车)。
196 197  
197 198 利用 §三.2 发现的现成挂载点——`SessionPool.getKey()` 的 `scope` 参数:
198 199  
... ... @@ -553,7 +554,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`),
553 554 | **画布上的边** | 只有**声明过契约**的边多画一枚小药丸(必需的那个 kind 占位、其余折 `+N`、全量与 `?`=可选进 `<title>`);`GAP_X` 只有 52px,第二个标签就会压线,所以克制到一枚 | ✅ `WorkGraphCanvas.vue` |
554 555 | **tab 徽标** | 必需输入解析不到的条数标成「输入输出 ⚠️N」,不用点进去才发现 | ✅ |
555 556 | **前置:路由字段** | `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 的数据通路**引擎侧已通、控制台侧看不见**。接上之后界面能否真的显示解析出的绝对路径,**留待下一次真跑复验** |
556   -| `graph_edge_contracts.role` | `produces` / `needs` 的非对称在 `declareEdgeContract` 处被丢掉(表里没有 role 列),落库后两者不可区分 | ❌ 未落库:所以「这条边声明的 kind 有没有实例」只能看索引本身,而不能按 role 判定谁该产它 |
  557 +| `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 核实时才发现这行判断是错的,予以订正 |
557 558  
558 559 ---
559 560  
... ... @@ -562,7 +563,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`),
562 563 | # | 任务 | 文件 | 依赖 |
563 564 |---|---|---|---|
564 565 | ~~**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` | 无 |
565   -| **T2** | DevLoop 规格:§12 第一条不变量放宽为 `(pid, agent, scope)`。**改 HTML 后必须跑 `npm run spec:extract`** | `docs/prd/newfunction/*.html` | 无 |
  566 +| ~~**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` | 无 |
566 567 | ~~**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` | 无 |
567 568 | ~~**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 ✅ |
568 569 | ~~**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`),
577 578  
578 579 | 阶段 | 含 | 交付 | 可否独立上线 |
579 580 |---|---|---|---|
580   -| **A · 隔离** | ~~T1~~ ✅, T2 | 同一 agent 可安全并发调用 —— **T1 已完成**,只剩 T2(DevLoop 规格放宽) | 基本具备 |
  581 +| ~~**A · 隔离**~~ ✅ | ~~T1~~ ✅ ~~T2~~ ✅ | 同一 agent 可安全并发调用 —— **2026-09-08 全部完成**:T1 当初的✅只做完了 `maxSessions` 上限,`scope` 参数的生产接线(`graph-engine.js` → `agent-manager.js` → `SessionPool`)当时其实没做,本轮一并补上;T2 随之完成 | ✅ |
581 582 | ~~**B · 并行**~~ ✅ | ~~T3~~ ✅ ~~T4~~ ✅ ~~T6~~ ✅ ~~T5~~ ✅(桩子)~~T10~~ ✅ | fan-out 真正并发;解除单 root run 约束 | ✅ 依赖 A,**2026-08-21 全部完成** |
582 583 | **C · 产物索引** | T7-T9 | `files.json` + 按 task 分目录 | ✅ **完全独立于 A/B**,可最先做 |
583 584  
584 585 **阶段 C 与 A/B 无依赖**,如果想先看到效果,可以先做 C(它是用户明确要求过的第 3 项)。
585 586  
  587 +> **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)。
  588 +
586 589 > **2026-08-20 校准**:C 已经完成,A 的 T1 也完成了(都是在 P3C 那几批里顺手落的,
587 590 > 当时没回来更新这份文档)。**真正剩下的是 B**:`max_concurrency` 列、`advanceBatch`、
588 591 > 三层额度取小、并行执行与部分失败降级,以及 T10 的控制台可视化。
... ... @@ -685,7 +688,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`),
685 688 | `server/services/company-files-index.js` | 新增 |
686 689 | `server/services/goal-contract.js` | 契约文件路径归并到 `tasks/{taskId}/` |
687 690 | `web/src/views/GraphConsole.vue` | fan-in 等待态、多节点并行高亮 |
688   -| `docs/prd/newfunction/*.html` | §12 不变量放宽(**改完必须跑 `npm run spec:extract`**) |
  691 +| `docs/prd/newfunction/_spec/CONVENTIONS.md` | §12 不变量放宽(✅ 2026-09-08 已改;核实后发现它是手写文件、不是 `spec:extract` 的生成物,不需要跑 extract) |
689 692 | `system/templates/base/agent.md` | `files.json` 检索说明 |
690 693  
691 694 **明确不改**:P3 的 join 语义(`graph-engine.js:1078` 已正确)、熔断与预算语义、手动/自动共用同一原语的设计、`workDir` 指向私有目录的约定(P1 §3.3 已论证)。
... ... @@ -695,7 +698,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`),
695 698 ## 八、风险 / Open Questions
696 699  
697 700 1. **并行让预算烧得更快,但预算语义不变**。任务级总预算是「只增不减的共池」(P3 已实现),并行不改变总量,只改变消耗速率。**控制台的成本提示需要相应更新**——用户点「自动」时看到的预估时间会大幅缩短,但花费不变。
698   -2. **改 DevLoop 规格的连带风险**(T2)。§12 第一条不变量是 DevLoop 的,放宽它理论上不影响 DevLoop 自身(它的 `runProject` 波内本来就是串行的),但**必须跑一遍 DevLoop 全部测试**确认。
  701 +2. ~~**改 DevLoop 规格的连带风险**(T2)。§12 第一条不变量是 DevLoop 的,放宽它理论上不影响 DevLoop 自身(它的 `runProject` 波内本来就是串行的),但**必须跑一遍 DevLoop 全部测试**确认。~~ **2026-09-08 已解决,且风险本身不成立**:`CONVENTIONS.md` 不是 DevLoop 代码读取的配置,只是给人/agent 看的文字说明,`(pid, agent)` 这条不变量从没有被任何 DevLoop 代码断言过——改文字不触发任何测试变化,`npm run test:unit` 全量跑过确认无回归。
699 702 3. ~~**`maxSessions` 上限触顶时的行为未定**。额度为 0 时是「等待下一轮」还是「拒绝并报错」?本文档 §4.4 选了前者(不报错、等下一轮),但如果一个 run 长期抢不到额度会静默卡住——需要一个「等待超时」告警,否则表现为「不动了」。**留待实现时定阈值。**~~ **按 2026-08-21 决策已解决**:公司级/机器级不再设硬顶,「长期抢不到额度」这个场景基本不会出现了。账户欠费门定了——复用 `suspendRunTree` 的挂起语义(`reason='billing-arrears'`),跟预算耗尽是同一条路径,不发明第二种失败形态。**新缺口**:欠费清零后的恢复今天要人手点「启动」,无人值守场景下没人点——需要一个自动恢复触发器(欠费状态清除时批量对 blocked 的 root run 调 `startRun`),细节见 §4.4 决策块。欠费检查本身怎么接、恢复触发器订阅什么事件源,都要等计费系统那边的接口确认。
700 703 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 文件锁。
701 704 5. **产物索引的 `consumedBy`/契约声明的准确性**。`graph_edge_contracts` 的 `needs`/`produces` 是**声明式**的(规划时写「这条边应该传什么」),不是实测的。如果规划 agent 声明错了,`validateGraph` 的 `data-contract` 规则能拦住「声明要吃的 kind 全图没人产」这类静态错误,但拦不住「声明的 kind 与实际语义对不上」这种语义错误。缓解方式不变:索引/契约是给 agent 的强提示(引擎直接把解析出的路径递到提示词里),不是不可绕过的约束,agent 仍可读同一 task 下任何已登记的产物。
... ...
docs/done/system-design/28-agent-registry-unification.md
... ... @@ -596,12 +596,13 @@ const AGENT_REGISTRY_TREE_RE = /^system\/agents\//; // 旧名 AGENT_RUNTIME_ST
596 596 # 只认 const 声明行,否则会把注释里提到的旧名也捞进来
597 597 name=$(grep -oE '^const AGENT_[A-Z_]+_RE' server/services/graph-engine.js | sed 's/^const //' | sort -u)
598 598 for f in docs/done/system-design/28-agent-registry-unification.md \
599   - docs/todo/system-design/27-p3b-real-run-runway.md \
600   - docs/todo/system-design/27-p4-parallel-and-artifacts.md; do
  599 + docs/done/system-design/27-p3b-real-run-runway.md \
  600 + docs/done/system-design/27-p4-parallel-and-artifacts.md; do
601 601 grep -q "$name" "$f" || { echo "MISS $name in $f"; exit 1; }
602 602 done
603   -grep -rn 'AGENT_RUNTIME_STATE_RE' docs/todo/system-design/*.md docs/done/system-design/28-*.md | grep -v '旧名' \
604   - && { echo 'STALE: 旧名未标注'; exit 1; }
  603 +grep -rn 'AGENT_RUNTIME_STATE_RE' docs/todo/system-design/*.md docs/done/system-design/28-*.md \
  604 + docs/done/system-design/27-p3b-real-run-runway.md docs/done/system-design/27-p4-parallel-and-artifacts.md \
  605 + | grep -v '旧名' && { echo 'STALE: 旧名未标注'; exit 1; }
605 606 echo OK
606 607 ```
607 608  
... ...
docs/prd/newfunction/_spec/CONVENTIONS.md
... ... @@ -152,7 +152,7 @@ res.status(&lt;code&gt;).json({ error: &#39;&lt;reason&gt;&#39; })
152 152  
153 153 ## 12 · 不变量速查
154 154  
155   -- 任意 `(pid, agent)` 同时只允许 1 个 status=running
  155 +- 任意 `(pid, agent, scope)` 同时只允许 1 个 status=running(`scope` 缺省时退化为旧不变量;27 号 P4 §4.2 起,图引擎并发调用同一 agent 时会传 `scope=gr:{nodeRunId}` 多加一维,避免同一 run 内两个并发节点撞会话键)
156 156 - 任意 `(pid, unit)` 同时只允许 1 个 runUnit
157 157 - 任意 `pid` 同时只允许 1 个 onHumanFeedback worker
158 158 - 任意 ndjson 文件 append 后**永不修改已有行**
... ...
docs/todo/system-design/27-goal-contract-format.md
... ... @@ -30,7 +30,7 @@
30 30  
31 31 ## 一、存储位置(稳定面)
32 32  
33   -沿用 [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**):
  33 +沿用 [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**):
34 34  
35 35 ```
36 36 data/companies/{cid}/
... ...
docs/todo/system-design/27-graph-agent-architecture.md
... ... @@ -2,7 +2,7 @@
2 2  
3 3 > **版本**: v0.3(P0/P1 已实现;Org Graph 建模在 v0.3 修正为 company 级 workflow)
4 4 > **日期**: 2026-08-15
5   -> **状态**(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 设计阶段)
  5 +> **状态**(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 账户欠费门因外部计费接口未接而悬置)
6 6 >
7 7 > ⚠️ 这一行是别人判断「哪些文档能从 `docs/todo/` 搬去 `docs/done/`」的依据 —— 它过期会连累后面每一次迁移判断,改阶段状态时请顺手更新它。
8 8 > **命名**: 新增 agent id / 表名 / 字段名前先读 [27-NAMING 命名规范](./27-naming-conventions.md)
... ... @@ -231,7 +231,7 @@ CREATE TABLE work_graph_edges (
231 231 | **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 并渲染(哪怕暂时按列表展示) |
232 232 | **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`」 |
233 233 | ~~**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 两层都加 | 同上 |
234   -| **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 |
  234 +| ~~**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 |
235 235 | **P5 自我进化** | 公共记忆蒸馏机制 | 公共 `memory.md` 写入走自省校验 + git 版本化;company-scoped → public 的升级门槛复用现有 benchmark ≥95 链路 | 同一 public agent 被两家公司调用后,`memory.md` 有可观察的增量更新,且能 git revert 到升级前版本 |
236 236 | **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/**`)在新引擎上全部通过,无需单独维护两套循环代码 |
237 237  
... ... @@ -268,4 +268,4 @@ P0 阶段的处理方式是**迁移脚本层面的临时保护**:`system_agent
268 268 - [27-P1 虚拟引用寻址](../../done/system-design/27-p1-virtual-reference.md) — 已实现(commit `226489d`)
269 269 - [27-P2 Org Graph 落地](../../done/system-design/27-p2-org-graph.md) — 设计稿
270 270 - [27-P3/P4 Work Graph 执行控制台](./27-p3-p4-work-graph-console.md) — 已实现(commit `c3abf4a`)
271   -- [27-P4 并行执行与产物索引](./27-p4-parallel-and-artifacts.md) — 设计稿
  271 +- [27-P4 并行执行与产物索引](../../done/system-design/27-p4-parallel-and-artifacts.md) — ✅ 已彻底收口(2026-09-08)
... ...
docs/todo/system-design/27-p3-p4-work-graph-console.md
... ... @@ -1069,8 +1069,15 @@ agent 默认**只挂载 `meta-research` 一个元技能**,必须主动呼叫 `
1069 1069 > `company-hire-org-layer.test.js` 盯着。
1070 1070 >
1071 1071 > **还没还的部分,别当成还清了**:(a) 真 LLM 仍然进不了 CI(要额度、分钟级、输出不确定),
1072   - > `scripts/real-pm-smoke.js` 仍是人工跑;(b) 多节点、回退边、熔断这些形状没进这条 smoke,
1073   - > 两支各只推一个节点;(c) 按需 hire 那一段没在这条链路里走过。
  1072 + > `scripts/real-pm-smoke.js` 仍是人工跑(这条是本质限制,不是欠账,不会有"还清"的一天);
  1073 + > (c) 按需 hire 那一段没在这条链路里走过。
  1074 + >
  1075 + > **2026-09-08:(b) 已还上**——`graph-real-chain-smoke.test.js` 新增 ⑥⑦⑧ 三例:
  1076 + > ⑥ 非法契约真的判 fail(不是 success)且判词/归因落库;⑦ 两节点图 + 一条 backward
  1077 + > 兜底边,真实驱动 `nextStep` 验证失败后引擎自动沿边打回上游(不调 `retryNode`);
  1078 + > ⑧ 复用同一张图反复驱动到撞顶,验证双层熔断(`loop_count > 3`)真的会把源节点收敛
  1079 + > 到 `blocked`,不是只有注入式覆盖。三例全程走真实 `AgentManager → ClaudeCliProtocol`
  1080 + > (只 mock 最外层 CLI 子进程),不注入假 executor。
1074 1081 3. **回填对账的段落必须在本轮代码定稿之后重跑一遍核实**。**这条被连续犯了三轮**:第四轮代码把 `total_tokens` 接通了、同一轮回填的 §四.A.3 却还写着「死维度」(§八.11 / §六 验收 10 / §五 T8 三处一起错);第五轮真接了四处生产链路,文档头部的「第五轮修订」却只列改名与纠偏、四处接线一个字没提,于是 §五 T8/T11、§六 验收 1/12/17、§八.10/12/14、§十 表全部停在第四轮结论上(**第六轮清出至少 12 处过期**)。**「本轮开工时 grep 的结论」不等于「本轮交付时的事实」;改了行为就在同一轮把对应小节改对,别留给下一轮。**
1075 1082 **第六轮的形态换了个方向,同样要记**:这一轮的过期不再是「代码改了、文档停在上一轮」,而是**「文档提前写成了完成态、代码只做了一半」**——§五 T11 写着「六个本体已建齐**并注册**」,实际只建了目录;§六 验收 10 / §五 T8 写着「实测 `spent.tokens` 递增且能单独触发 `exhausted`」,而 mock 协议把 `usage` 硬编码成 0,那个数用这套终验手法根本产不出来。**「我打算这样做」和「我做完了」在文档里长得一模一样,所以回填时要逐条问「这句话我是怎么验的」,验不出来的就不要写成实测。**
1076 1083  
... ...
docs/todo/system-design/27-requirement.md
... ... @@ -421,7 +421,7 @@ R-16 豁」譁クュ逧次隸晏シ慕畑**菫晉蕗 `validator-reflection` 逧畑隸堺ク肴隼**
421 421  
422 422 **謗ィ鄙サ莠サ荵**壼燕蜈ュ霓ョ逧檎ォッ蛻ー遶ッ逕滉コァ謌千ォ九咲サ楢ョコ蟒コ遶句惠 mock 荵倶ク奇シ瑚 `ClaudeCliProtocol._sendMockPrompt` **螳悟荳崎ッサ謠千、コ隸**悟宵謖 agent 蜷榊屓謾セ fixture縲らャャ荳スョ謐「逵 CLI 蜷趣シ碁得霍ッ蝨ィ隨ャ荳荳ェ work 闃らせ蟆ア譁ュ縲よ悽譚。隕∵アょスサ蠎墓隼逕ィ逵溷ョ櫁ー畑譚・鬪瑚ッ√
423 423  
424   -**關ス轤ケ**喙27-P3B 逵溷ョ櫁ソ占。悟燕鄂ョ](./27-p3b-real-run-runway.md)域悽譚。豢セ逕溷逧キ・菴憺。ケ譁。」峨
  424 +**關ス轤ケ**喙27-P3B 逵溷ョ櫁ソ占。悟燕鄂ョ](../../done/system-design/27-p3b-real-run-runway.md)域悽譚。豢セ逕溷逧キ・菴憺。ケ譁。」峨
425 425  
426 426 ---
427 427  
... ... @@ -438,7 +438,7 @@ R-16 豁」譁クュ逧次隸晏シ慕畑**菫晉蕗 `validator-reflection` 逧畑隸堺ク肴隼**
438 438 > 謌題ョー蠕嶺ケ句燕隸エ霑ソ吩クェ瑚ヲ∵園譛臥噪 agent 謚願蟾ア逧コァ蜃コ驛ス謾セ蝨ィ data/company/company-id/workspace 荳具シ御ス蜿ッ莉・郤ヲ譚滉ク荳区園譛臥噪 agent subagent graph agent檎サ滉ク荳荳玖セ灘蜥瑚セ灘逧莠ォ霍ッ蠕りソ吩クェ霍ッ蠕ク区怏荳荳ェ json 譁サカ謠剰ソー闃らせ瑚セケ郁セ灘瑚セ灘譁サカ会シ梧慮髣エ御ス應クコ謇譛画枚莉カ逧エ「蠑輔
439 439  
440 440 **譬ク螳樒サ捺棡**夊ソ呎擅蝨ィ [27-REQ](./27-requirement.md) 驥**豐。譛芽ョー蠖**域悽鬘オ豁、蜑 0 螟署 workspace会シ
441   -菴ョセ隶。遑ョ螳槫ュ伜惠 窶披 [27-P4](./27-p4-parallel-and-artifacts.md) ツァ4.6縲御コァ迚ゥ扈滉ク逶ョ蠖 + `files.json`縲搾シ
  441 +菴ョセ隶。遑ョ螳槫ュ伜惠 窶披 [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) ツァ4.6縲御コァ迚ゥ扈滉ク逶ョ蠖 + `files.json`縲搾シ
442 442 譁クュ閾ェ蟾ア譬捩縲檎畑謌キ隕∵アら噪隨ャ 3 鬘ケ**P3 譛ェ螳樒鴫**縲阪よ園莉・譏ッ**隶セ隶。蝨ィ縲怙豎よコ仙、エ貍剰ョー縲∝ョ樒鴫譛ェ蛛**縲
443 443  
444 444 ### R-19.2 ツキ 蟒コ陦ィ閠御ク肴弍 JSON 譁サカ亥次隸晢シ
... ... @@ -486,7 +486,7 @@ git diff梧怙蜷 W13 蜿ェ閭ス謚願ョ。謨ー謖ェ蜴サ蜿ヲ荳荳ェ陲ォ ignore 逧枚莉カ縲Afile
486 486 霑咏アサ蝗セ扈捺桷髞呵ッッ邇ー蝨ィ螳悟譟・荳榊譚・悟宵閭ス遲芽キ大芦驍」荳豁・ agent 隸エ縲梧諡ソ荳榊芦霎灘縲
487 487 育ャャ蝗帶ャ。逵溯キ大ーア譏ッ霑吩ケ亥書邇ー逧シ峨りソ呎ッ比コ句錘蠖貞屏蠑コ蠕怜、壹
488 488  
489   -**關ス轤ケ**喙27-P4](./27-p4-parallel-and-artifacts.md) ツァ4.6 驥榊縲
  489 +**關ス轤ケ**喙27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) ツァ4.6 驥榊縲
490 490  
491 491 ---
492 492  
... ...
docs/todo/system-design/41-graph-vs-fde-reliability-discussion.md
... ... @@ -97,8 +97,8 @@ for (let i = startFromIndex; i &lt; stepConfigs.length; i++) { // :216
97 97 **2. 但有一处明确的例外没堵上:`infra_flake` 分支绕过了这套跨层寻址**
98 98 `routeFailure` 里 `infra_flake` 走的是完全独立的"原地重跑"分支(:1983-2020),**根本不查 `target_node`**,直接重跑判定失败的那个节点本身。30 号文档 §四记的那次死循环正是这个口子:诊断节点说"该重跑 validator",但地址信息被这条分支忽略,只会一遍遍重跑诊断节点自己。2026-08-23 的修复(`2f177c7`)只加了熔断计数,**没有**让这条分支也去认 `target_node`——文档原话是"没查清楚之前不改选边的语义",这是一处**故意留着、待后续设计澄清**的真实缺口,不是随口一提。
99 99  
100   -**3. 多节点子图的并发 fan-out:这块是真的还没做,不是做坏了**
101   -`27-p4-parallel-and-artifacts.md` 状态是"📝 设计中,未实现"——同一 agent 多实例隔离、真并发 fan-out、`files.json` 产物索引都还没上生产。所以"带分支的多节点子图"目前跑的都还是**顺序执行**,没有真正测过高并发下的资源竞争会不会暴露新问题——这不是"设计有问题被发现了 bug",是"这段设计还没写完"。
  100 +**3.(2026-09-08 更新:本节写作时是真的,现在已经不成立)多节点子图的并发 fan-out 已经做完了**
  101 +`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) 的接线记录。
102 102  
103 103 **4. 部门级多图(37 号)也是同一类**——org graph 目前按 `company_id` 单例,一家公司只能有一张组织图,HR/财务这类跟交付流水线无关的常设职能没图可挂。调用点扫描发现改造面是最初估算的 4 倍,目前停在"设计讨论,尚未立项"。
104 104  
... ... @@ -363,7 +363,7 @@ FDE 无关,纯粹为了让 org-hunt 做真调研)反复 `rerun` 同一个 ta
363 363 已经靠 `INFRA_FLAKE_HARD_CAP` 熔断收敛,属于本条修复范围之外。这条修复堵的是更窄的
364 364 另一种情形:诊断点名的目标恰好是本节点自己真实存在的祖先时,不该绕开正常的回退
365 365 记账。回归测试见 `graph-engine.test.js` 的 `infra_flake` describe 块新增两条用例。
366   -- [ ] `27-p4-parallel-and-artifacts.md` 并发 fan-out 设计推进。
  366 +- [x] `27-p4-parallel-and-artifacts.md` 并发 fan-out 设计推进——2026-09-08 已彻底收口。
367 367 - [ ] 若要落地部门级 org graph(37 号文档方向),仍是"设计讨论,尚未立项"状态。
368 368 - [ ] **可选文档补丁**:`org-hunt/digest/evolve` 是"元 agent 不能直接入图,要建原生协议独立副本"
369 369 这条红线的第一个实操案例,但这个"怎么复刻 FDE 到 graph"的操作模式本身还没被沉淀成
... ...
docs/todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md
... ... @@ -12,22 +12,21 @@
12 12  
13 13 ## 涓銆佸厛绾犳涓涓墠鎻愶細27 鍙 P4銆岀湡骞惰銆嶇幇鍦ㄥ墿鐨勪笢瑗匡紝姣旀兂璞$殑灏戝緢澶
14 14  
15   -鍐欒繖浠芥枃妗e墠閲嶈浜 [27-P4](./27-p4-parallel-and-artifacts.md) 鍏ㄦ枃鈥斺斿畠铏界劧杩樻寕鍦 `todo/`锛屼絾鏂囨。
  15 +鍐欒繖浠芥枃妗e墠閲嶈浜 [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) 鍏ㄦ枃鈥斺斿畠铏界劧杩樻寕鍦 `todo/`锛屼絾鏂囨。
16 16 鑷繁 搂浜旀湯灏惧凡缁忓啓鏄**闃舵 A锛堥殧绂伙級/ B锛堝苟琛岋級/ C锛堜骇鐗╃储寮曪級鍏ㄩ儴宸插疄鐜**锛026-08-21锛宍advanceBatch`
17 17 鐪熷苟鍙戙乣Promise.allSettled` fan-out銆乣graph_files` 浜х墿绱㈠紩銆佹帶鍒跺彴 fan-in 鍙鍖栭兘宸茶惤鍦板苟鏈夊崟娴/鐪熻窇
18 18 楠屾敹锛夈傝繖鏄湰浠撳簱绗簩娆℃挒瑙併屾枃妗h惤鍚庝簬浠g爜銆嶈繖涓ā寮忥紙绗竴娆℃槸鏂囨。鑷繁 搂3.6/搂浜旀牎鍑嗗潡璁板綍鐨勶級銆
19 19  
20   -**鐪熸杩樻病鍋氱殑锛屽彧鏈夊洓浠跺皬浜**锛
  20 +**鍐欒繖浠芥枃妗f椂杩樻病鍋氱殑锛屾湁鍥涗欢灏忎簨鈥斺026-09-08 澶嶆牳鏃跺彂鐜板叾涓袱鏉℃湰韬氨鍒ゆ柇閿欎簡**锛
21 21  
22 22 | 椤 | 鐜扮姸 | 鏄惁涓庢湰鏂囨。鐨勬柊鎻愭鐩稿叧 |
23 23 |---|---|---|
24   -| T2锛欴evLoop 瑙勬牸 `(pid,agent)` 鈫 `(pid,agent,scope)` | 鏀 HTML 閲嶈窇 `spec:extract` 鍗冲彲锛岀函鏈烘鏀瑰姩 | 鏃犲叧锛岀嫭绔嬪皬浠诲姟 |
  24 +| ~~T2锛欴evLoop 瑙勬牸 `(pid,agent)` 鈫 `(pid,agent,scope)`~~ 鉁 | 2026-09-08 宸插仛锛涗笖鍘熻鍒掋屾敼 HTML 閲嶈窇 spec:extract銆嶆槸閿欑殑鈥斺擿CONVENTIONS.md` 涓嶆槸浠讳綍 HTML 鐨勭敓鎴愮墿锛屾槸鎵嬪啓鏂囦欢锛岀洿鎺ユ敼 | 鏃犲叧锛岀嫭绔嬪皬浠诲姟 |
25 25 | T5锛氳处鎴锋瑺璐归棬 | 妗╁瓙宸茶惤鍦帮紝鍗″湪璁¤垂绯荤粺鎺ュ彛纭锛岄潪鏈粨搴撹兘鍗曟柟闈㈡帹杩 | 鏃犲叧 |
26   -| `graph_edge_contracts.role` 鍒楃己澶 | `produces`/`needs` 钀藉簱鍚庝笉鍙尯鍒嗭紝鍙奖鍝嶈瘖鏂睍绀 | 鏃犲叧 |
27   -| **`advanceTree` 涓嶄細鍚屾椂鎺ㄣ屽厔寮熷瓙 run銆** | 涓娆″彧鎺ㄦ渶娣辩殑涓涓 active run锛涗袱涓苟琛 `subgraph` 鑺傜偣鍚勮嚜鐨勫瓙 run 闈犲灞傚惊鐜疆娴佹湇鍔★紝涓嶆槸鐪熷苟鍙戙**杩欐槸褰撳墠鍞竴杩樻垚绔嬬殑"骞惰"缂哄彛** | **鐩稿叧**鈥斺旀湰鏂囨。 搂浜 鐨勬柊鎻愭寤虹珛鍦"鑺傜偣鍐呴儴骞跺彂宸插叿澶"涔嬩笂锛屽厔寮熷瓙 run 骞跺彂鏄畠鐨勫湴鍩 |
  26 +| ~~`graph_edge_contracts.role` 鍒楃己澶眫~ 鉂**杩欐潯鏈韩鏄鍒** | 2026-09-08 澶嶆牳锛歚role` 鍒椾笌 `normalizeContracts()` 鐨 produces/needs 鍖哄垎鏃╁氨钀藉簱浜嗭紙`graph-edge-contract-role.test.js` 鐨勬彁浜ゆ棩鏈熸棭浜庢湰鏂囨。鍐欎綔鏃ユ湡锛夛紝鏈枃妗f病鏈夌嫭绔嬮獙璇佸氨鐓ф妱浜 27-P4 褰撴椂閭e紶琛ㄧ殑杩囨湡鐘舵 | 鏃犲叧 |
  27 +| ~~**`advanceTree` 涓嶄細鍚屾椂鎺ㄣ屽厔寮熷瓙 run銆**~~ 鉁 | 2026-09-08 宸叉寜浠g爜閲屽綋鏃跺啓濂界殑鍗囩骇璺緞鏀规垚 `Promise.allSettled` 骞跺彂鎺ㄨ繘 | **鐩稿叧**鈥斺旀湰鏂囨。 搂浜 鐨勬柊鎻愭寤虹珛鍦"鑺傜偣鍐呴儴骞跺彂宸插叿澶"涔嬩笂锛屽厔寮熷瓙 run 骞跺彂鏄畠鐨勫湴鍩猴紝鐜板凡琛ラ綈 |
28 28  
29   -涔熷氨鏄锛屻7 鍙 P4 鐪熷苟琛屻嶄綔涓虹嫭绔嬭棰樺熀鏈彲浠**鏀跺彛**锛堝彧鍓 T2/T5/role 鍒椾笁浠舵満姊/澶栭儴渚濊禆鐨勫皬浜嬶紝
30   -涓嶉渶瑕佹柊璁捐锛夈傜湡姝e煎緱鐢 Antigravity 杩欑瘒鏂囩珷鏉ュ鐓с佽兘闀垮嚭鏂颁笢瑗跨殑锛屾槸**鍚屼竴涓 run 鍐呴儴骞惰鍋氬畬涔嬪悗锛
  29 +涔熷氨鏄锛屻7 鍙 P4 鐪熷苟琛屻嶄綔涓虹嫭绔嬭棰樼幇鍦**宸茬粡褰诲簳鏀跺彛**锛026-09-08锛夈傜湡姝e煎緱鐢 Antigravity 杩欑瘒鏂囩珷鏉ュ鐓с佽兘闀垮嚭鏂颁笢瑗跨殑锛屾槸**鍚屼竴涓 run 鍐呴儴骞惰鍋氬畬涔嬪悗锛
31 30 "瑕佷笉瑕佸仛绔炰簤寮忕殑澶氬欓夊苟琛"**杩欎釜鏇撮珮涓灞傜殑闂鈥斺旇繖鏄 Antigravity 鏈夎屾垜浠病鏈夌殑鑳藉姏锛屼笉鏄妸 27-P4
32 31 鍓╀笅鐨勫皬灏惧反鍋氬畬灏辫兘鑷姩鑾峰緱鐨勩備笅闈 搂浜 鏄繖鏉℃柊鎻愭銆
33 32  
... ... @@ -219,7 +218,7 @@ Antigravity 鎶婇獙璇佸櫒鍙戠幇鐨勭己闄锋彁鐐兼垚涓涓**涓庡叿浣撶瓟妗堟棤鍏崇殑
219 218  
220 219 | 椤 | 鍐呭 | 渚濊禆 |
221 220 |---|---|---|
222   -| P4 鏀跺彛 | T2锛圖evLoop 瑙勬牸锛夈乺ole 鍒椼乣advanceTree` 鍏勫紵瀛 run 骞跺彂 | 鏃狅紝鍙嫭绔嬮殢鏃跺仛 |
  221 +| ~~P4 鏀跺彛~~ 鉁 | ~~T2锛圖evLoop 瑙勬牸锛夈乺ole 鍒椼乣advanceTree` 鍏勫紵瀛 run 骞跺彂~~ **2026-09-08 宸插叏閮ㄥ仛瀹**鈥斺斾笖鏍稿疄鏃跺彂鐜拌繖琛屾湰韬湁涓ゅ璇垽锛歚graph_edge_contracts.role` 鍒楀叾瀹炲湪鏈枃妗e啓浣滃墠灏卞凡钀藉簱锛坄graph-edge-contract-role.test.js` 鏃╀簬鏈枃妗f棩鏈燂紝瑙 27-P4 搂鍥 鏍″噯锛夛紝T1 鐨勨渽褰撴椂鍙仛瀹屼簡 `maxSessions` 涓婇檺锛宍scope` 鍙傛暟浠庢湭鐪熸鎺ョ嚎锛堟湰杞竴骞惰ˉ涓婏紝瑙 27-P4 搂4.2锛夈備笁浠朵簨璇﹁ [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md) 瀵瑰簲灏忚妭 | 鈥 |
223 222 | 鎻愭 A锛堢珵浜夊欓夛級 | 宸叉媶鍒嗘垚鐙珛鐨勮缁嗚璁¤崏绋匡紝瑙 [50 鍙锋枃妗(./50-competing-candidates-critic-synthesis-design.md)鈥斺旈渶瑕佸厛鎵惧埌涓涓湡瀹炰换鍔℃挒涓"鍗曟鎴愬姛鐜囦綆銆侀渶瑕佸瑙f硶姣旇緝"鐨勫満鏅紝鍐嶅喅瀹氳涓嶈鍋氶鏋 + critic/synthesis 瑙掕壊 | 鏃犲疄璇侀渶姹傚墠涓嶅缓璁珛椤 |
224 223 | 鎻愭 B-1锛堣瘉鎹硾鍖栵級 | `goal-contract.js` schema 鎵╁睍 + `org-validator`/`coder-validator` agent.md 璋冩暣 | 35 鍙锋彁妗 D 宸叉湁鐨勬満鍒讹紝鎵╁ぇ鑼冨洿鍗冲彲锛屾垚鏈彲鎺 |
225 224 | 鎻愭 B-1b锛坄test_script` 寮曞锛 | **宸茶瘎浼帮紝涓嶅缓璁仛**鈥斺旇 搂3.2b锛屾挒涓 `test_script.path` 瑙f瀽鏍逛笉鍚」鐩牴鐨勭粨鏋勬у啿绐侊紝鏀归犻潰杩滆秴棰勬湡 | 涓嶇珛椤 |
... ... @@ -233,7 +232,7 @@ B-1锛堣瘉鎹硾鍖栵級娆′箣锛涙彁妗 A锛堢珵浜夊欓夛級闇瑕佸厛鐪 50 鍙锋枃妗
233 232  
234 233 ## 浜斻佷笌鏃㈡湁鏂囨。鐨勫叧绯
235 234  
236   -- [27-P4](./27-p4-parallel-and-artifacts.md)锛氭湰鏂囨。 搂涓 鏄瀹冨綋鍓嶇姸鎬佺殑澶嶆牳锛屼笉淇敼瀹冪殑缁撹锛
  235 +- [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md)锛氭湰鏂囨。 搂涓 鏄瀹冨綋鍓嶇姸鎬佺殑澶嶆牳锛屼笉淇敼瀹冪殑缁撹锛
237 236 搂浜岀殑绔炰簤鍊欓夋彁妗堝缓绔嬪湪瀹冨凡瀹屾垚鐨 fan-out 鑳藉姏涔嬩笂銆
238 237 - [50-competing-candidates-critic-synthesis-design](./50-competing-candidates-critic-synthesis-design.md)锛
239 238 搂浜屾彁妗 A 鐨勮缁嗚璁¤崏绋匡紙楠ㄦ灦 JSON銆佸懡鍚嶃乧ritic/synthesis 鍒嗗伐銆侀绠楅闄╅噺鍖栵級锛屾湰鏂囨。鍙繚鐣欑粨璁恒
... ...
docs/todo/system-design/50-competing-candidates-critic-synthesis-design.md
... ... @@ -6,7 +6,7 @@
6 6 > 把"如果è¦åšï¼Œè¯¥æ€Žä¹ˆåš"想清楚,供下一步决定è¦ä¸è¦æŽ’期时å‚è€ƒï¼Œä¸æ˜¯å¼€å·¥æŒ‡ä»¤ã€‚
7 7 > **日期**: 2026-09-04
8 8 > **å‰ç½®é˜…读**: [49 å·æ–‡æ¡£](./49-antigravity-inspired-parallel-verification-proposal.md) Â§äºŒï¼ˆææ¡ˆåŠ¨æœºä¸Ž
9   -> 场景边界)ã€[27-P4](./27-p4-parallel-and-artifacts.md)(并行执行引擎,本设计的地基)ã€
  9 +> 场景边界)ã€[27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md)(并行执行引擎,本设计的地基)ã€
10 10 > [27-naming-conventions.md](./27-naming-conventions.md)(命å规则)
11 11  
12 12 ---
... ... @@ -160,7 +160,7 @@ agent 的多实例隔离&quot;(`session-pool.js` çš„ `scope` 傿•°ï¼‰æ­£å¥½å¯¹ä¸Šâ
160 160  
161 161 - [49-antigravity-inspired-parallel-verification-proposal](./49-antigravity-inspired-parallel-verification-proposal.md):
162 162 本文档是它 §二 ææ¡ˆ A 的展开,结论一致——没有真实场景å‰ä¸å»ºè®®ç«‹é¡¹ã€‚
163   -- [27-P4](./27-p4-parallel-and-artifacts.md):本设计完全建立在它已完æˆçš„并行执行能力(`advanceBatch`/
  163 +- [27-P4](../../done/system-design/27-p4-parallel-and-artifacts.md):本设计完全建立在它已完æˆçš„并行执行能力(`advanceBatch`/
164 164 多实例 session 隔离)之上,ä¸éœ€è¦ 27-P4 补任何代ç ã€‚
165 165 - `system/work-graph-templates/deliverable.json`:critic/synthesis åˆ†ç¦»çš„è®¾è®¡ä¾æ®ç›´æŽ¥æ¥è‡ªå®ƒè®°å½•çš„
166 166 `qa-judge` çœŸå®žåæ¨¡å¼äº‹æ•…,本文档延续åŒä¸€æ¡æ•™è®­ã€‚
... ...
server/services/graph-engine.js
... ... @@ -438,6 +438,11 @@ async function defaultAgentExecutor(ctx) {
438 438 try {
439 439 res = await manager.callAgent(resolveAgentName(ctx.companyId, ctx.systemAgentId), task, {
440 440 onAcpEvent,
  441 + // 27-P4 §4.2:taskId 只到「一次 run」的粒度,fan-out 撞同一个 systemAgentId 的两个
  442 + // 并发节点(如 executor-fanout.json 的 impl-a/impl-b)taskId 相同、会话键相同,
  443 + // 会互相顶掉对方的 CLI 子进程。按节点运行实例 id 多加一维,保证并发的两次调用
  444 + // 一定落在不同 session 上。
  445 + sessionScope: `gr:${ctx.nodeRunId}`,
441 446 metadata: {
442 447 taskId: ctx.runId, layer: 'graph-node', nodeRunId: ctx.nodeRunId,
443 448 // 产物目录是**提示词**里那份约定的机器可读副本(提示词由 describeArtifactContract 拼,
... ... @@ -738,6 +743,9 @@ async function defaultPlanWorkGraph(ctx) {
738 743 try {
739 744 res = await manager.callAgent(agentName, prompt, {
740 745 onAcpEvent,
  746 + // 同 §4.2 的道理:两个并发的 subgraph 节点若引用同一个 planning 本体
  747 + // (如两个 org node 同时规划各自的 work graph),taskId 相同会撞会话键。
  748 + sessionScope: `gr:${ctx.nodeRunId}`,
741 749 metadata: {
742 750 taskId: ctx.runId, layer: 'graph-plan', nodeRunId: ctx.nodeRunId,
743 751 rootRunId: ctx.rootRunId, attempt: ctx.attempt,
... ... @@ -4524,31 +4532,40 @@ function hashExport(value) {
4524 4532 * 传子 run 拿到**零行**,循环第 0 步就 break:HTTP 回 `ok:true / auto:true`,
4525 4533 * 而那个 run 里的 ready 节点纹丝不动(返回 ok 却什么都不做,是最坏的形态)。
4526 4534 *
4527   - * ponytail: 仍然只推**一个**「最深 active run」,同一 tick 里的**兄弟**子 run(比如两个并行
4528   - * subgraph 节点各自起的子 run)不会同时被推——它们靠 runAuto 的外层循环一轮一轮轮流服务,
4529   - * 不是真并发。真并发只发生在**同一个 run 内部**(advanceBatch 的 fan-out)。上限是「兄弟子 run
4530   - * 之间」这一维的吞吐,不影响正确性(不会有子 run 被饿死,只是不同时跑);升级路径是把这个
4531   - * for 循环也从「找到第一个 ok 就 return」改成对多个 active run 并发调 advanceBatch。
  4535 + * 兄弟子 run(比如两个并行 subgraph 节点各自起的子 run)**同一 tick 内并发推进**
  4536 + * (27-P4 收口):`Promise.allSettled` 对树里每一个 active run 各调一次 `advanceBatch`。
  4537 + * 安全性同 `advanceBatch` 自己并发拾取同一 run 内候选节点的道理——不同 runId 的行互不
  4538 + * 覆盖,`transitionNode` 的逐行条件更新连「同一 run 内竞争」都兜得住,跨 run 更没有共享的
  4539 + * 可写状态;一个 run 结算时可能连带 `settleRun` 到共同的父 run,那也只是「幂等地被判定
  4540 + * 收敛好几次」(§4.3 已经论证过的浪费而非 bug)。不再需要「深度优先」:advanceBatch 对没有
  4541 + * ready 节点的 run 只会原地返回 `no-ready-node`,谁先谁后不影响正确性。
4532 4542 */
4533 4543 async function advanceTree(runId, opts = {}) {
4534 4544 const self = getRun(runId);
4535 4545 if (!self) return result(runId, null, 'run-not-found');
4536 4546 // 仍然只做索引查询:按 root_run_id 拿全树,再在内存里裁出「自己 + 后代」这棵子树。
4537   - // 保留 listRunTree 的 created_at 顺序 —— 后建的一定更深,倒序即「最深优先」。
4538 4547 const scope = new Set([self.id, ...descendantRuns(self).map((r) => r.id)]);
4539 4548 const tree = listRunTree(self.root_run_id)
4540 4549 .filter((r) => scope.has(r.id) && r.run_status === 'running');
4541   - // 后建的 run 一定更深(子 run 总是在父节点 running 之后建的),倒序即「最深优先」
  4550 +
  4551 + const settled = await Promise.allSettled(tree.map((r) => advanceBatch(r.id, opts)));
  4552 +
  4553 + let firstOk = null;
4542 4554 let informative = null;
4543   - for (const r of tree.reverse()) {
4544   - const res = await advanceBatch(r.id, opts);
4545   - if (res.ok) return res;
  4555 + for (let i = 0; i < settled.length; i++) {
  4556 + const s = settled[i];
  4557 + // advanceBatch 内部已经把「executor 炸了」转成 fail outcome 喂给 settleNode,不会走到
  4558 + // 这里;这一分支兜的是它自己没兜住的意外(比如 defIndex 读图定义时抛的同步异常)。
  4559 + const res = s.status === 'fulfilled'
  4560 + ? s.value
  4561 + : { ok: false, runId: tree[i].id, reason: 'advance-error', error: s.reason && s.reason.message };
  4562 + if (res.ok && !firstOk) firstOk = res;
4546 4563 // minor 3:ok:false 时**不能**把所有原因都塌缩成 'no-ready-node'。子层返回
4547 4564 // 'budget-exhausted'(还带着 budget.exhausted 明细)而树层报「没活可干」,
4548 4565 // 自动模式的调用方和 REST 层就分不清「预算耗尽等人工加额度」和「真的跑完了」。
4549   - // 最深的那条有信息量的原因胜出;全是 no-ready-node 才落回原来的兜底。
4550 4566 if (!informative && res.reason && res.reason !== 'no-ready-node') informative = res;
4551 4567 }
  4568 + if (firstOk) return firstOk;
4552 4569 return {
4553 4570 ...result(runId, (getRun(runId) || {}).run_status, informative ? informative.reason : 'no-ready-node'),
4554 4571 // budget 只有预算闸门那条路径才带,透上来给控制台顶栏用
... ... @@ -4562,8 +4579,8 @@ async function advanceTree(runId, opts = {}) {
4562 4579 * 自动驱动:只要 pause_requested / stop_requested 都是假就在上一次 resolve 后立刻发起下一次。
4563 4580 * 与 advanceTree 同款语义 —— runId 是**子树根**,对子 run 起自动就只驱动它这棵子树。
4564 4581 * maxSteps 只是防跑飞的安全阀,真正的终止条件是预算(§四.A.3)与熔断(§四.D.2.1)。
4565   - * 27-P4 T4/T6 接入后,「同一个 run 内部」已经能并发;跨兄弟子 run 的并发仍是 advanceTree
4566   - * 那条 ponytail(一次只推最深的一个),不在本函数里。
  4582 + * 「同一个 run 内部」(27-P4 T4/T6)与「跨兄弟子 run」(27-P4 收口)现在都走并发,
  4583 + * 这里不需要再做任何事。
4567 4584 */
4568 4585 async function runAuto(runId, opts = {}) {
4569 4586 const maxSteps = opts.maxSteps || 1000;
... ...
server/src/core/agent-manager.js
... ... @@ -405,11 +405,16 @@ class AgentManager extends EventEmitter {
405 405  
406 406 // 优先使用 Agent 自带的 workDir —— body 模式下它已经是任务态目录
407 407 // system/agents/{aid}/{cid}/{taskId}/(getAgentConfig 已 mkdir,cwd 不存在 CLI 起不来)。
408   - // 会话隔离**不靠**这个路径,靠 agent.companyId / agent.taskId 进 sessionPool.getKey。
  408 + // 会话隔离**主要**靠 agent.companyId / agent.taskId 进 sessionPool.getKey;
  409 + // 但 (companyId, taskId) 只到「一次 run」的粒度——同一 run 内两个并发节点引用同一个
  410 + // system agent(fan-out 撞同 agentId,如 executor-fanout.json 的 impl-a/impl-b)时
  411 + // taskId 相同、会话键相同,会互相顶掉对方的 CLI 子进程。调用方(graph-engine)按
  412 + // 节点运行实例 id 传 options.sessionScope 来加一维,这里原样透传给 getKey 的第四个参数。
409 413 const effectiveWorkDir = agent.workDir || this.workDir;
  414 + const sessionScope = options.sessionScope;
410 415  
411 416 let session = await this.sessionPool.getOrCreate(
412   - agentName, agent, resolved.launcherConfig, protocolName, effectiveWorkDir
  417 + agentName, agent, resolved.launcherConfig, protocolName, effectiveWorkDir, sessionScope
413 418 );
414 419  
415 420 for (let attempt = 0; attempt < 2; attempt++) {
... ... @@ -456,7 +461,7 @@ class AgentManager extends EventEmitter {
456 461 });
457 462 await this.sessionPool.reset(session.key);
458 463 session = await this.sessionPool.getOrCreate(
459   - agentName, agent, resolved.launcherConfig, protocolName, effectiveWorkDir
  464 + agentName, agent, resolved.launcherConfig, protocolName, effectiveWorkDir, sessionScope
460 465 );
461 466 this._emitSessionEvent({
462 467 agentName, protocol: protocolName, status: 'reconnected',
... ...
server/tests/unit/agent-manager-session-scope.test.js 0 → 100644
  1 +/**
  2 + * Unit Tests — `options.sessionScope` 真的从 `AgentManager.callAgent` 一路传到
  3 + * `SessionPool.getKey`(27-P4 §4.2 补的那一维)
  4 + *
  5 + * ── 这条测试还的是哪笔债 ──────────────────────────────────────────────
  6 + * [27-P4](../../../docs/todo/system-design/27-p4-parallel-and-artifacts.md) §4.2
  7 + * 设计了「图引擎并发调用同一 system agent 时按 `gr:{nodeRunId}` 传 scope,避免撞会话键」,
  8 + * `SessionPool.getKey()` 的 `scope` 参数也确实早就在(`session-pool-scope-key.test.js` 锁的
  9 + * 正是 `SessionPool` 自己的存取一致性),但**从 `AgentManager._runViaProtocol` 到
  10 + * `SessionPool.getOrCreate` 这一段接线此前从未写过**——`callAgent` 的 options 解构里
  11 + * `sessionScope` 只是摆设,`_runViaProtocol` 两处 `getOrCreate` 调用都固定只传 5 个参数。
  12 + *
  13 + * 后果:`executor-fanout.json` 这类骨架里 `{{orgNode}}-impl-a` / `{{orgNode}}-impl-b`
  14 + * 引用同一个 `systemAgentId`,若真的被 `advanceBatch` 并发拾取(两者同一个 run,
  15 + * `taskId` 都等于 `ctx.runId`),`sessionPool.getKey` 算出来的键完全相同——第二次调用会
  16 + * 复用第一次那个还在跑的 CLI 会话,两个"并行"分支实际上在同一个会话队列里排队执行,
  17 + * 且不报错、不留痕迹。
  18 + *
  19 + * 本测试只钉「接线通不通」,不追真实 CLI(用注册制协议工厂换一个内存桩,同
  20 + * `session-pool-scope-key.test.js` 的手法):真实 agent 目录 + 真实 `AgentManager.callAgent`
  21 + * 全量跑一遍,`_runViaProtocol` 不做任何桩替换。
  22 + */
  23 +import { describe, it, expect, beforeAll, afterAll } from 'vitest';
  24 +import { EventEmitter } from 'events';
  25 +import fs from 'fs';
  26 +import path from 'path';
  27 +
  28 +const AgentManager = require('../../src/core/agent-manager');
  29 +const { registerProtocol } = require('../../src/core/protocol-factory');
  30 +const locator = require('../../services/agent-locator');
  31 +
  32 +/** 只满足 SessionPool.create() 用到的方法 + runPromptInSession 要的 sendPrompt。 */
  33 +let stubCounter = 0;
  34 +class StubProtocol extends EventEmitter {
  35 + constructor(agentInfo) {
  36 + super();
  37 + this.agentInfo = agentInfo;
  38 + this.isConnected = false;
  39 + this.sessionId = null;
  40 + }
  41 + async connect() { this.isConnected = true; }
  42 + async initialize() {}
  43 + async newSession() { this.sessionId = `stub-session-${++stubCounter}`; }
  44 + async sendPrompt() { return undefined; } // 立即"跑完",不 emit 任何流式事件
  45 + disconnect() { this.isConnected = false; }
  46 +}
  47 +
  48 +beforeAll(() => registerProtocol('claude-cli', () => StubProtocol));
  49 +afterAll(() => registerProtocol('claude-cli', () => require('@mas/acp-core').ClaudeCliProtocol));
  50 +
  51 +describe('AgentManager.callAgent — sessionScope 接线(27-P4 §4.2)', () => {
  52 + const AGENT_ID = 'zz-scope-agent'; // zz- 前缀 = 测试建的注册表条目
  53 + const BODY_DIR = locator.bodyDir(AGENT_ID);
  54 + const CO = 'zz-scope-co';
  55 + const ADDR = `${CO}:${AGENT_ID}`;
  56 +
  57 + const newManager = () => {
  58 + const m = new AgentManager({}, locator.REPO_ROOT, {});
  59 + m._resolveLogSession = async () => null;
  60 + m.hookRunner = { run: async () => {} };
  61 + return m;
  62 + };
  63 +
  64 + beforeAll(() => {
  65 + fs.mkdirSync(BODY_DIR, { recursive: true });
  66 + fs.writeFileSync(path.join(BODY_DIR, 'agent.md'), '# 会话作用域测试本体\n', 'utf-8');
  67 + fs.writeFileSync(path.join(BODY_DIR, 'config.json'),
  68 + JSON.stringify({ id: AGENT_ID, name: AGENT_ID, type: 'skill', status: 'active' }, null, 2), 'utf-8');
  69 + });
  70 +
  71 + afterAll(() => {
  72 + fs.rmSync(locator.companyDir(AGENT_ID, CO), { recursive: true, force: true });
  73 + });
  74 +
  75 + it('不传 sessionScope:同一 run(同 taskId)两次调用撞上同一个会话(这是被 27-P4 §4.2 指出的问题本身)', async () => {
  76 + const manager = newManager();
  77 +
  78 + await manager.callAgent(ADDR, 'T1', { metadata: { taskId: 'run-1' } });
  79 + await manager.callAgent(ADDR, 'T2', { metadata: { taskId: 'run-1' } });
  80 +
  81 + // 两次都落在同一个 taskId 下、没有 scope 区分 —— 会话池里只会有 1 个会话
  82 + expect(manager.sessionPool.sessions.size).toBe(1);
  83 + });
  84 +
  85 + it('传 sessionScope=gr:{nodeRunId}:同一 run 内两个并发节点各自拿到独立会话,互不覆盖', async () => {
  86 + const manager = newManager();
  87 +
  88 + const [a, b] = await Promise.all([
  89 + manager.callAgent(ADDR, 'impl-a', {
  90 + metadata: { taskId: 'run-2' }, sessionScope: 'gr:node-a',
  91 + }),
  92 + manager.callAgent(ADDR, 'impl-b', {
  93 + metadata: { taskId: 'run-2' }, sessionScope: 'gr:node-b',
  94 + }),
  95 + ]);
  96 +
  97 + expect(a.status).toBe('success');
  98 + expect(b.status).toBe('success');
  99 + // 同一个 taskId、不同 scope —— 两个会话都要留在池子里,不能有一个把另一个顶掉
  100 + expect(manager.sessionPool.sessions.size).toBe(2);
  101 +
  102 + const keys = [...manager.sessionPool.sessions.keys()];
  103 + expect(keys.some((k) => k.endsWith(':gr:node-a'))).toBe(true);
  104 + expect(keys.some((k) => k.endsWith(':gr:node-b'))).toBe(true);
  105 + });
  106 +
  107 + it('断链复现:把 sessionScope 从 options 里去掉,上一条用例会退回到 1 个会话(反向验证这条测试真的在盯接线)', async () => {
  108 + const manager = newManager();
  109 +
  110 + await Promise.all([
  111 + manager.callAgent(ADDR, 'impl-a', { metadata: { taskId: 'run-3' } }),
  112 + manager.callAgent(ADDR, 'impl-b', { metadata: { taskId: 'run-3' } }),
  113 + ]);
  114 +
  115 + // 没有 scope 区分:并发的两次调用会挤在同一个会话键上
  116 + expect(manager.sessionPool.sessions.size).toBe(1);
  117 + });
  118 +});
... ...
server/tests/unit/graph-engine-advance-batch.test.js
... ... @@ -287,4 +287,52 @@ describe(&#39;advanceBatch(27-P4 T4/T6)&#39;, () =&gt; {
287 287 expect(wdb.getRun(runId).run_status).toBe('completed');
288 288 expect(maxActive).toBeGreaterThanOrEqual(3); // entry 之后 a/b/c 应该同时在跑,而不是依次
289 289 });
  290 +
  291 + it('advanceTree 真并发推进兄弟子 run(27-P4 收口:不再是「一次只推最深的一个」)', async () => {
  292 + // entry → sub-a, sub-b 两个并行 subgraph 节点,各自规划出一张只有一个 leaf 节点的子图。
  293 + // 两个 subgraph 节点没有依赖关系,同一批 advanceBatch 里会一起从 ready 变 running;
  294 + // 各自的 leaf 节点则要等到**下一次** advanceTree 才会被推——这正是本用例要验的那一跳。
  295 + const companyId = nextCompany();
  296 + ensureAgents(['entry', 'sub-a', 'sub-b']);
  297 + const g = wdb.createWorkGraph({
  298 + companyId,
  299 + nodes: ['entry', { nodeName: 'sub-a', nodeKind: 'subgraph' }, { nodeName: 'sub-b', nodeKind: 'subgraph' }],
  300 + edges: [{ from: 'entry', to: 'sub-a' }, { from: 'entry', to: 'sub-b' }],
  301 + });
  302 + expect(g.errors || []).toEqual([]);
  303 + const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId, executionMode: 'manual' });
  304 + engine.markRunReady(run.id);
  305 + const runId = run.id;
  306 +
  307 + engine.setPlanWorkGraph(async (ctx) => {
  308 + ensureAgents(['leaf']);
  309 + const childGraph = wdb.createWorkGraph({ companyId, nodes: ['leaf'], edges: [] });
  310 + expect(childGraph.errors || []).toEqual([]);
  311 + return { workGraphId: childGraph.id };
  312 + });
  313 +
  314 + let active = 0;
  315 + let maxActive = 0;
  316 + engine.setAgentExecutor(async (ctx) => {
  317 + if (ctx.nodeName !== 'leaf') return { status: 'success' }; // entry:立即成功,不参与并发计数
  318 + active += 1;
  319 + maxActive = Math.max(maxActive, active);
  320 + await new Promise((r) => { setTimeout(r, 15); }); // 真实异步延迟,制造时间窗口重叠
  321 + active -= 1;
  322 + return { status: 'success' };
  323 + });
  324 +
  325 + await engine.startRun(runId, { mode: 'auto' }).driver;
  326 +
  327 + expect(wdb.getRun(runId).run_status).toBe('completed');
  328 + // 两个子 run 各自的 leaf 节点要在同一个时间窗口里跑——如果 advanceTree 仍然只推
  329 + // 「最深 active run」,sub-a/sub-b 的子 run 会被 runAuto 的外层循环一轮一轮轮流服务,
  330 + // maxActive 永远是 1。
  331 + expect(maxActive).toBeGreaterThanOrEqual(2);
  332 +
  333 + const subA = nodeByName(runId, 'sub-a');
  334 + const subB = nodeByName(runId, 'sub-b');
  335 + expect(wdb.getRunState(subA.childRunId).nodes.find((n) => n.name === 'leaf').status).toBe('success');
  336 + expect(wdb.getRunState(subB.childRunId).nodes.find((n) => n.name === 'leaf').status).toBe('success');
  337 + });
290 338 });
... ...
server/tests/unit/graph-real-chain-smoke.test.js
... ... @@ -334,6 +334,68 @@ describe(&#39;图执行真实链路 smokeï¼ˆé›¶æ³¨å…¥ï¼Œåªæ¢æŽ‰çœŸ CLI å­è¿›ç¨‹ï¼
334 334 }
335 335 }, 60_000);
336 336  
  337 + it('â‘§ åŒå±‚熔断真的会撞顶收敛到 blocked(27-P3/P4 §四.D.2.1,此å‰åªæœ‰æ³¨å…¥å¼è¦†ç›–)', async () => {
  338 + // å¤ç”¨ ⑦ 的形状(prd --success--> sdd --fail(backward)--> prd):sdd 这份 fixture
  339 + // æ¯æ¬¡éƒ½ç¡®å®šæ€§åˆ¤ input_underspecified,一直回退到åŒä¸€æ¡ backward 边,
  340 + // 真实驱动 nextStep åå¤è·‘,直到å•边硬顶 LOOP_HARD_CAP_EDGE=3 æ’žé¡¶
  341 + // (第 4 次命中该边)——验è¯ç†”æ–­ä¸æ˜¯é å‡ executor 摆出æ¥çš„分支覆盖,
  342 + // 是真实链路(真 AgentManager → 真 ClaudeCliProtocol mock 回放)跑出æ¥çš„æ”¶æ•›ç»“果。
  343 + const SDD_AGENT = 'sdd-planning';
  344 + const CO = `${COMPANY}-t8`;
  345 + const db = wdb.getDb();
  346 + const now = new Date().toISOString();
  347 + db.prepare(`INSERT OR IGNORE INTO system_agents
  348 + (id, name, scope, owner_company_id, legacy_path, status, created_at, updated_at)
  349 + VALUES (?, ?, 'public', NULL, ?, 'active', ?, ?)`)
  350 + .run(SDD_AGENT, SDD_AGENT, `system/agents/${SDD_AGENT}`, now, now);
  351 +
  352 + const g = wdb.createWorkGraph({
  353 + companyId: CO,
  354 + nodes: [
  355 + { nodeName: 'prd', systemAgentId: AGENT },
  356 + { nodeName: 'sdd', systemAgentId: SDD_AGENT },
  357 + ],
  358 + edges: [
  359 + { from: 'prd', to: 'sdd', condition: 'success' },
  360 + { from: 'sdd', to: 'prd', condition: 'fail', edgeKind: 'backward' },
  361 + ],
  362 + });
  363 + expect(g.errors || []).toEqual([]);
  364 + const run = wdb.createRun({ graphKind: 'work', graphId: g.id, companyId: CO, executionMode: 'manual' });
  365 + engine.markRunReady(run.id);
  366 + engine.startRun(run.id);
  367 +
  368 + // 一路真跑到收敛(blocked)——安全阀 20 步防跑飞(æ¯è½® prd+sdd ä¸¤æ­¥ï¼Œæ’žé¡¶æœ€å¤šéœ€è¦ 4 轮)。
  369 + let lastRun = wdb.getRun(run.id);
  370 + for (let i = 0; i < 20 && lastRun.run_status === 'running'; i++) {
  371 + const step = await engine.nextStep(run.id, { task: `T8 smoke:第 ${i} æ­¥` });
  372 + if (!step.ok) break; // 没有 ready 节点了——è¦ä¹ˆæ”¶æ•›å®Œï¼Œè¦ä¹ˆåœ¨ç­‰åˆ«çš„东西
  373 + lastRun = wdb.getRun(run.id);
  374 + }
  375 +
  376 + expect(lastRun.run_status).toBe('blocked');
  377 +
  378 + // 熔断确实是撞的**å•è¾¹**ç¡¬é¡¶ï¼ˆè¿™å¼ å›¾åªæœ‰ä¸€æ¡ backward 边,ä¸ä¼šå…ˆæ’ž run 级),
  379 + // 且账本上留痕:最åŽä¸€æ¡è®°å½• verdict=force_terminated,loop_count 越过 LOOP_HARD_CAP_EDGE。
  380 + const records = wdb.listLoopRecords(run.id);
  381 + expect(records.length).toBeGreaterThan(0);
  382 + const terminated = records.find((r) => r.verdict === 'force_terminated');
  383 + expect(terminated).toBeTruthy();
  384 + expect(terminated.loop_count).toBeGreaterThan(engine.LOOP_HARD_CAP_EDGE);
  385 +
  386 + // 被熔断收敛的是 sdd(回退边的æºèŠ‚ç‚¹ï¼‰ï¼Œä¸æ˜¯ prd
  387 + const sddRow = wdb.getDb()
  388 + .prepare(`SELECT status FROM graph_run_nodes
  389 + WHERE run_id = ? AND def_node_id = (SELECT id FROM work_graph_nodes WHERE work_graph_id = ? AND node_name = ?)
  390 + ORDER BY attempt DESC LIMIT 1`)
  391 + .get(run.id, g.id, 'sdd');
  392 + expect(sddRow.status).toBe('blocked');
  393 +
  394 + for (const a of [SDD_AGENT, AGENT]) {
  395 + fs.rmSync(path.join(__dirname, '../../../system/agents', a, CO), { recursive: true, force: true });
  396 + }
  397 + }, 60_000);
  398 +
337 399 it('â‘¢ 边界守å«åœ¨è¿™æ¡è·¯å¾„上是**å¼€ç€**çš„ï¼ˆæ³¨å…¥å‡ executor 时它æ’为å‡ï¼Œé‚£æ¡åˆ†æ”¯é›¶è¦†ç›–)', () => {
338 400 // 判æ®å–è‡ªå¼•æ“Žè‡ªå·±çš„å¼€å…³å‡½æ•°ï¼Œè€Œä¸æ˜¯åŽ»æ•° timeline 事件:
339 401 // 守å«åœ¨å®¿ä¸»ä»“库干净时本æ¥å°±ä¸äº§ç”Ÿäº‹ä»¶ï¼Œæ‹¿ã€Œæ²¡æœ‰äº‹ä»¶ã€å½“「没开ã€ä¼šå过æ¥åˆ¤é”™ã€‚
... ...