# 28-followups. Agent 注册表统一 —— 未完成与未确认项 > **所属**: [28 Agent 注册表统一](../../done/system-design/28-agent-registry-unification.md) 的遗留清单。 > 主文档已随实施完成移入 `docs/done/`;**这份留在 `docs/todo/`,装的是还没做完和还没定的部分。** > **日期**: 2026-08-18 --- ## 为什么单独拆一份 28 号的主体(G1-G4:本体归位 `system/agents/`、运行态按公司/任务下沉、公司只存引用、缺则创建) 已实施并通过真 Claude CLI E2E,主文档因此移入 `done/`。 但整份搬走会让下面这些**从待办视野里消失** —— 那种丢失是静默的, 正好是 28 号自己反复在治的那类故障。所以拆成两份:**已完成的作为记录归档,未完成的留在这里。** --- ## 一、未确认(方向已定、设计未定 —— 别当成拍板过的去实现) ### 1.1 dream 机制 **状态(2026-08-20 更新)**:设计已成文并已实现,见 [29-dream-design.md](./29-dream-design.md)。四条取舍(去敏判据 / 沉淀落点 / 触发时机 / 质量评估)均已拍板,五步推进顺序 + 定时挂载 + 定义文件写入均已落地, `innovator-evolver` 上真 CLI 跑通一次(40 条原料 → 4 条抽象经验,8 条引用全部核对回原文)。 已实现的: - 定时(每日一次,串行,挂在 `server/index.js` 的 `DISABLE_BACKGROUND_AGENTS` 早退之后) 整理**所有 agent 在所有公司**的原始记录,**显式提炼**(`dream-distiller`)+ 独立去敏判定(`dream-screener`)后沉淀 - 落点四选四:本体 `memory/experience.md`(经验积累)+ `SKILL.md` / `soul.md` / `agent.md` (受管区块,三道闸门缺一不写,见 29 号文档 §5.6) - 引用真伪自动查(编造出处直接拒)+ `auditDistilled()` 把结论与原料原文并排列出, 几秒内可回查 - REST(`/api/dream/*`)+ 控制台(`/dream`,导航栏 `🌙 Dream`):查总览/详情、手动跑单个本体(含 dry-run)、 触发并轮询全量扫描(2026-08-21) **仍未做的**:「结论对不对」的判据(机器判不了,只能靠人抽查,`auditDistilled` 的输出不是质量分)。 「把 dream 套进 org/work graph 来跑」评估过、用户明确选了不做—— org/work graph 是单公司作用域的,dream 是跨公司扫描,两者模型不匹配; 且 graph 引擎自带的验收机制(goal-contract benchmark/test-script)判的是「产物过没过验收脚本」, 对「经验提炼得对不对」这种自由文本判断没有对应机制,套不套 graph 都得单独设计 rubric。 **已经生效的约束**(这条是确定的,不随 dream 的设计变化): 公司态目录(`system/agents/{aid}/{cid}/` 下的 `memory/` / `conversations/`)是 dream 的唯一原料, **不许被任何清理策略当垃圾扫掉**。详见主文档 §5.6b,任务态清理已按此约束实现(见下 §3.2)。 --- ## 二、已实施但未验证 来自主文档 §11.4。「已实施」不等于「已验完」,这几项目前只有单测、没有真跑证据: | 项 | 现状 | 缺什么 | |---|---|---| | **新建本体路径**(验收 6 前半) | 只有单测 | E2E 那轮四个角色全部命中已有本体,`hireFromBaseMetaAgent` → agent-creator → `draft`→`active` 这条链没有真 CLI 证据 | | **完整 work graph 真跑**(验收 9) | 未做 | E2E 停在 org 层;交付物归宿(§4.4 按链路分叉)没被大批量产物压过 | | **边界守卫接住新建本体** | 只有注入式单测 | E2E 的 864→864 零增量**只证明了运行态没漏进 git**,没替「新建本体不被冤杀」作证(那一轮根本没触发新建)。见 §11.3 | --- ## 三、未实施的工程项 ### 3.1 ~~`source_kind`~~ 已删 / `legacy_path` **不能删**(验收 2 后半) **审计结论与本条原来的写法不一样,两列要分开处理。** **`source_kind` ✅ 已删(2026-08-20)**:它与 `origin` 是一一映射 (`global_agent`↔`builtin` / `company_hired`↔`created`),实查生产库 76 行零例外。 全仓唯一读侧是 `graph-db.migrate()` 里那条 origin 回填,而那是给「`origin` 列出现之前写下的行」 用的一次性迁移。所以顺序是**先回填、再删列**,两步都用 `hasColumn` 守着(幂等)。 写侧散在 3 处生产 INSERT + 35 个测试文件,一并清掉。 守卫 `source-kind-drop-migration.test.js` 走存量库重启那条路,**专门钉住顺序** —— 反过来做不会报错,只是那几行 origin 悄悄是错的(反向验过,弄反立刻红)。 **`legacy_path` ❌ 不能删,它还没死**。原文说「所有读取点都已改成从 id 推导」—— 那半句对新行成立,对存量行不成立: - 新写入的行 `legacy_path` 恒等于 `system/agents/{id}`(= 可由 id 推导,确实冗余); - **存量行里存着批次 4 之前的公司侧路径**(`data/companies/{cid}/agents/{role}`), 而 `graph-seed-service` 判「悬空行 / 孤儿行」正是靠**这个路径指向的目录还在不在**。 从 id 推导出来的路径永远指向 `system/agents/`,那条判据会整个失效。 实查生产库(2026-08-20):76 行里**还有 2 行**是老路径(`org-planner` / `org-coder` 指着已经不存在的 `data/companies/mycompany/agents/`)—— 正是这条判据要认出来的那种行。 > **删它的退出条件**(写在 `graph-db.js` 建表注释里,别只留在这份文档): > 库里不再有 `legacy_path <> 'system/agents/' || id` 的行。 > 到那一天它才真的零信息量。 ### 3.1b 顺带记一条:`getDb()` 会就地迁移生产库 `graph-db.getDb()` 内部调 `migrate()`。**任何 require 本模块并调 `getDb()` 的进程都会改生产库的 schema**, 包括一条随手敲的 `node -e "...getDb()..."`。 删 `source_kind` 那次,生产库的列就是被一条**查状态用的**一行脚本删掉的 —— 服务当时根本没重启,人也没打算做迁移。结果是对的(迁移幂等,76 行逐行核对与备份一致、 origin 全部按映射回填正确),但「读一下状态」不该顺手改 schema 这件事本身值得知道。 `assertNotProdDbInTests` 那道闸门**只挡测试进程**,故意不挡普通脚本(挡了服务自己也起不来)。 要只读地看一眼,用 `new Database('data/graph.sqlite', {readonly: true})`,别走 `getDb()`。 ### 3.2 ~~任务态清理策略(§5.6)~~ ✅ 已实现(2026-08-20) `server/services/task-state-cleanup.js` + `scripts/cleanup-task-state.js`,**默认 dry-run**。 四道判据全过才删:① 公司在 `company_agents` 名册里(`train/` `workspace/` 与测试残留天然排除); ② 目录名在**任务态白名单**里;③ mtime 早于 N 天(默认 14);④ `run_*` 对应的 run 已到终态。 > **第 ② 条是踩出来的**:第一版写成黑名单(「不在保护名单里的都算任务态」), > dry-run 直接选中了 `trend-scout-global/idearun/venv` 和 `youtube-crawler/idearun/data` > —— 一个 Python 虚拟环境、一个数据目录,都在**真公司**底下,都不是任务态。 > **删错不可逆,留着最多占磁盘 —— 判据必须是「认得出才删」,不是「不认识就删」。** 实跑(30 天窗口):待删 0,跳过 955(556 还新 / 74 不在名册 / 60 公司态 / 11 白名单外)。 公司态永远走「不许清」那条分支,守卫里有一条专门盯它。 ### 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 分钟在跑」,两边都不报错。 读写都已改走 `heartbeat-service` 的 `readHeartbeatConfig` / `writeHeartbeatConfig`, 与心跳挂载读的是同一个答案;入参校验归服务(`intervalMinutes` 直接进 `setInterval`, NaN / 0 / 负数会让定时器退化成每 tick 触发一次,而每次 tick 真起一轮 LLM),端点翻成 400。 **顺带修的一处**: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 绕开。 --- ## 四、验证口径提醒 主文档 §11.3 留了一段「文档与代码一致性检查」的 shell(常量改名同步)。 路径已随文档迁移更新完毕(`cd2066a`),实跑三份文档全部命中当前常量名 `AGENT_REGISTRY_TREE_RE`。 取值域**不要**抄进文档。毕业闸门的权威判据只有三处: - `server/services/agent-locator.js` 的 `missingBodyDefinition()` - `server/services/graph-service.js` 的 `promoteDraftIfComplete()` - `server/services/graph-service.js` 的 `isUngraduatedStatus()`(只挡 `draft` / `training`) 文档里复述取值域会产生第二份真相 —— 这个坑本仓库踩过: 一度按「≠ active 就挡」判,把库里 36 个 `enabled`/`idle`/`ready`/`trained` 的本体全判成「没填完」, `workgraph-planning` / `master` / `qa-judge` 全部雇不进来。 --- ## 五、已经立起来的守卫(别重复造,也别删) 这几条是沉淀下来的不变式测试(前三条出自 agent 目录重构那一轮,第四条出自 27-P3C 批 1,第五、六条出自 CLI 进程泄漏那一轮, 第七、八条出自 27-P3C 收尾之后那一轮)。 **第 1-4 条 + 第 8 条**守的都是「同一个事实只能有一处持有者」,而且**凡是靠扫描的都带「样本非空」前置断言** —— 少了那一条,扫描路径一写错就会以「零违例」的形式全绿,一条永远通过的不变式测试和没有这条测试是一回事。 另外三条形状不同,判据都不靠扫源码: 第 5 条守的是**资源生命周期**(每一条创建路径都要有对应的回收路径),把回收行为当黑盒去探; 第 6 条守的是**指路牌的有效性**(文档 / 清单里指向的东西必须真实存在); 第 7 条守的是**运行期闸门本身**(起真子进程去撞,看撞不撞得动 —— 静态扫源码判不出「这个测试会不会开库」)。 列在一起是因为「别重复造,也别删」这条规矩对它们同样适用。 | 测试 | 守什么 | 违例时 | |------|--------|--------| | `agents-root-single-holder.test.js` | 本体根路径只能由 `locator.AGENTS_ROOT` 持有 | 精确点名到文件行号 | | `no-stale-templates-path.test.js` | 活定义里不许出现已死的 `system/templates`(判据:git 跟踪的才是定义,未跟踪的是运行态产物) | 同上 | | `repo-hygiene.test.js` | tracked 与 ignored 不重叠 | 列出越界文件 | | `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 是同一个变量(测试隔离在用)」,**全文件却零引用**。 > 一个声明了意图、代码却不兑现的常量,比没有这个常量更容易骗人 —— 读的人会据此认为该模块认这个覆盖。 > 它同时也是「实现在、零调用方」这个惯犯模式的又一例(本仓库有人数到第五次,有人写「十几次」)。 > 第五条的来历同样值得记:`sessions` 池存的是**常驻**的 claude CLI 子进程,回收路径只有四条 > (协议 error / 子进程 disconnected / 手动删对话 / 手动 stop),**「正常跑完」不在其中**。 > 两路心跳又都用一次性 convId 打进来,于是每 5 分钟漏一个进程,实测单次运行 2h43m 攒到 > **82 个进程 / 17.5 GB RSS**。它不报错、不写日志、`git status` 干净,唯一的症状是机器变慢 —— > 和第四条一样属于「读代码读不出来、要去看运行态才发现」的一类。 > > 回收策略的判据写在 `agent-chat.js` 的 `CALL_KINDS` 表里,只有一条:**convId 会不会被下一轮复用**。 > 会复用的(pipeline 被驳回重做、人类多轮对话)留着走 TTL,不复用的跑完即回收。 > 不要按「是不是后台调用」来分 —— 五类后台调用里 pipeline 就是要留的那一类。 > 第六条是手工扫两轮之后固化下来的。手工那两轮扫出:package.json 里 12 处引用指向**从未在 git 历史存在过**的文件 > (`server/src/cli/auto-dev.js` 一族,`npm run example` 跑起来就是 MODULE_NOT_FOUND);CLAUDE.md 的 `docs/` 那行 > 列的 5 个文件名在文档移进 `done/` 后全成死链;数据存储整节把两个不同的 data 根当成一个写。 > 三类共性是**不报错、不影响测试、`git status` 干净**,只有照着做的人(或 agent)撞墙才发现 —— > 而 CLAUDE.md 是每个会话都会加载进上下文的文件,写错等于把错误信息直接灌给每一个后来者。 > > 它的判据刻意窄:只认「以仓库顶层目录开头 + 含 `/`」的路径,且**跳过 gitignored 的** > (那是运行态产物,在干净机器上本来就不存在,缺席不是文档错误 —— 与第二条同一条判据)。 > 上下文里的相对片段(`_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`」也会全绿,而那会把生产一起锁死。 **改动它们之前先想清楚**:这几条的价值全在于「红了才有用」。 放宽判据、加例外、或者把断言改成永远成立的形式,等于把守卫拆掉 —— 本仓库有过「隔离是表演」的测试污染前科。