diff --git a/docs/README.md b/docs/README.md index 790f602..e8e9190 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,9 @@ 判断依据:文档正文的显式「状态」标记(✅ 已完成 / 已实现 vs 📝 设计中 / 未实现)、未勾选的 checklist,以及与代码库实际实现的交叉核对(见各条目备注)。 -> **当前主线**:Graph Agent 架构重构([27-graph-agent-architecture.md](./todo/system-design/27-graph-agent-architecture.md))。该系列已完成的阶段文档(P1/P2)移入 `done/`,其余非主线待办在 `todo/future/`。 +> **当前主线**:Graph Agent 架构重构([27-graph-agent-architecture.md](./todo/system-design/27-graph-agent-architecture.md))。 +> 该系列已完成的阶段文档(P1 / P2 / **P3 收尾 P3C**)移入 `done/`,其余非主线待办在 `todo/future/`。 +> **当前未完成的是**:P3B 真跑跑道的几条验收、P3/P4 那笔「真实路径 smoke 进 CI」的欠账、P4(未实现)、28-followups。 ## done/ @@ -24,6 +26,8 @@ | [user-guides/08-base-agent-4-phase-guide.md](./done/user-guides/08-base-agent-4-phase-guide.md) | Base Agent 四阶段使用指南 | | [system-design/27-p1-virtual-reference.md](./done/system-design/27-p1-virtual-reference.md) | P1 虚拟引用寻址 — ✅ 已实现(commit `226489d`,`server/services/graph-service.js`):`agent.md` 不再逐公司拷贝,改为 `config.json.systemAgentRef` 引用 + `company-context.md` 承载公司语境 | | [system-design/27-p2-org-graph.md](./done/system-design/27-p2-org-graph.md) | P2 Org Graph — ✅ 已实现(`70a4727` + `f20d0d8`):org graph 定位为 company 级 workflow(执行流边),`orggraph-planning` agent 建公司时自动规划并招聘。真实建公司端到端验证通过 | +| [system-design/27-p3c-goal-task-model.md](./done/system-design/27-p3c-goal-task-model.md) | **P3 收尾** goal / task 两级模型 + 控制台任务列表 — ✅ 已完成(2026-08-20):五批实施 + §十七 清尾。含队列 / 并行槽位、`task-planning`、两套任务体系统一、墙钟预算改「只算活跃时长」。**§17.5 是真跑验收** —— 返工闭环第一次由真 agent(不是注入的假 executor)走通 | +| [system-design/27-p3c-batch3-handover.md](./done/system-design/27-p3c-batch3-handover.md) | 批 3 交接 —— ✅ 已收口(2026-08-20)。**事故记录归档**:一份未经复核的代码被第三方会话提交进 main。留着是因为 §六 那几条多会话协作规则是真踩出来的 | | [system-design/01-26](./done/system-design/) | 系统设计正文 01~26 章(14、25-harness-engineering-improvements 例外,见 todo/future/;27 例外,见 todo/),各章节状态见文内「状态」标记 | ## todo/(当前主线) @@ -37,9 +41,11 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d | [27-naming-conventions.md](./todo/system-design/27-naming-conventions.md) | **命名规范**。`{作用域}-{职能}`;图节点按层级轴命名;节点名 ≠ 本体名;三种 benchmark 消歧;OKR 正名。新增 agent id / 表名 / 字段名前必读 | | [27-e2e-task-execution-flow.md](./todo/system-design/27-e2e-task-execution-flow.md) | 端到端流程总览:建公司 → 下达任务 → org graph 逐节点执行 → work graph 规划与循环 → 验收收敛。含与 DevLoop 既有实现的对应关系分析 | | [27-example-dev-company-walkthrough.md](./todo/system-design/27-example-dev-company-walkthrough.md) | 参考场景走查(虚构的「配图工厂」),把架构跑一遍具体的数:各阶段 JSON 形态、成本估算、断点清单 | -| [27-p3-p4-work-graph-console.md](./todo/system-design/27-p3-p4-work-graph-console.md) | P3+P4(原 P4 回退+Loop-Validator 已并入)图执行引擎与执行控制台 — ✅ 已实现(`c3abf4a`),文档仍在补充中 | +| [27-p3-p4-work-graph-console.md](./todo/system-design/27-p3-p4-work-graph-console.md) | P3+P4(原 P4 回退+Loop-Validator 已并入)图执行引擎与执行控制台 — ✅ 已实现(`c3abf4a`)。**仍留 todo 的原因**:§十 末「四条硬规矩」第 2 条那笔欠账未还 —— 引擎用例一律注入假 executor / 假 planner,**没有一条真实路径的 smoke 进 CI** | +| [27-p3b-real-run-runway.md](./todo/system-design/27-p3b-real-run-runway.md) | P3 收尾之真跑跑道(从 mock 走到真 Claude CLI)— 📝 **仍在推进**。验收 7 ⛔ 真跑不成立(`readme-writer` 混进 org graph,未修);2/4/8/9 未在干净环境验。缺陷② 的根因(边界守卫误判并行会话)已于 2026-08-20 修掉,但**根因修了不等于验收点验了** | | [27-goal-contract-format.md](./todo/system-design/27-goal-contract-format.md) | 验收契约格式(`goal-benchmark` / `goal-test-data` / `goal-test-script` 的产物形态与跨节点传递) | -| [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 | +| [27-p4-parallel-and-artifacts.md](./todo/system-design/27-p4-parallel-and-artifacts.md) | **P4(回收编号)并行执行与产物索引** — 📝 设计中,未实现。fan-out 真并发 + 同 agent 多实例隔离 + `files.json` 产物索引。三个 Phase,C 可插队 | +| [28-followups.md](./todo/system-design/28-followups.md) | 28 号(Agent 注册表统一)的遗留清单 — 主文档已在 `done/`,这份装没做完的:dream 机制(只有口述、无设计)、任务态清理(源码树无限增长)、`source_kind`/`legacy_path` 删列、三项「已实施未验证」。§五 是**已立起来的守卫清单**(别重复造,也别删) | ## todo/future/(暂缓,非当前重心) diff --git a/docs/todo/system-design/27-p3c-batch3-handover.md b/docs/done/system-design/27-p3c-batch3-handover.md index 7fa94c4..3fd50ba 100644 --- a/docs/todo/system-design/27-p3c-batch3-handover.md +++ b/docs/done/system-design/27-p3c-batch3-handover.md @@ -2,7 +2,10 @@ > 记录时间:2026-08-19 17:1x > 记录人:Claude 会话 `lemonboss-8x`(本文档的作者,也是下面 87b776c / b0b2b62 两笔提交的执行者) -> 状态:**未收口**。等 lemonboss-78 的批 3 修复完成后一起处理。 +> 状态:✅ **已收口**(2026-08-20)。四条复核结论全部修完(记在 +> [27-P3C §15.3](./27-p3c-goal-task-model.md)),§五 那条卡住的 run 也已解开并真跑推进 +> (§17.4 / §17.5)。本文档从待办转为**事故记录归档**,留着是因为 §六 那几条协作规则 +> 是真踩出来的,不是纸上谈兵。 ## 一句话 @@ -167,16 +170,22 @@ task 翻 queued 后 `hasStartedRun()` 返回 false → `GraphConsole.vue:248` æŠ --- -## 七、下一步 +## 七、下一步 —— 全部完成(2026-08-20 回填) - [x] ~~lemonboss-78 补齐 §二 里缺的 3 条复核结论~~ —— 已补齐并提交(`5a31d00`),4 条均带独立探针实跑证据 -- [ ] 定 `87b776c` 的处理方式(建议修复前推) -- [ ] **N1** 修掉孤儿子 run(取消排队时还在烧 token)—— 唯一有直接金钱成本的一条,优先 -- [ ] **L1** 恢复路径对 manual 任务补一条出路(现在退回队列后没人接,sweep 空转) -- [ ] **N2** 「退回队列」透出到界面(`requeued` 字段全前端零读取;按钮全灰 + 图消失 + 假提示语) -- [ ] **N3** `isSlotRaceError` 第三条判据按探针结论改写,别再吞真错误 -- [ ] 工作区收敛干净后,重跑 `T-msyu6u9b-939c`(见 §五) -- [ ] 考虑给边界守卫加「多写入者 → 放弃判罚」的口子(§五 末尾) +- [x] 定 `87b776c` 的处理方式 —— **修复前推**,4 条作为后续提交解决 +- [x] **N1** 孤儿子 run —— 改为无条件走 `engine.stopRun`([27-P3C §15.3](./27-p3c-goal-task-model.md)) +- [x] **L1** manual 的恢复出路 —— 判据用 `attempt > 0` 而**不是** `started_at`(后者看着更贴切,但 `requeueBlockedRootRun` 退回队列时会把它清成 NULL,恰好在需要它的那一刻是空的,实测踩过) +- [x] **N2** 「退回队列」透出到界面 —— 加 `notice` 一行明说「已退回队列排队,坑一空会自动接着跑」 +- [x] **N3** `isSlotRaceError` —— 探针证伪后**删掉该分支**(被推走时条件更新返回 0、不抛;真正抛它的只有「调用方声明了非法转换」,那一支把调用方的 bug 当竞态吞掉且不留日志) +- [x] 重跑 `T-msyu6u9b-939c`(见 §五)—— ✅ **2026-08-20 已解开并推进**,见 [27-P3C §17.4 / §17.5](./27-p3c-goal-task-model.md) +- [x] 给边界守卫加「多写入者 → 放弃判罚」的口子(§五 末尾)—— ✅ 已加,见 [27-P3C §17.2](./27-p3c-goal-task-model.md) + +> **§五 那条 run 的结局值得单独记一句**:本文档判断「它卡住是因为边界守卫误伤」, +> 这一半是对的 —— 但守卫修完之后**它仍然跑不动**。第二道锁在墙钟预算上 +> (`blocked` 挂起等人的时间被算进「已用」,过夜的 run 永远醒不过来), +> 那把锁只有真去点一次重试才露出来。 +> **「修完 A 就能跑」是个假设,不是结论 —— 卡住的东西可以有不止一把锁。** > §二 那条「注释承诺的前提被本轮自己的改动改没了,注释还留着」已经是第三次立案 > (§十三.3 / §十四.4 / 本次)。第三次就不该再只当个案记 —— 值得考虑一条机器可查的守卫: diff --git a/docs/todo/system-design/27-p3c-goal-task-model.md b/docs/done/system-design/27-p3c-goal-task-model.md index d1df014..6eef668 100644 --- a/docs/todo/system-design/27-p3c-goal-task-model.md +++ b/docs/done/system-design/27-p3c-goal-task-model.md @@ -1,14 +1,15 @@ # 27-P3C. 公司目标与任务 —— goal / task 两级模型 + 控制台任务列表 -> **所属**: [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)。 +> **所属**: [27 Graph Agent 架构](../../todo/system-design/27-graph-agent-architecture.md) 的 **P3 收尾**(P3C),直接续 [27-P3/P4 执行控制台](../../todo/system-design/27-p3-p4-work-graph-console.md) §四.B 与 [27-P3B 真跑跑道](../../todo/system-design/27-p3b-real-run-runway.md)。 > **日期**: 2026-08-18 -> **状态**: **五批全部实施完毕并全绿**(§十三 ~ §十六)。132 文件 / 1257 测试,`web:build` 通过。开放问题已全部关闭;后续项见 §15.5 与 §16.8。 +> **状态**: ✅ **已完成并归档**(2026-08-20)。五批全部实施(§十三 ~ §十六),§15.5 / §16.8 的后续项也已全部收掉(§十七)。 +> 138 文件 / 1292 测试全绿,`web:build` 通过。**真跑验收见 §17.5** —— 返工闭环第一次由真 agent(不是注入的假 executor)走通。 > **⚠️ 批次与 phase 的对应不是一一对齐的**,判断「这份能不能搬进 done」时按批看,别按文档名看: > · **批 1-3 = 真正的 P3 收尾**(goal/task 两级模型 + `task-planning` + 排队),本文档的主体; -> · **批 4(并行)归 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3**,那边是设计权威,本文档只服从(见 §五.2 的警示块); +> · **批 4(并行)归 [27-P4](../../todo/system-design/27-p4-parallel-and-artifacts.md) §一.3**,那边是设计权威,本文档只服从(见 §五.2 的警示块); > · **批 5(两套任务体系统一)不属于任何既有 phase** —— 是跨阶段的技术债清理,风险面也与前四批完全不同(坏了是所有 agent 互相派不了活),见 §十二.6。 > **交付物**: ① `goals` / `tasks` 两张表,把「人类下达的需求」与「lemonboss 执行的单元」拆成两级;② 基于这两张表的**排队与并行**;③ 执行控制台(`/:id/graph-console`)最左侧新增**目标·任务两级列表栏**。 -> **命名**: 全文遵循 [27-NAMING](./27-naming-conventions.md),本文档**落地**的正是它 §四 定义但代码里一直没有载体的 `goal 1 : task N` 基数关系。 +> **命名**: 全文遵循 [27-NAMING](../../todo/system-design/27-naming-conventions.md),本文档**落地**的正是它 §四 定义但代码里一直没有载体的 `goal 1 : task N` 基数关系。 > **代码引用体例**: 沿用 27-P3/P4 头部约定 —— 只写 `文件名` + 函数名 / 常量名,**不写行号**。 > > **第二轮(2026-08-18,逐条对着代码复核初稿)**:抓到 7 处「照初稿实现就会出 bug」和 6 处补漏,正文已改,关键的错误结论**留档不删**(§三 的 ⚠️ 块、§五.1、§九.1 开头),因为它们是最容易再犯一次的那类。 @@ -47,7 +48,7 @@ C 是根因,A 和 B 是它的两个表征。**先拆两级模型,A/B 才有å ### 2.1 这不是新概念,是给一个已存在的悬空外键补上它的表 -[27-NAMING §四](./27-naming-conventions.md) 早已定义好基数: +[27-NAMING §四](../../todo/system-design/27-naming-conventions.md) 早已定义好基数: | 概念 | 基数 | 定义(原文) | |---|---|---| @@ -479,7 +480,7 @@ for (const t of queuedTasks) { **批 4 单独排期、单独验收**,理由:删掉一个唯一索引 = 删掉最后一道 DB 兜底,这个动作不该和「界面上能看到任务列表」混在一次上线里。 -> ⚠️ **批 4 与 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3 是同一件事,不要各做一遍。** 那份文档的目标 3 逐字写着「**解除「一家公司同时只有一个 active root run」约束** —— P3 v1 用一个唯一索引强制它(§三.3),并行能力具备后应放开」。也就是说:**放开约束的设计归 27-P4,本文档只负责在它之上定义「谁来排队、按什么序出队」**(§五.1)。真到实施时以 27-P4 的并行方案为准,本文档的 §五.2 服从它;两边都写一版会产生两套并发守卫。 +> ⚠️ **批 4 与 [27-P4](../../todo/system-design/27-p4-parallel-and-artifacts.md) §一.3 是同一件事,不要各做一遍。** 那份文档的目标 3 逐字写着「**解除「一家公司同时只有一个 active root run」约束** —— P3 v1 用一个唯一索引强制它(§三.3),并行能力具备后应放开」。也就是说:**放开约束的设计归 27-P4,本文档只负责在它之上定义「谁来排队、按什么序出队」**(§五.1)。真到实施时以 27-P4 的并行方案为准,本文档的 §五.2 服从它;两边都写一版会产生两套并发守卫。 --- @@ -738,7 +739,7 @@ for (const t of queuedTasks) { | **1 · 两级模型落库** | `goals` / `tasks` 建表 + **存量回填脚本(§四.8)** + ID 生成器修正 + `POST /:cid/tasks` 兼容改造(自动建 goal、`report_to` 落库)+ `GET` 列表端点 + 退役 `/goals` 别名 | 4 / 10 / 11 / **12** | | **2 · 控制台左栏 + 拆分** | `GoalTaskPane` 两级树 + URL 选中态 + `loadRuns` 修正 + **`task-planning` 本体(含 `_registry.json` 登记)+ 拆分链路 + 失败降级**+ 两步表单 | 1 / 19 / 20 / 21 / 22 | | **3 · 排队** | `run_status='queued'` + `RUN_TRANSITIONS` + `syncRunNodesToDef` + `task-scheduler`(漏斗钩子 + 三个辅助触发 + 重入保护 + 上游失败推导 blocked)+ `depends_on` 环检测 + 入队/取消/rerun 端点 + §四.7 两条禁令 + §九.1 全部回归点 | 2 / 3 / 5 / 6 / 7 / 8 / 9 / 13-18 / 23 | -| **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit。**与 [27-P4](./27-p4-parallel-and-artifacts.md) §一.3 同一件事**,实施以那份为准 | 另立验收 | +| **4 · 并行**(单独排期)| `max_parallel_tasks` + `DROP` 唯一索引 + 事务内 admit。**与 [27-P4](../../todo/system-design/27-p4-parallel-and-artifacts.md) §一.3 同一件事**,实施以那份为准 | 另立验收 | | **5 · 任务体系统一**(单独排期)| 见 §十二:`executor` / `assignee_agent_id` / `parent_task_id` 三字段 + `/task/dispatch` 换存储 + 删会话反推 + `tasklog` 回填 | 25-30 | > 批 3 的验收从 6 条涨到 12 条,是本次复核的直接结果 —— 13/14/15 三条各自对应一个「初稿会实现出来的静默故障」。 @@ -844,7 +845,7 @@ parent_task_id TEXT REFERENCES tasks(id), -- B 的任务链;A 的 depends_ ## 十三、批 1 实施记录(2026-08-18,commit `7f24ef0`) -> 体例照 [27-P3/P4](./27-p3-p4-work-graph-console.md) §十:**设计正文不改写,只在这里与实现对账**。 +> 体例照 [27-P3/P4](../../todo/system-design/27-p3-p4-work-graph-console.md) §十:**设计正文不改写,只在这里与实现对账**。 > 凡本节与前文冲突,**以本节为准** —— 前文是设计时的判断,本节是跑出来的事实。 ### 13.1 落地了什么 @@ -1199,3 +1200,112 @@ agent 的会话目录 —— 路由那 10 秒缓存就是为它加的。 但它是所有 agent 的工具说明,值得单独一次改动 + 通告。 3. `TaskTimeline.vue` 未动。它按字段名取值,本轮保住了全部 12 个字段,但 `agents` / `conversationCount` 的语义变了(见 §16.6),界面上的措辞可能要跟着调。 + +--- + +## 十七、收尾之后(2026-08-20)—— §16.8 与 §15.5 全部收掉,外加真跑挖出来的三条 + +> 体例同 §十三~§十六。这一节记的是 P3C 五批之后的清尾,**以及一次真跑**: +> 它才是这批工作真正的验收,也是三条缺陷的出处。 + +### 17.1 §16.8 / §15.5 的欠账(commit `8f4c1b6`) + +| 来源 | 项 | 结果 | +|---|---|---| +| §16.8-1 | `system/tasklog/` 改名保留 | ✅ 改成 `system/tasklog.bak-unify/`;回填脚本**两处都认**(先原位置再备份位置,顺序不能反:万一有人从备份恢复出一份新的 tasklog,那份才是要回填的) | +| §16.8-2 | agent 提示词里的端口 3000 → 3010 | ✅ 守卫 `agent-docs-port.test.js`。第一版扫出 5 条误报,全是某 agent 目录下 Python venv 里 playwright 的 ThirdPartyNotices —— 第三方文件不是提示词,已跳过 `venv/site-packages/workspace/conversations/train` | +| §16.8-3 | `TaskTimeline.vue` 的措辞 | ✅ `conversationCount` 批 5 之后只会是 0/1,界面却写 `Convs: N` —— 读的人会以为这条任务只有一次对话。改成有/无的表达 | +| §15.5-1 | 边界守卫误判并行会话的改动 | ✅ 见 17.2 | +| §15.5-2 | 机器级闸门 | ✅ `SessionPool.maxSessions()` + `SessionPoolFullError` + `_evictForRoom()`,守卫 `session-pool-max.test.js` | +| §15.5-3 | 批 5 | ✅ §十六 | + +### 17.2 边界守卫:从「乱扣帽子」改成「留证据、不判罚」 + +守卫的前提是「窗口内新增的仓库改动 = 这个节点写的」。**多会话共用 worktree 时这个前提是假的** —— +`git status` 的差集里没有「谁写的」这一维。实测已发生:批 3 的开发改动把一条 run 判成 +`boundary_escape` 卡住(就是 §17.4 那条)。 + +可检测的信号:节点**开跑那一刻**仓库就已经有源码目录的未提交改动 → 有人正在这个 worktree 上开发 +→ 窗口内新增的源码改动无法归属 → **降级为只记证据、不判罚**(与既有的 `unattributable` 桶同一语义), +而不是关掉守卫:证据照留,人一眼看得出「这次没判罚,因为当时仓库是脏的」。 + +`BOUNDARY_GUARD=strict` 强制判罚(CI / 干净环境),`=record` 一律只记。守卫 8 例,含关键对照: +**仓库脏时不判、干净时同样一处越界仍判 fail** —— 守卫没被削弱,只是不再乱扣帽子。 + +### 17.3 测试在往生产图库写(commit `20fed0a`) + +`agent-chat-tenant-isolation.test.js` 每跑一次往共享的 `data/graph.sqlite` 落 2 条 goal + 2 条 task。 +库里 31 条 goal 里 **22 条是它的残留**,而测试一直全绿。 + +根因是**批 5 的存储迁移留下的半截清理**:`/task/dispatch` 的元数据从 `system/tasklog/*.json` +换进了 `tasks` 表(§十六 第 3 步),而这个用例的 `afterAll` 只会清那个目录 —— +清理代码还在、清的东西已经不在那儿了。**这是这次迁移唯一没被任何测试照到的一面。** + +> **它是本仓库第四次踩「测试污染共享库」,但换了个方向进来。** +> 前三次(`BENCHMARK_DB_PATH` / `USERS_DB_PATH` / `SKILLS_DIR`,记在 `test-isolation-env.test.js` +> 文件头)都是「设了 env,生产代码不读」,那条守卫查的正是这个形态。 +> 这次是**一个字都没设** —— 没有可查的东西,守卫自然查不出来。 +> 静态扫源码也判不出「这个测试会不会开库」(要跨 require 链推导)。 + +修法两层:用例自己开一次性库;`graph-db.getDb()` 加**运行期闸门**(`VITEST` 为真且没设 +`GRAPH_DB_PATH` → 抛,报错里点名 env、点明「要在 require 之前设」,因为 `DB_PATH` 是模块级 const)。 +闸门放在 `getDb()` 不放模块顶层:只为取常量而 require 是合法用法,顶层抛会误伤。 +守卫 `graph-db-prod-guard.test.js` 4 例(含「没有 VITEST 时不生效」—— 防止把生产一起锁死)。 + +存量清理:删掉 `created_by LIKE 'zz-tenant-%'` 的 22 goal + 22 task,其余 9 条真实数据一条没动。 + +### 17.4 那条卡了两天的 run:真正卡住它的不是边界守卫,是墙钟预算 + +`run_9168bd24f46c`(`e2e-demo` 做 lemonai 官网)2026-08-18 16:19 判 `blocked` 之后再没能起来。 +17.2 修完误判之后它**仍然跑不动** —— 第二道锁在预算上: + +> 墙钟按 `now - spent.started_at` 算,而 `started_at` 是「第一次花钱的绝对时刻」。 +> 这与 `blocked` 的语义**直接对撞**:预算耗尽、节点判罚都会把 run 打到 `blocked`, +> 而 `blocked` 的定义就是**挂起等人工**。人隔夜来处置 → 等待那段被算成「已用」→ +> 一起跑就报 `wallclock_ms` 耗尽 → 再 `blocked` → 再等人。**过夜的 run 永远醒不过来。** + +而且报出来的原因具有误导性:写着 `wallclock_ms`(跑太久),实际是**等人等太久**。 +这条 run 只花了 7/100 次调用、69,961/5,000,000 tokens —— **预算用掉不到 2%**。 + +改法:`spent` 改记 `active_ms`(累计活跃时长),`started_at` 退化成「当前这一段的起点」, +不在 `running` 时为 null。起止结算挂在 `graph-run-sse.afterRunTransition` 这个**单一漏斗**上。 + +⚠️ 钩子必须放在那个 early return **之前**:进 `running` 既不是收敛、也不空坑,两个条件都不满足, +放后面就变成「只结算停下来、从不开始计时」,症状是 `active_ms` 恒 0、墙钟形同虚设,**而且照样全绿**。 + +**第一版的洞是真起服务、真点重试才撞出来的**(commit `9891c3b`):预算行挂在 root, +我就只认 root 的转换;而人工重试一个 work 层节点动的是**子 run**(`blocked → running`), +root 全程 `blocked` —— 子 run 跑整场、墙钟一次都没开始计。**单测当时全绿,因为它们只喂了 root。** + +存量重锚必须放在**开库路径**里:钩子只在「进 / 出 running」时触发,而升级那一刻正停在 +`running` 的 run 不会再进一次 `running`。⚠️ 第一版把这段迁移写进了 `graph-db.js` 的 `migrate()`, +而 `graph_runs` 是 `work-graph-db` 建的表、graph-db 的迁移块跑在它之前 —— 新库上直接 +`no such table: graph_runs`,`getDb()` 起不来。**和 §16.2 那次「加索引写进建表块」是同一类顺序问题,这是第二次。** + +### 17.5 真跑结果(2026-08-20) + +重启服务(迁移在真库上跑到)→ 重试 `org-planner-sdd` → 逐节点 `/next`: + +| 节点 | 结果 | +|---|---| +| `org-planner-sdd` | ✅ success(attempt 2) | +| `org-planner-benchmark` | ✅ success | +| `org-planner-validator` | ✅ success → 整张 work graph 五节点全 success,work run `completed` | +| (父节点自动拉回) | ✅ `reviveParentFromChild` 把 root 从 `blocked` 拉回 `running` | +| `org-coder` | → 起实现层子图 `run_91c4e38492dd` | +| `org-coder-impl` | ✅ success | +| `org-coder-validator` | 判 fail → **走回退边打回 `org-coder-impl`**(`loopCount: 1`),判词:项目根下只有一份计划文档,没有任何交付文件 | + +**返工闭环在真跑里成立了** —— 这是 P3 的核心机制第一次由真 agent(不是注入的假 executor)走通。 +预算 13/100 次调用、114,847/5,000,000 tokens。 + +### 17.6 这一轮的三条经验 + +1. **「修完 A 就能跑」是个假设,不是结论。** 17.2 修完误判之后那条 run 仍然跑不动, + 第二道锁(墙钟)只有真去点一次才露出来。**卡住的东西可以有不止一把锁。** +2. **单测喂什么形状,就只能守住什么形状。** 墙钟第一版的 8 条用例全绿, + 因为它们全都直接喂 root run —— 而生产里触发计时的是**子 run**。 + §16.7 说「存量数据 / 真实接口 / 肉眼可见三类要另外走一遍」,这里要补第四类: + **树形结构的机制,测试必须喂非根节点**。 +3. **同一类顺序问题第二次了**(迁移写错模块 vs §16.2 加索引写进建表块)。 + 判据可以固化成一句话:**凡是写迁移,先问「这张表是谁建的」,答案就是这段迁移该待的文件。** diff --git a/docs/todo/system-design/27-graph-agent-architecture.md b/docs/todo/system-design/27-graph-agent-architecture.md index 154c8f4..68835d9 100644 --- a/docs/todo/system-design/27-graph-agent-architecture.md +++ b/docs/todo/system-design/27-graph-agent-architecture.md @@ -2,7 +2,7 @@ > **版本**: v0.3(P0/P1 已实现;Org Graph 建模在 v0.3 修正为 company 级 workflow) > **日期**: 2026-08-15 -> **状态**(2026-08-18 校准,此前这一行落后两个阶段): P0 ✅ 已实现(`graph-db.js` + `graph-seed-service.js`,见第七节);P1 ✅ 已实现(commit `226489d`,`graph-service.js`);**P2 ✅ 已实现**(设计已归档 [27-P2](../../done/system-design/27-p2-org-graph.md));**P3 主体 ✅ 已实现**(`c3abf4a` 起七轮,见 [27-P3/P4](./27-p3-p4-work-graph-console.md) §十 实现记录 + 第六轮终验回填;收尾见 [27-P3B](./27-p3b-real-run-runway.md));**P4 📝 设计中/未实现**([27-P4](./27-p4-parallel-and-artifacts.md) 自述 v0.1 设计阶段) +> **状态**(2026-08-20 校准): P0 ✅ 已实现(`graph-db.js` + `graph-seed-service.js`,见第七节);P1 ✅ 已实现(commit `226489d`,`graph-service.js`);**P2 ✅ 已实现**(设计已归档 [27-P2](../../done/system-design/27-p2-org-graph.md));**P3 主体 ✅ 已实现**(`c3abf4a` 起七轮,见 [27-P3/P4](./27-p3-p4-work-graph-console.md) §十 实现记录);**P3 收尾 ✅ 已完成并归档**([27-P3C](../../done/system-design/27-p3c-goal-task-model.md) 五批 + §十七 清尾,2026-08-20 真跑走通返工闭环);**P3B 真跑跑道 📝 仍在推进**([27-P3B](./27-p3b-real-run-runway.md),验收 7 真跑不成立、2/4/8/9 未验);**P4 📝 设计中/未实现**([27-P4](./27-p4-parallel-and-artifacts.md) 自述 v0.1 设计阶段) > > ⚠️ 这一行是别人判断「哪些文档能从 `docs/todo/` 搬去 `docs/done/`」的依据 —— 它过期会连累后面每一次迁移判断,改阶段状态时请顺手更新它。 > **命名**: 新增 agent id / 表名 / 字段名前先读 [27-NAMING 命名规范](./27-naming-conventions.md) diff --git a/docs/todo/system-design/27-p3b-real-run-runway.md b/docs/todo/system-design/27-p3b-real-run-runway.md index 8c22760..e17ec51 100644 --- a/docs/todo/system-design/27-p3b-real-run-runway.md +++ b/docs/todo/system-design/27-p3b-real-run-runway.md @@ -5,6 +5,11 @@ > **日期**: 2026-08-16 > **起因**: [27-REQ](./27-requirement.md) R-18(招聘归 `workgraph-planning`;停用 mock 改真 CLI) > **前置**: P3 主体已实现并提交(`c3abf4a` 起七轮),测试 719/719 全绿 +> **状态**(2026-08-20 校准): 📝 **仍在推进**。第一批验收 1/3 已验、2 的后半与 4 未在干净环境重验; +> 第二批验收 7 ⛔ 真跑不成立(§六bis 缺陷③ `readme-writer` 混进 org graph,**未修**); +> 第三批 8/9 真 CLI 未验。 +> §六bis 缺陷② 的**根因**(边界守卫误判并行会话的改动)已于 2026-08-20 修掉, +> 见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md) —— 但**根因修了不等于验收点验了**,那几条仍要重跑一遍。 --- @@ -760,8 +765,8 @@ agent 读的是 `agent.md`,那一侧不改,模型就会继续以为「前三 | # | 标准 | 状态 | |---|---|---| | 1 | 服务启动后 `system_agents` 与 `system/templates/` 一致,无悬空 `company_hired` 行;新加一个模板目录、重启服务,它自动出现在表里 | ✅ **已验**(生产库实查 + hermetic 用例)。一处**已知且合理**的偏差:`system/agents/` 与 `system/templates/` 同名的 6 个 id(`agent-creator` / `org-innovator` / `innovator-{digest,evolver,hunt}` / `evolver-youtube-crawler`)按设计 skip,仍是 `global_agent`。图用到的本体都不在这 6 个里 | -| 2 | 起一个真 CLI 的 run,节点产出全部落在 `data/companies/{cid}/projects/{pid}/` 内;`git status` 在宿主仓库无任何新增/修改 | 🟡 **前半已验、后半被并行 agent 打破**(2026-08-17,§六bis):`workgraph-planning` 的产物真的落在 `data/companies/p3bfin/projects/p-24vxi8/workspace/` 内,`project-root.json` 四环闭合;宿主仓库出现的两处改动经查是**并行的 Master agent** 写的(缺陷②),已复原。被判罚的那个 work 节点自己一个字节都没往仓库写 | -| 3 | 故意让节点往产物目录外写,被守卫拒绝并在 timeline 留痕 | ✅ **真 CLI 已验**(2026-08-17,§六bis):git 扫描确实扫到了宿主仓库里的那处改动,`node.boundary` 事件带完整 `entries`,结果被降级判 fail 且 `fail_reason='boundary_escape'` 落库。**但同一次也验出了误判**:那处改动是并行的 Master agent 写的,不是被判罚的节点写的(W3「拦不住」第 5 条,见 §六bis 缺陷②)。判罚逻辑侧另有 W12 补完那一轮加的三条分支用例 | +| 2 | 起一个真 CLI 的 run,节点产出全部落在 `data/companies/{cid}/projects/{pid}/` 内;`git status` 在宿主仓库无任何新增/修改 | 🟡 **前半已验、后半被并行 agent 打破**(2026-08-17,§六bis)。**2026-08-20 补记**:后半那个「被并行 agent 打破」本身已经不再导致误判 —— 守卫改成「节点开跑那一刻仓库就脏 → 只记证据、不判罚」,见 [27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md)。**但这条验收标准本身仍未在干净环境下重验**。当时的原始记录:`workgraph-planning` 的产物真的落在 `data/companies/p3bfin/projects/p-24vxi8/workspace/` 内,`project-root.json` 四环闭合;宿主仓库出现的两处改动经查是**并行的 Master agent** 写的(缺陷②),已复原。被判罚的那个 work 节点自己一个字节都没往仓库写 | +| 3 | 故意让节点往产物目录外写,被守卫拒绝并在 timeline 留痕 | ✅ **真 CLI 已验**(2026-08-17,§六bis):git 扫描确实扫到了宿主仓库里的那处改动,`node.boundary` 事件带完整 `entries`,结果被降级判 fail 且 `fail_reason='boundary_escape'` 落库。**但同一次也验出了误判**:那处改动是并行的 Master agent 写的,不是被判罚的节点写的(W3「拦不住」第 5 条,见 §六bis 缺陷②)。判罚逻辑侧另有 W12 补完那一轮加的三条分支用例。**2026-08-20 补记**:误判那一半已修([27-P3C §17.2](../../done/system-design/27-p3c-goal-task-model.md),`BOUNDARY_GUARD=strict|record` 两个开关 + 8 例守卫,含「仓库干净时同样一处越界仍判 fail」的对照) | | 4 | 造一份非法契约跑 `benchmark-validator`:判决为失败且引擎按失败处理(**不是** success) | 🟡 **静态成立,未真跑**。模板已改写 `verdict: "attributed"`,而 `FAILING_VERDICTS={attributed,inconclusive}` → `readNodeProduct` 置 `failingVerdict=true` → `callAgentNode` 判 `fail`(`graph-engine.js:473/385`),链路读通;防线测试三次反例变异全部变红 | > 2 与 3 都要真 CLI 才验得了,而真 CLI 现在会先死在 W5/W6 上。**这两条应挪到第二批之后一起验。** diff --git a/docs/todo/system-design/27-requirement.md b/docs/todo/system-design/27-requirement.md index 93b0004..d273a43 100644 --- a/docs/todo/system-design/27-requirement.md +++ b/docs/todo/system-design/27-requirement.md @@ -563,7 +563,7 @@ git diff,最后 W13 只能把计数挪去另一个被 ignore 的文件。`file 统一还顺带修好两件事:30 分钟派单去重从「`readdirSync` 整个 tasklog 目录 + 逐个 JSON.parse」变成一条 SQL;两套同前缀不同源的 `T-` id 合一。 -**落点**:[27-P3C goal / task 两级模型](./27-p3c-goal-task-model.md)(本条派生出的工作项文档)。 +**落点**:[27-P3C goal / task 两级模型](../../done/system-design/27-p3c-goal-task-model.md)(本条派生出的工作项文档)。 --- diff --git a/docs/todo/system-design/28-followups.md b/docs/todo/system-design/28-followups.md index 9a77ceb..f6015eb 100644 --- a/docs/todo/system-design/28-followups.md +++ b/docs/todo/system-design/28-followups.md @@ -63,17 +63,26 @@ 只有设计,没有实现。任务目录 `system/agents/{aid}/{cid}/{taskId}/` 在源码树里无限增长。 **实现前必须先读主文档 §5.6b** —— 公司态是 dream 的原料,不是缓存,只能清任务态。 -### 3.3 `agent-chat.js:1209` 是心跳落点 bug 的第二个入口 +### 3.3 ~~`agent-chat.js` 是心跳落点 bug 的第二个入口~~ ✅ 已修(2026-08-20,`edeff2e`) `PUT /api/agent/:agentId/heartbeat`(前端 Agent 详情页走的就是它)用 `locator.bodyDir(parseAddress(id).agentId)` 把公司段丢掉,然后整份重写本体 `config.json`。 +后果是**静默无效**:公司态文件一旦存在,读侧优先取公司态,界面收到 `{success:true}` +而实际在跑的还是旧值;`GET /info` 同理报的是本体缺省。症状是「界面写 30 分钟、 +调度器按 15 分钟在跑」,两边都不报错。 -心跳落点已在 `company.js` 侧修好(写 `system/agents/{aid}/{cid}/heartbeat.json`), -**但这个入口没改**,且因此多了一个新问题:公司态文件一旦存在,读侧优先取公司态, -于是这个端点的写入对该 (agent, 公司) 变成**静默无效**。 +读写都已改走 `heartbeat-service` 的 `readHeartbeatConfig` / `writeHeartbeatConfig`, +与心跳挂载读的是同一个答案;入参校验归服务(`intervalMinutes` 直接进 `setInterval`, +NaN / 0 / 负数会让定时器退化成每 tick 触发一次,而每次 tick 真起一轮 LLM),端点翻成 400。 -修法(一行级):那段读改写换成 -`require('../services/heartbeat-service').writeHeartbeatConfig(req.params.agentId, req.body)`, -`GET :1203` 的 `heartbeat: config?.heartbeat` 同理换成 `readHeartbeatConfig`。 +**顺带修的一处**:404 闸门原来也用 `getBodyDir(id)`。`cid:role` 里冒号后面是**角色名**, +未必等于本体 id(`company_agents` 的映射)—— 拿角色名当本体 id 判存在性,凡是角色名 ≠ 本体 id 的 +都会被判成「agent 不存在」,而写侧其实找得到。**判 404 和写入必须问同一个问题。** + +守卫:`heartbeat-company-scoped.test.js` 补 3 例(反向验过,退回旧写法立刻红)。 + +> ⚠️ **一处没修,别当成修过了**:`GET /info` 的 404 闸门仍是 `getBodyDir(id)`, +> 对 `cid:role` 会直接 404。那是 `/info` 整体寻址的既有缺口(它还要按同一个目录读 +> `identity` / `agent.md` / `cron`),不在心跳这一条的范围内。测试里用裸 agent id 绕开。 --- @@ -95,12 +104,16 @@ ## 五、已经立起来的守卫(别重复造,也别删) -这几条是沉淀下来的不变式测试(前三条出自 agent 目录重构那一轮,第四条出自 27-P3C 批 1,第五、六条出自 CLI 进程泄漏那一轮)。 -**前四条**守的都是「同一个事实只能有一处持有者」,而且**六条都带「样本非空」前置断言** —— 少了那一条,扫描路径一写错就会以 -「零违例」的形式全绿,一条永远通过的不变式测试和没有这条测试是一回事。 +这几条是沉淀下来的不变式测试(前三条出自 agent 目录重构那一轮,第四条出自 27-P3C 批 1,第五、六条出自 CLI 进程泄漏那一轮, +第七、八条出自 27-P3C 收尾之后那一轮)。 +**第 1-4 条 + 第 8 条**守的都是「同一个事实只能有一处持有者」,而且**凡是靠扫描的都带「样本非空」前置断言** —— +少了那一条,扫描路径一写错就会以「零违例」的形式全绿,一条永远通过的不变式测试和没有这条测试是一回事。 -后两条形状不同:第五条守的是**资源生命周期**(每一条创建路径都要有对应的回收路径),判据不靠扫源码,而是把回收行为当黑盒去探; -第六条守的是**指路牌的有效性**(文档 / 清单里指向的东西必须真实存在)。列在一起是因为「别重复造,也别删」这条规矩对它们同样适用。 +另外三条形状不同,判据都不靠扫源码: +第 5 条守的是**资源生命周期**(每一条创建路径都要有对应的回收路径),把回收行为当黑盒去探; +第 6 条守的是**指路牌的有效性**(文档 / 清单里指向的东西必须真实存在); +第 7 条守的是**运行期闸门本身**(起真子进程去撞,看撞不撞得动 —— 静态扫源码判不出「这个测试会不会开库」)。 +列在一起是因为「别重复造,也别删」这条规矩对它们同样适用。 | 测试 | 守什么 | 违例时 | |------|--------|--------| @@ -110,6 +123,8 @@ | `runs-root-single-holder.test.js` | 凡是把 `process.env.GRAPH_RUNS_DIR` 绑到标识符上的模块,那个标识符必须在**代码里**(不是注释里)真正被用到 | 精确点名到文件行号 | | `agent-chat-session-reclaim.test.js` | agent-chat 的 `sessions` 池:一次性 convId 的调用跑完必须回收,可复用的(pipeline / 人类对话)必须留下并受 TTL 兜底;顺带锁 `CALL_KINDS` 的标注 | 探针 `/chat/:convId/stop` 报告会话还在 | | `no-dead-path-references.test.js` | CLAUDE.md 的仓库路径与 npm run、package.json 的 scripts/bin/main、docs/README.md 的相对链接,四类引用都必须指向真实存在的东西 | 列出死引用的出处与目标 | +| `graph-db-prod-guard.test.js` | 测试进程不许打开共享生产库 `data/graph.sqlite`(判据在 `graph-db.getDb()` 的运行期闸门上,这条测试是对闸门本身的黑盒验证) | 子进程撞库时抛,报错点名 env 与「要在 require 之前设」 | +| `heartbeat-company-scoped.test.js` | 心跳设置是公司级状态,两个入口(`/api/company/...` 与 `/api/agent/...`)都不许落到共享本体上,且写完要能从读侧读回 | 本体 `config.json` 的 mtime / 内容变了,或读侧读回的不是刚写的值 | > 第四条的来历值得记:`goal-contract.js` 里曾有一行 `const RUNS_ROOT = process.env.GRAPH_RUNS_DIR || null`, > 注释白纸黑字写着「与 graph-engine.RUNS_ROOT 是同一个变量(测试隔离在用)」,**全文件却零引用**。 @@ -137,6 +152,17 @@ > 上下文里的相对片段(`_base/`、`todo/future/`)、glob、`/…` 示意写法一律不判。 > 宽一档就会把这些一起判红,防线于是被人整条注释掉。 +> 第七条的来历是**第四次踩「测试污染共享库」**,而且换了个方向进来。前三次 +> (`BENCHMARK_DB_PATH` / `USERS_DB_PATH` / `SKILLS_DIR`)都是「设了 env、生产代码不读」—— +> `test-isolation-env.test.js` 查的正是这个形态。这次那个用例**一个字都没设**, +> 于是那条守卫没有可查的东西,`/task/dispatch` 每跑一次往生产库落 2 条 goal + 2 条 task, +> 攒到 22 条才被人翻库发现。 +> +> 静态扫源码判不出「这个测试会不会开库」(要跨 require 链推导),所以这一条改成 +> **运行期闸门 + 对闸门本身的黑盒验证**:起一个真子进程去撞,看撞不撞得动。 +> 判据里特意留了「没有 VITEST 时不生效」那一例 —— 少了它,闸门写成「永远不许开 +> `data/graph.sqlite`」也会全绿,而那会把生产一起锁死。 + **改动它们之前先想清楚**:这几条的价值全在于「红了才有用」。 放宽判据、加例外、或者把断言改成永远成立的形式,等于把守卫拆掉 —— 本仓库有过「隔离是表演」的测试污染前科。