28-followups. Agent 注册表统一 —— 未完成与未确认项
所属: 28 Agent 注册表统一 的遗留清单。 主文档已随实施完成移入
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。四条取舍(去敏判据 / 沉淀落点 / 触发时机 /
质量评估)均已拍板,五步推进顺序 + 定时挂载 + 定义文件写入均已落地,
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审计结论与本条原来的写法不一样,两列要分开处理。
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 白名单外)。 公司态永远走「不许清」那条分支,守卫里有一条专门盯它。
edeff2e">3.3 agent-chat.js 是心跳落点 bug 的第二个入口 ✅ 已修(2026-08-20,edeff2e)
agent-chat.js 是心跳落点 bug 的第二个入口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、<repo>/…示意写法一律不判。 宽一档就会把这些一起判红,防线于是被人整条注释掉。第七条的来历是第四次踩「测试污染共享库」,而且换了个方向进来。前三次 (
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」也会全绿,而那会把生产一起锁死。
改动它们之前先想清楚:这几条的价值全在于「红了才有用」。 放宽判据、加例外、或者把断言改成永远成立的形式,等于把守卫拆掉 —— 本仓库有过「隔离是表演」的测试污染前科。