diff --git a/docs/todo/system-design/27-p4-parallel-and-artifacts.md b/docs/todo/system-design/27-p4-parallel-and-artifacts.md index cc5edc5..e8f7083 100644 --- a/docs/todo/system-design/27-p4-parallel-and-artifacts.md +++ b/docs/todo/system-design/27-p4-parallel-and-artifacts.md @@ -565,7 +565,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), | **T2** | DevLoop 规格:§12 第一条不变量放宽为 `(pid, agent, scope)`。**改 HTML 后必须跑 `npm run spec:extract`** | `docs/prd/newfunction/*.html` | 无 | | ~~**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 决策大幅简化并重新定义**:不再有三层 min 计算,`advanceBatch` 只看本 run 的 `max_concurrency`。剩下两件事都**依赖计费系统接口确认**,暂不能开工:① 欠费检查怎么接(`checkBudget` 是任务级预算,跟账户欠费是两个概念,不能互相顶替);② 欠费清零后的**自动恢复触发器**——复用 `suspendRunTree`/`startRunSync` 现成的挂起/恢复状态机(`reason='billing-arrears'`),但今天从 `blocked` 恢复是人手点「启动」,无人值守场景需要一个订阅计费事件、批量调 `startRun` 的轮询/webhook,这是新代码,不是配置项 | `server/services/graph-engine.js` | T1, T3, T4,另需计费系统接口 | +| ~~**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 | | ~~**T6**~~ ✅ | 把 `advanceOne` 拾取之后的整段(建 ctx → 执行 → `settleNode`)抽成 `runClaimedNode(...)`,`advanceBatch` 用 `Promise.allSettled` 并发跑 claimed 节点。fan-in join 复用现有 `activateSuccessTargets`/`markReady`,**不需要新事务/锁**(并发安全性证明见 §4.3)。**2026-08-21 已实现并测过** | `server/services/graph-engine.js` | T4 ✅ | | ~~**T7**~~ ✅ | 产物索引:追加、查询、rebuild —— **已完成,形态换成 `graph_files` 表**(见 §3.6 校准块),不是 `files.json` | `server/services/work-graph-db.js` | — | | ~~**T8**~~ ✅ | 产物按 run/node 分目录 + 节点产出后自动进索引 —— **已完成**(`runs/{rootRunId}/nodes/{nodeRunId}/`) | `server/services/graph-engine.js` | — | @@ -626,6 +626,38 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), > subgraph 节点各自起的子 run)不会同时被推——它们靠 `runAuto` 外层循环一轮一轮轮流服务, > 不是真并发(已在 `advanceTree` 加了 ponytail 注释记这笔)。真并发目前只发生在**同一个 run > 内部**。这个跨 run 兄弟并发是否值得做,留给 T10 之后再看有没有真实场景撞上。 +> +> **2026-08-21 追加:T5 落地**(同一天,紧接上面那批): +> - 用户明确决策:欠费判据「先搭可插拔的桩子」——真实计费接口依赖计费系统那边确认,不等它, +> 先把挂载点和恢复机制落地。`setBillingGate(fn)` 与 `setAgentExecutor`/`setPlanWorkGraph` +> 同款注入模式;`checkBilling(run)` 未注入时恒 `{ok:true, inArrears:false}`。两处调用点 +> (`advanceOne`、`advanceBatch`)都紧跟在 `checkBudget` 之后——欠费是账户级门,预算是 +> 任务级门,两个维度不合并。 +> - `suspendRunTree(run, opts)` 加了 `reason`/`summary` 参数(默认值保持旧调用方——`checkBudget` +> 那条路径——行为不变),欠费走同一个函数只是 `reason='billing-arrears'`。`graph_runs` 新增 +> `block_reason` 列(`suspendRunTree` 写、`startRunSync`「启动」时无条件清空)+ 复合索引 +> `(run_status, block_reason) WHERE parent_run_id IS NULL`,配 `resumeBillingClearedRuns()` +> 的扫描查询:找出所有欠费挂起的 root run,对每条重新查一次欠费状态,清了就代其 `startRun`。 +> 谁来调用这个扫描函数、多久扫一次,留给计费系统事件/轮询接通之后再定。 +> - **审这段时抓到第二个真 bug**:`advanceBatch` 原来是「先查 budget、再查候选」,跟 +> `advanceOne` 的既有顺序(先确认真有 ready 候选,才查 budget——`advanceOne` 注释原话 +> 「拾到了 ready 节点(=真要花钱了)才查,避免空转时把已跑完的 run 误挂起」)反着来。 +> 后果:一个已经收敛(零 ready 节点)只是恰好预算也耗尽的 run,会被误判成 +> `budget-exhausted` 而不是正确收敛到 `completed`。接欠费门之前先把这个顺序修对, +> 两道新旧门用同一个正确的位置。 +> - 新增测试 `graph-engine-billing-gate.test.js`(7 条):零影响、`advanceOne`/`advanceBatch` +> 欠费挂起、`startRunSync` 清空 `block_reason`、`resumeBillingClearedRuns` 正确恢复/放过、 +> 按 `companyId` 限定扫描、上面那条排序 bug 的回归用例。 +> - **踩到并处理了一次真实的并发编辑事故**:写 T5 期间发现 `work-graph-db.js` 被另一个会话 +> 同时编辑(重写 `validateFailFallback`,31 号文档那条线,当时未完成、会让 51 个测试失败), +> 与我的 `block_reason` 改动混在同一份工作区文件里。没有跟着 `git add -A`,改用 `git diff` +> 自动生成的 patch 只保留自己的 3 处 hunk(`git apply --recount --cached`), +> 再用一次性的 `git worktree add --detach` 临时工作树(跑完 `git worktree remove` 清掉) +> 把这批 staged 内容单独跑一遍全量测试,确认自己的改动跟对方未完成的那部分互不依赖, +> 最后才 commit。过程中还手滑把上一条 commit 的说明文字贴成了另一条无关 commit 的原文, +> 用 `git commit --amend` 当场改正(那次 amend 合法——是刚提交、还没推送、也没有任何东西 +> 叠在它上面的自己的commit,不是在改别人的历史)。这条经验值得记:共享工作目录下, +> commit 前先 `git diff` 走查一遍两个待提交文件的内容,不要假设「我改的文件只有我的改动」。 ---