Commit a2f70192e514c9ca18efd11ff282dd0cc7423a90
1 parent
090ded5f
docs(27-P4): 记录 T5 落地 + 一次共享工作目录并发编辑事故的处理过程
T5 任务表行标完成,补一段实现记录(桩子设计、block_reason 列、 resumeBillingClearedRuns、advanceBatch 的第二个排序 bug)。 顺带记一笔过程:写 T5 期间发现 work-graph-db.js 被另一个会话同时编辑 (重写 validateFailFallback),用 git diff 生成的 patch 只保留自己的 hunk (git apply --recount --cached)避免带上对方未完成的改动,再用一次性 worktree 单独验证后才提交。留着这条经验:共享工作目录下 commit 前先 git diff 走查一遍要提交的文件,别假设自己改的文件里只有自己的改动。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
1 changed file
with
33 additions
and
1 deletions
docs/todo/system-design/27-p4-parallel-and-artifacts.md
| ... | ... | @@ -565,7 +565,7 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), |
| 565 | 565 | | **T2** | DevLoop 规格:§12 第一条不变量放宽为 `(pid, agent, scope)`。**改 HTML 后必须跑 `npm run spec:extract`** | `docs/prd/newfunction/*.html` | 无 | |
| 566 | 566 | | ~~**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 | 567 | | ~~**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 | -| ~~**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,另需计费系统接口 | | |
| 568 | +| ~~**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 | | |
| 569 | 569 | | ~~**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 ✅ | |
| 570 | 570 | | ~~**T7**~~ ✅ | 产物索引:追加、查询、rebuild —— **已完成,形态换成 `graph_files` 表**(见 §3.6 校准块),不是 `files.json` | `server/services/work-graph-db.js` | — | |
| 571 | 571 | | ~~**T8**~~ ✅ | 产物按 run/node 分目录 + 节点产出后自动进索引 —— **已完成**(`runs/{rootRunId}/nodes/{nodeRunId}/`) | `server/services/graph-engine.js` | — | |
| ... | ... | @@ -626,6 +626,38 @@ R-19 之后这两件事有实体(`graph_edge_contracts` × `graph_files`), |
| 626 | 626 | > subgraph 节点各自起的子 run)不会同时被推——它们靠 `runAuto` 外层循环一轮一轮轮流服务, |
| 627 | 627 | > 不是真并发(已在 `advanceTree` 加了 ponytail 注释记这笔)。真并发目前只发生在**同一个 run |
| 628 | 628 | > 内部**。这个跨 run 兄弟并发是否值得做,留给 T10 之后再看有没有真实场景撞上。 |
| 629 | +> | |
| 630 | +> **2026-08-21 追加:T5 落地**(同一天,紧接上面那批): | |
| 631 | +> - 用户明确决策:欠费判据「先搭可插拔的桩子」——真实计费接口依赖计费系统那边确认,不等它, | |
| 632 | +> 先把挂载点和恢复机制落地。`setBillingGate(fn)` 与 `setAgentExecutor`/`setPlanWorkGraph` | |
| 633 | +> 同款注入模式;`checkBilling(run)` 未注入时恒 `{ok:true, inArrears:false}`。两处调用点 | |
| 634 | +> (`advanceOne`、`advanceBatch`)都紧跟在 `checkBudget` 之后——欠费是账户级门,预算是 | |
| 635 | +> 任务级门,两个维度不合并。 | |
| 636 | +> - `suspendRunTree(run, opts)` 加了 `reason`/`summary` 参数(默认值保持旧调用方——`checkBudget` | |
| 637 | +> 那条路径——行为不变),欠费走同一个函数只是 `reason='billing-arrears'`。`graph_runs` 新增 | |
| 638 | +> `block_reason` 列(`suspendRunTree` 写、`startRunSync`「启动」时无条件清空)+ 复合索引 | |
| 639 | +> `(run_status, block_reason) WHERE parent_run_id IS NULL`,配 `resumeBillingClearedRuns()` | |
| 640 | +> 的扫描查询:找出所有欠费挂起的 root run,对每条重新查一次欠费状态,清了就代其 `startRun`。 | |
| 641 | +> 谁来调用这个扫描函数、多久扫一次,留给计费系统事件/轮询接通之后再定。 | |
| 642 | +> - **审这段时抓到第二个真 bug**:`advanceBatch` 原来是「先查 budget、再查候选」,跟 | |
| 643 | +> `advanceOne` 的既有顺序(先确认真有 ready 候选,才查 budget——`advanceOne` 注释原话 | |
| 644 | +> 「拾到了 ready 节点(=真要花钱了)才查,避免空转时把已跑完的 run 误挂起」)反着来。 | |
| 645 | +> 后果:一个已经收敛(零 ready 节点)只是恰好预算也耗尽的 run,会被误判成 | |
| 646 | +> `budget-exhausted` 而不是正确收敛到 `completed`。接欠费门之前先把这个顺序修对, | |
| 647 | +> 两道新旧门用同一个正确的位置。 | |
| 648 | +> - 新增测试 `graph-engine-billing-gate.test.js`(7 条):零影响、`advanceOne`/`advanceBatch` | |
| 649 | +> 欠费挂起、`startRunSync` 清空 `block_reason`、`resumeBillingClearedRuns` 正确恢复/放过、 | |
| 650 | +> 按 `companyId` 限定扫描、上面那条排序 bug 的回归用例。 | |
| 651 | +> - **踩到并处理了一次真实的并发编辑事故**:写 T5 期间发现 `work-graph-db.js` 被另一个会话 | |
| 652 | +> 同时编辑(重写 `validateFailFallback`,31 号文档那条线,当时未完成、会让 51 个测试失败), | |
| 653 | +> 与我的 `block_reason` 改动混在同一份工作区文件里。没有跟着 `git add -A`,改用 `git diff` | |
| 654 | +> 自动生成的 patch 只保留自己的 3 处 hunk(`git apply --recount --cached`), | |
| 655 | +> 再用一次性的 `git worktree add --detach` 临时工作树(跑完 `git worktree remove` 清掉) | |
| 656 | +> 把这批 staged 内容单独跑一遍全量测试,确认自己的改动跟对方未完成的那部分互不依赖, | |
| 657 | +> 最后才 commit。过程中还手滑把上一条 commit 的说明文字贴成了另一条无关 commit 的原文, | |
| 658 | +> 用 `git commit --amend` 当场改正(那次 amend 合法——是刚提交、还没推送、也没有任何东西 | |
| 659 | +> 叠在它上面的自己的commit,不是在改别人的历史)。这条经验值得记:共享工作目录下, | |
| 660 | +> commit 前先 `git diff` 走查一遍两个待提交文件的内容,不要假设「我改的文件只有我的改动」。 | |
| 629 | 661 | |
| 630 | 662 | --- |
| 631 | 663 | ... | ... |