Commit 3353b17492604fc8c67ee9f4aaf5eed3af60b9a1

Authored by yibo
1 parent 71cf45e1

docs(27-P3C): §十六 批 5 收尾 —— 五批全部实施完毕

记三件这一批换来的东西:

§16.5 第 3 步怎么做到不碰契约:dispatch 的九步副作用里只有第 ⑧ 步是存储,
把 saveTaskMeta/loadTaskMeta 变成表的适配器、其余八步一行不动,URL 与请求体
形态天然不可能变。字段落点的三个判断也记了(objectiveId 可推导所以不存、
sourceConvId 零读取方所以删、没选 JSON blob 因为 R-19.2 否过那个形态)。

§16.6 删掉的是「猜」不是「记录」:122 行的三策略启发式换成 parent_task_id
显式记录;getTaskTimeline 保留,树从表来、正文仍读 agent 的一手会话记录。

§16.7 两次「测试全绿 ≠ 没坏」:routes/tasks.js 漏改(没有测试走那条路)、
控制台栏宽(构建/单测/探针都不会报)。与 §16.2 那个「存量库一启动就炸」
(测试能覆盖但没人写)合起来是一条经验 —— 新建库的路径、纯逻辑的路径由单测
天然覆盖;「存量数据」「真实接口」「肉眼可见」这三类必须另外安排人走一遍。

§16.8 留了三条:tasklog 目录改名待做、tools-base.md 的端口仍是 3000(实际
3010,已真咬过人)、TaskTimeline.vue 的两处字段语义变了。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/todo/system-design/27-p3c-goal-task-model.md
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 2
3 > **所属**: [27 Graph Agent 架构](./27-graph-agent-architecture.md) 的 **P3 收尾**(P3C),直接续 [27-P3/P4 执行控制台](./27-p3-p4-work-graph-console.md) §四.B 与 [27-P3B 真跑跑道](./27-p3b-real-run-runway.md)。 3 > **所属**: [27 Graph Agent 架构](./27-graph-agent-architecture.md) 的 **P3 收尾**(P3C),直接续 [27-P3/P4 执行控制台](./27-p3-p4-work-graph-console.md) §四.B 与 [27-P3B 真跑跑道](./27-p3b-real-run-runway.md)。
4 > **日期**: 2026-08-18 4 > **日期**: 2026-08-18
5 -> **状态**: **批 1-4 已实施并全绿**(§十三 / §十四 / §十五)。**批 5 进行中**(§十六:第 1-2 步已落地,第 3-6 步未做)。开放问题已全部关闭。 5 +> **状态**: **五批全部实施完毕并全绿**(§十三 ~ §十六)。132 文件 / 1257 测试,`web:build` 通过。开放问题已全部关闭;后续项见 §15.5 与 §16.8。
6 > **⚠️ 批次与 phase 的对应不是一一对齐的**,判断「这份能不能搬进 done」时按批看,别按文档名看: 6 > **⚠️ 批次与 phase 的对应不是一一对齐的**,判断「这份能不能搬进 done」时按批看,别按文档名看:
7 > · **批 1-3 = 真正的 P3 收尾**(goal/task 两级模型 + `task-planning` + 排队),本文档的主体; 7 > · **批 1-3 = 真正的 P3 收尾**(goal/task 两级模型 + `task-planning` + 排队),本文档的主体;
8 > · **批 4(并行)归 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3**,那边是设计权威,本文档只服从(见 §五.2 的警示块); 8 > · **批 4(并行)归 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3**,那边是设计权威,本文档只服从(见 §五.2 的警示块);
@@ -1078,12 +1078,12 @@ manual,授权早就给过了,让它自己接回来是**兑现人刚才那次 @@ -1078,12 +1078,12 @@ manual,授权早就给过了,让它自己接回来是**兑现人刚才那次
1078 |---|---|---| 1078 |---|---|---|
1079 | 1 | `tasks` 表吸收 `executor` / `assignee_agent_id` / `parent_task_id` | ✅ `5cd91c3` | 1079 | 1 | `tasks` 表吸收 `executor` / `assignee_agent_id` / `parent_task_id` | ✅ `5cd91c3` |
1080 | 2 | `system/tasklog/*.json` 回填进 `tasks` 表 | ✅ `514ec3d` | 1080 | 2 | `system/tasklog/*.json` 回填进 `tasks` 表 | ✅ `514ec3d` |
1081 -| 3 | `/task/dispatch` 改写 `tasks` 表(**URL 与请求体一个字不动**) | ⏳ 未做 |  
1082 -| 4 | 30 分钟去重从「扫目录」改成一条 SQL | ⏳ 未做 |  
1083 -| 5 | `GET /api/tasks` 改读表;删 `TaskService.getAggregatedTasks()` 的会话反推 | ⏳ 未做 |  
1084 -| 6 | 调度器只管 `executor='graph'` | ⏳ 未做 | 1081 +| 3 | `/task/dispatch` 改写 `tasks` 表(**URL 与请求体一个字不动**) | ✅ `12880b8` |
  1082 +| 4 | 30 分钟去重从「扫目录」改成一条 SQL | ✅ 同上 |
  1083 +| 5 | `GET /api/tasks` 改读表;删 `TaskService.getAggregatedTasks()` 的会话反推 | ✅ `71cf45e` |
  1084 +| 6 | 调度器只管 `executor='graph'` | ✅ 同上 |
1085 1085
1086 -全量:**131 文件 / 1251 测试全绿**。 1086 +**批 5 完成,P3C 五批全部实施完毕。** 全量:**132 文件 / 1257 测试全绿**。
1087 1087
1088 ### 16.2 第 1 步踩到的:一个会让**所有存量库开不了**的顺序问题 1088 ### 16.2 第 1 步踩到的:一个会让**所有存量库开不了**的顺序问题
1089 1089
@@ -1126,3 +1126,76 @@ manual,授权早就给过了,让它自己接回来是**兑现人刚才那次 @@ -1126,3 +1126,76 @@ manual,授权早就给过了,让它自己接回来是**兑现人刚才那次
1126 > 顺带记一处过期:`tools-base.md` 里印的是 `http://localhost:3000`,而实际端口是 **3010** 1126 > 顺带记一处过期:`tools-base.md` 里印的是 `http://localhost:3000`,而实际端口是 **3010**
1127 > (`server/config.js`)。已经真咬过人 —— 回填出来的派单正文里有人手写了「端口 3010,不是 3000」 1127 > (`server/config.js`)。已经真咬过人 —— 回填出来的派单正文里有人手写了「端口 3010,不是 3000」
1128 > 来纠正它。这一处该在第 3 步一起改(改的是端口不是契约形态)。 1128 > 来纠正它。这一处该在第 3 步一起改(改的是端口不是契约形态)。
  1129 +
  1130 +### 16.5 第 3 步(最危险的一步)怎么做到不碰契约
  1131 +
  1132 +`/task/dispatch` 的副作用有九步,**只有第 ⑧ 步是「存储」**:
  1133 +
  1134 +```
  1135 +① 校验 from/to/task + 两个本体目录存在
  1136 +② 去重(30 分钟内同接收方 + 正文互相包含)
  1137 +③ 推断 parentTaskId
  1138 +④ 正文净化(\n → <br>,| → &#124;)
  1139 +⑤ 写目标 agent 的 todo.md
  1140 +⑥ 写发送者的 plan.md
  1141 +⑦ 更新 OKR 节点 → in_progress
  1142 +⑧ saveTaskMeta(taskId, {...}) ← 只换这一步的内脏
  1143 +⑨ autoExecute → POST /api/agent/{to}/chat
  1144 +```
  1145 +
  1146 +**把 `saveTaskMeta` / `loadTaskMeta` 变成 tasks 表的适配器**,其余八步一行不动 ——
  1147 +URL 与请求体形态天然不可能变。②③ 顺带从「扫目录」改成带索引的 SQL,**判据一个字没改**,
  1148 +换的只是从哪儿把候选捞出来。
  1149 +
  1150 +字段落点的三个判断:
  1151 +- **`objectiveId` 不单独存**:它恒等于 okr node id 的首段(`OkrService.parseNodeId` 与
  1152 + `reportRunOutcome` 同一口径),且两处读取方都是 `if (objId && meta.nodeId)` —— 从不单独用。
  1153 +- **`sourceConvId` 删掉**:全仓零读取方。正是本文档 §九.4quater 立过案的「实现在、零调用方」,
  1154 + 不往新表里再搬一份。
  1155 +- **`summary` / `execution_conv_id` 加列**(用户定案 A):前者当前只写不读但语义清晰、界面迟早要它;
  1156 + 后者有真读取方。**没有选「加一个 legacy meta 的 JSON blob 列」** —— §十二 引 R-19.2 明确否过
  1157 + 「建表而不是 JSON」,那会把一个已被否掉的形态重新引进来。
  1158 +
  1159 +验证方式是**真起服务打一遍**,不是只看测试绿:照 `tools-base.md` 印的请求体原样 POST → 200 +
  1160 +老形态响应;再打一次 → 409 且 `existingTaskId` 正确;`/task/report` 之后 `status→completed`、
  1161 +`summary` 与 `finished_at` 都在;`loadTaskMeta` 往返出来的形状与老 JSON 逐字一致。
  1162 +
  1163 +### 16.6 第 5 步:删掉的是「猜」,不是「记录」
  1164 +
  1165 +原 `getAggregatedTasks` 是 122 行的启发式:扫全部 agent 的 `conversations/`,按会话 id 里的
  1166 +`T-xxx` 归组,再用三条策略(meta 的 `parentTaskId` / from agent 的活跃会话时间包含 /
  1167 +master conv 的标题相似)**猜**父子关系。猜出来的树没法验证对错,而且每次请求都要遍历所有
  1168 +agent 的会话目录 —— 路由那 10 秒缓存就是为它加的。
  1169 +
  1170 +现在父子关系是 `tasks.parent_task_id`,派单时就显式写下来了。`task-service.js` **391 行 → 236 行**。
  1171 +
  1172 +**`getTaskTimeline` 保留**,但来源拆清楚:
  1173 +- 任务树(有哪些子任务)→ `tasks` 表,显式、可验证;
  1174 +- 时间线正文 → agent 的 `conversations/`,那是**一手记录,没有替代品**。
  1175 +
  1176 +两处口径变化(已写进代码注释):`agents` 从「会话里出现过的 agent 集合」变成「谁派的 + 派给谁」;
  1177 +`conversationCount` 从「扫出来的文件数」变成「有没有起过执行会话」(0/1)—— 为一个列表字段
  1178 +遍历所有 agent 的会话目录,正是这次要去掉的那笔开销。
  1179 +
  1180 +### 16.7 两次「测试全绿 ≠ 没坏」
  1181 +
  1182 +这一批里有两处只有**真跑**才会暴露的问题,测试一条都没红:
  1183 +
  1184 +1. **`routes/tasks.js` 漏改**:它自己还在算 `t.conversations.length`,而 `conversations` 是
  1185 + 会话反推那套的产物、已随之删除 → `GET /api/tasks` 直接 500。没有任何测试走那条路。
  1186 +2. **控制台 org graph 栏宽**(§十五 之外,`81629b7`):320px 减去 padding 只剩 292px,
  1187 + 而两个节点加一条边要 380px+ —— 节点名被从中间切断。构建、单测、探针全都不会报。
  1188 +
  1189 +> 与之对照,§十六.2 那个「存量库一启动就炸」是**测试能覆盖但没人写**的类型。
  1190 +> 三者合起来是一条经验:**新建库的路径、纯逻辑的路径由单测天然覆盖;
  1191 +> 「存量数据」「真实接口」「肉眼可见」这三类必须另外安排人去走一遍。**
  1192 +
  1193 +### 16.8 留给下一轮
  1194 +
  1195 +1. `system/tasklog/` 目录**还在**(4 个历史文件,已回填进表)。按 §十二.5 应改名为
  1196 + `system/tasklog.bak-unify/` 而不是直接删,跑顺一个版本再清。本轮未做。
  1197 +2. `tools-base.md` 里印的端口仍是 `localhost:3000`,实际是 **3010**。已经真咬过人 ——
  1198 + 回填出来的派单正文里有人手写「端口 3010,不是 3000」在纠正它。**改端口不改契约形态**,
  1199 + 但它是所有 agent 的工具说明,值得单独一次改动 + 通告。
  1200 +3. `TaskTimeline.vue` 未动。它按字段名取值,本轮保住了全部 12 个字段,但 `agents` /
  1201 + `conversationCount` 的语义变了(见 §16.6),界面上的措辞可能要跟着调。