# 更新日志 > 版本号含义:前两位是功能批次,末位是修补。**每个版本都写清楚改了什么,不做"仅版本号"的提交。** ## 2.4.1 — 2026-11-03 修掉 2.4.0 带进来的一个**致命平台缺陷**,外加一处写入侧断言。不新增功能。 ### 修复 - **2.4.0 在 Android 上会让所有写入失败。** 跨进程锁的进程身份查询只写了 `win32` 和 `linux` 两个分支,Android 落到最后一行直接抛 `LOCK_IDENTITY_UNAVAILABLE: unsupported platform android` —— 手机端每条写入都失败,记忆库等于被锁死。Android 同样提供 `/proc`(`/proc//stat` 与 `/proc/sys/kernel/random/boot_id` 都可读),现在与 Linux 走同一分支。 - **`appendChainBatch` 补行校验。** 它是内部函数,只接受调用方构造完整的行(必须带 `id`)。此前不做校验,缺 `id` 的行会被照常写进文件,而读取侧会把它们当坏行静默跳过 —— 现象是「写了 N 行、读回 0 条」。现在在任何 IO 之前失败,外层折成 `{ written: 0, error: 'BATCH_ROW_INVALID: ...' }`,零写入。 ### 验证 | 项 | 结果 | |---|---| | 回归 | 10/10 | | 三处前缀一致 | 挂载 / 路由 / 前端均 `/chain-shared` | | Android 分支 | 伪造 `process.platform = 'android'` 后不再抛 `unsupported platform`,改为走 `/proc` 路径 | | 批量断言 | 缺 `id` 的行被拒且零写入;非对象被拒;正常行写 1 条、读回 1 条、`skipped: 0` | ### 说明 - 改动面只有 `lib/store.js` 与版本号,没有新增功能,也没有改任何工具签名。 - **手机端装过 2.4.0 的请升级到 2.4.1**;桌面端行为不变。 - 这一版是从公开版 v2.4.0 基线复制后**只打这一处差异补丁**,没有整包覆盖私有版 —— 两版作用域语义相反,覆盖会互相破坏。 --- ## 2.4.0 — 2026-11-03 一次**可靠性**更新,不碰功能:把十一处「坏的时候看不出来」的缺陷一次修掉,并给写入加了一把跨进程锁。 ### 修复 - **P0-1 跨进程回滚误删**:两个 DSH 实例共用同一个 `chains.jsonl` 时,实例 A 写入失败会按自己记下的原长截断,把实例 B 刚写成功的记录一起删掉,而 B 仍以为成功。现在所有追加入口共享一把**票据文件锁**(PID + 进程启动时间 + 随机令牌),读状态 → 校验 → 追加 → 回滚全在同一临界区;拿不到锁会在 `storeLockTimeoutMs`(默认 2000ms)后返回 `LOCK_TIMEOUT`,**不会静默降级成无锁写**。 - **P0-2 粘行**:单条追加入口不检查文件末尾是否有换行,旧文件缺换行时两条 JSON 会粘成一行,`loadAll` 解析不了会**同时丢掉旧记录和新记录**。现在所有写入都走同一处边界检查。 - **P0-3 读取失败伪装成空库**:`loadAll` 读不到文件时返回空数组、与空库不可区分,导入还会忽略坏行计数,可能在已有损坏的情况下返回 `ok:true`。现在返回 `entries: []` + `error` 字段,导入与注入口都会显式拒绝。 - **P0-4 铁律没有优先预算**:联想段先占预算,铁律跟其他池按权重竞争、零预留;语义补位还绕过上限。预算压到 2000 时**铁律 0 条、语义补位 3 条照进**。现在注入顺序固定为「核心 → 铁律 → 其余池 → 联想细节 → 语义补位」,每层从同一个剩余预算里扣,且 `injectCap` 是硬上限。 - **P0-5 被截掉的记忆仍记为「已想起」**:命中记账发生在最终裁剪之前,实际没显示出来的条目照样写 recall 事件。现在只在最终保留的条目上记账。 - **P0-6 联想不足三条不真回退**:传入了三条联想但过滤后不足三条时,联想段不输出、其他池又被清空,两头空。现在按**最终可展示条数**决定是否启用联想。 - **P0-7 夜巡正则写成字符集合**:`/^[user-selected page text]/` 的方括号没转义,`today 我想吃饭` 这类真话被误排除、真正的网页引用反而留下。已修正,并顺带修了同类型的 `/→s*/`(缺反斜杠)。 - **P1-8 超大 merge 组栈溢出**:`rows.push(...数组)` 超过参数上限会抛 `RangeError`。现在有明确的批次上限与结构化错误。 - **P2-9 schema 与 handler 契约不一致**:工具说明写「坏项跳过、好项继续」,但 schema 严格校验会整批拒绝。现在整批结构校验与逐项业务校验的边界写清楚了。 - **P2-10 经验先占预算再裁三条**:被裁掉的条目不释放预算,白白挤掉其他池。现在装箱前就限制候选数。 - **P2-11 夜巡轮次按文件天数算**:同一天跑三轮只算一天,升级阈值永远不够。现在按每次夜巡的记录统计。 ### 新增 - **`storeLockTimeoutMs` 配置项**:记忆库锁等待上限,默认 2000ms,范围 100–60000ms,非法值回落默认。 ### 性能 | 场景 | 改前 | 改后 | |---|---|---| | 单次 append,无竞争(热) | 7.11 ms | **8.73 ms** | | 双进程竞争(热) | 7.37 ms | 31.23 ms | | 新进程首次调用(含进程身份查询) | 10.41 ms | 177.21 ms | **冷启动那 177ms 是一次性的**(首次查进程身份),热路径每次多约 1.6ms。 ### 说明 - 这次改动全部落在**写入正确性**与**注入预算**上,没有新增功能。 - `chain_import` / `chain_append` / `chain_touch` 的签名与行为**保持不变**。 - **互斥保护要求所有 DSH 实例都加载新版**;只升级一个实例时,另一个仍是旧代码。 --- ## 2.3.0 — 2026-11-01 新增批量落链工具。把私有版的 `chain_import` 移植到公开版:一次调用完成「批量写判断 + 定池 + 退役 + 合并」,带 dry-run 预览。 ### 新增 - **`chain_import` 工具**:一次调用完成夜巡落链的全部写入动作 —— 批量写判断(`appends`)、批量退役海马体碎片(`retire`)、把已有条目定池(`merge`)。执行顺序固定为 appends → retire → merge。 - **`dryRun: true` 只校验不写**(不生成 id、不改文件),返回「将会做什么」的预览。 - **逐项校验**:`pool` 必须是合法池名、`evidence` 非空、`retire` 的 id 必须存在且属于 hippocampus、`merge` 的每组 id 必须都存在。**不合格的项被跳过并写进 `errors`,合格的照常执行**。 - **`retire` 只允许 hippocampus** —— 别的池不许被退役(那是「改写我记得什么」,不可逆)。 - **返回真实 id**:新建条目的 id 立刻可被 `chain_recall` 查到。 - **`lib/import.js`**:批量逻辑(校验 + 写入 + 回滚报告)。 - **`lib/store.js` 的 `appendChainBatch()`**:一次 flush 写多行;失败回滚到原始字节,回滚也失败时核对落盘前缀并如实报告已写入的行数。 ### 说明 - **为什么值得有**:原先一次夜巡落链要 27 次工具调用(13 次 `chain_append` + 13 次 `chain_touch set-pool` + 1 次批量退役),每次都要重发整份上下文;会话越大,这件事越贵,而且它本身在推高上下文水位。现在压成 1 次。 - **判断质量不降**:批量只砍「重复调用」,分类的正确性仍由调用方逐条审。`dryRun` 就是给这一步用的。 - **不影响现有工具**:`chain_append` / `chain_touch` 的签名与行为保持不变。 --- ## 2.2.1 — 2026-10-31 小修。一处 bug 修复 + 三个新文件(都可选,不影响现有行为)。 ### 修复 - **夜巡的自动唤醒从未真正生效。** `lib/wake.mjs` 拿 `sessions.list` 返回的完整 `session-` 去和配置里的裸 uuid(不带 `session-` 前缀)做**严格相等**比较,永远不相等,于是每次夜巡只往 `wake.log` 记一条 `跳过:explicit-not-found` —— 报告照写、人却从不被叫醒。现在两边都归一化(去掉前导 `session-`),两种写法都能命中。 - 影响面:**只在显式配置了目标会话时出现**。没有配置时走「最近活跃会话」,不经过这段比较,本来就是好的。 ### 新增 - **`lib/triage.js`**:把海马体碎片整理成「四分支(`append` / `merge` / `retire` / `keep`)+ 提炼草稿」的批量分类 prompt,并对模型返回做结构校验(id 只能来自输入、每个 id 恰好覆盖一次、`append` 必须带 `draft`、`reason` ≤60 字、`pool` 合法)。**只产出建议,不写任何记忆。** - **`tools/triage-dryrun.mjs`**:读当前海马体碎片 → 调一次 LLM → 把分组建议与校验报告写成 JSON,同时打印 `chains.jsonl` 的 SHA-256 前后值,证明没有写库。 - **`skills/memory-triage-dispatch/`**:写给 AI 读者的操作手册 —— 用 subagent 当外接分类器(**不占主会话上下文、不需要 API key**)完成同一件事,含 prompt 生成、六条结构校验、落库顺序,以及五个实测踩过的坑。 ### 说明 - 外接接口需要 `TRIAGE_BASE_URL` / `TRIAGE_API_KEY` / `TRIAGE_MODEL` 三个环境变量。**缺任何一个会直接退出并指出缺哪个**,不猜测、不使用默认值。 - 不想给 key 就走 skill 里那条子代理路,效果相同。 ## 2.2.0 — 2026-10-31 本次从公开版 2.0.0 一次对齐至 2.2.0;2.1 未单独发布,其间的小更新一并收入本版。 ### 新增 - **矛盾候选 `chain_conflict`**:支持 `ids: [a, b]` 直接比较,以及按条目或查询查找同池候选;过滤库内高频泛词,按 strong 具体锚点数优先排序,减少只有泛词相似的误报。工具只提供候选、原文与分数,不自动判定矛盾或修改记忆。 - **同事件分组 `eventId`**:写入可选、不自动生成;检索和元记忆查询最多补带 2 条同组记录,标记 `sameEvent: true`,让同一次决策的前后判断可以一起读。 - **同事件自动退役**:新判断带 eventId、判断前段包含推翻词且同组存在未退役旧条时,追加 supersede 事件;核心池受保护,单次最多退役 2 条。相同载荷重试复用已写记录,避免把新判断误当旧条退役;不回写历史正文。 - **夜巡审查扩展**:增加「矛盾候选」与「半写断裂 / 同事件对账」两类检查,并列出最近 7 天历史 eventId 粗回填建议,暴露更新未对账及半写问题;这些检查只报告,不自动回填或退役。正文现有 10 个检查小节(不计概览),有关系边时另附「联想网络」。 ### 合入 2.1 期间的小更新 - 感知与元记忆面板、联想脉冲及图谱更新对齐当前实现;从真实事件读取状态,静默时不制造动画。 - 自动夜巡仅审查,不自动写权重归整或退役事件;手动 chain_patrol 仍可显式 apply 权重归整,避免定时任务擅改数据。 - 检索补词、工作记忆与原始片段处理沿用当前实现,保持缺少可选依赖时的降级路径。 ### 公开版适配 - 默认所有会话共享,数据目录仍为 `.chain-shared`;可用 `DSH_CHAIN_TARGET_SESSION_IDS`(逗号分隔)配置会话范围,缺省空字符串不按会话过滤。画布会话标签从服务端配置读取。 - Web API 挂载、路由及前端请求均保留 `/chain-shared`;包名与客户端模块名保留 dsh-chain-shared。 - 清理私人会话标识、角色与场景示例;只移植正式 .js,不搬备份文件,忽略所有 .bak*;不新增第三方依赖。 ## 2.0.0 — 2026-10-30 一次**结构级**更新:把「记忆」从一个库扩成一整套 **感知 → 联想 → 决策 → 元记忆** 的闭环。 1.6.x 解决的是「记下来、别丢」;2.0 解决的是「知道自己记了什么、为什么会想起它、为什么会忘」。 > ⚠️ 语义检索依赖 `@huggingface/transformers`(约 400 MB,**可选**)。不装也能用 —— > 所有语义路径自动退回关键词路,不报错,只是少一路召回。 ### 新增工具 - **`chain_about`** —— 元记忆的出口。问「关于 X 我知道什么 / 多可信 / 变过几次」, 返回每条的 **被想起次数 / 人工确认次数 / 生命周期状态 / 是否被取代 / 依据条数**。 只读:调它**不算"想起"**(想起靠注入命中或人确认,这两件事在事件层是分开的)。 ### 新增能力 - **注入里多了「这次联想到的」** —— 从本轮原话出发在链上走 1–2 跳,把走出来的判断贴回提示词。 走线是「分层挑选」:**名额先留给走出来的那一跳**,种子不会把预算吃光。 - **语义补位(可选)** —— 关键词路先跑;宿主的 `system-prompt/assemble` 是**异步** waterfall, 用它在组装阶段再补一次语义检索,把「字面没挂上、但意思接近」的补进来;失败一律原样返回。 总开关:`DSH_CHAIN_SEMANTIC=0`。 - **感知层** —— 把用户原话分成 chat / task / question / daily / emotion / decision / mixed,并给情绪极性。 本版**只记录、不改变行为**(见「已知限制」)。 - **元记忆面板** —— 画布上多一块:被想起合计、确认数、被取代数;节点详情显示真实权重与年龄。 - **技能拆成两个池** —— 「铁律」(必须 / 不许 / 红线:半衰期 730 天)和「经验」(教训 / 工具 / 排障:60 天) 不是同一种东西,混在一个池里会让红线跟着工具笔记一起淡掉。池数从 7 变 8。 - **夜巡可见** —— 之前它只写报告、界面上一片空白;新增 `/chain-shared/api/patrol` 与画布状态行 「夜巡 16:51 · 428 条」。夜巡报告仍是追加式(一天多段,不覆盖)。 ### 修复 1. **夜巡后的自动唤醒 100% 丢失** 唤醒触发点是**压缩**,而压缩发生在回合中途 → 那一轮必然还在跑 → `wake.mjs` 里「显式目标 + 正忙 → 跳过」把唤醒每次丢掉。投递本来就是 `mode:'queue'`(排队不打断), 这条跳过是多余的。已删除;子进程输出改写入 `wake.log`,失败不再无声。 共享版在此之上还改成:**不指定会话,交给 wake 挑最近说话的那个**。 2. **组装钩子 fail-open 会拖死全群** 钩子取不到会话 id 时**放行**,等于给所有会话都跑语义检索;模型首次加载约 10 秒, 会把全部请求一起拖住。改为 fail-closed:拿不到会话 id / 不是注入目标 → 直接跳过。 3. **一个变量放错作用域,同时打死注入与画布** section 回调抛 `ReferenceError` → 每轮注入返回空串 → 脉冲也不发布 → 画布没有动画。 已在 `apply` 顶层声明,并补了冒烟脚本同时检查「注入非空」与「脉冲递增」。 4. **Windows 下 sharp 版本冲突** `@huggingface/transformers` 带 sharp 0.34,宿主用 0.35,两个原生 DLL 在同一进程里撞, 先加载谁另一个就 `ERR_DLOPEN_FAILED`。**不靠调整加载顺序**:模型推理跑在 fork 子进程里 (`lib/embed-worker.js`),宿主进程一份都不加载。 5. **元记忆汇总节点显示占位值** —— `weight` 恒 0.5、`ageDays` 恒 0,看着像真数据。改为取来源真实值。 6. **年龄显示** —— 0.2 天也显示「0 天」,改为不到 1 天按小时。 7. **感知词表补口** —— 亲昵 / 关心类(「摸摸」「顺毛」「辛苦了」「早点睡」「晚安」)之前全读成 0, 补近义类后为 +0.33;词典 441 → 473 词。技术句仍保持 0。 ### 已知限制(写在这里,不当成完成) - **感知只记录、没接判断** —— 它还不改变行为(例如"说累时少塞技术细节")。 - **夜巡没有「现在跑一次」按钮** —— 想立刻验只能等压缩,或等 18:00 后到点。 - **跨语义召回仍不完整** —— 换个说法、字面一点不沾边的查询,语义路能补一部分,但不是全部。 ## 1.6.1 — 2026-10-30 一个中间版本,只做两件事:**让用户说过的话能进注入**,以及清掉几处会把它们弄丢的毛病。 ### 新增 - **注入段多了「最近用户说过的话」** 海马体是原料缓冲区,设计上永不注入;但它装的正是**用户的原话**,一点不给就会变成 「只剩技能」的记忆 —— 对话越久越像机器。现在开一扇小窗:最近 2 天内 4 条原文, 每条截 80 字,放在情绪之后、核心之前。**不摘要**,因为摘要会把用户的话改写成模型的措辞。 - **夜巡会把你说的日常转进日常池** 纯词面判据(不调用模型):谈到「我们 / 相处」→ 核心日常;生活细节与语气 → 片面日常; 转池用 `retag` 事件**原地改池,正文一个字不动**。每次最多 12 条,避免一次刷进太多。 - **日常层活得更久**(默认参数) 核心日常 10/10/20 → 20/60/180;片面日常 2/5/7 → 5/20/45;海马体归档 3 天 → 14 天。 原有配置仍可在设置面板覆盖。 ### 修复 这五条都是**会让功能看起来在工作、实际没生效**的那一类,所以每条都写清「症状 / 根因 / 修法」。 **① 工作记忆没有隔离守卫 —— 任何会话都会写同一份 `working.json`** - 症状:另一条会话聊完,本会话的「还没做完的事」里冒出别人的句子。 - 根因:写缓冲之前的会话判断只加在了海马体那一条路径上,工作记忆那段没有。 - 修法:写之前先判目标会话,不是它就整段跳过。**每一条写入路径都要守卫,不能只补一条。** **② `chain_touch` 的 `set-pool` 每次都报 `returned invalid output`** - 症状:手动给一条记忆定池,工具报错,看着像失败了。 - 根因:写入其实成功了,但输出结构的必填字段 `retired` 只在弃用分支返回。 - 修法:定池分支也返回 `retired: 0`。**判据:报错不等于没生效,改完要实测两条分支的返回值。** **③ `chain_touch` 的 `id` 是必填的,导致批量退役用不了** - 症状:想一次退役 N 条原料,必须先传一个空的 `id` 才能走 `ids` 分支。 - 根因:参数 schema 把 `id` 写成必填,而代码在 `id` 存在时会忽略 `ids`。 - 修法:`id` 改为可选(只留 `outcome` 必填)。 **④ 手动跑夜巡被标成「定时」** - 症状:报告小节和旁车文件里,手动触发与定时触发长得一样。 - 根因:手动工具调用没传触发原因,落到默认值。 - 修法:手动入口显式传 `trigger: '手动'`。 **⑤ 「最近用户说过的话」可能「header 说 4 条、实际只打出 1 行」** - 症状:这一节的标题写着 4 条,下面只有 1 行。 - 根因:**头部计数与正文渲染用了两把尺子** —— 计数只看"过滤后剩几条",而循环体里还有一条"空正文就跳过"。 事件记录的正文恰好是空串,一旦被喂进来就会命中这个错位。 - 修法:把"必须有字可打"这条判据挪进过滤器本身。**能进这一节,就必须有字。** ### 已知限制 - 换个说法仍有约一半检索不到(关键词 + 二元组,没有语义向量) - 转池判据是词面正则,可能把「说到吃的」的技术讨论也转进日常(可手动改池) - 夜巡仍是 O(n²) ## 1.6.0 — 2026-10-29 这一版全在「记忆怎么进 prompt」这一层:**注入量不再随记忆条数线性膨胀**,检索也不再靠原话逐字匹配。 ### 新增 - **注入预算:按字数装箱** 以前撞上 `injectMaxChars` 是"整段停"——后面的池直接消失,而且没有任何痕迹。 现在四个池的候选合成一个池,按权重全局排序逐条装;装不下就跳过这条继续试下一条; 装不下的条数写在段标题里(`另有 N 条未展开(chain_recall 可查)`),不静默丢。 实测:1000 条记忆时注入稳定封顶 24000 字符,四个池都有配额;137 条时约 9100 字符。 - **检索:两路融合(RRF)** 关键词打分只看"有没有命中罕见词",换个说法就零命中。 现在融合两路排序:**IDF 罕见词命中** + **字符二元组重合**,按名次融合(`1/(60+rank)`)。 没有词面证据的条目不进候选;新鲜度和权重只做平局裁决。 实测(14 条评估查询,含 6 条换说法):Recall@5 从 29% → **64%**,原话查询 100%。 - **工作记忆块** 注入段最前面多一块「还没做完的事」:从你的话里认出待办和承诺(`记得/待会/明天/答应过`), 最多注 3 条,3 天没提起自动过期,说"做好了/发过去了"就删掉相关那条。 纯正则,**不调用模型**,不进记忆链(存 `working.json`,不污染衰减与夜巡统计)。 - **检索评估工具**(`tools/`) `tools/eval-queries.example.json` 是样例查询(`paraphrase` 那类最能暴露短板), `node tools/eval-retrieval.mjs` 输出 Recall@3 / Recall@5 / MRR 与零命中清单。 换检索实现之前先跑它,别靠感觉。 ### 修复 - **`chain_recall` 原本把整句话当子串搜** —— 实测 14 条查询 Recall@5 = 0%,只要不是原话就永远搜不到。现在走 RRF。 - **夜巡 ③ 的碎片噪声**:滑窗切出的「的每句话」「后用户说」这类片段会被当成"共享稀有词"。 加词形过滤(首尾不能是虚字、不能整条都是虚字):③ 从 25 对降到 13 对。 - **注入里检索命中的分数显示**:融合分是 0.03 量级,按旧写法显示成全 `(0.0)`,看着像没命中。现在只显示命中词。 ### 已知限制 - 换个说法仍有约一半搜不到(两路融合补不了真正的语义)。要跨过这一档需要本地 embedding(约 40–90 MB 运行时 + 24 MB 模型),放在后续版本评估。 - 工作记忆只认显式说法("可能""也许"这类抓不到);泛化的收尾话(只说了"搞定了")可能一条都清不掉。 - 夜巡本身仍是 O(n²):1000 条时配对比较约 50 万次,**耗时没测过**(只有注入那条压测过)。 ## 1.5.12 — 2026-10-29 - **修复:海马体写入按目标会话过滤。** `writeHippocampusFragment` 原来在 `buildInjection` 之前无条件执行,而目标会话的判断在后者内部 —— 于是**任何会话的对话都会被写进同一个缓冲区**。现在先判 `isTargetSession` 再写: 单会话版只写目标会话,全会话版恒真、行为不变。 ## 1.5.11 — 2026-10-29 - **`chain_touch` 支持 `ids` 批量退役**(人工去噪用):一次调用退役 N 条原料,不用逐条点;返回值新增 `retired`。 背景:吸收只在"判断正文带了原话"时生效,而对话节奏类原料(「点完了」「传了」)压不成判断 —— 它们的正确处置是"已被某条判断代表,批量退役"。 ## 1.5.10 — 2026-10-29 - **吸收即退役**:写入一条判断时,被它覆盖的海马体原料自动退役(双向包含 / 二元组相似 ≥0.55)。 这解决"重复提交流程"—— 不是禁止重复写,是写完原料自己让位。`chain_append` 返回值新增 `absorbed`。 - **夜巡 ④ 与 README 补上「一步清空」的做法**:把同类片段合并成一条判断写进去,被覆盖的原料自动退役; 剩下的纯噪声三天后自己过期。 - **去重失败姿态改了**:从"查不了就照写"改成"重试一次,仍失败就跳过" —— 宁可丢一条原料,不往缓冲里堆重复。 ## 1.5.9 — 2026-10-29 · 首个公开发布版 7 个池 + 半衰期衰减 + 每轮现渲染注入 + 3D 星云画布 + 夜巡报告与自动唤醒 + 人工定池(`chain_touch` 的 `set-pool`)。 其中的六个夜巡链路问题(报告覆盖判定、海马体分组、未归类审计、按报告期数累计、`set-pool`、旁车 `.meta.json`)都在这一版闭环。 > 1.4.0 → 1.5.8 是内部迭代版本,没有对外发布记录,不在这里逐版展开。