# 29-DREAM · 经验汇流(dream)—— 设计草案 > **所属**: [28 Agent 注册表统一](../../done/system-design/28-agent-registry-unification.md) §5.6b 锁住的那条约束的下游设计;未确认项来源 [28-followups §1.1](./28-followups.md)。 > **日期**: 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()` 的全部逻辑是: ```js 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 写定义文件的硬约束(这几条不是建议) 1. ✅ **`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 字符仍然自足**(职能说清楚), 新增内容往后放;改动后应当有一条检查盯住这个窗口。 2. ✅ **这四个文件里有三个是 git 跟踪的定义**(`agent.md` / `soul.md` / `SKILL.md`), 不是运行态。dream 改它们 = 改仓库里的文件 → 会被边界守卫看见,也会进 `git status`。 → 需要明确:dream 的产出是**直接落盘**还是**提案 + 人工合并**。 3. ✅ **毕业闸门读这些文件**(`missingBodyDefinition` / `promoteDraftIfComplete`)。 把 `agent.md` 写空 / 写坏会让本体退回「定义没填满」,**雇不进任何公司**。 4. ✅ **必须可回滚且能分别回滚**:经验沉淀写坏了只影响上下文,`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** `/../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 落地) **「这条结论对不对」机器判不了**,需要理解内容。不假装有自动判据。 能自动做的只有两件,两件都做了: 1. **引用真伪可以自动查**。提炼产物的 `sources` 必须都出自这一轮真喂进去的原料, 编一个出处直接拒(连去敏判定都不用调,省一次)。 > 编了出处的经验是「无法验证」这件事最危险的形态:人抽查时顺着一个不存在的指针 > 放弃,然后**默认它是真的**。 2. **回查成本降到几秒**:`auditDistilled(agentId)` 把每条结论与它引用的**原料原文** 并排列出来,指针悬空标 `null`(原料被清理过 / 出处是编的)。 真跑那 4 条实测:**8 条引用全部取得回原文**,模型没编出处。 > 仍然没有的是「结论对不对」的判据。`auditDistilled` 的输出**不是质量分**, > 别当成那个用 —— 它只是把回查这件事变便宜了。 下面是原始的选项留档。 - 没有评价手段时,dream 的产出**无法验证对错**——它和 FDE 那边 「LLM 自己汇报分数」的问题是同一个(见 [architecture_todo_list](../future/architecture_todo_list.md) §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` 的自动写入不再冒充「关键事实」(已完成) 改法三条: 1. **换段**:自动写入落到 `## 原始记录(未提炼)`,`## 关键事实` 从此**只由提炼产物填**。 读的人和 agent 都能一眼分清「这是日志」还是「这是结论」;这一段同时就是 **dream 的输入面**。 2. **滤掉确定的噪声**:工具调用标记 / 空结果 / 只有心跳标记的任务。 判据刻意窄 —— **只滤明确的噪声,不猜「哪条重要」**。猜重要性需要理解内容, 那正是 dream 的活,在这里做等于把提炼下放到一个正则里。 3. **不再截成半句**:上限从 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 上线了) 1. ~~定时挂载没做~~ ✅ **已完成**(§5.6)—— `dream-service.startDaily()` 已挂在 `server/index.js` 的 `DISABLE_BACKGROUND_AGENTS` 早退之后,每日一次、串行扫、 定时器 `unref`。~~仍然没有的是 REST / 控制台入口~~ ✅ **已完成**(§5.7)—— `/api/dream/*` + `web/src/views/DreamConsole.vue`(导航栏 `🌙 Dream`)。 2. ~~还没写定义文件~~ ✅ **已完成**(§5.6)—— `writeDefinitionFiles()` 已实现, 落点是 `SKILL.md` / `soul.md` / `agent.md` 三个 git 跟踪的定义文件, 形状是受管区块(``,区块外不碰、整体重写幂等), 写之前过三道闸门(definition-write-gate → agent.md 前 2000 字符自足性 → 写完复核毕业闸门,不过立刻还原),任何一道不过就不写、原因逐文件返回。 3. ~~决策 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 调用都没起**。 --- ## 5.6 定时挂载(决策 C)+ 定义文件写入(决策 B)(2026-08-20,`e9f380a`) ### 定时:每日一次,串行 `dreamSweep` 逐个本体扫,**不并发** —— 每轮可能起两次 LLM 调用,几十个本体并发 = 同时拉起几十个 claude 子进程,这个代价本仓库付过(2h43m 漏出 82 个进程 / 17.5 GB, [28-followups §五第五条](./28-followups.md))。慢一点没有代价,空转保护已经把 「读一遍全量本体」的成本压到「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 写的」几轮之后再也分不开,分不开就没法分别回滚。 区块整体重写还顺带**幂等**:同一批经验重复跑不会越写越长。 三道闸门,缺一不写: 1. **definition-write-gate**(§B.3):当前内容必须已在 git 历史里,且没有未提交改动; 2. **agent.md 的前 2000 字符必须仍然自足**:`graph-engine.js:915` 的「角色定义摘录」 只看这一段,改了等于悄悄换掉规划 agent 读到的画像; 3. **写完复核毕业闸门**(`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 一致。