29-DREAM · 经验汇流(dream)—— 设计草案
所属: 28 Agent 注册表统一 §5.6b 锁住的那条约束的下游设计;未确认项来源 28-followups §1.1。 日期: 2026-08-20 / REST 与控制台入口 2026-08-21 状态: ✅ 四条取舍已拍板并已实现(五步 + 定时挂载 + 定义文件写入 + REST/控制台入口,均已落地并真跑验证,见 §四/§五/§5.7)。 仍未做的只剩「结论对不对」的判据——机器判不了,只能靠人抽查(决策 D,见上)。 标 ✅ 的是已生效的约束或有实测证据的事实;标 🔒 的是本轮拍板的决定。
〇、一句话
dream 是设想中定时跑的一套逻辑:整理所有 agent 在所有公司的 history 与 memory, 显式提炼后沉淀到 skill memory 上,让经验跨公司汇流,而不泄漏任何一家公司的具体信息。
一、开工前先看清原料:它现在是脏的
这一节全是实查 2026-08-20 的仓库状态得到的,不是推测。 结论很直接:在原料变干净之前,做提炼是没有意义的——垃圾进,垃圾出。
1.1 现在写进 memory 的不是「事实」,是截断的日志行
memory-service.appendToMemoryMd() 的全部逻辑是:
const taskOneLine = (task || '').split('\n')[0].substring(0, 80);
const resultOneLine = (result || '').split('\n')[0].substring(0, 100);
const entry = `- [${timestamp}] ${taskOneLine} → ${resultOneLine}`;
取第一行、砍到 80/100 字符。这不是提炼,是搬运 + 截断。
cto/_global/memory/memory.md 的实际内容:
## 关键事实
- [2026-08-20] [TASK from ceo] → ..._global/` 下不新增 `callback-*` 目录。反向验证过:临时把 `isTestEnv` 改成 `() => false`,该用例 FAIL。
- [2026-08-20] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01RsQ3gh5niCFyUZMFZj2hxF)
- [2026-08-20] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01MG6RqDgqcRCi5ageGk84Kt)
后三条是工具调用 id。它们作为「关键事实」被写下来、被注入下一轮提示词、 并且会成为 dream 的原料。第一条更糟:它是一句话的中段(开头被截掉了), 读起来像事实,其实缺主语。
§5.6b 说的错误版本是「每轮对话自动把原文写进共享 SKILL.md」。 现在这套已经是那个错误版本的单公司版了 —— 只是还没跨公司共享,所以还没泄漏。 dream 如果直接消费它,等于把这个错误放大到跨公司。
1.2 长期记忆是一个 3000 字符的滚动窗口
MAX_MEMORY_MD_SIZE = 3000。超了就截。所以「长期记忆」实际是最近若干条截断日志,
没有任何按重要性保留的机制。
1.3 「公司态目录」里有不是公司的东西
system/agents/{aid}/{cid}/ 是约定,不是校验。实查 96 个二级目录里混着:
| 形态 | 例子 | 是什么 |
|---|---|---|
| 真公司 | e2e-demo / mycompany / idearun |
✅ dream 的原料 |
| 保留段 | _global |
全局 agent 的落点,算不算「一家公司」需要定 ❓ |
| ArenaSK 时代的运行态 | train/ workspace/ |
❌ 不是公司,是训练 / 工作目录 |
| 测试残留 | tmp-w7-bootstrap-1786986291739-9n2xv、qroute-co-13/14、zz-* |
❌ 夹具,且带时间戳,会无限增长 |
dream 的第一步「枚举所有公司」现在没有可靠答案。
可靠的答案在库里:company_agents 表(28 批次 4 起「这家公司雇了谁」的唯一真相源)。
目录只能用来定位文件,不能用来枚举公司。
1.4 绝大多数 memory 是空模板
75 份 memory.md 里,只有 7 份有非模板内容(innovator-evolver / master /
org-innovator / innovator-digest / innovator-hunt / coo / qa-judge),
其余全是 _(刚入职,尚无记忆)_。
这条对设计有直接影响:dream 现在跑起来几乎没有原料。 它的价值随「真跑的量」增长,不是上线即有收益的东西。
二、已经生效的约束(不随设计变化)
✅ 公司态目录是 dream 的唯一原料,不许被任何清理策略当垃圾扫掉(§5.6b)。
任务态({aid}/{cid}/{taskId}/)可清,公司态({aid}/{cid}/memory/ 与 conversations/)不可清。
✅ 沉淀必须是提炼后的抽象,不是自动搬运原文。 错误版本真的发生过(28 号第一轮验收挖出的最严重 blocker):直接灌原始对话 = 把 A 公司的机密喂给 B 公司。
✅ 目录布局本身就是 dream 的主查询索引:
某个 agent 在所有公司的全部记忆 == glob: system/agents/{aid}/*/memory/,一个 glob 就够。
(前提是 1.3 那个「二级目录不都是公司」的问题被解决。)
三、需要定的取舍
🔒 决策 A —— 去敏的判据:独立判定 agent + 写作要求(2026-08-20 拍板)
定案 = A3 + A1 组合:提炼 agent 按 A1 的写作要求产出,产出后由另一个只负责判 「这段话能不能给别家公司看」的 agent 过闸门,判不准就拒绝沉淀。
理由(也是本仓库反复验证过的一条):两种错的代价不对称时,判据要偏向贵的那一侧
(P3B §八 关于 FAILING_VERDICTS 的那段)。「少沉淀一条经验」与「泄漏一次机密」显然不对称。
失效方向因此是「少沉淀」,不是「泄漏」。
下面三种形态的对比留档,说明为什么选它。
决策 A 的备选(留档)
这是安全边界,也是整个设计里唯一「做错了会造成真实损害」的一条。三种可选形态:
| 方案 | 判据 | 代价 |
|---|---|---|
| A1 白名单式:只允许提炼成「不含任何专有名词的方法论」 | 产出里不许出现公司 id、人名、产品名、URL、文件路径 | 最安全;但也最容易把有用的经验一起滤掉(「给 X 公司做官网时发现静态站点要先定验收脚本」→ 只剩「要先定验收脚本」) |
| A2 黑名单式:过滤已知敏感模式后放行 | 正则 / LLM 扫一遍公司 id、密钥形态、路径 | 保留信息量更多;漏一个模式就是一次泄漏,而且是静默的 |
| A3 二次判定:提炼 agent 产出后,由另一个 agent 只负责判「这段话能不能给别家公司看」 | 判定是一次独立的 agent 调用,双方不共享上下文 | 成本翻倍;但与本仓库既有的 validator / triager 模式同构,且失效方向可控(判不准就拒绝沉淀,宁可少沉淀) |
🔒 决策 B —— 沉淀落点:四个都要,按「改动的可逆性」分层(2026-08-20 拍板)
用户定的是:system/agents/{aid}/memory/ 和 SKILL.md 都要写,
必要时还要迭代 soul.md / agent.md。
这比原来三选一的范围大得多,因为它引入了「agent 改写自己的定义」这件事。 下面这一节是这个决定的直接后果,不是可选的补充说明。
B.1 四个落点不是一回事,风险差三个数量级
| 落点 | 是什么 | 谁受影响 | 可逆性 |
|---|---|---|---|
{aid}/memory/(本体根) |
经验积累,跨公司那一份 | 只影响这个 agent 读到的上下文 | 删掉即可,无副作用 |
SKILL.md |
能力定义:这个 agent 会什么 | 同上,但会进 Skill 市场的语义 | 覆盖前留旧版即可回滚 |
soul.md |
人格:它怎么说话、怎么取舍 | 所有雇了这个本体的公司 | 改坏了表现为「行为漂了」,不报错 |
agent.md |
行为契约:它必须做什么、禁止做什么 | 所有雇了这个本体的公司 | 改坏了可能让整条链路静默失效 |
28 号的核心裁决是「本体只有一份」 —— 所以写 agent.md / soul.md 的影响面是
所有雇了它的公司,而不是提炼出这条经验的那一家。这恰恰是 §5.6b 担心的那种扩散,
只不过现在是有意为之。因此它必须比前两个落点多两道东西:
B.2 写定义文件的硬约束(这几条不是建议)
- ✅
agent.md有一处 2000 字符的截断,但作用点要说准:graph-engine.js:915给规划 agent 看的「角色定义摘录」只取agent.md的前 2000 字符(readFileSync(f).slice(0, 2000)), 超出部分静默丢弃。agent 自己那条装配链(prompt-builder)不截断,读全文。 → 后果是:dream 往agent.md末尾追加内容,规划侧读到的画像不会变; 但如果追加把重要约束顶到 2000 之后(例如重写时调整了顺序),规划侧会无声地少看到那一段。 → 判据:写agent.md时保证前 2000 字符仍然自足(职能说清楚), 新增内容往后放;改动后应当有一条检查盯住这个窗口。 - ✅ 这四个文件里有三个是 git 跟踪的定义(
agent.md/soul.md/SKILL.md), 不是运行态。dream 改它们 = 改仓库里的文件 → 会被边界守卫看见,也会进git status。 → 需要明确:dream 的产出是直接落盘还是提案 + 人工合并。 - ✅ 毕业闸门读这些文件(
missingBodyDefinition/promoteDraftIfComplete)。 把agent.md写空 / 写坏会让本体退回「定义没填满」,雇不进任何公司。 - ✅ 必须可回滚且能分别回滚:经验沉淀写坏了只影响上下文,
agent.md写坏了影响所有公司。 两者混在同一次提交里,回滚粒度就没了。
B.3 🔒 定案:直接落盘,但写之前必须确认这份文件已经在 git 历史里(2026-08-20 拍板)
用户定的是直接写,不走提案;前置条件是「别导致直接修改而没有 git 历史」。
已实现:server/services/definition-write-gate.js,两条判据都满足才放行:
① 文件被 git 跟踪;② 该文件没有未提交改动。判不出来(不在仓库 / git 不可用)一律拒绝。
为什么是「拒绝并报告」,不是「脏就先替它提交一把」: 后者看着更顺手,但那正是本仓库记过的一类事故 —— 多会话共用同一个 worktree (27-P3C §15.4 / §17.2)。一份未提交的改动很可能是另一个会话正在写的东西, 替它提交等于把别人的半成品写进 main,还署错了名。所以把「要不要提交」留给人。
守卫 definition-write-gate.test.js 5 例,含「拒绝时必须给出下一步」那条 ——
撞上闸门的人(或 agent)要能照着做,否则只能猜。
决策 B 的备选(留档)
「skill memory」目前没有对应的实体。候选落点:
| 落点 | 形态 | 问题 |
|---|---|---|
B1 system/agents/{aid}/memory/(本体根,跨公司那一份) |
与公司态并列,一个 agent 一份跨公司经验 | 目前只有 zz-scaffold-readonly-fresh 有这个目录,等于新建一层;但它天然与 §5.6b 的 glob 对齐,读的时候本体根 + 各公司态一起加载 |
B2 本体的 SKILL.md |
复用现有概念(5 个本体已有 SKILL.md) | 这正是 §5.6b 点名的错误版本的落点;SKILL.md 是「能力定义」,不是「经验积累」,混在一起之后没法分别回滚 |
B3 <repo>/../data/skills/ 那 27,059 条 Skill 仓库 |
与 ArenaSK 的 Skill 市场打通 | 那是面向分发的仓库,写进去等于对外发布;且跨了两个 data 根 |
(拍板结果见上:B1 与 B2 都要,另加 soul.md / agent.md 两个定义落点。)
🔒 决策 C —— 触发时机:定时,每日一次(2026-08-20 拍板)
备选留档如下。
⚠️ 与 §1.4 的冲突要知道:实查 75 份
memory.md里只有 7 份有非模板内容, 所以每日跑起来绝大多数 agent 会是空转。这不是反对定时,是要求实现时做到: 没有新原料就不起 agent 调用(判据:上次 dream 之后有没有新的 session 摘要), 否则每天会有几十次「读完发现没东西」的 LLM 调用在烧钱,而且日志里全是噪声。
| 方案 | 说明 |
|---|---|
| C1 定时(如每日一次) | 简单;但没有真跑的日子会空转,有大量真跑的日子又攒得太多 |
| C2 按量(某 agent 新增 N 条 session 摘要后触发) | 与实际产出量对齐;需要一个计数落点 |
| C3 手动(控制台一个按钮 + API) | 先手动跑一段时间看提炼质量,稳定了再自动 |
🔒 决策 D —— 能自动查的自动查,不能自动查的把回查成本降到几秒(2026-08-20 落地)
「这条结论对不对」机器判不了,需要理解内容。不假装有自动判据。 能自动做的只有两件,两件都做了:
- 引用真伪可以自动查。提炼产物的
sources必须都出自这一轮真喂进去的原料, 编一个出处直接拒(连去敏判定都不用调,省一次)。 > 编了出处的经验是「无法验证」这件事最危险的形态:人抽查时顺着一个不存在的指针 > 放弃,然后默认它是真的。 - 回查成本降到几秒:
auditDistilled(agentId)把每条结论与它引用的原料原文 并排列出来,指针悬空标null(原料被清理过 / 出处是编的)。
真跑那 4 条实测:8 条引用全部取得回原文,模型没编出处。
仍然没有的是「结论对不对」的判据。
auditDistilled的输出不是质量分, 别当成那个用 —— 它只是把回查这件事变便宜了。
下面是原始的选项留档。
- 没有评价手段时,dream 的产出无法验证对错——它和 FDE 那边 「LLM 自己汇报分数」的问题是同一个(见 architecture_todo_list §1)。
- 一个便宜的起步:dream 每次产出都保留原料指针(哪几条 session 提炼出这一条), 人抽查时能顺着回去看。可追溯比「可评分」便宜得多,也更早有用。
四、推进顺序 🔒 上游与 dream 一起做(2026-08-20 拍板)
进度:五步全部实现、真 CLI 跑通一次(§5.5),定时挂载与定义文件写入也已落地。 决策 D 落到「引用真伪自动查 + 回查成本降到几秒」。未做的只剩「结论对不对」的判据 —— 那一条只能靠人。
用户定的是两件一起做,不分先后。拆成可验收的步骤:
| 步 | 内容 | 为什么不能省 |
|---|---|---|
| 1 | ✅ appendToMemoryMd 停止截断搬运(§1.1)—— 已完成 |
它原来把工具调用 id 当「关键事实」写进 memory.md 并注入每一轮提示词。dream 直接消费它 = 把这个错误放大到跨公司 |
| 2 | ✅ 「有哪些公司」改查 company_agents 表(§1.3)—— 已完成 |
目录里混着 train/ workspace/ 和带时间戳的测试残留,拿目录枚举公司必然把它们当原料 |
| 3 | ✅ dream v0:枚举 → 提炼 agent → 去敏判定 agent(决策 A)→ 写落点 —— 已完成 | 主体 |
| 4 | ✅ 定义文件的 git 历史闸门(决策 B.3)—— 已完成 | 覆盖一份尚未进入 git 历史的内容 = 让上一版永久消失,且不报错 |
| 5 | ✅ 可追溯:每条沉淀记住它是从哪几条记录提炼来的 —— 已完成 | 没有它,dream 的产出无法验证对错 —— 与 FDE 那边「LLM 自己汇报分数」是同一个问题 |
空转保护(决策 C 的直接要求):没有新原料就不起 agent 调用。
本文档仍不是实现规格。决策 D(质量怎么评)未定;B.2 里「定义文件直接写还是走提案」 也还需要你点头 —— 我按「走提案」写在了步骤 4,但那是我的建议,不是你拍的板。
五、实现记录
5.1 步骤 1:memory.md 的自动写入不再冒充「关键事实」(已完成)
改法三条:
- 换段:自动写入落到
## 原始记录(未提炼),## 关键事实从此只由提炼产物填。 读的人和 agent 都能一眼分清「这是日志」还是「这是结论」;这一段同时就是 dream 的输入面。 - 滤掉确定的噪声:工具调用标记 / 空结果 / 只有心跳标记的任务。 判据刻意窄 —— 只滤明确的噪声,不猜「哪条重要」。猜重要性需要理解内容, 那正是 dream 的活,在这里做等于把提炼下放到一个正则里。
- 不再截成半句:上限从 80/100 放宽到 200 并取完整第一行。 截断残渣读起来像事实、其实缺主语,比不留更坏。
踩到一条值得记的:噪声判据初版有「任务与结果都短于 8 个字符就丢」。 那是按英文长度想出来的阈值 —— 中文 8 个字是一句完整的话, 实测「结果甲甲甲甲甲」(7 字)直接被丢,整条记录凭空消失。已删掉该规则。 凡是拿长度当信息量代理的判据,都要先问一句「这是按哪种文字算的」。
守卫 memory-raw-section.test.js 5 例:关键事实段一个字不动、噪声一条不写、
长句不再被砍(带「样本确实超过旧上限」的前置断言)、多次写入累加在同一段、
无 toolCall 不写(保住原有行为,别顺手放宽)。
5.2 步骤 2 / 5 与 3 的骨架(dream-service.js)
步骤 2 —— 公司清单只认 company_agents 表。
companiesOf(agentId) 查表,另把保留段 _global 补上(全局 agent 的运行态落在那里,
没人「雇」它,所以不在表里)。目录从此只用来定位文件,不用来枚举公司。
守卫里造了两个「看起来像公司」的目录(train/ 与 tmp-w7-bootstrap-1786986291739-9n2xv),
断言它们不进原料 —— 那正是实查到的两种残留形态。
空转保护 —— 水位线按「条数」而不是 mtime。
mtime 会被任何一次无关写入推动(例如别处重写整份 memory.md),条数只在真的多出记录时增长。
拿 mtime 当判据的话,空转保护会在「文件被碰过但内容没变」时失效。
没有新原料时 dreamOnce 直接返回 skipped: 'no-new-material',一次 agent 调用都不起。
步骤 5 —— 可追溯。
每条原料带 source({公司}:memory.md#{行号}),提炼产物落盘时把 sources 一并写进
experience.md 的引用行。人抽查时能顺着回去看。
决策 A 的失效方向在代码里是硬的:判定 agent 抛异常也算拒绝,
并把「为什么」写进 rejected[] —— 否则现场只看到「少了一条」,查不出是判否还是判炸。
5.3 步骤 3:两个本体 + 默认实现(已完成)
两个本体(都过得了毕业闸门,missingBodyDefinition 为空):
| 本体 | 职责 | 产出契约 |
|---|---|---|
dream-distiller |
把一批原始记录提炼成可复用经验 | ``json 数组,每条{title, body, sources};没有值得留的就交[]` |
dream-screener |
逐条判「这条能不能给别家公司看」 | ``json 对象{shareable: boolean, reason}` |
决策 A 的独立性写在两处,缺一不可:
- 代码层:判定一条一次调用,提示词里只放
title+body,不放原料; - 本体层:
dream-screener/agent.md明写「你看不到原料,也看不到是哪家公司——这是故意的」。 守卫里有一条专门断言这句话在(expect(screener).toContain('看不到原料'))—— 否则下一个人会顺手把原料塞进提示词「让它判得更准」,而那恰好把独立性拆掉了。
两处失效方向都是硬的:
- 提炼取不出 JSON → 抛,不是静默返回空。「产出格式坏了」和「没提炼出东西」是两件事, 混为一谈会让前者以后者的形式永远藏下去(每天显示「今天没经验可沉淀」,真相是解析一直在失败)。
- 判定解析不了 /
shareable不是布尔 → 判否,并把原文片段带进reason, 让人看得出是「判否」还是「判炸」。
测试写法上纠了自己一次:第一版把解析与失效分支在测试里重抄了一遍再断言 —— 那是在验一份复制品,代码改了它照样绿。改成加一条
setDreamAgentCaller缝, 只换掉「真起 CLI」那一层,解析、判空、失效方向全走真代码。本体写错过一次,被守卫抓住:
agent.md里的相对引用按 cwd 深度 3 ({aid}/{cid}/{taskId}/)解析,不是按文件自身位置 —— 仓库根是../../../../../。 我按文件位置写了../../../,agent-md-refs.test.js当场判红。
5.4 还没做的(别当成 dream 上线了)
定时挂载没做✅ 已完成(§5.6)——dream-service.startDaily()已挂在server/index.js的DISABLE_BACKGROUND_AGENTS早退之后,每日一次、串行扫、 定时器unref。仍然没有的是 REST / 控制台入口✅ 已完成(§5.7)——/api/dream/*+web/src/views/DreamConsole.vue(导航栏🌙 Dream)。还没写定义文件✅ 已完成(§5.6)——writeDefinitionFiles()已实现, 落点是SKILL.md/soul.md/agent.md三个 git 跟踪的定义文件, 形状是受管区块(<!-- dream:begin/end -->,区块外不碰、整体重写幂等), 写之前过三道闸门(definition-write-gate → agent.md 前 2000 字符自足性 → 写完复核毕业闸门,不过立刻还原),任何一道不过就不写、原因逐文件返回。决策 D✅ 已落地(见上)—— 但「结论对不对」仍然只能靠人抽查。
5.5 第一次真 CLI 跑通(2026-08-20)
前置:存量记忆的搬家
写入侧改了(§5.1),但存量不会自己搬家。实查现有 ## 关键事实 里
没有一条真事实 —— 全是重复的心跳行、工具调用 id、和以 ... 开头的半句:
- [2026-08-18] [SYSTEM HEARTBEAT] → 🔧 [TOOL_CALL] Bash (ID: toolu_01M1t4...)
- [2026-04-09] [TASK from master] → ...ates/community-manager/train/evolve/SKILL_v1.md` |
而 memory.md 会被注入每一轮提示词 —— 这些东西一直在冒充事实。
scripts/migrate-memory-raw-section.js 把它们搬进「原始记录(未提炼)」:
19 个文件 / 269 行,先 dry-run 再 --apply,改前逐个 .bak
(这些是 gitignore 的运行态,没有 git 历史兜底)。
判据刻意窄:只搬 - [YYYY-MM-DD] … → … 这一种形状(旧自动追加的唯一形状)。
人手写的事实不长这样,不动它 —— 搬错比留错更坏:留下最多多一条噪声,
搬错是把人写的东西降级成日志。
真跑结果
对 innovator-evolver(40 条原料,本仓库最厚的一份)跑 dreamOnce:
{ "distilled": 4, "accepted": 4, "rejected": [] } 耗时 114 s
四条产出都做到了抽象,原料里的具体名字全被抽掉:
yt-dlp / Invidious → 「本地下载工具 → 备用镜像源」;
scorer.js / SKILL_v1.md / system/templates/... 一个都没出现。
顺着 sources 抽查过(可追溯的第一次实际用途):4 条引用全部对得上原料。
例如「按关键词覆盖率打分会被刷到虚高分」那条引了 #21/#22/#27/#28,
而 #27 原文正是「从零重写,采用 expected_output 逐 case rubric + dimCeil 上限机制,
杜绝 coverage-based」。它是跨 4 条记录归纳出来的,不是逐条改写 ——
这正是「一次调用吃掉全部原料」那个选择要换来的东西。
闸门单独验过(这一轮零拒绝 = 没被考验)
真跑里 4 条全过,说明去敏闸门一次都没触发。零拒绝不等于闸门有效,
所以单独喂了三条给 dream-screener 真跑:
| 输入 | 判定 | 理由(原文) |
|---|---|---|
| 含公司名 + 域名 + 绝对路径 | ❌ false | 「出现了具体域名 lemonai.ai 和绝对路径 /Users/…,第三方能据此直接认出是哪家公司/哪台机器」 |
| 心跳日志原文摘录 | ❌ false | 「是心跳日志的原文摘录,不是重写过的经验,属于原文搬运;且工具调用 ID 是可能关联到具体运行实例的标识符」 |
| 干净的抽象经验 | ✅ true | 「没有公司名/路径/URL…读起来是重写过的原则而非原始对话摘录」 |
三条判据(可识别主体 / 原文搬运 / 正常放行)都按 agent.md 写的那样生效了, 而且拒绝理由都点到了具体是哪几个字 —— 那是 agent.md 里明确要求的。
空转保护实跑
第二次调用(原料没变):{"skipped":"no-new-material"},一次 agent 调用都没起。
e9f380a">5.6 定时挂载(决策 C)+ 定义文件写入(决策 B)(2026-08-20,e9f380a)
定时:每日一次,串行
dreamSweep 逐个本体扫,不并发 —— 每轮可能起两次 LLM 调用,几十个本体并发
= 同时拉起几十个 claude 子进程,这个代价本仓库付过(2h43m 漏出 82 个进程 / 17.5 GB,
28-followups §五第五条)。慢一点没有代价,空转保护已经把
「读一遍全量本体」的成本压到「N 次目录读」。
挂点:server/index.js 里 DISABLE_BACKGROUND_AGENTS 早退之后,调
dream-service.startDaily()。定时器 unref;只挂定时不立刻跑一轮
(启动那一刻跑没有额外收益,反而让每次重启都烧一遍)。一个本体抛异常不拖垮整轮,
但必须留下是谁抛的 —— 否则现场只看到「今天没沉淀」,查不出是没原料还是一直在抛。
没做的:REST / 控制台入口。现在触发只有两条路:等每日定时,或代码 / 脚本直接调
dreamOnce(agentId)。人想立刻看一次提炼结果,目前没有 API/页面。
定义文件:受管区块 + 三道闸门
落点是 SKILL.md / soul.md / agent.md(决策 B 的另外两个落点,本体根
memory/experience.md 在步骤 3 已经在写)。形状是一对
<!-- dream:begin/end --> 之间的受管区块,区块外一个字节不碰 —— 自由追加会让
「哪些是人写的、哪些是 dream 写的」几轮之后再也分不开,分不开就没法分别回滚。
区块整体重写还顺带幂等:同一批经验重复跑不会越写越长。
三道闸门,缺一不写:
- definition-write-gate(§B.3):当前内容必须已在 git 历史里,且没有未提交改动;
- agent.md 的前 2000 字符必须仍然自足:
graph-engine.js:915的「角色定义摘录」 只看这一段,改了等于悄悄换掉规划 agent 读到的画像; - 写完复核毕业闸门(
missingBodyDefinition),不过就立刻还原 ——agent.md写坏会让本体退回「定义没填满」,雇不进任何公司。
任何一道不过:不写,原因逐文件返回(definitions[])。不写不该是静默的。
守卫:dream-service.test.js 新增 3 例,对真被 git 跟踪且干净的本体(dream-screener)
走真路径 —— 真写、验区块外字节不变 / 幂等 / 毕业闸门仍过,finally 还原并断言还原干净;
agent.md 那条验「不写 + 理由点名前 2000 字符 + 一个字节都没动」。dreamSweep 加了
agents 参数,让错误隔离那条能真调而不是真扫全量本体。
全量:144 文件 / 1330 测试全绿。
5.7 REST / 控制台入口(2026-08-21)
只做了这一件事,没有跟着做「dream 改成 org/work graph 来跑」—— 两者被单独评估过(见对话记录):org/work graph 的执行模型是单公司作用域的 (org graph = 公司级 workflow,work graph = 公司内某任务的节点图),dream 却是 跨公司扫描(按 agent 扫全部公司的记忆),套进去要么破坏这个作用域假设, 要么得新开一种「跨公司图」类型。而且 graph 引擎自带的「自动质量判断」 (goal-contract 的 benchmark / test-script、triager 驳回重做)判的是「产物有没有 过验收脚本」,对「这条提炼的经验对不对」这种自由文本判断没有对应机制—— 那道题不管包不包进 graph 都得单独设计一个 rubric,是同一件事,graph 化本身 不会替你解决它。所以只做了用户点了头的这一半。
REST(server/routes/dream.js,挂在 /api/dream)
路由本身没有新业务逻辑,全部委托给已经测过的 dream-service.js:
| 端点 | 做什么 |
|---|---|
GET /api/dream/agents |
总览:每个可 dream 的本体,公司数、上次跑的时间、上次沉淀/拒绝了几条 |
GET /api/dream/agents/:agentId |
详情:状态、定义文件(agent.md/soul.md/SKILL.md)现在能不能写及原因、auditDistilled() 的可回查列表 |
POST /api/dream/agents/:agentId/run |
手动跑一次({dryRun} 可选;dryRun 只看有没有新原料,不起 LLM 调用) |
POST /api/dream/sweep |
手动触发全量扫描,202 立即返回,后台跑(串行扫全部本体可能要几分钟到几十分钟,不能塞进一次 HTTP 请求里等) |
GET /api/dream/sweep/status |
轮询上面那个后台任务:sweeping / results / error |
sweep 的并发锁是路由自己的状态(内存里一个 sweepState,不落库)——
dream-service.js 没有「正在跑」这个概念,dreamSweep() 单纯是个 promise。
两次并发 POST /sweep 第二次会拿到 409,防的是「人在控制台连点两下」而不是
分布式场景(单进程部署,够用)。
守卫:server/tests/unit/dream-routes.test.js,monkeypatch 掉
dream-service 导出对象上的六个函数(不走真文件系统,不会误扫真仓库的
76 个本体),只验路由这一层独有的东西——六个端点接没接对、sweep 的并发锁
状态机对不对。dream-service.js 自身的正确性由 dream-service.test.js 覆盖,
这里不重复验。
控制台(web/src/views/DreamConsole.vue,路由 /dream,导航栏 🌙 Dream)
一个表格 + 两个弹窗,没有别的:本体列表(点「详情」看状态/定义文件可写性/ 经验审计,点「跑一次」或「dry-run」手动触发单个本体),页头一个「全量扫描」按钮 (触发后台 sweep,15 秒轮询一次状态直到跑完)。
真跑验证(2026-08-21)
起真服务器(DISABLE_BACKGROUND_AGENTS=1 bash start.sh),对真数据打过一轮:
GET /api/dream/agents 拿到全部 76 个本体真实的 companies / lastRunAt;
GET /api/dream/agents/innovator-evolver 的 definitions 如实报出
agent.md/soul.md 可写、SKILL.md 不可写(没有 git 历史)及具体原因,
experience 把 §5.5 那次真跑沉淀的 4 条经验连同原料原文一起吐出来;
dry-run 对 innovator-evolver 正确报 no-new-material(材料已在上次真跑时被
消费,本轮没有起一次 LLM 调用)。控制台页面在浏览器里过了一遍同样的路径
(列表 → 详情弹窗 → dry-run 弹窗),数据与直接打 API 一致。