Commit be8de85b743a8e9ec651f1eb79e531ddd10fafa1
1 parent
e1206459
docs(27-P3C): 抓到一处路径分叉,就在 task-goal.json 的核心链路上
并行会话把 system/agents 那一族收敛到 locator.AGENTS_ROOT 之后 (e120645,13 处跨 5 文件,我复验 3/3 通过),顺着同一个模式查 company.js 里的 data/companies 那一族,撞见一个真的分叉: 写 company.js:writeTaskGoalArtifact path.resolve(__dirname,'../..','data','companies',...) 不认 GRAPH_RUNS_DIR 读 graph-engine.js:runRootDir(describeTask / goal-contract 都走它) RUNS_ROOT ? path.join(RUNS_ROOT, root_run_id) : ... 认 三 company-paths.js:companyRootDir runsRoot ? path.join(runsRoot,'_companies',cid) : ... 认,但形状又不同 同一个事实三种拼法。GRAPH_RUNS_DIR 一旦设上(目前只有测试在用), 写进 A 处、从 B 处读,describeTask 拿到 null 后静默退回兜底文案 「执行图节点任务」—— agent 从此拿不到真实任务描述,且没有任何报错。 goal-contract.test.js 的注释佐证读取方那套形状才是约定。 生产环境两边恰好都退到同一个默认分支,所以今天不发作。但批 1 会把 artifact 落盘点从「下达时」前移到「入队时」,正好要动这个函数 —— 修的时机就是那时。新增验收 12b 专门验它。 方法论也记一条:我第一次只 grep 了 TEMPLATES_DIR 这个名字,因此漏 掉同一文件里语义相同但叫 DATA_DIR 的 3 处。同义异名的副本按名字 grep 永远找不到,要按值 grep。 这是本文档攒到的第五个同类实例。同一个事实只能有一处持有者 —— 路径也是事实。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Showing
1 changed file
with
20 additions
and
3 deletions
docs/todo/system-design/27-p3c-goal-task-model.md
| @@ -485,11 +485,27 @@ for (const t of queuedTasks) { | @@ -485,11 +485,27 @@ for (const t of queuedTasks) { | ||
| 485 | 485 | ||
| 486 | 全部挂在 `server/routes/company.js`。错误响应沿用现有 `{ error, hint, ... }` 形态。 | 486 | 全部挂在 `server/routes/company.js`。错误响应沿用现有 `{ error, hint, ... }` 形态。 |
| 487 | 487 | ||
| 488 | -> **批 1 顺手收敛一处**(不是为改而改 —— 批 1 本来就要大改这个文件):`company.js` 里 `const TEMPLATES_DIR` 被**重复定义了 7 次**,其中 6 处硬编码 `path.resolve(__dirname, '../../system/agents')`,只有 1 处(`:220`)用了权威访问器 `locator.AGENTS_ROOT`。2026-08-18 的 agent 目录重构刚把这个路径搬过一次 —— **下次再搬,改 6 处、漏 1 处就是一个静默 bug**。收敛成模块级的一份 `locator.AGENTS_ROOT` 即可。 | 488 | +> **⚠️ 批 1 必须修一处路径分叉 —— 它就在本文档的核心链路上。** |
| 489 | > | 489 | > |
| 490 | -> 这与本文档反复点名的是**同一个病**:`canDispatch` 的取值域被前端复述过一份(§六.3 的教训)、`ACTIVE_ROOT_RUN_STATUS` 在两个文件里逐字重复(§九.1)、`isUngraduatedStatus()` 的取值域不许在别处复述(§四.9)。**同一个事实只能有一处持有者 —— 路径也是事实。** | 490 | +> `task-goal.json` 的**写入方与读取方用的不是同一套路径规则**: |
| 491 | > | 491 | > |
| 492 | -> (顺带记一条**不做**的:`company-service.js` 的 `const TEMPLATES_DIR = locator.AGENTS_ROOT` 名字已过期,读的人会以为它指 `system/templates`。但改名要动 50 处、跨 6 个文件、零功能收益 —— 不值得,留着这条记录说明它是已知的、不是没人看见。) | 492 | +> | | 位置 | 认不认 `GRAPH_RUNS_DIR` | |
| 493 | +> |---|---|---| | ||
| 494 | +> | **写** | `company.js` 的 `writeTaskGoalArtifact` → `path.resolve(__dirname,'../..','data','companies',cid,'runs',rid,'artifacts')` | ❌ **不认** | | ||
| 495 | +> | **读** | `graph-engine.js` 的 `runRootDir`(`describeTask` 与 `goal-contract` 都走它)→ `RUNS_ROOT ? path.join(RUNS_ROOT, root_run_id) : …` | ✅ 认 | | ||
| 496 | +> | 第三种 | `company-paths.js` 的 `companyRootDir` → `runsRoot ? path.join(runsRoot,'_companies',cid) : …` | ✅ 认,但**形状又不一样** | | ||
| 497 | +> | ||
| 498 | +> 同一个事实**三种拼法**。`GRAPH_RUNS_DIR` 一旦被设上(目前只有测试在用),写进 A 处、从 B 处读 —— `describeTask` 拿到 null,**静默退回兜底文案「执行图节点任务」**,agent 从此拿不到真实任务描述,而**没有任何报错**。`goal-contract.test.js` 的注释可以佐证读取方那套形状才是约定:`{GRAPH_RUNS_DIR}/{rootRunId}/artifacts/task-goal.json`。 | ||
| 499 | +> | ||
| 500 | +> 生产环境两边恰好都退到同一个默认分支,所以今天不发作 —— 但**批 1 会把 artifact 的落盘点从「下达时」前移到「入队时」**(§四.3 / §四.4 的描述冻结点),正好要动这个函数。**修的时机就是那时**:让写入方复用读取方的同一个 helper(`runRootDir` 提到 `company-paths` 或直接导出),三种拼法收敛成一种。 | ||
| 501 | +> | ||
| 502 | +> **`system/agents` 那一族已经不用管了** —— 2026-08-18 的 `e120645` 已把全仓库 13 处(跨 5 个文件,含 `company.js` 里 6 个 `TEMPLATES_DIR` + 3 个同义异名的 `DATA_DIR`)收敛到 `locator.AGENTS_ROOT`,并新增 `server/tests/unit/agents-root-single-holder.test.js` 锁住不变式。**批 1 新增的代码必须遵守它**:不许自己拼「以 `../` 开头、以 `system/agents` 结尾」的字面量,否则这条测试会红。 | ||
| 503 | +> | ||
| 504 | +> 这些全是**同一个病**的不同实例,本文档已经攒到第五个:`canDispatch` 的取值域被前端复述(§六.3)、`ACTIVE_ROOT_RUN_STATUS` 在两文件逐字重复(§九.1)、`isUngraduatedStatus()` 的取值域(§四.9)、`system/agents` 的 13 份路径、以及上面这三种 run 目录拼法。**同一个事实只能有一处持有者 —— 路径也是事实。** | ||
| 505 | +> | ||
| 506 | +> 方法论也记一条:我第一次只 grep 了 `TEMPLATES_DIR` 这个**名字**,因此漏掉了同一文件里语义相同但叫 `DATA_DIR` 的 3 处。**同义异名的副本按名字 grep 永远找不到,要按值 / 按路径字面量 grep。** | ||
| 507 | +> | ||
| 508 | +> (一条**刻意不做**的:`company-service.js` 的 `const TEMPLATES_DIR = locator.AGENTS_ROOT` 取值已正确、只是名字过期,读的人会以为它指 `system/templates`。改名要动 50 处、跨 6 个文件、零功能收益 —— 不值得。留着这条记录,是为了说明它**是已知的,不是没人看见**。) | ||
| 493 | 509 | ||
| 494 | ### 6.1 先做一件清理:退役 OKR 的 `/goals` 别名 | 510 | ### 6.1 先做一件清理:退役 OKR 的 `/goals` 别名 |
| 495 | 511 | ||
| @@ -598,6 +614,7 @@ for (const t of queuedTasks) { | @@ -598,6 +614,7 @@ for (const t of queuedTasks) { | ||
| 598 | 10. 列表端点在 20 goal × 3 task 的公司上**不 N+1**(`graph_run_nodes` 的进度用一次 `GROUP BY` 出)。 | 614 | 10. 列表端点在 20 goal × 3 task 的公司上**不 N+1**(`graph_run_nodes` 的进度用一次 `GROUP BY` 出)。 |
| 599 | 11. `/api/goals` 与 `/api/company/:cid/goals` 的 OKR 别名退役后,`OkrDashboard` / `CompanyDashboard` / `bin/lemon.js` 全部功能正常(它们走的是 `/api/okr`)。 | 615 | 11. `/api/goals` 与 `/api/company/:cid/goals` 的 OKR 别名退役后,`OkrDashboard` / `CompanyDashboard` / `bin/lemon.js` 全部功能正常(它们走的是 `/api/okr`)。 |
| 600 | 12. **回填**:跑迁移脚本后,既存的每条 root run 在左栏都以 goal + task 的形式出现,描述从 `task-goal.json` 读回(§四.8)。**这条必须在批 1 就验** —— 不验就等于批 1 让历史任务更看不见了。 | 616 | 12. **回填**:跑迁移脚本后,既存的每条 root run 在左栏都以 goal + task 的形式出现,描述从 `task-goal.json` 读回(§四.8)。**这条必须在批 1 就验** —— 不验就等于批 1 让历史任务更看不见了。 |
| 617 | +12b. **路径分叉已修**:在 `GRAPH_RUNS_DIR` 指向临时目录的条件下走一遍「下达 → 入队 → 读回描述」,`describeTask` 拿到的是**真实任务描述**而不是兜底文案「执行图节点任务」(验 §六 那处三种拼法已收敛为一种)。 | ||
| 601 | 13. **停止即出队**:`max=1`,任务 A 跑着、B 排队,对 A 点「停止」→ **B 在几秒内自动起跑**(验证 §五.1.1 的漏斗钩子覆盖了 `stopRun` 这条不走 `settleRun` 的路径),不是等 60s 兜底。 | 618 | 13. **停止即出队**:`max=1`,任务 A 跑着、B 排队,对 A 点「停止」→ **B 在几秒内自动起跑**(验证 §五.1.1 的漏斗钩子覆盖了 `stopRun` 这条不走 `settleRun` 的路径),不是等 60s 兜底。 |
| 602 | 14. **预算耗尽即出队**:把 A 的预算调到极小让它烧成 `blocked` → B 同样自动起跑。 | 619 | 14. **预算耗尽即出队**:把 A 的预算调到极小让它烧成 `blocked` → B 同样自动起跑。 |
| 603 | 15. **排队期间 replan**:B 排队时点「重新规划组织」改动 org graph → B 出队后**按新图跑**,没有孤儿节点、没有卡死(验证 `syncRunNodesToDef`)。 | 620 | 15. **排队期间 replan**:B 排队时点「重新规划组织」改动 org graph → B 出队后**按新图跑**,没有孤儿节点、没有卡死(验证 `syncRunNodesToDef`)。 |