# 27-P4. 并行执行与产物索引 > **所属**: [27 Graph Agent 架构](./27-graph-agent-architecture.md) 的 Phase P4 > **版本**: v0.1(设计阶段,未实现) > **日期**: 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`),本体名不带前缀。 > **配套**: [27-E2E](./27-e2e-task-execution-flow.md) §7.4(fan-out 的两个阻塞点)、[27-EXAMPLE](./27-example-dev-company-walkthrough.md) 阶段 6(fan-out/fan-in 的具体场景) --- ## 〇、关于编号:这个 P4 不是原来那个 P4 27 号总纲 §六 里原本的「**P4 回退 + Loop-Validator**」已经**并入 P3 实现完毕**(总纲该行已标注「已并入上一行,不再单列」)——双层熔断、五项结构校验含 `fail-fallback-edge`、三层暂停/停止级联、按 `contract_hash` 判据的重试,全部在 `c3abf4a` 落地。 本文档**回收这个空出来的编号**,装的是 P3 明确排除、且现在成为最大能力缺口的内容。与 P5(自我进化)/ P6(DevLoop 收编)不冲突,是它们之前的一步。 --- ## 一、目标与非目标 ### 目标 1. **fan-out 并行执行** —— 解除 P3「一次只推进一个节点」的限制,让同一波次的多个 ready 节点真正并发跑。 2. **同一 system agent 的多实例隔离** —— 这是并行的**前置条件**,卡在两个既有不变量上(§三.2)。 3. **解除「一家公司同时只有一个 active root run」约束** —— P3 v1 用一个唯一索引强制它(§三.3),并行能力具备后应放开。 4. **产物统一目录 + `files.json` 索引** —— 所有 org/work graph 节点 agent 的产出物落在 `data/companies/{companyId}/` 下按 task 分目录,根目录 `files.json` 作为输入输出的统一检索入口,供所有 agent 引用。 ### 非目标 - **不做 L4**(system agent 自我迭代)—— 属 P5,见 [27-E2E](./27-e2e-task-execution-flow.md) §八。 - **不做 DevLoop 收编** —— 属 P6。 - **不做**跨公司的并行调度(多公司同时跑的全局资源仲裁)—— 本阶段并发上限按「单机总量」控制即可,见 §四.4。 - **不做**节点执行中途的断点续传 —— 沿用 P3 的重试语义(整个节点重跑)。 - **不做**把 `conversations/` 与 `system/tasklog/` 下的历史记录纳入 `files.json` —— 那是运行态日志,不是产物。 --- ## 二、背景:为什么并行是现在最大的缺口 [27-EXAMPLE](./27-example-dev-company-walkthrough.md) 阶段 6 描述的典型场景是:`org-coder` 把工作拆成三个可并行的 unit,各自跑完再 fan-in 到整合节点。这是 R-6 原话里明确要求的(「可以 fan out 并行多个 phase 的 code subagent」)。 **P3 把图结构和 join 语义都做好了,唯独没做并行本身**: - ✅ `graph-engine.js:1078` —— 出边评估时「目标节点若**所有**入边条件都满足则置 ready」,fan-in 的 join 语义已经正确。 - ✅ 图上可以有多个节点同时处于 `ready`。 - ❌ `graph-engine.js:908` —— 拾取 ready 节点的 SQL 是 `... WHERE status = 'ready' ORDER BY created_at, id **LIMIT 1**`。所以三个并行分支只会被一个一个串行跑掉。 也就是说,**图已经是 DAG,波次也分得出来,只是每次只走一个节点**。这不是设计缺陷,是 P3 刻意的范围切分——因为并行会立刻撞上 §三.2 的两个不变量。 --- ## 三、现状代码走查(决定设计边界的关键证据) ### 3.1 引擎侧:单节点拾取 `server/services/graph-engine.js:908`: ```sql SELECT * FROM graph_run_nodes WHERE run_id = ? AND status = 'ready' ORDER BY created_at, id LIMIT 1 ``` `advanceOne(runId)` 的语义是「拾取**一个** ready 节点 → running → 执行 → 评估出边 → 把满足条件的下游置 ready → 返回,**不循环往下跑**」(文件头注释 16-17 行)。手动模式一次点击一步、自动模式靠外层反复调用,两者共用同一原语——这个设计在 P4 要保留,只是原语从「一个」变成「一批」。 ### 3.2 两个阻塞不变量(并行的真正门槛) **① `CONVENTIONS.md:155`**: > 任意 `(pid, agent)` 同时只允许 1 个 status=running 如果并行跑三个 `org-coder` 的子任务,它们是**同一个 system agent 的三次调用**,直接违反这条。 **② `session-pool.js:29-32`** 的会话 key: ```js getKey(agentName, agentConfig, workDir, scope) { const base = `acp:${agentName}:${agentConfig.client}:${agentConfig.deploy}:${workDir}`; return scope ? `${base}:${scope}` : base; } ``` 同 agent 同 workDir 会**复用同一个 session**。并发调用会撞在一起——两个并行分支的对话会互相污染上下文。 💡 **关键发现**:`getKey` 的签名**已经预留了 `scope` 参数**,但第 45 行实际调用时是 `this.getKey(agentName, agentConfig, workDir)`——**没传**。也就是说隔离机制的挂载点已经在,只是从未被使用。这让 P4 的改动比预想小得多。 ### 3.3 一家公司只允许一个 active root run `work-graph-db.js:108-110`: ```sql CREATE UNIQUE INDEX IF NOT EXISTS idx_graph_runs_one_active_root ON graph_runs(company_id) WHERE parent_run_id IS NULL AND run_status IN ('ready','running','paused'); ``` `company.js:1188` 的注释写明这是「v1 约束(27-P3P4 §一非目标)」。注意**这条约束在 org 层就存在**,不是等到 work 层 fan-out 才需要——因为 org graph 本身也是被这套引擎跑的。 ### 3.4 现成的 fan-out 实现(可借鉴,但不在图引擎里) `orchestrator.js:265` 的 `executeWithDependencies()`:`Scheduler.getExecutionGroups()` 按 `dependsOn` 分组 → `Promise.allSettled` 组内并行、组间串行(天然 fan-in 屏障)→ 单点失败降级成 `{status:'ERROR', retryable}` 而不终结整组。 这套东西**图引擎没有用**(消费方只有 orchestrator 和 result-tracker)。P4 不必直接复用它的代码,但**它的错误降级策略值得照搬**:单个分支失败不应该让整批一起崩。 ### 3.5 并发上限的既有约定 `docs/prd/newfunction/_spec/plan-cost-concurrency.md` §④ 已经给出三层上限的数字,P4 应当对齐而不是另立一套: | 层级 | 上限 | 原因 | |---|---|---| | 项目内 L1 并行 | ≤ 5 | 每个 Claude CLI ≈ 100MB RAM + 1 CPU core | | 单机系统并发 | ≤ 20 个 ACP session | 合理 RAM 占用 ≈ 2GB | | 同 company 项目并发 | ≤ 3 | 避免单用户挤占 | | Bash 子进程 | ≤ 50 | 防 fork bomb | ### 3.6 产物目录现状 - ✅ `goal-contract.js` 已实现验收契约的跨节点传递(R-15.6 验收先行)。 - ✅ `graph-engine.js:283/407/444` 有节点级产物路径:`data/companies/{cid}/agents/{systemAgentId}/.../attempt-{n}.json`。 - ❌ **没有 `files.json`**(全仓库无此文件、代码无引用)。 - ❌ 产物按 **agent** 分目录,不是按 **task** 分目录——同一 agent 在不同任务下的产出混在一起。 --- ## 四、设计方案 ### 4.1 `advanceOne` → `advanceBatch`(保持原语单一) **不新增第二个推进原语**。P3 「手动与自动共用同一个 advanceOne」的设计要保留,只把「拾取一个」放宽为「拾取一批」: ```js // 拾取至多 N 个 ready 节点(N = 本 run 的有效并发额度,见 4.4) advanceBatch(runId, { maxConcurrency }) → { results: [...], activated: [...] } ``` - **手动模式**:`maxConcurrency = 1`,行为与今天的 `advanceOne` 完全一致——「下一步」仍然只推进一个节点,用户能看清每一步。 - **自动模式**:`maxConcurrency = 有效额度`,一次拾取整批并发跑。 - `advanceOne(runId)` 保留为 `advanceBatch(runId, {maxConcurrency: 1})` 的薄封装,P3 已有的调用方和测试不用改。 **拾取的原子性**:P3 的幂等靠 `WHERE status='ready'` 条件更新(受影响行数=0 即视为被别人拿走)。批量拾取要沿用同一手法——**在一个事务里把这批节点一次性置 `running`**,而不是先 SELECT 再逐个 UPDATE,否则两个并发的 `advanceBatch` 会拾取到重叠的集合。 ### 4.2 同一 agent 的多实例隔离 **这是并行的前置条件,必须先做。** 利用 §三.2 发现的现成挂载点——`SessionPool.getKey()` 的 `scope` 参数: ```js // 图引擎调用 AgentManager 时带上 scope,取值用节点运行实例 id(graph_run_nodes.id) // 同一 system agent 的两次并发调用因此落在两个不同的 session 上,互不污染 const scope = `gr:${nodeRunId}`; ``` **为什么用 `graph_run_nodes.id` 而不是 `def_node_id`**:同一个定义节点在重试时会有多次 attempt,但同一时刻只有一个在跑;而并行的是**不同定义节点**引用同一本体。用运行实例 id 天然满足「并发的两个调用一定拿到不同 scope」。 **`CONVENTIONS.md` §12 第一条不变量要相应放宽**: ``` 旧:任意 (pid, agent) 同时只允许 1 个 status=running 新:任意 (pid, agent, scope) 同时只允许 1 个 status=running ``` ⚠️ 这条不变量属于 DevLoop 的规格文件。改它意味着**修改 `docs/prd/newfunction/*.html` 再跑 `npm run spec:extract`**(`_spec/` 是生成物,不能手改,pre-commit hook 会拦)。这是 P4 里唯一一处要动 DevLoop 规格的地方,且只是放宽维度、不改语义。 **workDir 保持不变**:仍然是 agent 的私有目录。P1 §3.3 已论证过 `workDir` 必须指向私有目录(CLI 子进程以它为 cwd 读 `soul.md` 等相对路径文件),并行不改变这一点——隔离靠 session scope,不靠换目录。 ### 4.3 部分失败的 fan-in 语义 三个并行分支,2 成功 1 失败,fan-in 节点怎么办? **沿用 P3 已实现的 join 语义,不加特例**:`graph-engine.js:1078` 的规则是「所有入边条件都满足才置 ready」。失败的那条分支走的是 `fail` 边,不满足 fan-in 节点的 `success` 入边条件,所以 **fan-in 节点不会被点亮**,图停在那里——这正是期望行为。 需要补的是**可观测性**,不是新语义: - run 层面要能回答「我卡在等哪几个节点」。控制台已有的节点状态足以推导,但要在 UI 上显式标出「fan-in 节点 `X` 在等 `A`(fail) / `B`(success) / `C`(success)」,否则用户只看到「不动了」。 - 失败分支按 P3 既有路径处理(`fail` 出边 → 归因 → 回退或 blocked),P4 不改。 ### 4.4 并发额度:三层,逐层取小 对齐 §三.5 的既有数字,不另立一套: | 层级 | 上限 | 落在哪 | |---|---|---| | **单个 run** | 默认 5 | `graph_runs` 新增 `max_concurrency` 列,建 run 时写入,可按任务覆盖 | | **单公司** | 默认 3 个 active root run | 替换 §三.3 那个「只允许 1 个」的唯一索引 | | **单机** | ≤ 20 个 ACP session | `SessionPool` 加 `maxSessions`(`plan-cost-concurrency.md` §④ 已经提出要加这个配置,至今未加) | **有效额度 = min(三层剩余)**。`advanceBatch` 拾取前先算这个值,为 0 就本轮不拾取(不是报错,等下一轮)。 **任务级总预算不变**:P3 已实现「按 root_run_id 共池的 backward_count + 任务级预算挂 root run」,并行不改变预算语义——**并行只是让预算烧得更快**,这一点要在控制台的成本提示里体现。 ### 4.5 解除单 root run 约束 ```sql -- 替换 work-graph-db.js:108 的 idx_graph_runs_one_active_root DROP INDEX IF EXISTS idx_graph_runs_one_active_root; -- 不再用唯一索引硬限,改为建 run 时按 4.4 的「单公司额度」做计数检查 ``` **为什么从「索引强制」改为「计数检查」**:唯一索引只能表达「≤1」,表达不了「≤3」。计数检查要放在**同一个事务**里(先 `SELECT COUNT(*)` 再 `INSERT`),否则并发建 run 会超额。 ### 4.6 产物统一目录 + 产物索引表(R-19 定案后重写) > **v0.2 重写**:本节原设计是「公司根放一个 `files.json`」。[27-REQ](./27-requirement.md) R-19 > 改为**建 sqlite 表**,并把「边即输入输出」这层显式化。原设计的目录约定保留。 #### 为什么这一节从「优化项」变成「阻塞项」 第四次真 CLI 冒烟(自动模式)跑出的断点:`org-planner-prd` 失败,它自己的归因说得很准 —— **「不是上游没产出,是它的产出结构上到不了我手上」**。核实:上游 `brd` 的 `brd.md` / `contract.json` 落在**它自己的节点私有目录**,共享区 `artifacts/` 只有 `project-root.json` 与 `task-goal.json`,而 `brd --success--> prd` 这条边 **不携带任何文件引用**。 同一张 work graph 内部,上游到下游**没有任何数据通路**。这不是引擎 bug —— 边一直只承载控制流(`condition` / `edge_kind` / `fail_reason`), 而 [27-P3/P4](./27-p3-p4-work-graph-console.md) §四.B.3 把 R-4 要的「边的输入输出」 落成了**从 `output_ref` 推断**的 UI 展示,没有实体。推断的那一端是私有目录,下游读不到。 **结论:不通这条路,任何多节点的图都只能跑完第一个节点。** 它不依赖并行,应从 P4 里 单独拆出来先做。 #### 目录约定(沿用原设计) ``` data/companies/{companyId}/ tasks/{taskId}/ brd.md · prd.md · sdd.md acceptance.json 验收契约(goal-contract.js 的路径归并到这里) {nodeName}/ 节点私有的中间产物(attempt-{n}.json 等) ``` **按 task 分目录而不是平铺**:同一家公司会跑很多 task,平铺的话第二个 task 的 `prd.md` 会覆盖第一个。按 task 分既避免覆盖,也保留历史。 **不走 `workspace/`**(R-19.3 定案):`workspace/` 的现有语义是 [06-沙盒自治](../../done/system-design/06-sandbox-autonomy.md) 定的 「**Agent 自治领地,框架不干预**」。产物索引要的是「**框架管着的、可检索的**交付物区」, 两种所有权混在一个目录里,索引就无法保证不漂移。 **根路径用代码推导,不入库**(R-19.3 定案):库里只存**相对公司根**的路径。 理由是库里存绝对路径会烂 —— W13 清掉的那 10 条 `legacy_path` 悬空行就是这个病 (指向早已删除的目录)。推导方式与现有 `runRootDir` / `nodeArtifactDir` 一致, 需要覆盖时给环境变量(测试隔离已在用 `GRAPH_RUNS_DIR` 这招)。 另:**库里没有 `companies` 表**,公司是 `company.json` + 目录扫描,所以也没有 「company 表的定义」这个落点。 #### 两类数据流,都要(R-19.3 定案①) 纯「文件挂在边上」有硬伤:边是 `(from, to)` 二元关系,只能表达**相邻**节点间的传递。 而 R-9.3 定的「验收产物由 planner 生成、由 validator 执行」是**非相邻**依赖 —— org graph 是 `org-planner → org-coder → org-validator`,`acceptance.json` 从 planner 流到 validator,**跳过 coder,这两个节点之间没有边**。 | | 沿边传递 | 任务级共享 | |---|---|---| | 例 | `brd.md` → 紧邻的 prd 节点 | `acceptance.json` → 任意下游 validator | | 绑定方式 | 边上声明契约(`kind`) | 挂在 task 上,按 `kind` 检索 | | 受拓扑相邻性限制 | 是 | **否** | #### 契约与实例分两阶段(R-19.4) `workgraph-planning` 画图时**不可能知道文件名**(节点还没跑),但它能声明 「这条边传一份 `brd`」。所以: - **规划时**:边上声明**契约**(`kind`),落 `graph_edge_contracts` - **运行时**:节点产出落盘后,引擎记**实例**(`rel_path`),落 `graph_files` 由此得到一个原设计没有的收益:**引擎能在节点跑之前校验「你声明要吃 brd,但上游没有任何 节点产 brd」**。这类图结构错误现在完全查不出来,只能等跑到那一步 agent 说「我拿不到输入」 (第四次真跑正是这么发现的)。 #### 表结构 ```sql -- 产物实例:运行时由**引擎**追加。不靠 agent 自觉写,否则索引必然与实际文件漂移 -- (本轮已被验证过多次:凡「靠 agent 自觉」的环节几乎全部落空)。 CREATE TABLE IF NOT EXISTS graph_files ( id TEXT PRIMARY KEY, -- gf_ company_id TEXT NOT NULL, task_id TEXT, -- 任务级共享产物按它检索 rel_path TEXT NOT NULL, -- **相对公司根**,根由代码推导 kind TEXT NOT NULL, -- brd|prd|sdd|acceptance|contract|artifact|log produced_by_run_id TEXT, produced_by_node_run_id TEXT, -- 与 graph_run_nodes.output_ref 同源 created_at TEXT NOT NULL, superseded_by TEXT -- 同名产物的版本链,可空 ); -- 边上声明的数据契约:规划时写,只有 kind 没有实例。 -- edge_layer 区分两层边表 —— org_graph_edges.id 是 INTEGER、work_graph_edges.id 是 TEXT, -- 与 graph_run_nodes.def_node_id 同一处类型不整齐(见 27-P3/P4 §四.A.2 的注)。 CREATE TABLE IF NOT EXISTS graph_edge_contracts ( id TEXT PRIMARY KEY, edge_layer TEXT NOT NULL CHECK (edge_layer IN ('org','work')), edge_id TEXT NOT NULL, kind TEXT NOT NULL, required INTEGER NOT NULL DEFAULT 1, -- 0 = 可选输入,缺了不算错 created_at TEXT NOT NULL ); ``` #### 引擎怎么用 **产出侧**:节点结算时,除了写 `output_ref`(节点级指针,不变), 再按落盘的文件往 `graph_files` 追加实例。两者关系与原设计一致 —— `output_ref` 回答「我这次跑出了什么」,`graph_files` 回答「这家公司有哪些产物、谁能用」, 后者由前者派生,所以要有 **rebuild** 能力(索引损坏时从节点行重建,类比 `_rebuildIndex()`)。 **输入侧**(这是补上断点的关键):节点执行前,引擎按 「该节点**入边**声明的 `kind`」+「本 task 的共享产物」去 `graph_files` 查实例, 把**真实路径**拼进提示词。 > 这比原设计的「注入一句『索引在 files.json,按 kind/taskId 自己检索』」更可靠: > 后者仍然依赖 agent 自觉去查、自觉拼对路径,而第四次真跑证明 agent 拼不出来 —— > **引擎直接把输入递到它手上**,不给它猜的机会。 --- ## 五、任务划分 | # | 任务 | 文件 | 依赖 | |---|---|---|---| | **T1** | `SessionPool` 加 `maxSessions` 上限 + 调用方传 `scope`(`gr:{nodeRunId}`) | `server/src/core/session-pool.js`、`server/src/core/agent-manager.js` | 无 | | **T2** | DevLoop 规格:§12 第一条不变量放宽为 `(pid, agent, scope)`。**改 HTML 后必须跑 `npm run spec:extract`** | `docs/prd/newfunction/*.html` | 无 | | **T3** | `graph_runs` 加 `max_concurrency` 列;DROP `idx_graph_runs_one_active_root`,改为事务内计数检查 | `server/services/work-graph-db.js` | 无 | | **T4** | `advanceBatch(runId, {maxConcurrency})`:事务内批量拾取并置 running;`advanceOne` 降级为薄封装 | `server/services/graph-engine.js` | T3 | | **T5** | 并发额度计算(三层取小)+ 额度为 0 时的等待语义 | `server/services/graph-engine.js` | T1, T3, T4 | | **T6** | 并行执行 + 错误降级(照搬 `orchestrator.executeWithDependencies` 的 `Promise.allSettled` 策略,单分支失败不终结整批) | `server/services/graph-engine.js` | T4 | | **T7** | `files.json` 服务:追加、查询、rebuild | `server/services/company-files-index.js`(新增) | 无,可并行 | | **T8** | 产物落盘路径改为 `tasks/{taskId}/`;节点产出后自动写 `files.json` | `server/services/graph-engine.js`、`goal-contract.js` | T7 | | **T9** | node agent 的 prompt 注入 `files.json` 检索说明 | `system/templates/base/agent.md` 等 | T7 | | **T10** | 控制台:fan-in 等待态可视化(「在等哪几个节点」)+ 并行运行中的多节点高亮 | `web/src/views/GraphConsole.vue` | T6 | | **T11** | 测试:批量拾取的并发幂等(两个 `advanceBatch` 不拾取重叠集合)、部分失败的 fan-in 停等、额度上限生效、`files.json` rebuild 一致性 | `server/tests/unit/` | T4-T8 | ### 三个阶段 | 阶段 | 含 | 交付 | 可否独立上线 | |---|---|---|---| | **A · 隔离** | T1, T2 | 同一 agent 可安全并发调用 | ✅ 可以,此时引擎仍串行,但隔离能力已具备 | | **B · 并行** | T3-T6, T10 | fan-out 真正并发;解除单 root run 约束 | ✅ 依赖 A | | **C · 产物索引** | T7-T9 | `files.json` + 按 task 分目录 | ✅ **完全独立于 A/B**,可最先做 | **阶段 C 与 A/B 无依赖**,如果想先看到效果,可以先做 C(它是用户明确要求过的第 3 项)。 --- ## 六、验收标准 1. **并发隔离**:同一 system agent 被两个并行节点同时调用,两次对话的上下文互不污染(各自有独立 session)。 2. **批量拾取幂等**:并发调用两次 `advanceBatch`,拾取到的节点集合无交集,且没有节点被漏掉。 3. **fan-out 真并行**:一张有 3 个并行分支的图,自动模式下三个节点同时处于 `running`(而不是依次),且总墙钟明显短于串行。 4. **部分失败停等**:3 个并行分支中 1 个失败时,fan-in 节点保持 `pending` 不被点亮;控制台能显示「在等哪几个节点」。 5. **额度生效**:`max_concurrency=2` 的 run 上有 5 个 ready 节点时,同时 running 的不超过 2 个。 6. **单公司多 run**:同一公司可同时有 3 个 active root run,第 4 个被拒绝且错误信息明确。 7. **手动模式不变**:`maxConcurrency=1`,「下一步」行为与 P3 完全一致(P3 已有测试全绿)。 8. **`files.json`**:节点产出后索引自动追加;删掉 `files.json` 后 rebuild 能从节点行完整重建;两个 task 的同名产物(都叫 `prd.md`)不互相覆盖。 --- ## 七、影响面 | 文件 | 改动 | |---|---| | `server/src/core/session-pool.js` | `maxSessions` 上限;`getKey` 的 `scope` 参数实际启用 | | `server/src/core/agent-manager.js` | 调用 `getKey` 时传 `scope` | | `server/services/work-graph-db.js` | `graph_runs.max_concurrency` 列;DROP 单 root run 唯一索引 | | `server/services/graph-engine.js` | `advanceBatch`;额度计算;并行执行与错误降级;产物路径改 `tasks/{taskId}/` | | `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`**) | | `system/templates/base/agent.md` | `files.json` 检索说明 | **明确不改**:P3 的 join 语义(`graph-engine.js:1078` 已正确)、熔断与预算语义、手动/自动共用同一原语的设计、`workDir` 指向私有目录的约定(P1 §3.3 已论证)。 --- ## 八、风险 / Open Questions 1. **并行让预算烧得更快,但预算语义不变**。任务级总预算是「只增不减的共池」(P3 已实现),并行不改变总量,只改变消耗速率。**控制台的成本提示需要相应更新**——用户点「自动」时看到的预估时间会大幅缩短,但花费不变。 2. **改 DevLoop 规格的连带风险**(T2)。§12 第一条不变量是 DevLoop 的,放宽它理论上不影响 DevLoop 自身(它的 `runProject` 波内本来就是串行的),但**必须跑一遍 DevLoop 全部测试**确认。 3. **`maxSessions` 上限触顶时的行为未定**。额度为 0 时是「等待下一轮」还是「拒绝并报错」?本文档 §4.4 选了前者(不报错、等下一轮),但如果一个 run 长期抢不到额度会静默卡住——需要一个「等待超时」告警,否则表现为「不动了」。**留待实现时定阈值。** 4. **`files.json` 的并发写入**。并行节点会同时追加索引,需要串行化(文件锁或走 SQLite)。本文档设计的是 JSON 文件形态(符合用户要求「一个 files.json 文件」),但**并发追加是真实风险**——可能需要退化为「引擎内单写者队列」。 5. **`files.json` 与 `consumedBy` 的准确性**。`consumedBy` 是**声明式**的(规划时写「谁应该读它」),不是实测的。如果规划错了,索引会误导下游 agent。缓解:把它当提示而非约束,agent 仍可读任何 `files.json` 里的条目。 --- ## 九、与其他阶段的关系 - **P3**:本阶段完全建立在 P3 的引擎之上,不推翻任何 P3 决策。P3 的「手动/自动共用同一原语」「join 语义」「熔断与预算」「三层暂停级联」全部保留。 - **P5(自我进化 / L4)**:无依赖,可并行推进。但 L4 的「agent dream 离线批处理」会受益于 `files.json`——跨公司汇总当日执行经验时,产物索引是天然的输入源。 - **P6(DevLoop 收编)**:T2 放宽的不变量是 P6 的前置——DevLoop 的 `runProject` 波内串行(`loop-controller.js:609` 注释 `// 串行跑`)正是被同一条不变量挡住的,P4 做完后 DevLoop 自己也能并行。