# Changelog ## [Unreleased] ## 🆕 新增 - **记忆溯源有了读出口:`GET /api/dsh-mneme/memory/provenance?memoryId=`——「这条记忆是哪次蒸馏带进来的、原文里到底有没有那句话」第一次可直接查(#421;实现由 @z2Ace0107 在 #509 贡献)**:`distill_records` 此前只有写入路径,写进去就再没有读它的地方,所以「这条记忆来自哪个窗口」只能靠逐帧解压宿主会话日志去对,而 #399 的原始判断是「只显示条数、不返回原文,等于没解决」。面板与 Bearer 数据面共用同一份实现(`src/provenance-http.js`),按 #411 那条纪律只许剩鉴权与路径不同:解析、答复、错误码→状态各只有一份。状态码阶梯是 `401 → 400 缺 id → 409 distill-records-disabled → 503 provenance-unavailable → 200`,其中 **409 与 503 必须分开**——`src/index.js:589-591` 早就写明 503 只该出现在旧宿主/装配缺失,不该被读成「用户没开」;而这条端点**永远不出 404**:未知 id 给 200 + 空 windows,读端点不替调用方判断记忆是否存在(把「功能关了」写成 404,等于让代理与重试逻辑在唯一它读的通道上读成「这条记忆不存在」)。依附既有 `distillRecords.enabled`、不新增开关,判据 `isDistillRecordsEnabled` 只认严格的 `true`(其余一律算关),与写侧 `src/summarize.js:988` 逐字同源,并有一条静态锁钉住这件事。**语义是「关 = 不再经 HTTP 提供」,不是「关 = 删掉」**:保留期裁剪与写入在同一道门里,先开后关的库里历史原文仍在盘上(`dsh-mneme reclaim` 仍能清),这条不对称写进了 `docs/DISTILL.md` §10 与 `docs/STORAGE.md`,真正的修法另账。`dsh-mneme-serve` 那边把该键从面板持久化配置展开进 cfg——否则 daemon 会对**每个请求**谎报「功能关了」,包括用户明明开着、库里也有活跃原文的那些;只靠 bundle 打开、从没在面板点过的用户仍然宿主认、daemon 不认,这一格写进 `docs/DAEMON.md`。开关是**启动快照**:面板关掉要到下次重启才 409、写侧同样到重启才停,该键已在设置白名单与 `NESTED_FLAG_PATHS` 登记,界面带「待重启」。用例覆盖:400 缺 id 赢 409、409 赢 503、409 只带一个键、关着时已知/未知 id 同答(不当存在性预言机)、关着时一次 SQL 都不发、开关开着才读得到;既有多条直调用例原先三处装配全都拿不到 `enabled: true`——那是假绿灯,现已换成显式夹具。 - **把本机记忆打成一个包发给同样装着 dsh-mneme 的人:`dsh-mneme share` / `receive` / `undo-share`,面板「⋯」菜单里同一批三件事(#386 车道 A–D)**:此前把记忆给别人只有 `/export`(一次拿全库)这一条粗路子——对方收不下、也挑不了,而发的人无法知道自己到底递出去了什么。这一版把「分享」做成一个有协议的动作而不是一个文件:**包 `mneme.share/v1` 只含活跃行**,`agent_scope` / `workspace_scope` 与本机 home 前缀一律不进包,命中密钥/PII 的行**整条不进**(不打码——半截密钥既不能用也不安全,读者会以为那是原文)。出包先回一份**报告**再给包:`included` / `deduped` / 六个跳过原因的计数,`included` 是实际发出的条数、够格进包的是 `included + deduped` 两个数同屏,跳过清单截到 50 条并注明是否截断(截断与否由计数器判,不由清单长度判)。**3 MiB 水位量在紧凑序列化的字节上**,超限回 413 加同一份报告与字节数,分批靠 `--type` / `--tag` 两只旗标;CLI 写盘也走紧凑序列化,所以「面板下到的包」和「CLI 写的包」是同一串字节——同一个 `pack_id` 不该有两种字节形状。收侧的闸门同样只有一份:一事务、按 `share_external_id` 幂等(`external_id = sha256(pack_id\0type\0title\0content_hash)` 前 16 位)、**一条不合格整包拒**,干跑是默认、`--apply` / `confirm: true` 才真写(`confirm` 只认真值 `true`,缺省、`"true"`、`dryRun:false`、对象一律干跑)。收进来的行带 `source = "imported"`,而「本机巩固不许改写别人导入的原文」这条契约只由 `src/memory-source.js` 的 `isImportedRow` 一处定义,挂在 `saveWithDedupe` 汇流口与巩固/睡眠/整理的九个行池上——否则一轮巩固就把别人的记忆洗成我们两家的混杂物。**整包退出是物删不是归档**:归档会占住那一格的 UNIQUE 键,重导即撞,等于「收掉了却收不回来」;`undo-share` 收的也是**包文件**而不是裸 16 位包号,让人抄十六进制去删自己库里的行,抄错一位就是「删了个没删成的包,还自以为收掉了」。宿主内面板面与 Bearer 数据面吃 #411 那条纪律——解析、答复、状态码、错误码映射、`dry_run` 口径各只有一份,实际允许的差异只有三处(鉴权、路径、面板侧那一个 `Content-Disposition`),跨面用例把「除这三处之外不许长出第四处」钉住;错误码集合从 `validateSharePack` 的源码静态锁出来,新增码没登记会当场转红而不是静默回 500。 ## 🎨 面板 - **设置页加了一栏粘顶分组目录,一键跳到各分类(#518)**:设置页的卡片攒到需要滚半屏才找得着一个开关,而用户要的「一个分类直接跳到某一节」在这里有个前提——`.mneme-set` 自己是整块滚动的容器。目录由**分组标题那一张表**生成(五个标题与五个 chip 从同一个 map 里产出,不手写第二份清单:两份清单迟早会漂,而漂了的目录会把人领到不存在的锚点),标题在渲染的同时把自己注册成跳转目标。粘顶是因为不粘顶的目录滚两下就飞出视野,那不叫方便跳转;跳转目标自带 `scroll-margin-top`,否则点一下正好把那个分类标题停在目录底下——「跳过去了」和「看得见」是两件事。窄屏收成横向滚动而不是换行:换行会让粘顶条吃掉半屏高度,还会连带把上面那个偏移弄失准。`nav` + `aria-label` + `focus-visible` 是键盘与读屏那一半,用降透明度区分禁用态会把文字压到 AA 以下。 - **宿主「极简模式」为什么不生效,现在写在用户点开关的那一页(#514)**:现象是用户在「设置 → 个性化 → 人格定义」开了人格却发现没生效,而解释一直只长在状态页那张告示卡上——他开关了归开关,原因在另一个 tab。定位结论是**宿主侧压制、不是插件缺陷**:极简预设(minimal)把整条 system prompt 接管,`runtime context` 整个不进,记忆 / 画像 / 人格一律到不了模型;插件的轻量档刻意不关人格。这一版把告示搬到人格卡与运行模式卡各渲染一次,两处共用同一份取数(`useInjectStatus`),并把「轻量 / 标准」与「极简模式(minimal 预设)」这两个会同屏出现的词在文案里明确分开——不先分开说,用户永远会把宿主的 minimal 记成插件的 light。告示只在 `suppressed`(autoInject 开着**且**宿主确实报了 minimal)时为真:主动关掉注入的用户、preset 未知的宿主一个字的打扰都收不到。 ## 🐛 修复 - **dream 的 merge 被 `dreamMergeGuard` 永久拦死:提示词还在催合并,46 轮零合并、每轮照付满载 token(#521)**:两个护栏从来没对齐过——`archive` 早就有逃生门(`reason` 命中 `ARCHIVE_RATIONALE_RE` 的「重复/过时」词表就放行长保留类型),`merge` 没有;而 `src/lang.js` 的巩固提示词【任务】第 1 条明确要求「识别主题相近的条目 → merge」,【硬性规则】里的长保留类型禁区**只对 archive 说过**。报告者的实测:46 轮巩固、`skipped` 决定 299 条里 280 条(93.6%)被这条护栏拦下且 `action` 全是 merge,整轮 `applied` 合计 375 条、**merge 恒为 0**;模型提的永远是同一簇 preference,全被拦 → 什么也没变 → `updated_at` 不动 → 下一轮同样条目还在 200 条窗口里 → 再提一次。单轮 `input_count` 46 轮全部 200(窗口饱和),同一条记忆 46 轮里被提出去合并 40 次。**模型没错,护栏也没错,两者从未对上。** 两条一起改:① 提示词(zh/en × consolidation + conflictFull 四处)补上 merge 的类型禁区,与 archive 那句同形——「长保留类型仅当确属重复或过时才可 merge,且 reason 必须写明;只是主题相近请输出 keep」;② `src/dream/decisions.js` 的 merge 护栏加逃生门,条件与 archive 护栏同一条词表且**要求所有 id 同类型**(跨类型合并本就另受 `allowCrossTypeMerge` 约束,逃生门不跟着放宽)。reason 只写「主题相近」不会命中词表——这是故意的:相近不等于重复,只有确属重复/过时才值得牺牲两条独立表述。**默认值不变**(`dreamMergeGuard` 仍默认 `true`),报告者也明确不请求关掉它——它默认开有理由(E8 实测 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉)。两条链路共用同一 `validateDecisions`(`src/dream.js:1244` 主链路 + `src/dream/sleep.js:393` sleep 链路),改一处两处都受益。回归测试 +3:真重复放行、只有理由不同类不放行、同类型但只写「主题相近」仍拦。 - **面板看不出「一整类动作 100% 失败」:`status: degraded` + `applied: 3` 从外面看完全是健康轮(#521 附带)**:`skipped` 明细此前只落库,`GET /dream-status` 的响应里没有聚合,不自己写 SQL 展开 `dream_runs.skipped` 根本看不出问题——报告者那个 bug 跑了一整天才发现。`skipped[].error` 还带 `decision[N]:` 序号前缀,朴素 `GROUP BY` 会把同一原因拆成 20 组。新增 `normalizeSkipReason` / `summarizeSkipped`(住在 `src/dream/decisions.js`,与产生这些 error 的校验器同一文件,措辞不会漂移),只剥序号前缀与具体 id/类型集合,其余措辞原样保留;按 `(action, reason)` 二元组计数——同一句原因在 merge 和 archive 上是两回事,混在一起会误导。响应新增 `skippedCount`(原始条数)与 `skippedReasons`(聚合桶,按条数降序),面板状态页在最近巩固卡下方多一张告警色卡,最多列 4 类、其余折叠成「另有 N 类」。 - **上一版那三条(#521)在评审里被挑出五处,逐条修完**:① **跳过明细原先藏在默认折叠区里**——`StatusPanel` 的「后台运转」区默认收起且收起时不挂载子卡,而跳过卡就挂在那儿,于是「一整类动作 100% 被拦」在首屏照样看不见,等于把 #521 的病换个地方犯。现在明细提到 `TokenFlowSection` 的 KPI 带下方(首屏,复用同一次 `/dream-status` 请求,不新增请求),渲染体抽成 `SkippedReasonsNotice` 供两处共用,折叠区不再重复一份——同一个数字在首屏与展开区各出现一次,读者会以为有两批跳过。② **`normalizeSkipReason` 只剥得掉第一个序号前缀**:一条决策可能攒下多条错误,`skipped[].error` 用 `"; "` 拼接,而原正则 `^` 锚定、只换一次,`decision[1]:` 留在字符串中间,聚合时同类原因又被拆成多组——正是 #521 抱怨的那个病。改成先按 `; decision[N]:` 切段再逐段归一化。③ **审计文本随开放路由出网关**:校验器回给模型的自由文本尾巴(`got: ""`)可能夹带记忆正文、也可能被截断成没有右引号(那样按引号配对的替换根本剥不掉),而 `/dream-status` 的 policy 是 `open`。现在整段丢掉,明细仍原样留在 `dream_runs.skipped` 供排查;`action` 也一并走 `ACTIONS` 白名单,不在枚举里的归成空串——面板少一个动作标签,比让上游脏数据决定响应形状安全。④ **否定理由能穿过 merge 逃生门**:「并不重复,只是主题相近」与 `"not a duplicate"` 都含「重复」/`duplicate` 子串,放它们过闸等于让模型用一句否认换来合并许可——恰好把 #339 要防的失真放回来。新增 `NEGATED_DUPLICATE_RE` 一票否决(archive 护栏从 #104 起带着同一个洞,属既有行为,本批不动,已记在代码注释里)。⑤ **提示词示例与新规则自相矛盾**:`conflictFull` 的 merge 示例没有 `reason` 字段,模型照搬就会输出无理由的 merge 而被校验器拒掉;`consolidation` 的示例理由写的是「主题相近」,恰是新禁区明说不要 merge 的那种。四处示例(zh/en × consolidation + conflictFull)一并改成「重复」/`duplicate`。回归测试 +3(否定理由中英各一例、多段前缀 + `got:` 尾巴剥离、跳过明细首屏可见且只有一份实现)。 - **一条只错在分类词上的窗口,被当成「模型没按格式写」反复重放(#502,社区报告 #493 追加的第二形态)**:#493 让蒸馏解冻之后,报告者贴出对照——`summarize_compress` 18:32 成功、18:35 又失败,而失败行的 `metadata` 里 `raw_has_bracket = true`、`raw_len` 2574/3299:**JSON 是合法的,条目是被 `type` 枚举全部滤掉的**。模型自造了中文分类(`"type": "环境/版本"`、`"用户需求"`)并多带 `id` / `source` 键,于是 `entries.length === 0` 而 `arr.length > 0` → `ok:false` → 记 `invalid summary JSON` → 游标不推进 → 同一个窗口一遍遍重放。根因是两套不一致(对照 `main` 已核):`summarize.js` 的 `VALID` 吃 **7** 个类型,`lang.js` 里普通 `summary` 的中英两版只列 **4** 个(`codingSummary` 才列全),且两个 prompt 都没有一句「只能用这些值 / 不要输出额外键」(grep `只能用这些值|枚举外的值|额外键` = 0 命中)——喂给模型的菜单和卡内容的闸门对不上,模型按最像中文标签的方式发挥是合理结果。修法按报告者排的 1 → 3 做,**都零行为变更**:① 失败形态分开(`describeParseFailure`:`no-array` / `syntax` / `filtered-empty`),`filtered-empty` 的错误消息写成 `no valid summary entries (arr=N, valid=0)`,审计 `metadata` 补 `failure_kind` / `arr_count` / `invalid_entries` / `sample_types`(模型实际用的分类词原样带出,不看这一眼就得去猜);② `summary` 的中英两版枚举列全为 7 个,并各自写明「`type` 只能取上面枚举里的值,不要自造分类;每项只输出上面列出的键,不要附加 id、source 等额外字段」——两种语言必须一起改,只改一种的话另一种语言的窗口照样被滤空,而孪生守卫看不见「两边一起过期」。另外补了 `raw_tail`(正文 > 500 字时给末 200 字):有些模型先写几条合法条目、再在后面跑偏,前 300 字看不出来。**`type` 归一化(中文近义词 → 枚举)刻意不做**:它能把内容救回入库,但会改变入库分类分布,等 ① 的统计出来再判断值不值——这条判断也是报告者的排序(1 → 3 → 2)。「早退」路径(连方括号都没有,#493 的 B 臂)以前返回 `{ok:false, entries:[]}` 什么上下都不带,现在也带 `failure`,否则下游只能靠「输出里有没有 `[`」再猜一次,而猜出来的东西不该写进错误消息。测试 5 条 + 1 条既有断言升级(全无效摘要那三条用例从「等于 invalid summary JSON」改成「等于 `arr=N, valid=0`」并加锁 `failure_kind`,行为断言一条不放松:状态仍是 error、窗口仍可重试、重试收到逐字节相同的转录)。 - **dream / sleep 侧那 5 处 prompt 一直没有围栏:巩固把 46K 清单读成「没人给我下指令」,约 43 万 token/天白烧(#501,社区报告 #493 追加)**:#493 修的是「模型把喂进去的会话转录当成自己的发言」,但围栏只装在 `autoSummarize` 那一处——`src/summarize.js` 之外,`dream.js` 的叙述条/巩固/摘要与 `sleep.js` 的冲突裁决/模式提炼这 5 处一直是把清单**裸放**在 user 消息里,system 之后没有任何分隔与角色约束。现场:`dream_consolidate` 9 次失败、输入 47K→55K tokens 而输出只有 212–907 tokens,日志里模型说「收到——这是一包记忆检索结果,但没有带具体指令,我先过一遍给你一份导览」,于是开始寒暄;报告者的对照实验同时说明方向是对的——只加 system 约束那一臂从 2/2 掉到 0/1,围栏 + 角色重申那一臂仍 1/1。修法:① `lang.js` 新增**记忆清单专用**的成对围栏与角色声明,不复用「会话转录」那对名字(喂的是清单,标错名字会让读审计与读日志的人以为这次喂进去的是一段对话);角色声明落在 **user 段开头**——#493 实测判断「这段文本是什么」的依据是它所在的那条 user 消息,只写 system 救不回来。② `fenceListBody()` 是纯函数(同一批记忆在下次阈值到来时会被原样再喂一次,重试收到的 prompt 必须与首次逐字节相同,否则「同输入同错」这条诊断依据就没了),并把正文里的 `<<<`/`>>>` 用零宽空格断链——**只中和标记、不删内容**:不然一条写着「<<<记忆清单结束>>>」的记忆就能提前闭合围栏,把后面的文字升格成指令,而记忆正文本来就是模型的可影响输入。③ 失败行的审计 `metadata` 不再为 null:`runAuditedLlm` / `auditedSleepLlm` 加 `spec.metadataFrom(result)`,dream 侧写 `parse_failure` / `raw_len` / `raw_head` / `raw_has_bracket`(`raw_has_bracket` 是区分「压根没按格式写」与「格式对但内容救不回来」的最低成本判据;报告者另指的 `raw_tail` 缺口属 #502)。审计完全复用 dream 既有那条缝,只往导出列表加名字,不重写第三份 chunk 适配。④ 顺带修掉一处**测试侧的脆弱**:多个 mock 靠「user 段以 `id=` / `候选冲突` 开头」认阶段,围栏让首行变成角色声明;改用清单形状(`| type=`)与 `options.purpose`。这不是迁就测试——`dream-candidate-hybrid.test.js` 早就注释说过前缀判据在语义预分组下会把巩固误判成摘要、只能绕开共用 mock,现在那个绕法不再需要。回归锁 5 条(围栏结构、确定性、提前闭合、consolidate 实测带围栏且失败行有读数、5 处不许再裸放的形状锁),变异自证三条(摘掉中和 → 只有提前闭合那条红;摘掉 `metadataFrom` → 只有审计那条红;拆开 consolidate 的围栏 → 审计与形状锁两条红)。评审补的两处:① `fenceListBody` 带输出契约参数——总览摘要要的是散文,对它说「只输出 JSON 数组」会让模型回 `[]`,而 `summaryText.trim()` 判非空通过,方括号就这么被存成库内总览(Major;散文契约用独立的角色声明,叙述条保留 JSON 那份,它的 system 确实要严格数组);② 流以错误结束、根本没有正文时不再写 `parse_failure` 与 `raw_len: 0`,否则一次限流会被读成「模型没按格式写」。两处各自做过变异自证。 - **同标题的记忆不再被当成新记忆写第二份:去重候选取的是「该类型最重要的 100 行」,不是同标题那几行(#505)**:`saveWithDedupe` 的候选集一直是 `store.list({ type, limit: 100 })`,而 `list` 的默认排序是 importance——那是「该类型最重要的 100 行」,与「找出同标题的那一行」无关。某类型里 importance 高于待写行的行数一旦超过窗口,同标题行与待写行双双落在窗口外,`find` 恒不命中,于是**静默走创建分支**:不报错、不告警,合并语义随库增长而失效(实测在 1047 行的类型、importance=5 有 180 行时稳定复现)。改为按同标题定向取(`store.activeByTypeAndTitle`,无窗口),`TRIM(title)` 与比较式对齐——标题入库时不 trim,用 `title =` 精确匹配会漏掉存量里带首尾空白的标题,退回同一种静默降级。dream / narrative 两条分支的去重键本来不含标题(语言切换后标题会变),所以它们仍走「该类型活跃行」的候选集,只是窗口换成无界且不按 importance 排序。写路径的代价随最大类型的行数线性长(本机实测同类型 1000 / 5000 / 20000 行 = 0.13 / 0.68 / 4.63 ms),所以配了一条 `(type, TRIM(title))` 表达式索引把它压平到 ~0.00 ms:`TRIM` 本身不是瓶颈(同句去掉它只差 0.07 ms),缺的是能把条件当 seek 用的索引。有一条用例用 `EXPLAIN QUERY PLAN` 钉住这条查询真的选中该索引——表达式写歪一点优化器就静默不认,而所有行为用例照样全绿。 - **模式扫描不再谎报「没东西可扫」:政策轴闸门按下的行会占满取数窗口(#515)**:睡眠侧的模式提炼取候选池时带着 `limit`,而「这个作用域被按下了」那道闸门(#471 的可逆政策轴)过去是在 SQL **返回之后**才过滤——被按下的行照样吃掉窗口名额。库里可扫的自家行不足 200、而按下的行塞满前 200 格时,过滤完剩 0 行,回执写着 `no memories to scan`,症状是「巩固什么都没做,日志却说没东西」。修法是把判据下推进查询(与外来行同一格,走 store 侧的 WHERE),并把重取做成按需:窗口不够用时放宽 `limit` 再取一次、且只放宽 `limit`,三条过滤轴一条不许动;闸门没按下且窗口够用,就仍只付一次有界查询。扫描窗口跟着 `sleepPatternMinMemories` 的配额走(那个键允许配到 1000)——否则配额调大之后无界重取每次巩固都会触发,等于把省下来的那一半又花回去。 ## [0.9.4] - 2026-10-21 ## 🆕 新增 - **agent 主动整理接口接上调用入口:`memory_organize` 工具 + `organizeEnabled`(#231,默认关)**:引擎在 v0.8 就造好了(`src/organize.js`:dryRun 出比对报告 → agent 判断 → apply 落库,筛除只归档不删,全程复用 `dream_runs` 的回执语义),但当时刻意「不进工具列表、不加独立开关」,于是它一直是**只有测试能调用**的功能——`tools.js` 的十一个工具里没有它,两张 API 脸 0 命中,全仓 `.organize(` 零调用点。这一批接的是油门:新工具 `memory_organize`(`mode: dryRun|apply`,缺省 dryRun)+ 顶层扁平开关 `organizeEnabled`(默认 `false`)。**默认关与「关 = 不注册」是同一件事的两面**:开关关掉时工具根本不进注册表,模型看不到就不会调——沿用 v0.8.5 工具暴露闸的定性(比在描述里劝模型别用更可靠);轻量档预设也关,但理由不是成本而是协议:dryRun→apply 要求模型拿住上一轮的 `run_id` 再回来,弱模型漏这一环就是一次被拒的调用。预设不剥夺显式开启的权利(合并顺序「用户开关 > 轻量预设 > bundle」,同 `continuityRescueEnabled`)。**补上 `opts.hiddenIds` 这个零生产者的可信上下文**:`organize.apply` 收它、却从来没人传(工具没接,自然没有生产者)——不接就等于让整理接口成为第二条绕过 scope 治理的改库通道。现在由工具层按 #170 复核项 4 的口径算好传入(`register_document` 的 `hiddenEvidenceIds` 同形):他 scope 的 explicit 行按「不存在」处理,回执文案维持「unknown, archived or out of scope」那一句,不泄漏存在性。**同一件事还有报告那一半**:比对只看三列**值**(不看 `*_scope_source`、不调 `isVisibleInScope`),而 `agent_scope` 是模型自己给的参数,所以把报告原样回给模型就等于开了一个探测口——「那一档有没有这个标题」,拿到 `id` 与 `title` 就是答案。现在落点先过一遍调用方可见性:看不见的行按「不存在」说(`verdict: new`、不带 `match`),`counts` 跟着重算——留着引擎那份计数,`exact: 1` 配一个不带落点的条目本身就是存在性 oracle,等于把泄露从 id 挪到计数器。审计行仍存原始落点,那是给操作者看的证据面,不是给模型的读数。经整理落库的候选与 `memory_save` 同源盖章(会话身份 `auto`、显式参数改盖 `explicit`),否则这条路写出来的行会全部躺在未标注(全局可见)那一档。测试 +13(`test/organize-tool.test.js`):默认不注册 / 开启后注册、lightMode 预设值、dryRun 一行不写只留回执、apply 三态计数、拒绝没有报告的 apply、拒绝缺 `run_id`、未知 mode、scope 盖章两例、存在性泄漏的**正反两向**(`strictScope` 关时照常给出落点——修安全问题不许顺带改成少召回)、落地侧 out-of-scope 一例。两条新守卫各做过变异自证:摘掉可见性过滤 → 只有泄露那条红;`counts` 换回引擎那份 → 只有计数断言红(`actual 0, expected 1`)。`hiddenIds` 那条**第一版是错的**:最初拿「别人 scope 的行」测,可那行永远不会成为报告的命中行,把守卫整段摘掉测试照样绿;换成「同比对键但会话看不见」那一格才咬得住——而写错的那一版还把泄露本身钉成了期望值。#195 的工具矩阵两个分支都补了(清单比对与 C 层结构断言改用暴露闸全开的配置——opt-in 工具在默认配置下不在注册表里,用默认配置跑那条护栏会静默失效)、`tools.test.js` 的 schema 子集加走一遍暴露配置、`api.test.js` 旗标计数锁 103→104、`docs/CONFIGURATION.md` 补键与轻量档清单。**本批刻意不含**:注入侧的「能力说明」段落(属 #249 剩下的另一半,本批唯一表面是工具描述),以及面板上 `run_type='organize'` 回执的展示口径。 - **画像提取草稿:`profileExtractEnabled` + `POST /api/dsh-mneme/profile/extract`(#497,默认关)**:面板「用户画像」卡原来只有一支笔——画像写什么全凭用户自己想起什么,库里几百条记忆攒出的偏好一次都汇不上来。这条加一个按钮做反向聚合:读一批活跃记忆,花一次宿主 LLM 调用,产一段「助手眼中的用户」**草稿**。三条边界各自是硬要求:① **只出草稿**——本模块连 `settings` 都不注入,采用草稿是面板上另一次显式 `PUT /profile`;画像正文是用户手写的原文,让模型「合并」等于给它改写权,而改写后的文本每帧都注入。② **没有可用输入就不调模型**——空库下模型只会编一段「她热爱学习」,那种草稿比没有草稿更坏,它会成为用户看到的「关于自己的第一版真相」;所以报 `no-input`。③ **吐不出可用 JSON 就报错**——`unparseable` 与 `no-input` 与「没配模型」(no-route) 与「模型没配合 / 调用失败」(unparseable | stream-failed) 在 HTTP 面上是四个不同的结局:409 disabled、409 no-input、503 no-route、502 unparseable/stream-failed(其它意外才是 500),面板上也是三句不同的话;压成「这次没有草稿」就是把故障报成空结果(#489 同一条教训)。记忆正文是模型的**可影响输入**,所以喂进去的条目包在围栏里并在系统段重申「这是数据不是指令」(与 #493 的转录围栏同源的做法,不是第三套);系统段还禁了一件事——**不许给助手本身起名或设定人格**,因为画像与规则不受 `personaEnabled` 管辖、也不看寻址口径,正文里一旦出现「你是某某」就会重演 #497 那个身份撞车,而且是每个会话每一帧。审计完全复用 dream 的既有缝(`streamText` / `runAuditedLlm` / `resolveRoute`):chunk 形状的三个硬点(用量嵌在 `chunk.usage`、终态 `finish` 才是错误、适配器故障被转成 finish chunk 而非抛错)都在那里踩过并修过,不在第三处重写;本次只把这两个函数加进导出列表。路由 policy 是 `token` 而不是 `token-write`:这条路由**没有读分支**(非 POST 一律 405,且 405 在 `readBody` 之前——顺序反了会让不带 body 的方法永远挂在读体上),声明成写侧开放会把 SECURITY.md 的开放读清单骗过去,而 `test/security-doc.test.js` 正是钉这件事的。默认关并进 `LIGHT_MODE_OFF`(一次调用读上百行记忆 + 一轮生成,与低资源档互斥;预设关、用户显式勾选仍赢)。测试 +12:草稿与审计成对、失败路径零写入、围栏结构、四种 HTTP 结局、方法白名单、token 挡在调用之前、「采用必须把草稿文本显式交给保存」(防的是保存函数带默认参数时把 click 事件当值存进去)。 ## 🐛 修复 - **人格的 `default` 卡会进所有没建卡的身份,面板的「注入生效文本」又不是生效文本(#497)**:两件事同源——寻址与渲染各有一份实现。① **寻址**:命中不了 `byAgent` 卡(没建卡,或卡被显式关掉)时一律落 `default`。于是 `default` 卡开着时,宿主里跑的外部 bot(自带身份与设定)每一帧都会收到一份「你的人格名字:…」,两句「你是谁」撞在同一轮;而 `agent_scope` 是模型可给的参数,不构成边界。这条也让界面上「关掉这张卡」实际不生效——关掉的卡照样从 `default` 拿回一份人格。新增 **opt-in** 键 `personaStrictAddressing`(默认 `false`,历史行为逐字节不变):打开后这两种情况都不注入人格,`default` 退回它的本意——只兜**这一帧解析不到会话身份**的无名会话。不默认改是因为它是口径变更,老用户升级后不许有人格静默消失。② **同源**:`lib/client.js` 那段「Agent 眼中的你(注入生效文本)」原本是前端自己拼的(只有 profile + rules 两条列表,没有 `[用户设置]` 表头、没有「- 用户画像:」前缀、**完全没有人格三行**,注释还写着「零后端新请求」),标题却声称是模型实际看到的读数。渲染整体搬到 `src/user-settings.js`(`escapePromptVars` / `resolvePersonaCard` / `renderUserSettingsBlock`),注入器与预览共用这一份;`inject.js` 的 `renderUserSettings` 只剩一行委托,转义闭包改为委托同一实现(其余调用点不动)。`GET /api/dsh-mneme/persona` 因此多出 `preview`(`text` / `source` / `agent` / `configGate`),并支持 `?agent=<身份>` 逐张卡预览;面板只显示后端给的文本,读失败时 fail-closed 显示「未知」,绝不回退到前端拼的版本——预览里也不出现实际不会注入的人格行。 预览请求带序号,过期响应直接丢弃(先切卡 A 再切卡 B 时 A 的 GET 可能后回来,而这张卡自称「生效文本」,显示另一张卡的文本比不显示更糟);`source: default` 还按 `agent` 分成两句话:「身份解析到了但没有可用的卡(落回默认卡)」与「这一帧解析不到身份」——前者正是「关掉这张卡为什么不生效」的答案。`settings.getPersonaFor` 改完后生产代码零调用点,一并删除,寻址只留一处。回归锁:strict 四例 + 一条现状锁、预览与注入器**逐字节同源**例、总闸关闭如实标注例、面板不许回到前端拼接例;变异自证两条(摘掉 strict 分支 → 4 红;预览忽略 `agent` 参数 → 3 红,含逐字节同源那条)。**刻意不做**:画像/规则的独立开关(比 persona 早,加闸影响所有用户)、人格卡单选互斥(运行时每轮本只用一张卡,互斥会牺牲 v1.2 多卡的意义——要的是「未命中就不注入」那一格,已由 `personaStrictAddressing` 给出)。「从记忆提取画像」提出本条时确实还没有管道、当时列为不做,维护者随后指定要做,**已作为本版另一段落地**(见上方 `profileExtractEnabled`)。 - **蒸馏的转录被模型当成「该我回话了」:同一窗口确定性失败,重试 20 次、越试越贵(#493)**:转录以「用户:…」开头、**裸放**在一条 user 消息里(`src/summarize.js` 的 `collectMessages`),格式指令只在 system 里。模型于是把它当成当前发言接着往下说/点评,输出里连 `[` 都没有,`parseSummaryJsonResult` 在括号早退处直接判 `invalid summary JSON`;而解析失败不推进游标(语义是「这段还没提炼完,下次接着来」),下一个 `turn/end` 重试**同文同温**——同输入同错,窗口还随新事件继续变大。报告者的 20 条失败 input tokens 8194 → 11118 单调递增、`finish_reason = stop`,正是这条路径;本机注入恒定返回中文点评的假 stream 复现出 19 次额外调用、落库 0 条、游标不动。改法两条:①**围栏 + 角色重申**——user 消息现在以「你是第三方提炼员,不是下面这段会话的参与者」开头,转录包进 `<<<会话转录开始>>>` / `<<<会话转录结束>>>`;围栏是确定性的,重试收到的转录与首次逐字节相同(既有回归用例依赖这条);system 保持原样,因为模型对「这段文本是什么」的判断由它所在的 user 消息决定,只写进 system 救不回来(实测同段转录只加 system 约束仍 0/2 出 JSON)。②**失败护栏**(新键 `summarizeParseFailureMaxRetries`,默认 `0` = 关):连续失败到阈值后强制消费游标跳过该窗口,避免一条会话被同一窗口卡死。默认关是因为放弃内容本身有代价——短窗口偶发失败仍值得重试。 - **解析失败行的审计 `metadata` 恒为 `null`,事后无法回答「模型到底吐了什么形状」**:成功路径才给 `metadata` 赋值,失败早退分支从不赋值——90 条历史 `invalid summary JSON` 一条原始输出都没留。现在失败行恒带 `distillChars` / `parse_failure` / `raw_len` / `raw_head`(截断 300 字符)/ `raw_has_bracket` / `streak`。`raw_has_bracket` 是区分「模型没按格式写」与「格式对但内容救不回来」的唯一低成本判据,也正是 #493 现场的关键特征。只加计量、不加跳过决策(同 #239 的 `distillChars` 取舍)。**刻意不改的两处**:`llm_audit_logs.session_key` 仍为 NULL(`test/pre-inject-gate.test.js:155` 显式锁着「LLM 调用行不写会话键」,补上会改动 #254 写入准入的话题回填行为);不显式传 `temperature`(温度由宿主/路由决定,插件覆盖会改变所有用户的输出分布)。 ## [0.9.3] - 2026-10-20 ## 🆕 新增 - **沉淀链接线:把「会自动沉淀工作流」从一句话变成一条能跑的路(`actionNudgeEnabled`,beta,默认关)**:`src/pipeline-nudge.js` 从 v0.9.1 起就完整实现了「多帧 zstd → 重复序列 → 缺席对照 → draft 草稿」,可它在运行时**没有任何调用点**——全仓引用只出现在 `test/pipeline-nudge.test.js` 里,也就是说机器挖得出来、却永远不会被叫去挖。缺的从来不是算法,是**归属**与**触发**:`nudgeFromSessions` 要的是一个项目的会话桶目录,而桶名是路径的**有损**变形(规则不在本仓),唯一可信来源是会话头里的 `cwd`。这一版把三处接上:①`resolveProjectSessionDir` 用 `cwd` 反查桶,查不到就**整跑跳过**(`skipped:true`)而不是降级成全局普查——后者会把 A 项目的动作算到 B 头上,而结果看起来完全正常;②新增 `nudgeForProject` 把「归属 → 挖掘 → 落盘」收成一个入口,调用方没有机会漏掉归属解析;③新增 `src/pipeline-nudge-auto.js` 挂在 `turn/end` 上按项目跑,冷却 10 分钟(**观测新鲜度**参数,不是性能参数)、单次预算 4 秒、延到 `setTimeout(0)` 的宏任务里跑(解压会话日志是同步 I/O,压在回合收尾那一帧上等于让每轮都卡一下)、一次只跑一个项目、抛错只 `warn` 不冒泡(`session/event` 那条链上还挂着摘要与续期)。**它只产提议,不产草稿**:自动跑恒 `materialize:false`,落进 `/proposals/<项目键>/`;升格仍然要人批,`boundaries` 里那句 `autoActivate:false` 与跳过分支共用同一个常量(`NUDGE_BOUNDARIES`)——两条出口说两句不同的话,读的人就会以为其中一条忘了做。旗标与看图(`actionPipelineEnabled`)、记账(`actionRunEnabled`)**三件互相独立**,开任何一个都不会顺带打开另一个;它进 `LIGHT_MODE_OFF`,是三个 M.O. 键里最重的一个。 ## 🎨 面板 - **M.O. 面板多了「现在沉淀一次」:自动沉淀只在 `turn/end` 之后跑、还有冷却,人想立刻看结果时原本没有入口**:按钮走 `POST /api/dsh-mneme/nudge`,与自动跑**同一条** `nudgeForProject`,区别只有 `materialize` 恒关——它写提议、不改仓库。九句回执全部中英双语齐全,且刻意把三种结果说成三句不同的话:`saved`(提出 n 条)、`none`(这一遍没挖到新的重复动作)、`unreadable`(会话日志读不出来,是环境问题)。后两者在界面上长得一样时,人就只能瞎猜是「没东西」还是「坏了」。会话里读不到 `cwd` 时按钮**禁用**(服务端只会回 400,点了必然失败的按钮比没有按钮更糟);禁用态用虚线边而不是降透明度——整块降 `opacity` 会把文字一起压到 AA 以下。顺带把 `lib/client.js` 里最后三处「手法」改词,并把 `actionPipelineEnabled` 的 hint 从「沉淀和触发都还没做,建议暂时保持关闭」改写成三件能力的真实分工。 ## 📚 文档 - **`docs/WORKFLOWS.md` 新增 §10「自动沉淀:工作流从哪长出来」**:§1 里「一条新工作流从哪来」此前一直是零代码,这一节补上来源——产物表(提议/草稿/定稿各住哪、谁批准)、归属为什么不能拿全机会话去挖、四种 `reason` 里 `unreadable` 必须与 `no-bucket` 分开(前者是故障、后者是关于这个项目的答案,混起来等于把故障报成空结果,端点层因此一个回 503、一个回 200)、触发控制表(`turn/end` / 10 分钟 / 4 秒 / `setTimeout(0)` / 一次一个,每行都写了为什么是这个值),以及「提议落盘为什么必须单独一个开关」:`nudgeFromSessions` 原本只要算得出项目键就**无条件**写提议,而面板每刷一次就 GET 一次——那是每刷一次往记忆库里塞一批文件。`persistProposals` 因此独立出来,预览路由显式传 `false`,这也是 `/nudge` 的 policy 是 `token-write` 而不是 `open` 的原因。`SECURITY.md` 英中两张表同步登记(`/nudge` 同时进「开放读」与「受门控」两张清单),`docs/CONFIGURATION.md` 补 `actionNudgeEnabled` 键与轻量档清单。 ## 🐛 修复 - **术语漂移:整个 M.O. 的「手法」改回「工作流」**:M.O.(Modus Operandi)从 v0.9.0 起在正文里一直写作「手法」,而它与同一个功能在其余位置的叫法(`工作流`、`workflow`、`pipelines`)并不是同一个词——一个功能两个名字,读者得先猜它们是不是一回事。改法覆盖 `src/` 24 处、面板 3 处、文档 11 处,**已发版的 CHANGELOG 条目与测试里作为夹具主题词的「手法」不改**(历史不追溯,夹具词锁的是路线图插入逻辑、不是产品术语)。`git grep 手法 -- src` 现为零命中。 - **作用域反查自 v0.8.0 起每次都抛错走降级,canonical 归一与成员校验静默失效(#487)**:`scope.js` 曾裸读 `ctx?.workspaceRegistry`,而 `workspaceRegistry` 不在 `inject` 声明里(`index.js:45`)。cordis 4 的 `ctx` 是 Proxy,**裸读未注入属性会抛 `cannot get property ... without inject`,而不是返回 `undefined`**——于是 registry 反查自 v0.8.0 起必然抛错走降级:canonical path 归一与成员资格校验静默失效,`workspace_scope` 只剩会话原始 cwd。改为与 `webServer` 同一形状的免 inject 读取(`api.js:205` / `index.js:583`):有 `ctx.reflect.get` 时走它,旧宿主的普通对象才裸读;**不加进 `inject`**——`index.js:39-44` 记着 v0.7.23 移出 `inject` 想支持 headless,结果 apply 阶段宿主未就绪直接崩。服务缺失(headless / 旧宿主 / 未激活)是合法状态,静默按「未提供」处理、不告警;真故障(`list()` 抛错)仍 `warnOnce` 一次并降级 `header.cwd`。回归锁用「读未声明属性即抛」的 Proxy 复刻真实宿主形状——既有三处 registry 测试都用普通对象伪造 ctx,而普通对象裸读不抛错,所以这条路径此前在测试里看着是通的。 - **`/nudge` 的三处「没看全」被报成「确实没有」(#489 评审)**:① **写盘口**——方法分流只拦 `PUT`/`POST`,其余直落 `run(null)`,而 `persistProposals` 按「不是 `GET`/`HEAD` 就算写」算,于是带 token 的 `PATCH`/`DELETE` 会走进写入分支、把提议落进 `memoryDir`:一个不需要 body 的静默写盘口,与这个端点「`GET` 只预览、写动作一律要 `POST`」的卖点正好相反;改为入口白名单(`GET, HEAD, POST, PUT`),其余 405 + `Allow` 头,在 `readBody` 之前拦,测试同时锁状态码、`Allow` 头与「`memoryDir` 下一个文件都不许出现」(只看状态码会漏掉「先写再报错」)。② **预算外路径**——归属解析最多同步扫 500 个桶 × 3 个文件,原先完全不看 `budgetMs`,于是「真卡就调 `budgetMs`」这句话对这一段是空话;现在桶循环与文件循环里各查一次 `deadline`,超时回新 `reason: budget-exceeded`,在 `/nudge` 上按环境问题报 503(扫描没跑完不是「这个项目很干净」的答案),`nudgeForProject` 自己按同一条截止线转交,避免「解析用 A 截止、挖掘用 B 截止」的双预算。③ **截断谎报**——收集桶名时边收边截:数到 `maxBuckets` 就 `break`、之后才 `sort()`,于是留下哪 500 个由文件系统遍历顺序决定;目标项目的桶一旦落在被截掉的那部分,函数返回 `no-bucket`,而 `no-bucket` 的语义是「扫完了,确实没有」,一次截断就此被报成「这个项目没跑过会话」;改为先收全、排序、最后截断,新增 `reason: bucket-limit` 与 `bucketsTotal` / `bucketsTruncated` 两个读数。另修正 `config.js` 里「30 分钟/项目」的注释(实际 `NUDGE_AUTO_MIN_INTERVAL_MS` 是 10 分钟)。**一处如实说明**:把 `sort()` 去掉(先截断)这个变异**没有被测试咬住**——测试用临时目录的 `readdirSync` 顺序恰好是字母序,两种写法结果相同;要真正覆盖得有能控制 `readdir` 顺序的注入点,记为已知覆盖缺口,不假装它被锁住了。 ## 内部与测试 - **多帧 zstd 的反证断言绑了 Node 的整块解语义,legacy 腿下一次 runner 升级就红(#490)**:`session-usage.test.js` 原反证断言 `zstdDecompressSync(buf)` 只能拿到第一帧,依赖的是「整块解只吐第一帧」这一 Node 旧行为;**24.22.0 起解码跨 chunk 边界也能解出所有帧**(nodejs/node#65865),断言在 `test-host-legacy`(装 latest 24.x)上过期——同一分支上 24.22.0 两条 legacy 腿全红、main 上 24.21.0 全绿,也就是说 main 那次「全绿」并不证明这个版本问题已经消失,runner 下次升到 24.22.0 就会红。改成数 zstd magic 帧头,既与产品代码 `forEachFrame` 的切帧判据同源、又不绑定解码器语义;第二行断言用 `stat.frames + stat.failed`——「帧头数 = 成功数 + 失败数」是实现契约,而「帧头数 = 成功数」只在 `failed` 恒为 0 时成立,是巧合不是规矩。**已知局限写进注释**:这条锁的是「切帧判据仍是 magic」,不是碰撞免疫——payload 里撞上 magic 时计数器与解码器会一起错(半片也解得出来),实测 `frames` 同样变成 4,这条测不出来。 ## [0.9.2] - 2026-10-19 ## 🆕 新增 - **功能开关的第三态「待重启」:把「面板显示已改 ≠ 运行时已改」这个落差摆到界面上(#481)**:功能开关的生效节奏一直是**启动期**的——`lib/index.js` 在装配时把 kv 里的覆盖合并进一份 cfg,注入器与后台任务闭包持有那份快照,此后运行中的实例不再重读(`index.js` 装配处的注释早就写明「合并结果只作用于本次启动」)。这是有意为之的设计,不是 bug;**缺的是提示**:面板上「已改」徽标亮起只说明值进了库,不说明运行中用上了它。实测代价很具体——人格总闸在面板上显示已开启、人格段却整整一轮没注入,排查到最后才发现唯一变量是那份冻结 cfg 里 `personaEnabled` 仍是 `false`,把插件关开一次就好了;而在此之前,用户能拿到的唯一反馈是「怎么没用」。修法分两层,**后端只做比较、前端只做显示**:`featureSnapshot()` 新增 `pendingRestart`,列出「kv 里有显式覆盖、且该覆盖与冻结值不同」的键(嵌套点号键走既有 `NESTED_FLAG_PATHS`,比较用 `Object.is` 而不是 `!==`——NaN 与 ±0 上两者结论相反,开关值里不该出现它们,但「值是否相同」的语义要对齐);面板把 `flagTag` 从两态扩成三态(待重启 / 已改 / 默认)并在卡片顶部加一条提示行,写明「重启 DSH,或在插件管理里把本插件关开一次」。**为什么必须由后端算**:前端手上只有 `overrides`,它能回答「改没改」,回答不了「用没用上」——那需要另一份冻结值,前端拿不到。`effective` 也反推不出待重启,它按「overrides 优先」取值,恰好在有覆盖时把落差盖住了,所以只能是新字段而不是新语义。`config` 缺席时(旧调用方直连、未传 cfg)返回空数组而不编造,`pendingRestart` 恒为数组、恒不为 `undefined`(面板按长度渲染,字段缺失会渲染出 `undefined.length`)。既有 `overrides` / `effective` 语义一字未动,回归锁四条:值不同→列出、值相同→不列、无覆盖→不列、无 cfg→空数组。 - **作用域级可逆闸门:让一路记忆能被单独按下与松开,而不必改配置重启(#471,默认关)**:此前的开关全是**进程级**——想停掉某一路(比如「别再把工作区 A 的记忆注进来」)只有两条路,改全局配置(一起停,误伤别的路)或逐条 `memory_forget`(太细,几百条要手工捞,还会漏掉中途新写的行)。缺的不是开关数量,而是**另一根轴**:本仓早就有完整的事实轴(`memories.agent_scope` / `workspace_scope` 与 `scope_changes` 审计),却没有任何**可逆政策轴**——一个「人给的、可撤回的、存续到下次开工的」暂停状态,它与标注正交:按下不改任何一条记忆的标注,松开也不需要把标注改回来。实现是 `scope_state` 表(`dimension` × `scope_value` 唯一索引 + `state` 二值 `active`/`paused` + `note`),配 `src/scope-state.js` 承载判定,闸门挂在 `gateByScope` 旁并**前置**于既有 `strictScope` 硬过滤——于是两条轴串联而不是互相替代:先按人给的政策把整路按下,再按标注做可见性过滤。**重复按下同一状态不刷新 `decided_at`**(用 `ON CONFLICT ... DO UPDATE SET decided_at = CASE WHEN scope_state.state = excluded.state THEN scope_state.decided_at ELSE excluded.decided_at END` 一条语句表达):「什么时候真的变了」比「最后一次谁点了按钮」更接近审阅时想知道的事实,反复点同一个按钮不该把它变成刚发生的事。覆盖范围刻意与既有的四处 `gateByScope`/硬过滤落点对齐(三条检索路 + 注入候选池),**并单独接了巩固与睡眠两侧的全部读入口**(日间:活跃库尺寸、叙述条 inputs、巩固 `runDream`、**总览 `summaryAll`**;夜间:冲突消解、归档降级、模式发现、关系补全四个相位)——只改检索与注入会让整理与叙述仍读到被按下的行,那等于闸门只关了一半。**睡眠侧尤其要紧,因为那几个相位不只读**:`phaseDemotion` 会 `setArchived` 与 `demoteToSummary`(真改归档面),`phasePatterns` 会铸出 `type=pattern` 的新行——闸门开着而每 6 小时一次的睡眠照样在被按下的作用域上动手,「按下」就只等于让它在白天看不见。两处位置纪律也写进了注释:降级的过滤放在循环之外而不是循环体里 `continue`(一旦将来有人在循环体里加副作用,漏掉那条 `continue` 就是静默改到被按下的行),模式发现的过滤排在 `.slice(0, limit)` 之前(否则被按下的行照样占掉名额,它们既不会被扫、也不该挤掉别人)。**总览那一处是唯一「必须在源头挡」的**:总览落库走 `saveWithDedupe` 且不带 `agent_scope`/`workspace_scope`,它是**无作用域**的 summary 行,而 `gateInjection` 只拦匹配暂停作用域的行——没有标注的行永远不会被它拦下。于是被按下的内容一旦进了总览就变成「洗干净的全局行」,此后一直注入;别的入口漏一次只漏那一条,这里漏了是每轮洗一遍。回归用例把这条写成了可执行断言:总览落库后其 `workspace_scope` 恒为 `null`,将来若有人把总览改成带作用域的,它会红。两级语义沿用 #254:`enabled` 开判定、`enforce` 开拦截,`enforce` 关时只留审计行而注入照常(线上回滚不必改配置重启)。`set_scope_state` 是唯一新工具,回执**如实回显库里当前的 `decided_at`** 而不是回显入参——重复按下时这两个值不同,回显入参会让界面显示一个从未落盘的时间。默认全关时输出与改动前**逐字节一致**,且有回归用例锁住这条。 ## 🐛 修复 - **`--flag` 的值以 `--` 开头时被当成新旗标吞掉:一次 CI 红背后的真实 CLI 缺陷**:崩在 `test (windows-latest, 22)` 的那条用例是 `reclaim 默认 dry-run 报数字`,报错却是「未配置访问 token」——命令自身没问题,问题在 token 的形状。token 由 `randomBytes(24).toString("base64url")` 生成,而 base64url 的字母表**含 `-`**,于是约 1/4096 的 token 前两位都是 `-`,拼进命令行就成了 `--UYoOHxr6…`;旧 `parseArgv` 只要看到下一个词以 `--` 开头就当上一个旗标没有值(`token = true`),`flagValue` 再按「`true` 等于没给」回退 `DSH_MNEME_TOKEN` 与 `~/.dsh-mneme/cli.json`,两者都空 → 报未配置 token。**这是随机性,不是测试写法**:同一个缺陷平时一律绿,只在 token 恰好掷中这 1/4096 时才现形,因此它潜伏了整整一个发布周期而没有任何检查会红。影响面也不止 token:`--type` / `--title` / `--content` 只要值以 `--` 开头就同样坏,而这三个值完全由用户输入。修法是把「像不像旗标」从字符串前缀改成**登记表**:新增 `KNOWN_FLAGS` 收录全部取值旗标与布尔旗标,`flagKeyOf()` 认 `--foo` / `--foo=bar` 两种写法取键名,只有**已登记**的词才算旗标,其余一律当上一个旗标的值——`--limit --json` 这类旧语义(`limit` 无值、`json` 为布尔)分毫不动。选登记表而不是加引号规则:引号要经过 shell 才生效、PowerShell 与 cmd 的转义还不一样,而登记表在进程内就能判定。回归锁六条,最关键的两条覆盖 CI 现场(断言 `flags` 的键恰好是 `["token","url"]`,token 尾巴不得成为旗标)与「未知 `--` 词按值处理」;另有一条文本锁扫 `cli.mjs` 里所有 `flagValue`/`flagBool` 调用的字面量键名,要求全部已登记——漏登记的后果正是这里修的静默吞值,所以让它变成一条会红的测试而不是等下一个 1/4096。顺带补 `invokedDirectly` 守卫(`realpathSync` 双侧比较,因全局安装走 `.bin` 符号链接,字符串相等会误判),使 `cli.mjs` 可被测试 import 而不触发 `main()`;在旧实现上撤掉修复,这两条会变红,恢复后 6/6 绿。 - **`dreamMaxTokens` 默认 131072 撞服务商硬上限,巩固 180 次失败 0 次成功(#478)**:`dreamMaxTokens` 的默认值被**原样当作 `max_tokens` 发出去**,而很多 OpenAI 兼容网关的上限小得多(实测报错是 `max_tokens 不能超过 65536`)。请求在服务商侧就被 400 拒掉,连推理都没开始——`dream_runs` 在 2026-10-10 ~ 10-18 八天里 `dream_consolidate` / `dream_summarize` / `dream_narrative` 三个相位合计 **180 次失败、0 次成功**,而用 `sleepMaxTokens` 8192 的睡眠相位和硬编码 4096 的实体抽取一直是好的,分界线就是「预算有没有超过 65536」。两处都修:**面板能改了**(`dreamMaxTokens` / `sleepMaxTokens` 两个键其实早在 `src/settings.js` 的白名单里,是前端 `FEATURE_NUMBERS` 只列了一项,导致 UI 根本没渲染——这类「后端支持、前端没接」的缺口不会报错,只会让用户以为改不了),**并且能自己降下来**:`withMaxTokensFallback` 照 `withEffortFallback` 的同构写法,只在 400 且错误信息里认得出显式上限时降级重试一次(`不能超过|不得超过|至多|at most|maximum value is|<=` 后跟数字才认,认不出就返回 `null` 不猜),`AbortError` 直通、`cap >= 当前值` 时终止不循环。**降级不是猜数字**:把服务商自己声明的上限当依据,比我们闭着眼睛写一个「安全值」诚实。接线锁是本次的重点——`dream.js` 三个使用点和 `sleep.js` 两个相位必须全部走 `withMaxTokensFallback`,任何一处退回 `maxTokens: config.xxxMaxTokens` 直发都会让面板承诺的「超上限自动降级」变成假话,所以测试直接读源码断言「没有这个模式」并数接入点个数(3 + 2);变异验证:把睡眠侧改回直发即红,还原 8/8 绿。 - **人格卡删除 × 永远不可见(v2 修法)**:卡标签的 × 复用了规则删除按钮的 `mneme-set-del` 样式,其 `opacity:0` 只在 `.mneme-set-row:hover` 下解除,而卡标签不在该悬停容器里,导致 × 永远透明——功能在、入口不可见(v1.2 实测踩中)。#457 曾以内联 `opacity:1` 覆写修复,但悬停变红仍走 `.mneme-set-del:hover`,连带常点亮了规则区的删除按钮,被 #459 回滚。本修复改用独立类 `mneme-persona-del`(常显、悬停变红),不触碰 `mneme-set-del`,规则区行为分毫不动;回归锁直接钉死卡删除按钮的 `className` 值,防 #457 式修法回归。 ## 📚 文档 - **README 不再把「离线」写成默认行为**:两份 README 合计 12 处「默认全离线 / offline by default / 默认零网络依赖」与代码默认值相反——`autoDream` 与 `autoSummarize` 默认开(`src/config.js`),两者都把记忆正文交给 `ctx.llm.stream(...)`(`src/dream.js` / `src/summarize.js`),`embedProvider` 默认仍是 `openai`,只有显式改成 `local`(ONNX)才真正离线。改法:定位句改成「记忆只落在本机」,语义检索改成「把 `embedProvider` 设为 `local` 后可以完全离线」,隐私节补一条出站事实(巩固与摘要走宿主模型)与一条「想连巩固也离线就换本地模型或关掉两个开关」;`dsh-mneme/README.md` 的「完全离线的语义记忆引擎」改成「可以完全离线运行的语义记忆引擎」。新增 `test/readme-claims.test.js` 两条守卫:绝对离线声明不许回来、隐私节必须写出「巩固走宿主模型」——删掉假话却什么都不说,误导是一样的。GitHub About 同步换成不声称离线、补上四路召回与 MCP/CLI 的版本。 - **评审揪出的两处「说了但没说全」(PR #476)**:第一,隐私节的离线条件只写了「把宿主换成本地模型」——但 `dreamProvider` / `dreamModel` 与 `summarizeProvider` / `summarizeModel` 是**配置优先于宿主默认模型**的显式覆盖(`src/config.js` 与 `src/dream.js` 的 `resolveRoute`,Issue #25 的 config-first 排序),配了这两组就直连那家 provider,换宿主根本不生效;现已把两组键名一并点名,中英各一处。第二,守卫测试本身在 packed 包里会崩:`files` 清单把整个 `test/` 发出去,而包里没有仓库根 `README.md`,`readFileSync` 在**加载阶段**就 ENOENT,把「这不是仓库」报成「断言失败」。同一毛病既有的 `security-doc` / `release-docs` / `workflow-guards` 三条测试都有,本 PR 只是让它多一个入口。修法不改 `files`(那会动发布内容、且只修一条留着三条),改为根 README 不存在时整组 `skip` 并写明原因——实测重新 `npm pack` 解包后 2 条 skipped / 0 fail。 - **自动蒸馏写入的行 100% 拿不到向量(PR #477)**:`scheduleEmbed` 首行 `if (txDepth > 0) return;` 把事务内写入的嵌入直接丢弃,旁边注释写的是「deferred to the transaction's commit」,可 `transaction()` 的 COMMIT 分支从来没有补跑——**注释承诺的行为从未存在**。而自动蒸馏的每一笔写入都走 `service.transaction()`,所以 `session:*` 这些行一条都没拿到向量,「语义检索」对它们是一直静默失效的。这是 #372 的同模板 bug:实体抽取当年查出来入队补跑了,嵌入漏了。修法照 #372 先例——事务内改为入队(`pendingEmbed`),`transaction()` 提交且 `txDepth` 归零后 `drainPendingEmbeds()` 补跑,回滚时按 `embedBaseline` 截断回本层基线(绝不给已回滚的行嵌向量);队列不设上限,因为 `cap` 丢最旧等于把「静默丢弃」换个量级带回来。顺带修 `searchVector`:维度不匹配的旧向量(换过嵌入模型)以前会喂进 `cosine` 拿假 0 分混进结果尾部,现在按维度跳过。回归锁 5 条(`test/distill-tx-embed-gap.test.js`),在未修复代码上 4/5 变红。 ## [0.9.1] - 2026-10-18 ## 🆕 新增 - **M.O. 从「看得见」走到「跑得出账」:运行账落在项目里,执行窗口由人显式打开(beta)**:v0.9.0 的 M.O. 只有一张图和候选,没有任何一条「跑过」的证据——读侧早就能把执行情况算成四态,可账本一直没人写,`runs` 恒为空数组、所有节点恒为 `candidate`,跳过与绕开的退休提议永远发不出来。这一版补上写侧:`src/pipeline-runs.js` 把一次 run 落到 `<项目>/.mneme/pipelines/.runs.jsonl`,与手写的 `.json` 分开,一次 run 不会冲掉人的 diff。**开窗必须由人的显式动作划**:工作流的 `trigger` 是人话,拿它去匹配用户消息只能猜,而猜错的代价不是少记一笔,是把一条从没跑过的流程记成跑过、还把节点染成可信——所以窗口边界是 `/mo-run start ` 与 `/mo-run stop`,窗口之外发生的一切一个字都不记,漏记可接受、编造不可。四个取舍都有代价意识:窗口按会话开、一个会话同时只允许一个(再开会被拒而不是顶掉,顶掉会让前一次执行悄无声息地消失);事件先缓冲到 stop 才配对(工具的结果可能比调用晚很久,边到边配会把「还没等到结果」误判成「没有这个调用」);只在显式 stop 结账、不按回合关(一条流程常跨多个回合,按回合关会切成几条半截账);热路径永不抛(挂的是宿主会话事件通道,可选插件的可选能力把 agent 一步带崩,比丢一次抢救严重得多)。**「账本坏了」与「从没跑过」严格分开**:逐行收,好行照收、坏行进 `problems`(带行号、标出是否只是一截尾巴),状态降 `partial`/`broken`,并撤掉坏账本的 500——它会让客户端当成离线,还吞掉读得出来的那张图。账本目录自带 `.gitignore` 也是为这件事:`.runs.jsonl` 一旦被提交,两条并行车道先后追加同一份账本就会产生冲突标记,而那些标记不是合法 JSON,一份流水污染会升级成工作流状态永久消失。独立旗标 `actionRunEnabled` 默认关且进 `LIGHT_MODE_OFF`——**看图与写账是两件事**,混成一个键会让只想看图的人不知不觉获得写入能力。 - **会话事件翻成观察:只有跑在命令执行器里的调用才算工作流的一步**:接线前的实测就把原方案否掉了——400 个会话里工具名是 `pwsh`(1399)/ `read`(1709)/ `grep`(1040)/ `glob`(183),而工作流里人写的 `tool` 是 `npm test`、`git push ` 这类**命令行**,两者不重叠,直接拿 name 去比一条都对不上;唯一能对上的是「命令跑在哪个工具里」,`pwsh` 的 `arguments.command` 才是那条命令行。三条纪律都是「不许编造」的具体形态:**非命令执行器一律不产观察**(`read`/`write`/`glob`/`grep` 是 agent 的日常动作,硬把文件路径拼成「命令行」去匹配只会制造噪声,而噪声的代价是把某一步凭空染色);**只有拿到结果的调用才产观察**(配对靠 `callId`,「调了但没结果」既不是成功也不是失败,拿它当一次成功使用是编造,如实进 `dangling`);**成败是三态**(拿不到就留 `null`,把「不知道」折叠成「失败」会给这一步打上不该有的 `refuted` 标记)。 - **归属规则只出候选,绝不下结论**:`tool` 字段是人手写的自由文本——单命令、多命令并列、带占位符,还有 4 步是空的,拿这种文本做精确匹配注定命中率不高,而**误匹配的代价远大于漏匹配**(把「推 tag」记成「推分支」,`deriveState` 就会把错误的那一步染成可信)。所以这一层只产「候选 + 理由 + 三种匹配强度(`exact` / `weak` / 认不出来)」,**认不出来是合法答案**。弱匹配用**词前缀**而不是「第一个词相同」:真实工作流里 `git` 出现 5 次,按首词匹配会让一次 `git push` 报出六条候选,噪声盖过信号;压到 2–3 条之后人才愿意看。报告分三桶(有候选 / 认不出来 / 拼不出命令行),并把**结构性无法归属的步骤**单列出来——那是「为什么这一步永远没账」的答案。ponytail 复核随即砍掉四块替不存在的写者预留的代码(整块的 `attributionReport`、`at`/`actor` 两个没人读的字段、`SEPARATOR_RE` 的三个从未出现的分支、`PLACEHOLDER_RE` 的两种从未出现的形态),模块 189 → 165 行。 - **P1/P2/P3 三级落地:从「回执列出干了什么」到「人确认才写账」再到「观察旁挂与脱节检出」**:三条实测把原方案一步步逼掉——单条命令匹配手写步骤表,唯一归属仅 **14.2%**(非发版会话 4.4%,区分力只有 3 倍);整段命令当分组键每会话中位 539 组、最多 7373 组,人扫不过来;换动词当键,密度最高的是 PowerShell 管道语法,说明**「命令 ≈ 动作」这个预设本身是错的**;只留业务命令(git/gh/npm/node/docker)后业务段占 **6.5%**,那才是人能扫的量级。于是 P1 只报「本次观察到哪些业务动作、各几次」由人对账(`stop` 在没归属到步骤时不再只说「没有落盘」,而是同时给出观察清单——清单才是这一版真正的交付物);P2 把写账从 `stop` 挪到 `/mo-run confirm`——**「没确认」≠「没发生」**(人没提到的步骤什么都不写,若把「忘了勾」记成「被跳过」,`RETIRE_AFTER_SKIPS` 就会据此发出错误的删步提议),`unsure` 与 `notDone` 必须分开,「全部没做」也是一条合法且完整的记录;P3 把两样都有的东西对一遍——**人说这步做了 + 声明的 tool 本次一次都没出现**(实测背书:`gh pr merge --rebase` 在 **21723** 次调用里出现 0 次),系统只把矛盾摆出来让人判,**不改步骤表**。观察走旁挂 `/observations/<项目键>/.jsonl` 而不是给账本加字段——账本是 v0.9.0 发出去的数据契约,为 P3 加字段是动已发布契约。**左右各一道隐私闸**:观察行只存 `{key, count, flags?}`,形状闸**验原始输入再构造**,任何带 `command`/`args`/`segments` 的输入大声拒(静默剥离会把上游的形状 bug 藏进一次成功写入);P1 那边同样只保留动词/子命令/旗标、位置参数一律丢弃——回执会显示在界面上、可能被截图,而完整命令行里几乎必然含绝对路径。端到端实测当场抓出四个真 bug(脚本路径被当子命令、`git -C log` 把值当子命令、带点的文件名通过校验、旗标被重复渲染),全部属于同一条红线。 - **步骤新增 `assert` 与 `optional`:机器能不能验完这一步,从此在数据里说得出口**:`assert` 不是普通字段,它是「可断言节点 vs 判断节点」这整个划分的**唯一载体**——没有它,「哪些步骤能被机器自动盯着」在代码里就没有地方表达(评测报告的实测是 0/19 步,一条流程全靠人判,而那个事实当时没处可写)。加它要一次改四处(校验 / 归一化 / 节点投影 / 首屏计数),**漏任何一处症状都是「加了但面板看不见」且不报错**,所以先装一道字段白名单锁再改:往步骤里塞 `assert`/`onFailure`/`retries`,三个全部静默消失,正是这类「声称改了但产物里查不到」的同型装置。两个语义决定:没写断言时投影成 `null` 而不是空数组(后者会让「作者没写」与「写了但一条都没通过」长得一样,而验收时这是两件事);首屏新增 `assertable` 计数与既有 `contrast` 并列(后者数「写了没有」、内容仍靠人判,前者数「机器能不能判」,0/19 是实话不是缺陷,但清单上必须看得见)。`optional` 缺省 `false`(=必经),**显式落键**而非缺省缺键——「这步可不可跳」是判据要读的语义,读侧拿到的一律是布尔;存量步骤表零改动。 - **R1/R2:跳过有了性质,前置违规与 tool 过时都有了提议**:同一句「这一步总被跳」,在**可选**步骤上是不该提议删的(只发 `optional-skip-ok` 信息项),在**必经**步骤上则同时意味着「删掉这一步」与「这条路不对」两种读法,**两种提议并存、谁也不吞谁**——第一版把 `drop-step` 挂在 `else if` 里永远到不了,被自己的测试抓出来。R1 的前置违规跨整本账本**逐 run** 查(保住 run 边界,`tallyRuns` 已经把 run 抹掉了),按 `run|step|need` 去重、同一步对不同前置各报一条(修图的人要知道断在哪条边上),一次就报。`retirementProposals` 的封闭问题表从两类**穷举到五类**——新类不配问法会落进「岔路」那条毫不相关的问法里。 - **沉淀链:从真实会话日志里把反复出现的手法挖成 draft 工作流,但判据不给结论**:`src/pipeline-nudge.js` 走「多帧 zstd 逐帧解 → 重复序列候选 → 能算的缺席对照 + 答不出的封闭问题 → `status:"draft"` 草稿(过 `validatePipeline`)」。`buildCounterfactual().verdict` **恒为 `null`**:机器只写观察到的,其余留「(待补)」,答案只能由人回答、经 `applyAnswers` 写进 `steps[].evidence[]`。专家团复核抓出七处,最贵的一条是「空回答不算已答」——`applyAnswers(c, {})` 与 `applyAnswers(c, null)` 曾返回 `ok:true` 并推进到 `answered`,那是判据链路上唯一一处「没人回答也算答了」;另有一条把「机器不给结论」从产者的自觉变成**边界的性质**:校验层拒绝任何非 `null` 的 `counterfactual.verdict`(缺键也拒)。其余是跨工作区不并账、`unanswered` 补回出口、草稿与提议改原子落盘(一个半截文件曾能让整个项目层的工作流从面板上消失)、按层过滤后的相邻不许当真相邻、`failures` 不再用 `null` 同时表示「未接线」与「确实没有失败记录」。 - **人格定义页 v1 → v1.2:从「一张卡」到「按 agent 寻址的多张卡」(beta,默认关)**:v1 是存储/API/开关/注入/面板五件套(`user_settings` 加 `persona` 键、`PERSONA_FIELDS` 白名单挑拣、坏 JSON 容错回空对象、`GET`+`PUT /persona` 双面路由、`inject.js` 里人格段前置而 `rules` 恒最硬、**任一闸关时输出逐字节等于历史版本**)。v1.2 按实测三反馈升级:设置键变成 `{default, byAgent}` 多卡形,**v1 的单对象读作 `default`、零迁移**;注入按会话身份(`session.header.agentPreset`,与 `scope.js` 同源读法)寻址对应 agent 的卡,身份缺失/无卡/卡关一律落 `default`;面板改多卡列表,补三个字段的可见标签(v1.1 反馈:`aria-label` 看不见)与保存失败可见错误行(v1 的静默 `catch` 吞掉了 404,实测踩中)。**一次修好又回滚的教训**:卡标签删除的 `×` 复用了规则卡的 `mneme-set-del` 样式,而那个类只在 `.mneme-set-row:hover` 下解除 `opacity:0`,卡标签不在该悬停容器里——功能在、UI 永不可达;修复(内联 `opacity:1`)后又因同一原因整体回滚(#459),留作已知问题。占位示例也从使用者自己的两个人设名换成了中性名。 ## 🐛 修复 - **面板的检查更新把「成功」报成了「失败」,星星入口指向了「谁已经点过星」**(#460):面板拿 `updateInfo.status` 去比一组后端 `classify()` 根本不会返回的名字,于是成功的检查一律落进错误分支。顺手补一道跨文件契约锁**双向**钉住(面板比较的每个状态必须是 `classify` 能返回的值,取数路径必须对得上注册的路由),并把「重新注入错名字」作为判别验证——新锁会点名它。星星入口原先链到 `/stargazers`,那是「已经点过星的人」列表、页面上没有 Star 按钮,改为仓库主页。 - **向量的「有向量」不再是一句空话:写入回执、行级模型出处与模型感知的回填选择器**(#450):`embedFor`/`schedule` 返回结构化回执(`ok`/`failed`/`skipped` + 原因)且 `schedule` 不再吞掉 promise,失败进 200 条环形账本(不接 `llm_audit_logs`:高频回填会淹没 LLM 调用的追溯面)。`vector_meta.model_hash` 是「最后一次成功写入的模型」,换模型即被整表覆盖,**无法回答「这一条向量是谁产的」**——`memories` 新增 `embedding_model` 列做行级出处,存量一律 `NULL`(出身未知),**不按时间推断模型名**(猜出来的名字会被日后审计当作证据,而它不是);清空向量时连同出处一起清。回填与 `reindexMissing` 改走新选择器(空向量 OR 出身未知 OR 模型不匹配),换模型后旧向量才真正被重算,而不是永远占着「有向量」名额虚增覆盖率;`maxTotal=0` 的语义定为一灌到底,启动期回填的三个写死数(500/10/200)改成配置项,返回体让 `capped` 与 `drained` 不再不可区分。 - **账本的 ids 跨块引用被 `catch` 静默吞掉,`fix-step-tool` 永远少发而界面无异常**(CodeRabbit #458 四条里核实的三条):F1 账本 `ids` 原声明在 `if` 块内,观察旁挂那块跨块引用必 `ReferenceError`,被外层 `catch` 吞成「没有观察」——提升为 handler 级 `runIds`,回归测试走**真 handler + 真旁挂文件**锁住「观察进了判据」这条正向信号(负向断言抓不到它,因为吞掉之后一切看起来都正常)。F2 `config.memoryDir` 默认值是 `~/.dsh/memory`,而 `observationDir` 不展开 `~`,`readdir` 永远落空,补 `expandHome` 与 `candidates.js` 同构。F3 `rework-path` 的两种成因(必经步骤被跳 / 被绕开成常态)共用一句问法,跳过场景答非所问,改为提议带 `cause` 字段、`nudge` 按成因选封闭问法、旧形状退回原问法。 - **`candidates.js` 的每一个换行都是「双 CR + LF」,从出生一路进了 main 又发到了 npm**:各 454 处。`\r` 在 JS 里是合法空白,解析、加载、1869 条测试全绿——**当时没有任何检查会红**。代价落在别处:任何改动这个文件的人 diff 都是整文件 908/908,噪声盖住真改动;同一份文件不同工具数出行数不同(`Get-Content` 908 行、编辑器 454 行);发布产物里带着畸形行尾。只动行尾、一个字符不碰(字节差 454 恰等于处数,剥掉全部 CR 后与 main 原版**逐字节相同**),并补一道**扫字节序列本身**的锁:既有的 `lib-smoke` 字节一致性锁抓不到它,那个锁只比 src↔lib **彼此**是否一致,两边一起坏就一路放行;新锁直接查这个序列并报出文件与处数(注入 534 处双 CR 后精确变红、还原回绿)。 - **桌面版装的是 0.26.1,落在 `dsh-better-sidebar` 的 `^0.18.0` 之外**:peer 是 optional 且消费端全防御式(服务注入 + `typeof` 检查,issue #88 模式),范围只反映生态兼容面而非 API 锁,故放宽上界到 `<1` 而不是逐版追加。 - **合并人格页后 `effective` 键计数锁少了一键**:冲突时 git 把两边的加法串自动拼上,但只体现了一个 `+1`——实际上两边各加一键(`actionRunEnabled` 与 `personaEnabled`),应为 99。顺手把那条 26 项加法串收成一个字面量:那串每合并一次就要重算一遍,拼得越长越没人验算,换成 99 配一行注释反而可查。同步重新生成 `lib/`(合并时 `lib/config.js` 与 `lib/settings.js` 的冲突标记是脏的,`sync` 后清零)。 ## 🎨 面板 - **记忆页三栏改两栏、实体页去冗余、热力图改成按窗口分位染色**(#448,三轮复检):记忆页左栏加宽到 264px 让四个重要性 chip 排成一行,重要性块跟在检索区之后而不是钉在底部;实体页搜索与计数吸顶、按类型分组的侧栏、画布 320 → 440、补「没有匹配」提示;设置页去掉自动弹出的版本横幅,改手动检查更新按钮(带行内结果)+ 星星入口 + 版本页脚,并把 M.O. 开关提到首屏(首屏预算 8 → 9,注释同步改);实体侧栏补 `box-sizing: border-box`——`width:100%` 加水平内边距会把 224px 的栏撑破,`overflow-x: hidden` 再把「×N」提及数裁掉。热力图撤掉写死的阈值(750k/1.5M/2.5M)改用**每窗口分位**分桶,低使用量的库也能铺满四档、空窗口降为 0 档,网格收到 GitHub 贡献图口径(11px 格 / 2px 间距 / 月份标签 / 一三五标注)。顺带补了一道**反向** i18n 锁:既有的「每键恰好两行」计数锁看不见「零行」的键(它从计数里消失了),而 `Icon` 遇到未注册名字返回 `null`,于是新增「每个字面量 `t()` 都得有词条、每个传给 `Icon` 的名字都得已注册」的锁(73 → 74),并做过「临时删掉某键两行词典 → 旧锁仍绿、新锁点名」的判别验证。 ## 📚 文档 - **门面加「最近更新」、补四种安装来源、修掉一圈文档漂移**(#466):根 README 新增 News 表(日期 | 版本 | 头条)与**四种安装来源**对照(npm 包名 / Git / tarball / 本地绝对路径),并写明 `#path:` 不可省的理由——省掉装到的是仓库根那份壳(`0.0.0`、无 `dsh` 字段),宿主不会把它当插件;`github:slow-stack/mneme` 这种写法实测装出来就是个 `_pnpmPlaceholder` 空壳,正确写法是 `#path:/dsh-mneme`。同时补 M.O. 章节、补路线图 v0.9 行、删掉两个 README 里早已不存在的幽灵键 `sessionLifecycleEnabled`(`src/` 全无命中,v0.7.11 已清理)。子 README 的「模型工具(9 个)」改成 10 个(漏了 `memory_register_document`,而同一份文档的架构图里早就写着 10——**自相矛盾比写错更容易被忽略**)、「9 种记忆类型」改成 10 种(补 `document` 指针,issue #230)、设计文档 5 篇 → 11 项。 - **`SECURITY.md` 支持版本表走到 0.9.x,并删掉一个不存在的安全供应商**(#467):英中两张支持版本表同步到 `0.9.x`,停止维护通知改成「尽力维护」而不是写死某个版本会终止;功能描述里写死的版本号(`Standalone External API (v0.7.12, opt-in)`)全部去掉——**功能描述不该绑版本**。删掉 `OrbisAI Security` 六处:仓库里没有任何其他痕迹,它是 v0.7.1 一笔提交带进来的**幽灵供应商**,换成 CI 实况(gitleaks 全历史 + dependency-review 看 PR diff + OSV-Scanner 看 lockfile + Dependabot 告警 + secret scanning)。中文段此前整体落后英文段一个版本,这次一并补齐——包括「未配置 token 时远程端点实际处于禁用状态」这句,它正好是 fail-open 真相的反面。配套新增 `test/security-doc.test.js` 六条断言,其中两条把「开放读清单 / 受门控清单」与 `createApi` **真实装配出的 `route.policy` 双向比对**——写进文档的路由清单从此不再可能悄悄漂移。 ## 🔐 安全 - **外部访问 API 的鉴权从「每个 handler 自己记得写」变成「路由注册的必填字段」(#462 / #464)**:审计前的实际形状是 39 条路由里 20 条有 `requireAuth`,其中 6 条只写了写方法、读方法裸奔;`GET /export`(导出整库,含 `archived` / `forgotten`)与 `GET /external-api`(明文回吐 Bearer,且会在**未鉴权**的请求里就地铸造 token)两条最贵重的路由都没有门。根因不是「忘了写」,而是**漏写不会响**:鉴权写在 handler 内部,少写一行的表现是「功能正常」。改法是把鉴权搬到结构上——新增 `export const ROUTE_POLICIES = Object.freeze(["open","token","token-write"])` 与 `export function createRouteRegistrar(webServer, apiToken, disposers)`,每条路由注册时必须声明 policy,**漏声明或写错值 → `createApi` 装配期直接抛**,而不是运行时静默放行;`token-write` 的语义是「只有非 GET/HEAD 才要 token」。39 条逐条归档为 open 19 / token-write 6 / token 14,23 处手写 `requireAuth` 与函数本体一并删除(grep 归零),`routes: 36` 这个会腐化的字面量改成 `count()`。**唯一行为差异**:少数「方法不对 + 未鉴权」的请求由 404 变 401(先判门再进 handler),方向是收紧。回归锁 6 条进新增的 `test/route-policy.test.js`(漏声明必抛 / 重复 `kind:path` 必抛 / 三种 policy 语义 / 装配后全部合法 / `count()` 与挂载数一致)。**这组锁替代了原计划的权限矩阵测试**:矩阵要抓的「新增路由忘了加鉴权」在结构上已经写不出来,测试只需锁住「结构本身还在」。 - **请求体上限从「没有」改成 4 MiB,超限回 413 而不是静默截断(#462 评审跟进)**:`readBody` 原先无上限,且超限时返回**空串**——`parseBody` 把空串解析成 `{}`,而 `PUT /profile`、`/rules`、`/persona` 把「字段缺失」当成「用户要清空」,于是一个 5 MiB 的请求可以**静默清空画像 / 规则 / 人格**。现在超限 `resolve(null)` 并由 21 处调用点统一早退回 413;错误分支同时改成 `resolve(overflow ? null : "")`,避免 413 之后又被空串抹掉状态、让调用点拿空串继续写。两条既有测试原先是空断言(旧超限用例只断言 `notEqual(200)`,而 `/create` 成功返回 201),改为明确断言 413 + 库里没新增 + 画像/规则原样保留。 - **供应链三处收紧**:`security.yml` 的 OSV 扫描由 `fail-on-vuln: false` 改为 `true`(有漏洞即红,不再只报告);`release.yml` 的 npm 发布加 `id-token: write` 与 `--provenance`(发布产物带可验证来源);仓库侧打开 secret scanning、push protection 与 Dependabot security updates。`main` 的保护规则 required checks 追加 gitleaks 与 dependency-review 两条(`enforce_admins` 仍为 false,这是刻意留的:紧急修复通道要留着)。 - **secret scanning 告警 #1 结案:不是泄露,是语料漏改,修法给语料加锁(#465)**:告警说 `dsh-mneme/test/helpers/write-admission-samples.js:76` 有明文 OpenAI Key,实际是刻意构造的形状样本(`sk-proj-` + 26 个顺序字母 + 10 个顺序数字 + `ABCD`)。该文件里 GitHub PAT / Slack / JWT / Stripe 四条凭据早已用 `assemble()` 拆成两段拼接(注释写明理由:push protection 会以 GH013 拒推、gitleaks 全历史扫描会红),唯独第五条漏了——**「逐条记得改」本身不可靠**。改为同样走 `assemble()`,并新增一条守卫用例:扫 `test/helpers/` 下所有 `.js`,任何完整形状的凭据字面量都会让测试变红。守卫的规则前缀与生产 `src/sensitive-scan.js` 对齐(`ghp|gho|ghu|ghs|ghr` / `xox[abprs]-` / `sk-(?:proj-)?`),并加**同源反查**:每条规则给一个探针,断言探针同时被守卫自己的正则与生产 `scanSensitive` 命中——只查一边的话,任一侧收窄都不会红,正是这次漏掉的失效模式。双向变异验证过(守卫去 `ghs` → 红;生产去 `ghs` → 红)。 ## 🧰 发版与文档 - **发版准备改成「机械的归机器、判断的归人」(Release Prep 工作流重做)**:原先的 `scripts/release-prep.mjs` 只覆盖版本号、测试数与 CHANGELOG 占位节,而它对两个 README 的版本表**有一半是静默失效的**——根 README 的路线图与 News 表版本列写的是两段式 `**v0.9**`,旧正则只认三段式 `**v0.9.0**`,于是它打印「无版本表格行,跳过」,一个字节都没改。把「没做」说成了「不需要做」,比报错更坏:v0.9.0 那次发布准备(a9f8e35)只有 5 个文件变动,根 README 的路线图与 News 一字未动,就是这条路径留下的痕迹。现在版本表扫描拆成两段式与三段式两条独立正则(**分开认**是刻意的:给根 README 插 `**v0.9.1**` 会凭空造出一个不存在的小版本条目),`syncReadmeTables()` 同时做三件事——三段式版本历史表插占位行、两段式路线图插 minor 占位行、把遗留的「🚧 准备中」回填成已发布。**它刻意不把「✅ 已发布(v0.9.0)」改写成「(v0.9.1)」**:脚本跑在打 tag 之前,改了等于在 PR 里提前宣布一个还没发出去的版本,PR 一旦放弃就成了谎话。返回的是 actions 数组而不是布尔,因为「没插」有三种原因(没有版本表 / 已是最新 / 插了),入口要能如实说明;**actions 非空而文件没变则直接 exit 1**,脚本自己的 bug 不许静默发布。 - **`SECURITY.md` 的支持版本表只核对、不改写,落后时警告不中断**:跨 minor 发版(0.9 → 0.10)时整张表要向下挪一格并同步改停止维护通知,那是维护策略的判断——机器写容易写出「0.9.x 已停止支持」这种真事故。脚本只核对中英两张表的首行是否等于 `${major}.${minor}.x` 并写明期望值。**刻意不硬失败**:表先改则 `package.json` 还旧、测试红;版本号先改则表落后、测试红——两头都堵死,人就会绕过这套流程手工发版,那才是最坏结果。代价是那个 PR 的 CI 一开始就是红的,直到人把表挪完,而这正是 `test/security-doc.test.js` 该红的时候。 - **根 README 的 News 表明确划在自动化之外**:它是「日期 | 版本 | 头条」的编辑内容,脚本从不碰,改由工作流在 PR 正文与 commit message 里列进人写清单(连同 CHANGELOG 条目、占位行主题描述共三件)。`CONTRIBUTING.md` 的发布流程由 6 步改为 7 步:第 1 步跑工作流、第 2 步补人写内容、第 3 步跨 minor 挪 SECURITY 表、第 5 步合并后打 tag、第 6 步说明 tag 触发 `release.yml` 的完整链路(三方一致校验 → 测试矩阵 → `--provenance` + `publish-gate.mjs` → 从 CHANGELOG 抽正文建 Release),手工 `npm publish` 降级为第 7 步的「例外,不是流程」。工作流的 PR 正文改用 `--body-file`(多行中文塞进 `--body` 需要一串 `$'\n'` 转义,读的人看不懂、改的人不敢动)。回归锁 8 条进新增的 `dsh-mneme/test/release-docs.test.js`:两份真实 README 的版本表仍被认得出、两段式能插新 minor、三段式插完整 patch 号并带日期、已发布的行不被改写、插入不混入裸 LF、重复跑幂等、SECURITY 同 minor 通过跨 minor 报中英两条、节缺失或表格形状变了也必须报错。 - **Release 正文从「CHANGELOG 逐字搬运」改成「标题 + 一句导语 + 折叠细节」(新增 `dsh-mneme/scripts/release-notes.mjs`)**:此前 `release.yml` 用内联 `node -e` 把 CHANGELOG 该节原样写进 Release 说明,于是 v0.9.0 的 Release 页有 29921 字符、单条子弹最长 3318 字符——正文里全是决策理由与变异验证,读者要点开一屏才知道这条在说什么。抽取搬到独立模块并改成**分层渲染**:每条子弹保留粗体标题与一句导语(默认可见),其余正文折进 `
`,**CHANGELOG 本体一个字不动**(那是含决策理由与审计线索的完整记录,压缩只发生在对外渲染这一步)。导语切分规则全由真实语料逼出:句末标点只认 `。!?` 而**刻意不认半角句号**(版本号里的点是同一字符,认了会切在版本号中间);软标点断点必须**括号平衡**(v0.9.0 实测切出过「GET /external-api(明文回吐 Bearer,」这种把从句切断的收尾);导语不得短于 24 字符(有条首句只有 8 个字的「两处水源在说谎。」,离开上下文没有信息量);正文本身比上限还短时整段就是导语、不再加省略号(否则凭空说「后面还有」)。回归锁 17 条进新增的 `test/release-notes.test.js`(含真进程端到端:渲染后 CHANGELOG 字节不变、找不到小节必须非零退出且报出是哪一版),`test/release-workflow.test.js` 补一条「正文抽取必须调模块、不许退回内联脚本」。**两条评审意见都是真 bug,各补一条锁**:一是「短正文保护」原先守在 `lede()` 末尾、排在软断点搜索之后,短正文里只要有逗号就会被加省略号,逗号恰是末字时省略号后面什么都没有、折叠块是空的——这条保护必须最先执行;二是渲染单位错成了「行」,而 CHANGELOG 里有 **179 行缩进续行**(0.8.6 / 0.8.7 / 0.8.12 等老版本集中出现,另有 59 行缩进子列表),逐行 trim 会把续行甩到条目的 `
` 外面并丢掉缩进,读者看到的是「正文只有一句,剩下的莫名其妙散在下面」——改为先 `splitBlocks()` 切出「一条子弹 + 它的全部续行」再按**条目**渲染,且**导语只从条目首行取**(把续行拼回正文再切,换行会混进可见行,续行又被推回 details 外面)。变异验证三轮共十处全部变红(删短正文保护 / 去掉括号平衡 / 导语上限归零 / 软断点 `consumed` 少记一位 / details 与正文之间去掉空行 / 删入口守卫 / 删版本格式校验 / 短正文保护挪回末尾 / 退回逐行渲染 / 导语改从整段取)。 - **发版准备把新版本节插在了 `[Unreleased]` 之上,且把攒着的条目留在原地(第一次实跑暴露,随后按评审意见补两处)**:改造后的 Release Prep 工作流第一次在真实 main 上跑 v0.9.1,`gh pr create` 因组织策略失败(GitHub Actions 不允许开 PR,属合理默认,不改),但脚本已经跑完并推了分支——于是它的产出能被直接审。产出是错的,两个位置错误:新版本节被插在 `# Changelog` 之后、也就是 `## [Unreleased]` **之上**,得到「`[0.9.1]` 排在 `[Unreleased]` 前面」的颠倒顺序;`[Unreleased]` 里攒的 9 条条目**原地不动**,新节里只有一条「🐛 修复 /(待填)」。两处都有同一个历史痕迹作证:v0.9.0 那次发版(`a9f8e35`)之后靠一笔人工提交 `77df0fb`「v0.9.0 版本节补上内容——把 [Unreleased] 攒的 27 条搬进来」才掰回来——**机器算得对的事不该留给人补**。现在先切出 `[Unreleased]` 到下一个 `## [` 之间的正文:非空则整段搬进新版本节、`[Unreleased]` 留空;为空才插占位节。搬位置不搬内容,一个字不改,所以「新节里该写什么」仍然只由往 `[Unreleased]` 写条目的人做。**两条评审意见都是真问题,各补一条锁**:一是用 `moved`(`- ` 条目数)当「有没有搬」的判据是错的——正文非空但一条 `- ` 都没有时 `moved` 是 0,入口会打印「占位节(`[Unreleased]` 本来是空的)」,而它显然不空;改为独立字段 `movedContent` 决定日志分支、`moved` 只用来把话说得更具体。二是 `body.trim()` 会吃掉首行的前导空格,`[Unreleased]` 首行若是四空格缩进代码块就会降级成普通段落,与「原样搬」矛盾;改为只剥节边界空行(并以原文行尾拼回,免得 CRLF 文件退化成混合行尾)。回归测试 8 条 → 10 条。 - **发版准备工作流真正跑通:重跑不再撞分支,开 PR 失败会告诉人该去开哪个开关(两次实跑暴露的两个独立真因)**:2026-10-18 把 v0.9.1 在真实 main 上跑了两遍,**八步里前七步全成功,每次都卡在最后一步**,但两次的原因完全不同。①第一次(run 39123249758)报 `GitHub Actions is not permitted to create or approve pull requests`——工作流 job 里写着 `pull-requests: write`,可它只是声明式权限,真正的总开关在仓库设置(Settings → Actions → General → *Allow GitHub Actions to create and approve pull requests*),实测 `gh api repos/slow-stack/mneme/actions/permissions/workflow` 返回 `can_approve_pull_request_reviews: false`;更麻烦的是**这个开关在组织层默认关闭时仓库层根本改不动**(`gh api -X PUT` 直接 409 Conflict:*The organization does not allow GitHub Actions to create or approve pull requests*),所以除了给报错补上「去哪个页面、打开叫什么名字的开关」,代码这边能做的只有把失败说清楚。②第二次(run 39126751114)在**更早的位置**就挂了:`! [rejected] release-prep/0.9.1 -> release-prep/0.9.1 (fetch first)`——上一次运行已经把同名分支推上去了,这条工作流原本**不可重入**。不可重入的代价不是「重跑失败」这么轻:人会以为流程坏了,转去手工发版,整套自动化就此被绕开,而那正是这套流程想避免的结果。修法是把推分支那一步改成三档守卫:分支上有**进行中的 PR** → 什么都不动、静默退出(分支上可能已经有人补了 CHANGELOG 条目,那正是留给人的那部分工作,覆盖掉比不能重跑更糟);**没有 PR 且顶端是上次机器产出**(提交者是 `modusensus`、标题以 `chore: v<版本> 发布准备` 开头)→ 按重跑处理,用 `--force-with-lease=refs/heads/<分支>:` 覆盖(不是 `--force`:远端在我们 fetch 之后被推过就拒绝);**其余情况** → 停下并要人先确认那条分支。回归锁加在 `test/workflow-guards.test.js` 两条,五处变异验证(去掉 PR 查询 / `--force` 退回无条件 / 删掉「顶端不是机器产出就停」的判定 / 报错文案去开关名 / 报错文案去具体报错)全部变红;其中后两条第一次是 GREEN——因为对全文 grep 会被注释里解释这件事的文字骗过,改用 `codeOnly()` 剥注释后才真正锁住。 ## 📦 仓库结构 - **仓库根补一份「只是壳」的清单,让单仓多包探测器认得出可安装的子包(dshfind 显示「不可安装」的修复)**:本仓库一直是单仓多包——真正可发布的包在 `dsh-mneme/`,仓库根只有文档。这在 npm 与 `dsh plugin add` 眼里没有问题(用 `#path:` 或绝对路径指到子目录即可),但**依赖「仓库根有清单」的工具会判错**:dshfind.com 的探测器在 `probe-install.mjs:182` 返回 `no-manifest`,页面直接写「Not an installable plugin package」,而这句判词与实际能不能装无关。同类项目(包名恰好等于仓库名)不会被误判,我们的包名 `@modusensus/dsh-mneme` 与仓库名 `mneme` 不同,探测器无从推断,只能看根清单。**补的是一份刻意的空壳**:`private: true` + `version: "0.0.0"` + **不带 `dsh` 字段**(带了探测器会把仓库根当插件本体,装出来是一个没有 `lib/index.js` 的空包)+ **不带 `type` 字段**(带了会改变仓库根 `test/` 与 `scripts/` 下 CommonJS 的模块语义,`scripts/release-prep.mjs` 与 `test/mcp-package.test.js` 的运行方式随之变化)。子包声明写进 `pnpm-workspace.yaml` 而不是 `package.json` 的 `workspaces`:**npm 一看到根 `workspaces` 就把 prefix 提到仓库根、在仓库根找 lockfile**,于是 `dsh-mneme/` 里的 `npm ci` 报 EUSAGE 退出 1——`ci.yml`、`release-prep.yml`、`release.yml` 三条工作流全在子目录跑 `npm ci`,会一起炸掉;`pnpm-workspace.yaml` 只有 pnpm 与探测器读,npm 不读,零副作用。**同时改掉一圈因这件事而变假的表述**:两份 README 的安装章节原本写「子目录包、仓库根没有 `package.json`」,现在改说「单仓多包、根清单是仓库壳」;`CONTRIBUTING.md` 的仓库布局与发布红线、`AGENTS.md` 的仓库布局、`ci.yml` 跨包平价腿的注释、`release.yml` 的「勿把 link 指到仓库根」说明同步更新——**文档说「根目录没有 package.json」而代码里有,比没有文档更坏**。 ## 🧪 内部与测试 - **注释里的裸行号改成「符号名 + 日期」**:审计发现一批行号引用已经腐坏。判据刻意**不取**「本批改动使该行内容变了」——那会把早就腐坏的引用一起算进来;改为两个维度交叉:锚点法(把引用点名的符号写成正则,看它在基线里能否找到)与平移法(用 `git diff --unified=0` 算净位移),结论 3 / 18 / 0——本批推歪 3 条已改、既有腐坏 18 条只登记不改、反常 0 条。**单靠任一法都会判反**:早期探针只跑锚点法、还把锚点抠成了引用正文里的字样,于是把本批推歪的一条误判成「既有腐坏」。修法照既有约定:不留裸行号,写成「<文件> 的 <符号名>,YYYY-MM-DD 在 :<行号>」——只把数字改对是治标,下次再插一行又错,而注释里的行号不会有人来测。 - **零消费者的代码与「替未来预留」的字段一次性清掉**:`runner` 里赋值后立刻 `void` 的死代码、只出现在 import 行的导入、`confirm` 回执里三块零引用字段、`BUSINESS_VERBS` 从 24 个砍到实测命中过的 6 个(21723 次调用只出现 git/gh/npm/node,其余是「猜未来会用到」的预留)、`looksLikeSubcommand` 的五道 `if` 收成一个正则(字符集天然挡住旗标、含点文件名、`$(…)` 片段与超长值)。**顺带修掉一处隐私回流**:`hints[stepId]` 存的是完整命令行,而它会经提议流进命令回执——P1 的分桶层刚花力气把位置参数剥掉防路径泄露,这里等于从另一个口子把路径带回来,改成只存次数。实际净减 16 行(不是清单预估的 28,有几处加了说明注释抵消)——**「删了 28 行」这种账要按实际算,不然它就是下一轮审计里的假话**。 ## [0.9.0] - 2026-10-17 ## 🆕 新增 - **M.O. 给图留一个出口:绕行、候选分层与「跳过 ≠ 绕开」(beta)**:一个只会被照着走的图会让 agent 失去自主性——它要么**沉默地服从**(照着走,做错了也不知道错在哪),要么**沉默地偏离**(觉得不适用就跳了,不记账,没人知道这次执行了几成)。一条规矩如果没留出口,走不通的人不会遵守它,会自己凿一个**而且不告诉你**。所以图上必须有一扇合法的逃生门:agent 判断「这条路这次可能不对」时,能按风险档决定记账先走还是停下问人。分界是一句话:**护栏,不是轨道**——允许偏离,但偏离要响。**分档(2026-10-16 拍板 B 档)**:自报 R0/R1 **且**不跨闸门 **且**兜底清单没命中 → `decidedBy: "model"`,**记账先走**;其余 → `"human"`,**停下等点头**。一句话:**自主性住在 R0/R1,刚性只住在它必须住的地方。** 三条各自独立、都要过:① 风险档是自报的,单靠它不够;② **兜底清单**(`IRREVERSIBLE_PATTERNS`)机械匹配、**不看自报**——自报说 R0 但命令里有 `git push` 照样拦,这是「模型在自己上下文里给自己打分不可靠」的直接应用,不是不信任而是**不给单点判断**(清单可审计可 diff 可追责,让模型再判一次只是把同一个错误问两遍);③ **闸门保护**——这条绕行如果走下去、图上某个 `gate` 会永远不被经过就拦住,**与风险档无关**(闸门本来就放在「错了很难撤」的地方,绕开它等于把安全阀绕掉,哪怕全是只读动作)。**两条路由读的是两处不同的东西**:`/pipeline` 读定稿(住仓库、可提交可 review),`/candidates` 读候选(住 `/candidates/<项目键>/`、只在本地)。候选不进仓库是刻意的:它是草稿,一天可能长十条死九条,进仓库就是垃圾、也会被当成规程引用。**候选是定稿的唯一入口,定稿只放人亲手批过的东西**——这正是「一个会自己起草 SOP 的学徒」那句的字面意思。`<项目键>` 取「目录名 + 路径哈希前 8 位」:同一项目每次算到同一个键,但不把绝对路径写进文件名(那里面是本机用户名)。**升格两条都要满足**:走过两次(至少一次 `worked`——「复用第二次才算类型,第一次只是实例」)**且**写得出反事实;够格之后系统**提议**、不自己写进仓库(提交是 R1、推送是 R2,分开两档、人始终握着一道),`promoteCandidate()` 只返回内容不写盘,补出来的东西照样要过 `validatePipeline`——**升格不是降低标准,是补齐标准**。**怎么死**:只走过 1 次且 30 天没再被走到 → 归档(`dropped`,**文件留着**当负清单的邻居);`failed` 直接进负清单(走不通是知识);人否掉记理由。候选不需要人维护——它只增不减地长,死掉的是「没被再走到」的那些。**「跳过」与「绕开」是两个指标,这一版把它们分开了**:跳过 = 这次不用这一步(≥3 次 → 提议删掉**这一步**);绕开 = 这条路整个不对(≥3 次 → 提议重看**这条路径**)。混在一起数,两种提议就都发不出来。**绕行不给节点染色**——被绕开不等于这一步坏了(可能只是这次不需要),染色留给真正被推翻过的 `refuted`(唯一有硬判据的信号)。**四处只有实机才现形的坑**:① 兜底清单第一版把**工具名当成动作**,于是 `gh pr view`(只看一眼 PR)也被拦下,直接毁掉「自主性住在 R0/R1」——工具名 + **子命令**才是动作,条目因此拆成 `commands`/`requires`/`forbids` 三层;② `requires` 与 `forbids` 的语义不能混:**requires 表达「必须出现才算数」,forbids 表达「出现就排除」**,对「默认安全、显式才危险」的动作(`gh api` 不写方法时是 GET)只能用 requires,用 forbids 表达「默认安全」会让只读调用被误拦;③ 三种处置第一版写成 `if/else if`,**绕行把跳过吃掉了**——一次绕行很可能同时是「这一步这次没用」+「从这里岔出去了」,两件事驱动的提议不同,吞掉一个就少一条提议,改成各自独立累加;④ **变异验证本身会假绿**:`node --test --test-name-pattern` 在**零匹配时照样报 pass**,拿断言里的提示语当用例名跑,得到的是「变红 ✗」的假象(同类的还有「只查最终树的冲突标记」)。**边界(写在契约的 `boundaries` 里,免得以后有人以为忘了做)**:`autoPromote: false` / `autoApprove: false` / `injectIntoMemory: false`——不做自动升格、不做绕行自动批准、不把候选塞进注入路径,也不做「图自动重写」(图的变化一律经过提议 + 人批,回写是新版本)。**执行侧仍未开始**:所以这一版只有判定与落账的代码,**没有触发绕行的地方**,提议与批准的动作也还没有落点。回归锁 24 条进新增的 `test/candidates.test.js`(分档、兜底清单的 34 条子命令判定、requires/forbids 语义、项目键不含绝对路径、候选按形状归并、两次才够格、失败留痕、归档不删文件、升格必须过 `validatePipeline`),另 7 条进 `test/pipeline.test.js`(跳过与绕开分开计数、绕行不染色、两种提议互不吞并、`suggestions` 字段必须在、候选路由与三条边界);**关键规则逐条变异验红**(兜底失效、闸门失效、子命令不分、候选不归并、reason 不再是硬要求、升格跳过校验、归档改成删文件、绕行吃掉跳过——各自精确红对应那一条)。 - **设置面板把折叠线划在「新手之外」而不是「冷门」,条目名改说人话(issue #427,纯 UI 层取舍与文案)**:issue 实测的账是——「功能」卡 24 个常显开关,分组名(基础/检索与增强/巩固与睡眠/多会话隔离)是架构图的分区而不是人的问题,条目名是术语(`bm25SearchEnabled`/`preInjectGate.enforce`),折叠线划在「不常用」上,于是新用户敢动的只有 3–4 个,其余 20 个要读文档才敢碰。本次只动 `lib/client.js`(手写文件,不参与 sync)与测试,`config.js` 一位未动——验收红线就是 git diff 不出现任何 `.default(...)` 变更。**①折叠线挪到「新手之外」(建议 1)**:新增 `FEATURE_FIRST_SCREEN = ["group.basic", "group.scope"]`,新首组只装 3 个总开关(autoInject/autoSummarize/autoDream),作用域组整体留在首屏——「它记的东西会不会被别的会话看到」是新用户的必答题,藏进折叠等于没答;其余组整组收进新的「全部设置」第一层折叠(`showAll`),原「高级」降为它内部的第二层(`showAdv` 保留,7 个条目一位不动,标签「更少用的 7 个开关」与数组长度互相锁)。首屏可点项 = 3 总开关 + autoInject 的 2 个子开关 + scope 2 开关 + 「全部设置」按钮 = 8,压在验收线 ≤ 8 上。成员是从 core/dream 组里**移走**而不是渲染时过滤——`renderGroup` 是唯一的组渲染路径(`g.items.map(flagRow)` 字面量保留在其内部,`inject-parent-gate.test.js` 的门照过),同一项因此只可能渲染一次。scope 组不拆进 basic 还有个现实理由:`client.test.js:991` 的组成正则锁着 `key: "group.scope", items: ["scopeEnabled", "strictScope"]`,整组留首屏让既有锁零改动。**②条目名与 hint 重写(建议 2)**:22 个条目中英成对改写——名 = 一句「打开会怎样」(「顺带带上最近几轮原话」「别人设为私有的记忆我看不见」),hint = 「开:…;关:…(术语,默认 X)」三段式,术语与默认值沉到句尾;`writeAdmission` 与 `preInjectGate` 两级在 hint 里互相指路(「要真拦,见下一项/需先打开上面那项」),`hybridInject`/`selectiveInjectEnabled`/`adaptiveThresholdEnabled` 按**建议 5** 点明是「带什么回来」的三档而不是三个不相干的并列开关;「默认 X」逐条对照过 `src/config.js` 的 schema 值。删掉 `basicToggle`/`advancedToggle.hint` 三对旧键,新增 `group.basic` 与 `allToggle`,`features.desc` 补一句「先看常用,其余收在全部设置」。**③「默认 / 已改」徽标(建议 3)**:`FeatureRow` 加 tag prop,`flagTag(k)` 读 `state.overrides` 并用 `hasOwnProperty` 判「已改」——`settings.getFeatureFlags()` 无值时返回 `{}`,键不在其中就是仍取 config schema 默认值,面板自判、不加后端字段、不动 API 面;两个徽标类照抄 contrast 测试已验过的语义色组合,`test/contrast.test.js` 零豁免通过。**回归锁**进 `test/client.test.js`:首屏层组成正则、basic 成员必须从旧组移走(负向断言 core/dream 组头不再以这两个开关开头——防「渲染过滤」式回归造成双重渲染)、`restGroups` 只在 `showAll` 展开后渲染且「高级」按钮序在其后、`FEATURE_ADVANCED_BOOLS` 恰 7 项、徽标与分组/折叠键中英成对(occurrences ≥ 2)、判定必须走 overrides 而非后端字段。既有锁(inject-parent-gate、A4 scope 组、i18n 计数、contrast)全部零修改通过,全量 1772 tests 全绿。 - **M.O. 从「全局」改成「项目级 + 有名字的清单」(上一刀的回填)**:上一刀把工作流本体放进了**插件包自己的** `pipelines/`,于是不管在哪个项目、哪个会话打开,读到的都是同一套——「项目级」这个词当时根本没落地。发布前的实测反馈指出:不管在哪个项目,读到的都是同一套工作流。根因三处都在这一刀自己写的代码里:① `defaultPipelineDir()` 取的是 `import.meta.url` 的上一级,也就是**装了哪个包就读哪个包的**;② 面板把它注册成右栏**页面类型**,而右栏是面板级、与项目无关,会话头那个座位的标签又写死成「M.O. · 手法」,于是到处长一个样;③ 一进面板就是一张图,`id` 与 `title` 只画在图顶一行小字,没有能点选的条目——Hermes 那边是一串能干活的条目,——条目列表才是这个功能该有的形状。**改法**:本体搬到 `<项目>/.mneme/pipelines/.json`,读的时候**项目层在前、全局层(包内 `pipelines/`,今天为空且有意为空)在后,顺序即优先级**;同 id 两处都有则项目层赢,并把被挡住的那份记进 `shadowed`——**不静默丢弃**,否则以后有人往全局层加一条同名工作流、发现它「不见了」,会去翻文件系统而不是查这条记录。项目层的坏文件进 `unreadable`(带 `scope`/`dir`/`detail`,`detail` 点名是哪个文件)而不拖死全局层:一层写坏不该让人连兜底那份也看不见;全局层是自家发出去的包,坏了照抛。「默认项目级」这句话的落点就在这里,而它不只是口味:本机 7,647 条记忆里带 `workspace_scope` 的只有 227 条(≈3%)、活跃的 2,629 条里 2,517 条(95.7%)为空——默认全局等于把一条本地规约推给所有项目,会原样重演「全局变成谁都不看的杂物堆」。**`project` 由客户端从会话读出来带过去,服务端不猜**:插件侧的 `ctx` 上只有 `logger`/`llm`/`reflect`/`webServer`/`inject`/`commands`,没有 cwd;取不到时**如实标「项目未知」,绝不回退 `process.cwd()`**——回退会把「读不到」变成「读到别处」,而后者在界面上看不出来,是最难查的一类错。路由 `GET /api/dsh-mneme/pipeline` 增加 `?project=<绝对路径>`,**不是绝对路径就回 400 `project-not-absolute`**:忽略它比拒绝危险得多——忽略会静默退成「全局」,而全局看起来一切正常。面板第一屏从「一张图」改成**清单 + 图同屏**(清单当选择器:名字 + 作用域 + 步数/闸门数/对照覆盖率 + 状态计数;画布在下面,默认展开第一条,点别的条目只是换图)——中间那一版做成「清单 ⟷ 图」两屏来回切,结果画布藏在一次点击后面、界面上没有任何提示,反馈连着两次说「看不到画布和节点」;作用域标签只取目录名(`projectLabel()`),绝对路径里有本机用户名,而面板是会被截图的那一面。**两处必须拆成两个组件的地方**:清单与图不能在一个组件里按 `openId` 分支——hook 不能条件调用,那样写会在点开那一下炸成「Rendered more hooks than during the previous render」,运行期才看得见;投影缓存也按项目 + 按工作流分桶,否则同一个进程里开着两个项目时会**把 A 项目的清单端给 B 项目看,而画面看不出错**。**回归锁**:`test/pipeline.test.js` 14 → 18(清单行自带规模与作用域、同 id 项目层赢且报出遮蔽、项目层坏文件不拖死全局层、不给项目根就只读全局层),其中原来那条「`pipelines/` 必须进 `package.json` 的 `files`」**原地反转**成「不许在包里」——两头都防:进包则装完之后**每个**项目都能读到它,「项目级」又悄悄退回全局级,而这一幕在任何一次本地测试里都看不出来(测试读的是仓库目录);`test/client.test.js` 三条既有锁按新签名改写,并新增「第一屏是清单不是图」。全量 `npm test` **1796 / 1795 pass / 0 fail / 1 skipped**。同一段 `[Unreleased]` 里上一条 M.O. 文末那句「`package.json` 的 `files` 漏了 `pipelines`(已补,并加锁)」在本条被反转,两条一起发布时**以本条为准**。**一处只有实机才现形的自伤**:新写的详情屏容器与既有的「节点详情」面板**撞了同名类**`.mneme-pipe-detail`——后写的 `flex:1` 赢下冲突,先写的 `max-height:180px; overflow:auto` 却留着,于是详情屏被钉在 180px、里面的画布 `flex:1` 分到 0 像素,屏幕上只剩一片空白加一条滚动条(截图确认)。只查颜色的 `test/contrast.test.js` 抓不到这类撞车,所以补了一条通用的类名撞车锁(`.mneme-pipe-*` 每个类名只许有一条规则,连扫描量一起钉住,避免正则失配时假绿),并给画布与窄态列表加了高度下限——`flex:1` 的基准是 0,祖先没有确定高度时(浮层与居中 sheet 都是 `max-height` 封顶)画布会「在 DOM 里、但看不见」。窄态阈值从 640px 降到 320px:那张图是竖链、固有宽度只有 174px,栏窄一点反而更该画图——640 那档是按当年摊得很开的假数据定的,真数据下它把右栏(通常 400~600px)整个推给「一列节点卡片」那一支,于是「看不到图谱」。缩放条也提到宽窄分支**外面**,成了「新代码到没到」的肉眼标记。画布可缩放:`−` / `+` / 「适应宽度」三个按钮,外加 Ctrl/⌘ + 滚轮。缩放靠改外层容器的**百分比宽度**而不是 `transform`——百分比改的是布局尺寸,容器照常出滚动条,平移顺带就有了;`transform` 只改绘制,放大出去的那一半永远滚不到。滚轮那一条**自己挂非被动监听**:React 的 `onWheel` 是挂在 root 上的被动监听,`preventDefault()` 会被浏览器忽略(控制台那句 `Unable to preventDefault inside passive event listener`),Ctrl+滚轮会变成「画布和整页一起缩」——这个错本地测不出来,所以测试锁的就是这两个字面量。**一处只进浏览器控制台的自伤(实机复现)**:右栏那个 tab 的类型注册与正文座位注册原本写在同一个 `try` 里。宿主对重复的类型 id 会抛(`sidebarRight: tab type id "…" is already registered`),而这两件事的生命周期并不一样——类型挂在宿主服务 `sidebarRightTabs` 上、插件重挂时**留着**,正文座位挂插件 scope、随插件回收。于是第二次挂载时类型那一抛把正文一起带走:界面上只剩宿主的 fallback「这类内容还没有可用的查看方式」,而 `catch` 里只有一句 `console.warn`,界面上零痕迹。改成两个 `try` 各自兜底 + 类型注册前先 `tabs.get(id)` 幂等判断;回归锁按「类型注册块里必须有 catch、且必须出现幂等判断」来钉(把幂等判断去掉即变红)。**已知边界**:画布观感未动(后续再补);执行侧与运行记录写入仍未开始,所以清单上每一条的状态计数都还是「候选 19」。 - **M.O.(Modus Operandi · 手法,beta):服务端旗标 + 客户端三面**:新增 feature flag `actionPipelineEnabled`,默认 false(opt-in)。`config.js` 的 schema 与 `settings.js` 的白名单成对登记,`test/api.test.js` 的旗标计数锁 96→97,并补默认值与 PUT 往返断言——只改白名单不够:effective 的默认值只从 `config.js` 的 schema 来,缺 schema 时无 override 会静默少一个键。显示名给全名「M.O.(Modus Operandi · 手法)」,缩写不靠注释解释、靠设置项与 hint 的第一句让用户第一眼看见;内部标识(旗标键与数据契约 `mneme.pipeline/v1`)保持原样,改名纯亏。客户端在 `lib/client.js`,三个座位(会话头 action、对话行卡片、右栏 tab)**全部在旗标为真之后才注册**,无旗标时一个座位都不注册;模块级 inject 仍只有 `["slots","locale"]`,右栏是软依赖。**本阶段只到「界面就位」**:服务端没有 M.O. 的表与路由,界面接的是内置样例数据,旗标默认关,因此对用户可见行为零影响。 - **会话蒸馏写入也盖作用域章(项目记忆的地基)**:`scopeEnabled` 开启时,此前只有 `memory_save` 那条工具路径标注 `agent_scope` / `workspace_scope`,而机器提炼是写入量的绝大多数——实测某次真实部署的库中 7,385 条 `source='session:*'` 只有 1 条带 scope,于是「项目记忆」在库里几乎不存在。现在 `summarize.js` 复用同一把解析器(`scope.js` 的 `createScopeResolver`)与同一把闸门(`scopeEnabled`),值取 `session.header` 的 `agentPreset` 与 registry 反查的 canonical path(`header.cwd` 兜底,取不到落 NULL,解析绝不阻塞写入),来源标 `'auto'`——auto 不构成硬过滤,只吃软加权。合并进既有行时不盖章:作用域属于那一行原来被提炼出来的语境。关闸门时一个 scope 字段都不加,老行为逐字节不变。副作用:去重键含 scope,新盖章行与存量未标注行不再互相合并,过渡期可能出现同题重复——跨项目隔离是本次想要的效果。 - **面板补上蒸馏留存的两个开关,文档里那句「面板可启停」不再是空头支票(#399 跟进的第三步)**:`docs/CONFIGURATION.md` 写着 `distillRecords.enabled` 与 `distillRecords.retentionDays`「两个键都走 feature_flags 白名单(面板可启停)」,而后端确实全都就位(`config.js` 的 schema、`settings.js` 的白名单、`api.js` 的 `NESTED_FLAG_PATHS`)——唯一缺的是面板:`lib/client.js` 的 `FEATURE_GROUPS` 是一份写死的清单,这两个键在该文件里全历史零命中。用户照文档去找开关,够不着;验收这个功能只能绕开产品界面直接写库。现在留存总闸挂在 core 组末位,保留天数收在它的子块里、只在总闸开着时展开(关着一行不写,改天数没有意义),子块按组尾渲染所以总闸必须在组末。新增的整数输入行 `type` 刻意用 `text` + `inputMode="numeric"` 而不是 `number`:number 输入框在部分内容被判非法时 `e.target.value` 会变成空串,草稿与用户所见脱节,就没法把非整数挡在本地并退回生效值。中英各两条文案,hint 讲清两件事——这是**原始对话**落盘(隐私面,所以文案要说明私密推理与工具结果不落),以及天数只管启动期裁剪(reclaim 走另一条 `--older-than`,不读这个键)。8 条变异逐条验红(草稿不从 effective 初始化、天数行不看父开关、非整数不退回生效值、enabled 从组里删掉、数值键不进清单、中文文案删掉、子块不接到组尾、数值行不从 numRow 渲染);其中头一条断言起初写成子串匹配,把 `eff["distillRecords.enabled"]` 也算了进去——删掉组里那一项照样全绿,改成把 core 组的 `items` 数组抠出来单独看才成立。**检验「某容器里有没有 X」必须定位到容器再查,子串断言会被别处的合法引用蒙混过关。** - **M.O. 从「界面就位」走到「读真数据」:工作流的本体住仓库,面板不再画假数据**:上一版客户端三面接的是内置样例数据(`lib/client.js` 的 `PIPELINE_MOCK`),服务端一条路由都没有——界面能开、形状对,但图上那 8 个节点与 3 条运行记录全是常量。这一刀补上读侧与数据面,并把那份常量删干净。**住址的选择是这一刀的核心**:工作流是要跑的东西,必须能提交、能 diff、能整版回退、能 PR、能 review、能被 CI 读——记忆库一条都给不了(表里改一行没有 diff);而「为什么这么定」与「跑得怎么样」反过来只有记忆库记得住。所以分工是**记忆是「为什么」,仓库是「怎么跑」,图是「看起来怎么样」**,三者不下同一张桌子:把工作流塞进记忆库,它会长成一份死文档。新增 `pipelines/.json`(一条工作流一个文件)与 `src/pipeline.js`(读盘 → 校验 → 投影,零运行时依赖)。**为什么是 JSON 不是 YAML**:本包运行时依赖只有 `morphicons` 一个,为配置文件引 YAML 解析器不划算,手写子集解析器则是给未来埋一个「读起来对、跑起来错」的坑;人读的形态交给面板渲染,不靠文件格式。**校验很硬**:`id`/`name`/`project`/`version`/`status`/`done` 与每一步的 `phase`/`title`/`when`/`do`/`done`/`why` 缺一不可,`risk` 限 R0–R3、`who` 限 main/subagent/human/ci、`needs` 必须指向存在的步骤、图里有环直接报错——任何一处不合规就**整条读不出来**,不做「跳过坏文件继续」:半个工作流比没有工作流更危险,因为它会被当成完整的跑。报错一次列全(`PipelineError.problems`),不让人一轮修一条。新增只读路由 `GET /api/dsh-mneme/pipeline[?id=]`,**刻意不做功能旗标闸门**:旗标管的是客户端座位(关=一个座位都不注册),不是数据面,给它加闸门只会让「为什么面板打不开」多一种成因;读不出来回 500 + `pipeline-unreadable`,`detail` 就是校验器那份清单。答复契约 `mneme.pipeline/v1`——**前端渲染与 agent 读的是同一份数据**,画面只是它的投影(只画不存,agent 就什么也读不到)。投影里 `counterfactual` 装的是准入判据(「没有它,这次会不会失败」)而不是备注,答不出对照的步骤在图上要显得比答得出的虚;节点状态四值(`refuted` / `trusted` / `stale` / `candidate`)**全部从运行记录推**,不由作者写死——今天还没有任何运行记录的落点,所以全体恒为 `candidate`,而这正是面板上那句 beta 免责必须摆在脸上的原因。客户端四处(会话头图标、居中 sheet、右栏 tab、对话行结算卡)改成读同一份模块级缓存,杜绝「会话头说有 3 条候选、右栏说有 5 条」这种两个数字都对、没人查得出来的分家。**三处踩到的坑**:① `package.json` 的 `files` 原本没有 `pipelines`——漏了的话源码树里跑得好好的,发布出去的包一条工作流都读不到(已补,并加锁);② 右栏 tab 原先打算把 hook 直接塞进 `slots.register` 的回调,改成包一层具名组件:那个回调是不是当组件渲染由宿主决定,赌错就是「Rendered more hooks than during the previous render」这种只在运行时炸、测试看不见的错;③ 校验器写完后自己验出两个 bug——`tallyRuns` 建了计数对象却忘了放回 Map,于是所有节点都算无记录;测试对 node 用了不存在的 `needs` 字段(node 不暴露 needs,判达点必须走 `edges`)。回归锁 14 条进新增的 `test/pipeline.test.js`(必填字段、坏文件整条抛、重复 id、悬空 needs、成环、投影形状、状态四值、空目录、`files` 白名单),另有 5 条客户端接线锁进 `test/client.test.js`(假数据不得复活、四个座位共用一份缓存、数据面路由名、右栏 tab 必须走具名组件、空态与「全候选」两句文案中英齐全),**5 条逐条变异验红**。文档同步:新增 `dsh-mneme/docs/WORKFLOWS.md`(住址的选择、文件形态、对外接口、状态如何推导、已知坑、还没做的),`AGENTS.md` 模块地图加一行,中英 README 的文档表各加一行。**已知边界**:执行侧(触发、拆子 agent、接进 CI/CD)与运行记录的写入都还没做,今天只有读侧;`source.memoryIds` 是留给本机的口子(记忆 id 是本机私有数据,不进仓库文件),今天还没有人填它。 ## 🐛 修复 - **候选层的项目键不再借平台的 `basename()`:同一条路径在 Linux 上不再退化成整条路径**:`projectKey()` 原先用 `path.basename(root)` 取目录名,而它**跟着运行平台走**——posix 上 `\` 不是分隔符,一条 Windows 路径会被整条当成文件名,再经「只留安全字符」清洗与 32 字符截断,得到 `C--Users-<用户名>-Desktop-<项目名前段>`:**用户名的残片进了目录名**,而这把键的设计前提恰恰是「不把绝对路径写进文件名」。**这条缺陷在 Windows 腿上是结构性看不见的**(`win32.basename` 两种分隔符都认,旧实现在本机算出来的就是对的),所以它在 CI 上的形状是 ubuntu 四腿全红、windows 两腿全绿。改成按 `[\\/]` 两种分隔符直接切、取最后一段非空片段,与平台无关;反斜杠、正斜杠、尾随分隔符三种写法结果一致,Linux 路径上的结果与改动前逐字相同。新增回归锁 1 条(三种写法都必须取到目录名,且目录名里不得残留分隔符与用户名),并已用**变异检验**确认杀伤力:把实现退回平台相关写法,新旧两条用例同时转红。**这条锁在 Windows 上永远是绿的、也永远抓不住回退**,注释里已写明它的杀伤力只在 ubuntu 矩阵腿上——免得日后有人在 Windows 上跑绿了就把它当冗余删掉。 - **面板上的作用域标签不再借平台的 `basename()`:Windows 路径不再整条画进面板**:`projectLabel()` 原先用 `path.basename(projectRoot)` 取目录名,而它**跟着运行平台走**——posix 上 `\` 不是分隔符,一条 Windows 路径会被整条当成标签。这个函数的注释本来就写着「只取目录名,不把整条绝对路径画出去:绝对路径里有本机用户名,而面板是会被截图、被贴进 issue 的那一面」,旧写法在 Linux 上恰好把这条规矩破了:标签里带着本机用户名整条。改成**语义显式指名**——含反斜杠按 Windows 语义解析、否则按 posix,两个平台的正确结果就此一致;`""` 回退整串的原语义一位不动。**这条缺陷在 Windows 腿上是结构性看不见的**(`win32.basename` 两种分隔符都认,旧实现在本机算出来就是对的),所以它的杀伤力只在 ubuntu 矩阵腿上。新增回归锁 1 条(反斜杠 / 正斜杠 / posix 三种写法都必须取到目录名,标签里不得残留分隔符与用户名,取不到时回退原串),并已用**变异检验**确认杀伤力:把实现退回平台相关写法,本用例连同两条既有锁一起转红。 - **面板的保存失败不再静默,被拒的草稿退回生效值**:功能开关卡片里,写入失败的提示只有一个座位、且带 `!state` 条件——首屏加载失败才渲染;`state` 一到手,此后每一次保存失败都不出声。于是「框里的值」与「在用的值」可以长期分家而人看不出来:新加的保留天数输入行把这条路径变成可达的(填 99999 提交,服务端按 1–3650 拒掉,面板照旧显示 99999)。改三处:`put` 读出服务端 400 的 `error` 正文(`api.js` 那条 400 本来就是为此把键名与范围写进去的,此前被丢掉只剩状态码),失败写进独立的 `saveError` 并在卡片描述下方渲染(不跟首屏那条走),返回值告诉调用方成败——字符串项与整数项都在失败时把草稿退回生效值。4 条变异逐条验红(摘掉渲染座位、丢掉 error 正文、整数项不退回、字符串项不退回);认「座位存在」的断言起初只查 `setSaveError` 的出现次数,而写入路径调它、渲染处读它,两处都在就算命中,必须把渲染位置单独钉住。**已知边界(本次刻意不动)**:两个语言下的文案是「保存失败 — <服务端原文>」,服务端原文是英文;要汉化得在服务端另造键值对,那是另一件事。 - **失败表的保留期从写死改成可配(issue #275 伞下)**:`failure_memories`(用户纠正 / 反思失败记录)的启动期清理此前是 `store.deleteOldFailures(new Date(Date.now() - 90 * 86400000).toISOString())`——排在 `createStore` 之后、`cfg` 装配之前,90 天硬编码在表达式里。它是四张启动期清理表(`llm_audit_logs` / `distill_records` / `recall_runs` / `failure_memories`)里**唯一没有键可陈述**的一张:另外三张各有自己的保留期键(`llmAudit.retentionDays`、`distillRecords.retentionDays`、`recallRetentionDays`),只有它既改不了也看不见。**这次动的不是「加一个键」,是「挪一行清理」**:新增顶层扁平键 `reflectionFailureRetentionDays`(`z.natural().min(1).max(3650).default(90)`,形态对齐 `recallRetentionDays`),并把那条清理下移到 `cfg` 装配之后、与另外三张并排,从此读的是装配后的值(装配前读 `rawCfg` 会漏掉合进来的开关,另外三张的注释就是这条教训)。**默认值 90 一位不动**,不配置的库行为逐字节一致。**为什么这次不做成面板可调**:进 `settings.js` 白名单要同时补 `api.js` 的 `NESTED_FLAG_PATHS`、`lib/client.js` 的面板行与 `test/api.test.js` 的计数锁(#254 的教训:漏一处 `effective` 会静默缺键),那是三处机械改动、属「有人真要调」才值得花的面积;这一刀只补「写死的数字」这条欠账,面板留作后续,`lib/` 与白名单一位未动。**下限取 1 而非 0**:这张表的用途是挖掘重复出现的纠正模式,窗口太短就只剩最近一次;要彻底停掉「记录用户原话」,源头关 `reflectionFailureTracking` 比写了再删更省(也少一段盘上停留)。**已知边界三条**:① `dsh-mneme reclaim` 不读这个键——它只覆盖 `dream_run_inputs` / `archived_embeddings` / `distill_records` 三张表,本表不在其中,所以「让 reclaim 按同一个天数回收本表」目前做不到,得等 #275 那张伞往下收;② 本表没有 session 归属字段(#435 评审的同一条 concern),归属仍靠本地库的文件边界 + 启动期裁剪;③ 保留期只在**启动时**读一次,改完要重启才生效。回归锁 5 条进新增的 `test/failure-retention.test.js`:行为锁(删窗口外留窗口内、边界两侧各留 1 小时裕量只锁比较方向、空表返回 0 不抛)、接线锁(清理必须排在 `cfg` 装配之后、天数必须取自 `cfg.reflectionFailureRetentionDays`、写死的 `Date.now() - 90 * 86400000` 不得复活)、配置锁(默认 90、`0`/`3651`/`2.5` 被拒、键形与 `recallRetentionDays` 一族同形、README 与 `docs/CONFIGURATION.md` 必须同步列出该键——加了键不改文档正是这一族欠账的来源)。**五条变异逐条验红**:天数写回 90 / 改读 `rawCfg` / 把清理挪回装配之前 / 默认值改成 30 / 文档里删掉该键,各自让对应用例转红,还原后摘要一致。 - **写路径不再为了取 20 条正文而物化整个记忆库(issue #430)**:`saveWithDedupe` 的质量过滤近重复判定只要最近 20 条 `content`,代码却是 `store.all().slice(0, 20)`——全表 `SELECT`(`updated_at DESC`,无 WHERE 无 LIMIT)逐行走 `toRow`(27 字段、三次 JSON 解析)之后只取前 20 条正文。`memoryQualityFilter.enabled` 默认 true,于是这笔开销挂在**每一次存记忆**上:5k 行实测 `all()` 96.3ms / `all().slice(0,20)` 95.0–114.1ms,而它真正要的那份数据(`SELECT content FROM memories ORDER BY updated_at DESC LIMIT 20`)是 0.06ms 级——三个数量级,也是本次唯一那种量级的改动。新增 `store.recentContents(limit = 20)` 承接它,口径与 `all().slice(0, 20)` 严格一致:**不加 WHERE**(archived/forgotten 行仍参与近重复判定,`all()` 也不筛)、复用同一份 `updated_at DESC`、同刻顺序未定义、limit 走 `sanitizePage`(0/负数/undefined 落回 20,调用方不必自己防)。**同一批把 `store.list()` 的 `SELECT *` 换成与 `all()` 共用的显式列清单**(`memoryColumns` 由 PRAGMA 派生、排除 embedding;`toRow` 本就不输出向量,`SELECT *` 只是把全表最重的列拖进内存再丢弃):无界档 5k 行 263ms → 117ms,窗口档 50ms → 41ms,受益的是本函数**全部**调用方(面板分页、document 索引、sleep 模式池、注册器精确 supersede 的全量扫描)。**第三处落在写提交路径上的是 `syncMirror`**(每个写事务的 `finally` 都会走到):它此前 `store.all().filter(...)` 物化整库再逐行丢弃不可见行,现在改走 `store.list({ limit: null, order: "chrono", excludeTypes })`——`list` 默认的 `includeForgotten:false` + `includeArchived:false` 与旧 filter 逐字同口径(store 里就是这两条 clause),document 闸改由 `excludeTypes` 在 LIMIT 之前排除,`#296` 第二批那条行为(闸关时顺手删掉陈旧的 documents.md)原样保留。**顺带发现 `chrono` 排序里藏着一棵临时 B 树**:`ORDER BY updated_at DESC, id DESC` 用不上 #202 建的 `idx_memories_updated`(EXPLAIN 多一条 `USE TEMP B-TREE FOR LAST TERM OF ORDER BY`,100.6ms),去掉平局键后走纯索引扫描(23.4ms,裸 SQL 不含 toRow 21.4ms——代价全在那棵树,不在 `toRow`)。改动只针对**无界档**(没有跨页拼接,今天唯一调用方 syncMirror 只用它算覆盖集,与顺序无关);**分页档的 id 平局键一位不动**——跨页拼接要求稳定,同刻两行在相邻页之间会漏一条或重复一条,谁去掉谁错。**端到端 A/B(同一份 5006 行 fixture + 真镜像 + 质量过滤,各 9 轮取中位)**:`saveWithDedupe` 370.24ms → 273.95ms,每次业务写约 −96ms(≈ −26%)。**回归锁 6 条**进新增的 `test/store-write-path-columns.test.js`:`recentContents` 与它替掉的 `all().slice(0,20)` 按集合等价**且逐位同序**(含 0/-1/undefined 落回 20、archived 行仍在、空库;顺序那条锁由评审提出,变异检验确认「取到的行一个不少、只是全反序」的写法集合锁抓不到)、`list()` 不泄漏 embedding、**SQL 级探针**(包住 `store.db.prepare` 逐条断言:无 `SELECT *`、无 `embedding`、`recentContents` 那条必须带 `LIMIT`)、质量过滤写路径不得再碰 `all()`(抛错探针)、syncMirror 不得再碰 `all()`(抛错探针 + 真镜像)、document 闸在渲染之前就由 SQL 决定(含反向判据:闸开→documents.md 出现且含该 id,闸关→删除)。**6 条变异逐条验红**,其中两条初版抓不住、补齐判据后才转红的值得记下来:「syncMirror 不排除 document」起初全绿(假 document 行根本没进镜像,判据没判别力),补种**真** document 行(`store.saveDocument` 要求 `doc_path`)+ 闸开/闸关反向断言后才红;「`recentContents` 退回 `SELECT *`」起初全绿(列清单里恰好没有 embedding),把探针扩到 `recentContents` 自身那条 SQL 后才红。**已知边界(刻意不动)**:①这笔收益按**活跃集比例**走,不是常数——在几乎无归档行的干净库上 syncMirror 的新写法(100.6ms)反而比旧 `all()+filter`(92.6ms)略贵,把一半行标 archived 后是 68.9ms 对 99.4ms;质量过滤会持续把低分行归档,真实库非零归档,所以按老写法钉「一定更快」的断言不能写;②本轮只动「每次业务写都付」的两条,`dream.js` / `dream/sleep.js` / `recall-stats.js` 里的低频 `all()` 留给后续批次;③要再压 syncMirror 那层行数线性得换按 type 分页取,而**任何截断都还不能加**——「宣称覆盖活跃集」与静默截断不能共存(#278 第一批的纪律,注释里原位保留)。 - **连续性抢救的触发器从「试过压缩」改成「历史真的被折叠了」(#249 N3 后续)**:宿主的压缩插件在 **每一步** 都会试一次压缩(`agent/pre-step` 里的 `compactIfNeeded`),而代理层会把原生压缩整个拒掉(本机实测:两个长会话 12,000+ 事件里 `compaction/start` 与带 `error` 的 `compaction/end` 各 1,287 / 2,895 次,`compaction/summary` **0** 次)。原口径把 `compaction/start|summary|end` 一视同仁地当成「压缩边缘」,于是**每次失败尝试都置位一次**:一次都没折叠成功的会话,触发器退化成「每步都救一次没被折叠的历史」。实测注入量 359 / 625 次,而真正落地的折叠只有 39 / 161 次 —— 降 91% / 88%。**根因在宿主的事件契约里**(`dsh-compaction-basic/lib/index.js`):`compaction/end` **成败两条路都发**,成功路只 spread `lifecycle`,失败路额外带 `error`(`:484` vs `:495`);只有 `compaction/summary` 是「正文真的提交了」的发射点(`commitCompactionBody`,`:634`),而 `compaction/start` 只是开锁标记(源码注释自述「a failed close deliberately leaves the unmatched start detectable」)。现在只认三种**内容真的被移除**的事件:`compaction/summary`、`compaction/prune`(tool-result-pruner 把超预算的旧结果真的折叠出表面,`cmp-pruner.js:159`)、以及**不带 `error`** 的 `compaction/end`;`compaction/start` 与带 `error` 的 `compaction/end` 不再置位。**为什么不能只认 `compaction/summary`**:那个口径在本环境得 0 次注入 —— 那不是修好,是把这个功能停用。**注入洪流本身是有代价的**:「不变不重复」判重按全文比较,而快照的 `next_step` 取最近一条 assistant 回复的尾部 200 字符,每一步都在变(实测 331/331、551/596 全部不同),判重挡不住它;每条注入都是往工作线里追一条消息,是代理层按内容哈希记账的 `fold-reconcile` 的候选改写源之一。回归锁 3 条(`test/continuity.test.js`):start 与带错 end 都不置位且提案不落库、三种真落地都置位、以及照实测形状复刻的「50 次失败尝试 → 0 次注入 / 1 次真 prune → 恰好 1 次注入」。**这三条是必需的**:文件里既有的 11 处 `compaction/end` 触发点都不带 `error`,按新旧两种口径都算落地,一条都抓不住这次改动。变异检验 3/3 逐条验红(退回旧口径 → 3 条转红;只认 summary/prune 漏掉无错 end → 8 条转红;带错 end 也放行 → 2 条转红)。触发口径同步写进 `docs/CONTINUITY.md` 与 `src/config.js` 的键注释。 - **连通性测试不再把「档位被拒」报成连接失败(issue #215 后续)**:面板的「测试连通性」按钮此前是仓里**唯一**「接受并转发调用方档位、却没有兜底」的 LLM 调用面——`src/api.js` 的 `testLlmConnectivity` 直接调 `llm.stream`,档位被拒就 502 报红;而巩固/睡眠/蒸馏/总览/实体抽取的真实路径全都包在 `withEffortFallback` 里(实体抽取是 `src/index.js` 的手写等价版),被拒后丢掉 `reasoningEffort` 重试一次即成功。于是同一份配置「后台一直在正常跑、点测试却报错」,把用户引向「换哪家 Provider 都失败」这个错误结论(实际换 Provider 无用——档位是模型侧的能力声明,与线路无关)。现在探针复用 `dream.js` 的 `withEffortFallback` + `EFFORT_REJECT_RE`(单一来源,不手写正则),路径与语义与真实 run 完全一致;降级跑通返回 200 并在响应里置 `effortDropped: true`,面板在中英双语下把「连通了、但不是你配的档位」讲出来——**静默成功和静默失败一样有害**:前者让人以为档位生效,后者把人赶去换 Provider。真失败(鉴权、余额、网络)照旧 502 带原始原因,绝不重试、也不带 `effortDropped`(不制造假降级)。**流级拒收也已覆盖**:适配器把 provider 的拒收折叠成终止 `error` finish chunk(不抛错)时同样触发降级,只在 catch 里兜底的实现等于死代码。**同时清掉一处从落地当天就是错的前提**:面板与测试里那句「sleep 无回退重试,测试是事前唯一的暴露口」——`git merge-base --is-ancestor 939fbfc 25f90e4` 证明回退(2026-09-08)早于该注释(2026-09-17),它不是腐化而是出生即错,两处注释与新增静态锁一并收口。**回归锁 7 条(改写 1 条既有锁)**:抛错路径降级、流级拒收降级、真失败仍 502 且不盲重试、**抛错型真失败(socket 断开)同样不盲重试**、无档位配置时不报降级、防分叉静态锁(`api.js` 不得自带拒收正则、必须 import 单一来源且只经 `withEffortFallback` 一次)、面板降级呈现(严格 `=== true` 读取 + 文案只在成功行 + 双语成对)。既有那条「抛错即 502」锁按行为变更**原地反转**成「降级即 200」,并保留其调用计数断言(降级必须是恰好两次:带档位一次、无档位一次)。**13 条变异逐条验红**(含「探针不做降级」「流级拒收不交还」「真失败吞成成功」「**catch 路径盲重试**」「**catch 路径把真故障吞成成功**」「重试仍带档位」「自带拒收正则」「只加中文」「陈旧前提复活」等);其中「重试仍带档位」由降级用例先一步抓住(重试仍被拒 → 502 而非 200),比字段断言更靠前,属预期。**已知边界**:`result === undefined` 那道守卫按构造不可达(两边用同一份 `EFFORT_REJECT_RE` 判同一个字符串,中间无变更;codecov patch 会把它标为 uncovered)——保留它是因为该分支一旦真被走到就意味着判据漂移,宁可退回已捕获的流失败原因也不让探针 500;本仓无 `c8 ignore` 先例,不为两行引入新约定。 - **实体抽取的失败原因不再塌缩成一句话,审计行补上它归属的那条记忆(issue #405)**:`src/index.js` 在流以 error/aborted 结束时写死 `writeAudit("error", "llm stream aborted or errored")`,`reason.failure{message,code,status,providerRetryAfterMs}` 一个字段没读——主库 `llm_audit_logs` 里 `operation_type='entity_extract'` 共 1464 行,其中 error 735 行全是这一句,429 / 配额 / 鉴权 / 超时在账本上长得一模一样,排障只能求报告人贴日志。现在改走仓里已有的 `describeStreamFailure()`(`src/dream.js`,`api.js`/`summarize.js`/`dream/sleep.js` 早就在用同一份),把 code/status/message 归一后落进 `error_message`;拿不到细节时退回原字面量,不制造新形状。同一行的 `related_memory_ids` 恒为 `[]`,审计行因此不知道这条失败属于哪条记忆——而该值本来就该从 `extractEntities(memory, …)` 的 options 里传下去,补一行 `if (memory.id) options.memoryId = memory.id;` 即可。**记账纪律照旧**:没有产生任何行就必须留空,挂一个不存在的 id 就是造虚账(`src/service.js` 的既有注释),所以取不到 id 时仍是 `[]`。**已知边界(本次刻意不动)**:`success` 仍然在流一结束就写,而 JSON 解析在其后的 `src/entities/extractor.js`——「流成功但输出畸形」必然记 success,这是结构决定的,不是这一刀能关的口子。5 条回归锁(真实 finish chunk 带 `reason.failure` / 无细节兜底 / 抛错路径 / 取不到 id 不造假 id / 防分叉静态锁——归一逻辑只许有一份)。 - **注入审计行写真实 query,并找回蒸发掉的 recall_runs 保留期清理**:两处水源在说谎。其一,`recall_runs.query` 恒写空串(实测注入行 100% 为空),而调用方 `inject.js` 本来就带着 `lastUserQuery` 传进来——信息是在 `service.js` 的落账块里被丢掉的。落账改记真值;`NOT NULL` 约束与「真没有才空串」的占位语义不变(改列约束要重建表,不值得)。下游 `recall-stats` 按 mode 分账、不读 query,面板数字逐字不变。其二,`purgeRecallRunsOlderThan` 与它的启动调用在 `dd0ed78`(v0.7.12 发布提交)里整块蒸发,而配置键 `recallRetentionDays`、README 与 `docs/CONFIGURATION.md` 一直宣称它有保留期——那张表于是涨到 18,442 行、跨度 2026-09-08 → 2026-10-13,只增不减。(数字出自一次真实部署的库,非新装环境。)**这两件必须同批交付**:query 里从此装的是用户原话,保留期就不再是「表膨胀」,而是「原话无限期留存」。启动调用读装配后的 cfg 而非 rawCfg,面板改保留期才生效;清理失败只 warn,绝不挡插件启动。 - **四个启动期清理都必须出声**:failure-table / llm_audit / distill_records 三处原先静默吞异常(`catch { /* non-fatal */ }`),与 `llm_audit` 注释里「失败只 warn,与失败表清理同款」的说法正好相反——注释是规约,代码不合规。连同 `recall_runs` 一起统一改成 `catch (err)` + `ctx.logger.warn`。理由是这一族清理的失败方式刚刚演过一遍:函数与调用一起静默蒸发,表涨到两万余行而无人察觉;清理再失败而仍不出声,同一类回归照样看不见。 - **失败表的 query 落当轮用户原话,取法与注入侧共用一份**:`failure_memories` 2,367 行里只有 69 行 query 非空(2.9%),但这个分母是误导的。按「expected 是否落在某次 `dream_runs.decisions` 里(±12h 窗口)」把它分成两侧:dream 侧 2,118 行、非空 0 行(0.0%);入口侧 249 行、非空 69 行(27.7%)。dream 侧恒空是**设计使然**——`applyMerge` / `applyConflict` / `applySupersede` / `applyDifferentiate` / `applyUpdate` 五个巩固动作本来就不该有「用户原话」,机器批注没有意图可言。第二口径(`receipt_chain` 按 run_id 还原碰过的 memory_id,±120s 窗口)独立复核给同向结论:命中侧 0.2–0.4%,未命中侧 8.5%。所以真正要修的是入口侧:`memory_update` 工具的 `reason` 是可选参数,模型不填就没有 query——27.7% 是「模型愿意填」的比例,不是「拿不到」的比例。而工具 `execute` 的第二个参数同时握着改写动作与本轮会话,正是注入侧在吃的东西,所以没给 reason 就退回当轮用户原话,取法复用 `lastUserQuery`——**只把它 export,不复制第二份**。复制一份的代价本仓库付过几次,而「`visibleSeqs` 过滤被 rewind/compact 遮蔽的事件」这种细节在副本里最容易漏,漏了就会把用户撤回的话写进审计表;新测试专门有一条锁它。两边都取不到时写 `NULL` 而不是空串:空串会假装「有意图」,`recall_runs` 那一列当年就是被空串占位糊过去的。`expected` / `actual` / `failure_type` / `memory_id` / `before` 五个字段一个都没动。 - **mint 的记忆带上管道来源,机器写的不再落回 manual 兜底**:pattern 表 144 行 `source` 全是 `"manual"`,看着像「人手工写的」——实测是错的。按 `dream_runs` 的 `patterns.applied` 逐轮求和得 144,与表内行数 144 相等(72 轮 sleep 里 48 轮 ok,auto 轮 0),它们全是 sleep 的 `phasePatterns` mint 的;`"manual"` 只是 `saveWithDedupe` 没拿到 `source` 时的兜底值。于是「这类条目从哪来」在数据面上不可读,靠它做出的判断也跟着错。改法:**来源由调用方(管道)声明,不从决策对象取**——`create` payload 是模型写的,信 `d.source` 等于让模型给自造条目盖「人写的」戳。三条 mint 管道各自声明(`dream` 主链路 `"dream"`、sleep 两处相位 `"sleep"`),面板确认冲突那条**刻意不传**(由人触发,manual 兜底恰好是对的意思),注释写明是刻意的免得日后被当成漏传;取不到就一个字段都不传,落回原有兜底,行为与改前一致。连带更正 `service.js` 里那句「pattern 只由手工写入(实测 40 条全 source=manual)」——它的**前提**是错的,但它支撑的结论(巩固层护住而注入面不给看是同一口径两头不一致)不依赖那个前提,所以注释改写时把这点写清,免得把仍有效的决策一起推翻成无依据的推断。**不改 `?? "manual"` 兜底本身**(那会一次动到面板 / recall-stats / dream 等所有消费方的可取值),**也不回填存量 144 行**(改写用户记忆库不可撤销,留给人拍板)。 - **修掉一个能把宿主钉死的轮询**:`setEmbedder` 里的 `if (emb.ready === true || attempts >= EMBED_READY_POLL_LIMIT)` 把上限判断写在了会抛错的读取**右侧**——`emb.ready` 是 getter(OpenAI embedder 经 `settings.getVectorConfig()` 读库),getter 一抛错整条 `||` 当场中断,右侧的天花板永远轮不到。触发条件是真实宿主走的正路:插件被禁用或重载时 `fiber.dispose()` 会 `store.close()`,此后每 100ms 读一次 ready 就抛一次 `Error: database is not open`,定时器活过注释写明的「~3s ceiling; never poll forever」。修成先 `readReady()` 带 try/catch(只 warn 一次)再自增判定,探针读数由「读 46 次 / 天花板 31 被击穿 / 残留 1 个 Timer / 进程挂住」变成「31 次 / 未击穿 / 0 Timer / 正常退出」。**上限判断别写在会抛错的读取右侧**——短路求值让右侧在左侧抛错时根本不执行。 - **注释里的行号引用改成带符号名带日期**:审计指出本批在 `inject.js` 头部插了导出注释后行号整体 +5,而树内注释与交付记录仍写旧值。查下来面比这条大:本批给 `index.js` 插了 28 行、给 `tools.js` 插了 1 行 import,把**别的文件里**对它们的引用也一并推歪了。判据不取「本批改动使该行内容变了」——那会把早就腐坏的引用一起算进来;改用两个维度交叉:把引用点名的符号写成正则看它在基线里能否找到(锚点法),再用 `git diff --unified=0` 算净位移(平移法)。结论是本批推歪 3 条(已改)、既有腐坏 18 条(只登记不改,改动面不在本批写域)、反常 0 条。修法不是改数字(明天还会错),而是**去裸行号、改成「<文件> 的 <符号名>,<日期> 在 :<行号>」**——静态行号是腐坏不是笔误,只改数字治标。早期探针只跑锚点法、且把锚点抠成了引用正文里的字样,于是把一条本批造成的腐坏误判成既有腐坏;**自动从注释里猜「这条引用指哪个符号」不可靠,锚点必须人工定。** - **会话蒸馏的作用域章:两处「读起来对、跑起来错」(自动评审核出)**:CodeRabbit 在 PR 上报了四条,逐条对着代码核验后,两条是真缺陷、一条是真问题、一条是文档歧义,全部在这一条里交代。**其一,去重命中后新行丢章**:`scopeEnabled` 与 `summarizeDedupeMode` 同开时,第二轮蒸馏会命中第一轮的行;候选筛(type + source + 时间窗)不看 scope,真正决定并不并的是 `saveWithDedupe` 的 `scopeMatches` 门。原实现用「命中就不传章」表达「不重贴既有行」,可新条目的 scope 因此变成 null——与已带章的候选不等 → 门拒 → 回落新建,而那一行**同样是新行、同样该带章**却没有章。实测两轮蒸馏留下同题两行、一行 `auto` 一行 NULL,恰好是「跨作用域隔离没做成、同题重复倒留下了」。改成两维都无条件传,让合并分支按它自己的口径处理(它只认 explicit 来源升级,从不采纳这里传的 auto 字段),「不重贴」与「新行必贴」才相容。**其二,header 字段读取会穿出解析器**:`scope.js` 文件头写着硬约束「任何异常都被吞掉」,但它只覆盖了 `session.header` 的取得与 registry 查询,header 的**字段**读取裸着——宿主给出会抛错的 accessor 时异常直接穿出,蒸馏路径上一个窗口的记忆与游标一起丢、工具路径上是一次 `memory_save` 直接失败。两处字段读取各自护住,失败落 NULL 并 warn 一次。**其三**,居中回退面板(老宿主没有右栏服务时的唯一完整入口)此前挂在没有 `tabIndex` 的元素上收 Escape、打开时也不移焦点,键盘用户只剩 Tab 摸到关闭按钮一条路;现与记忆库浮层同款:打开时焦点移进面板、Escape 挂在 window 上收、关掉把焦点还给 trigger。**其四**,配置文档里 `scopeEnabled` 一行补明「标注只贴新建行:并入既有行时不重贴」。**回归锁 4 条**(`test/scope-distill-stamp.test.js`),三条经变异验证确认会变红:回退其一时两个方向各红一条(跨作用域那条把新内容并进未标注行、同作用域那条反被门拒),回退其二、其三各红一条;还原后哈希逐一核对。 ## 🧪 测试 - **ego 2-hop 用例不再拿墙钟当规格线,改钉确定性的遍历工作量(issue #409)**:原用例断言 `ms < 50` 并自称「spec §7」——那个 50ms 预算在 `docs/` 里找不到出处,而且它要抓的东西本身也不对:全量并行跑实测最坏 60.4ms,于是 CI 偶发翻车;真正的退化(把逐节点取邻接改成全图扫描)在 121 个节点上根本看不出区别。ego 路由的遍历工作量由 `limit` 卡死、与图规模无关,且请求内 `service.getRelations` 的唯一调用方就是这次遍历,所以现在数它的调用次数:小图(121 节点)上界 3×limit;再拿约 5× 大的图(641 节点)跑同一个查询,断言工作量不随图长——一条不含时钟的确定性锁,全图扫描型退化会立刻越界。墙钟只保留 250ms(≈4× 实测最坏值)的离群告警,不再是规格线。 - **蒸馏留存的完整回路由装配级用例锁住**:新增两条端到端用例,从 setting 一路走到磁盘——`setFeatureFlags` 写进 kv(面板真正的写入面)→ 真装配插件(bundle 里说关、kv 里说开)→ 驱动一轮 `session/event` 的 `turn/end` → 复查库里两张表。一条断言开着的会长行,另一条断言**从没设过这个开关时一行都不写**。另有 `test/failure-query.test.js`(7 条,含「用被撤回遮蔽的用户消息不算原话」——这条同时锁住「没有抄第二份取值实现」,naive 副本会漏掉可见面过滤)、`test/mint-source.test.js`(9 条,含**对抗条**:决策对象自带 `source: "manual"` 时管道来源照样赢,并先用 `validateDecisions(...).ok === true` 证明这个威胁真实存在)、`test/embed-ready-poll.test.js`(1 条三断言,钉住上限读取次数、越过之后不再读取、只 warn 一次)、`test/recall-retention.test.js`(七条,故意分「行为锁」与「接线锁」两半——弄丢的正是「两半都在」这件事,只锁一半,另一半断了也没人知道)。 ## 🧹 工程 - **发布闸门从假绿改成真闸门(`release.yml` 的 `Verify published version`)**:原先这一步是 `echo "Published: $(npm view "@modusensus/dsh-mneme@${V}" version)"` —— 命令替换里的失败**不会**让 `set -e` 退出。v0.8.16 那次日志白纸黑字写着 `npm error code E404 / 404 No match found for version 0.8.16`,step 与 job 却都是 success:**真发不出去**(token 失效 / 权限不足 / npm 侧拒绝)与发布成功在 CI 上是同一种颜色,只有人翻日志才看得出来——而闸门的意义恰恰是没人翻日志的时候。换成 `scripts/publish-gate.mjs`:轮询 registry 公开 HTTP 面直到该版本可见,超时(默认 10 分钟 / 每 15 秒一次)即非零退出,日志里带上每次探测的原因与最终的重试次数。**为什么是轮询**——registry 有传播延迟,v0.8.16 实测 publish→可见约 4.5 分钟,一次性查询只有「忽略失败」(就是原来的假绿)或「拿传播延迟当发布失败」(假红)两种结局;而幂等跳过那条路径(版本已存在)首次探测即通过,所以只有真发新版才付等待成本。探测走公开 registry HTTP 面而不是 `npm view`:不依赖 `.npmrc` 里的 token、不依赖 npm CLI 的 E404 语义,且能区分「还没传播」与「网络异常」——两种都重试,但日志里必须分得开,否则排障又得靠猜。超时预算的语义刻意定成「等多久才放弃」而非「多久之后就算没发布」:探测成功即通过、不再回头比期限(单次探测自带 15 秒 HTTP 超时,晚到的成功最多超出十几秒),因为版本真的在 registry 上就是发布成功——消灭假绿不该顺手制造假红。14 条回归锁(假时钟 + 注入 probe/sleep/now,零网络零等待)钉住两端与这条语义:版本已发布 → 首次探测即通过、不白等;等满预算仍看不见 → 必须 `ok:false` 且入口退出 1,谁把「拿不到」改成「跳过校验」谁转红。附带修掉一个把成功报成崩溃的退出路径:`process.exit()` 紧跟 HTTP 请求会在 Windows 上撞到 libuv 的 `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`(成功路径实测退出码 0xC0000409),改用 `process.exitCode`。已知边界(已由下一条的独立改动收口):`release` job 当时仍只 `needs: [verify, test]`,publish 失败时 GitHub Release 仍会建出来。 - **Release 不许抢在 publish 之前(`release` job 的 `needs`)**:GitHub Release 是对外的「这个版本已发布」,registry 才是唯一事实。原先 `release` 只 `needs: [verify, test]`,于是 publish 真失败(token 失效 / registry 拒绝)时 Release 照样建出来:Release 页写着 v0.8.x、registry 上却没有,用户照 Release 装回的是上一个版本——与 `Verify published version` 同源的一处豁口,那边是校验假绿,这边是声明抢跑。改成 `needs: [verify, test, publish]` 后两条路径都收口:publish 失败 ⇒ 本 job 被跳过;dry_run 时 publish 被跳过、而本 job 的 `if` 同为假 ⇒ 不会发出无从兑现的发布声明。回归锁进 `test/release-workflow.test.js` 三条:release 必须等 publish;仍必须等 verify 与 test(免得修依赖顺序时顺手绕开版本号与 CHANGELOG 校验);publish 内必须调用那道阻塞闸门、且不得退回 `echo "Published: $(npm view …)"`(否则 release 等的是一个不做校验的空壳,第一条形同虚设)。工作流是 YAML 而本仓不引解析依赖,锁落在 `needs: [a, b]` 的内联列表形状上:改成块状列表会红,那是真变化、值得人看一眼再改锁,而不是静默失效。 - **注释里写死的行号引用改成「符号名 + 日期」**:三处在写下时都是对的(`tools.js` 的 `memory_save` 里 `resolveSessionScope` 到 `workspaceLabel` 那一段、`autoStamping` 那一行、`scope.js` 的 `createScopeResolver`),但写死行号的引用不是笔误而是会腐坏——跨文件插行会把别处的引用整批推歪,把数字改对只治当下,下次再有人插一行又错,而注释里的行号没有任何测试会去测它。改成「<文件> 的 <符号名>,<日期> 在 :<行号>」:符号名是稳定锚点,日期说明这个行号属于哪一刻的树。行为零改动,只动注释。 ## [0.8.16] - 2026-10-12 ## 🆕 新增 - **面板「Token 去向」三屏:把账本里已有的成本数据摊开(issue #412)**:原「LLM 消耗」卡只有一个总数与调用次数——`getLlmAuditStats` 早就返回了 `by_source`/`by_status` 却被丢弃,用户看得见花了多少、看不见花在哪。后端**新增**两个聚合并原样保留 `by_source`/`by_status`(既有形状锁不动):`by_operation` 按 `operation_type` 分组,因为 `trigger_source` 太粗——它把 `dream_consolidate` 与 `dream_summarize` 并成一个 `autoDream`,恰好把 2026-10 审计里最大的一项(7 天 token 的 37%)藏起来;`by_day` 按天升序给趋势图供数。前端把单数字卡换成三屏:KPI 带(7 天总量 / 调用次数 / 巩固占比 / 待确认冲突,后两项分别来自 `by_operation` 与 `mneme:conflicts-changed` 事件、与 DreamStatusCards 同源)、按组件 donut(半径 15.9155 使周长≈100,`strokeDasharray` 可直接填百分比;段间留缝;`write_admission` 只计量、token 恒 0,不画弧只留图例)、7 天柱状(纵轴上限取整到整 M)+ 8 周热力(右缘对齐数据末日、左侧补齐到周一,超窗格子隐藏)。「缺日补零」与「未到 vs 已知为空」严格分开:`stats` 为 `null`(请求还没回来)整块不画柱,`{}`(已知为空)才补七根零柱;8 周热力单独请求 `days=56`,复用 7 天那份数据会让 48 格恒为 0。原 9 张状态卡降级进折叠区,开合开关挂在 `summary` 而不是 `details` 上——挂在 `details` 会把子卡片的点击一起吃掉。面板侧 6 条回归锁进 `test/client.test.js`,三条变异检验(缺列不补零、热力复用 7 天数据、开合挂错节点)各自转红后才还原。 - **蒸馏原文留存 + 记忆级溯源(issue #399,opt-in 默认关)**:蒸馏是一条**有损**管道——它把窗口喂给 LLM 拿回几条记忆,然后消费掉游标,原文此后在库内没有任何痕迹。后果是三件事同时成立:记忆没写进去的细节事后**无法回查**(不是没保存,是没地方找);一条记忆**说不清自己出自哪段对话**;蒸馏质量出问题时**没有基线可比**。信息论依据(arXiv 2609.36059 §3 Eq.1)很直白:旁路索引不丢信息——`I(Y;M,H,X)=I(Y;M,H)`,留存原文不改变已蒸馏产物的互信息,但取回原文的能力被保住了。所以这不是「再存一份」的冗余,而是把单向替换变成可回指的**叠加索引**。形态是两张只增不改的新表(`CREATE TABLE IF NOT EXISTS`,无 ALTER、无回填、**不动 `memories`**):`distill_records`(蒸过的窗口原文,主键 `(session_id, seq)`,`INSERT OR IGNORE` 幂等——同一窗口重蒸不翻倍且保留首次落盘时间)与 `memory_provenance`(`(memory_id, session_id, seq_start, seq_end)` 唯一索引 + `ON CONFLICT DO NOTHING`,每条新记忆回指来源窗口)。写入点在 `summarize()` 里**顺手落**:那里已经持有会话事件、已经算出窗口边界,不新建常驻管道、不新开监听器,于是留存范围与蒸馏范围天然一致;落盘与记忆写入同事务(游标消费也在同一个事务里),不存在「记忆写了但原文没落」的半截状态。**最要紧的约束是隐私谓词只有一份**:渲染与落盘都从 `publicTranscriptParts(event)` 取数,因为两条路各写一遍过滤逻辑必然漂移,而漂移方向必然是「存下来的比 LLM 看过的更多」——模型没看到的私有内容反而进了持久库,最糟的方向。口径:真实用户提示与助手公开 `text` 块两条路都进;私有推理链、插件注入的机器内容(AGENTS.md / skill 正文 / 文件变更通知)两条路都不进;工具调用入参与结果、code 输出**只进渲染、不落盘**——这是刻意的路径差异而非漂移(`codingRetrospect` 靠工具输出沉淀踩坑根因,渲染路径必须带它;但它体积大、含路径与命令、不属于对话内容),差异方向仍满足「落盘的 ⊆ 渲染的」。留存文本**不受** `distillMaxChars`(默认 24000,超长保尾部)与工具裁剪 `trim(args,300)` / `trim(out,500)` 约束——截断过的「原文」没有回查价值。删除语义两分:到期裁剪**只删 `distill_records`**,溯源链接保留指向空区间(面板显示「来源已过期」而不是查无此记忆),**绝不级联删记忆**;删记忆是另一条路,`store.remove(id)` 在事务内显式清链接(不靠 `ON DELETE CASCADE`——`store.js` 从不启用 `PRAGMA foreign_keys`,翻它会顺带激活 `recall_events → recall_runs` 那条既有约束的行为,不是本次范围)。配置照 `llmAudit` 形状:`distillRecords: { enabled: false, retentionDays: 90 }`,默认关 + 进 `LIGHT_MODE_OFF`(`retentionDays` 不进——它是时长不是行为面);两个键都进 settings 白名单与 api.js `NESTED_FLAG_PATHS`(#254 的教训:漏了后者 effective 会静默缺键)。启动期裁剪与 `reclaim` 都读装配后的 cfg(面板改了保留期也认),且默认关时启动期不动手(一张空表不该被每次启动扫一遍);`dsh-mneme reclaim` 报告从两项变三项,入口语义一字未动。已知边界三条:**无历史回填**(只对此后发生的蒸馏生效,从记忆反推原文是编造)、**从未蒸馏过的窗口不留记录**(窗口太小已消费游标 / 插件当时关着 / 没触发过蒸馏 / LLM abort,是「留存范围 = 蒸馏范围」的推论而非丢数据)、**不覆盖工具结果与 code 输出**。回归锁六条 + 五条:幂等(删游标换实例重跑同窗口仍一行)、隐私过滤(四类注入 + reasoning + 工具输出绝不落盘,而渲染路径仍带工具输出)、未截断(超长提示记录里逐字全文、渲染里找不到头部标记)、裁剪不毁资产(原文 0 / 链接在 / 记忆数不变 / `provenanceFor` 仍可查)、默认关零写入(写入调用计数为 0,连收集都不做)、进 reclaim dry-run 报告(报真实行数与列文本大小且一字节不改,执行只删窗口外原文),另加启动期裁剪读装配 cfg、白名单与 PUT 往返(含范围外整数被拒)、轻量模式只关 `enabled` 不动 `retentionDays`。非目标:不嵌入、不进 `fuseRecall`、不进注入、不做取代判定、不做 value histories 查询、不动 8000/300/500 三刀——这两张表是旁路索引,不是第二条检索路(接进召回是另一个决定,成本与召回质量都要重新测)。设计文档见 [docs/DISTILL.md](docs/DISTILL.md)。 - **面板手动新增记忆(issue #386 第 5 条)**:面板此前只能读、改、删、导入,**没有任何手写入口**——想记一条只能切到对话里让模型调 `memory_save`,而模型看不见的东西(一次现场结论、一条口头约定)恰恰最需要手动落库。「更多操作」菜单新增「新增记忆…」,弹层收类型 / 标题 / 正文 / 重要性 / 标签,字段与 `memory_save` 工具对齐。类型下拉**刻意不含 `document`**:那是 agent 注册长文档的指针行,铸造口唯一(`registerDocument`,#230),列出来只会让人点了才被拒。**表单不放 scope / sensitivity**:显式归属走详情抽屉那条路,它带「放宽可见性」二次确认;在新建表单里顺手盖章会绕开那道确认。新增后端路由 `POST /api/dsh-mneme/create`,落库走 `service.saveWithDedupe`——去重键仍是 `(type, title, scope, sensitivity)`,所以**同类型同标题会并入既有条目而不是长出第二行**,服务端回 `action: "created" | "merged"` 决定提示语说「已新建」还是「已并入」:两者对用户是不同的事实,都报「已保存」会让人以为自己新写了一条、实际旧行被改。三处校验比 standalone 的 `POST /memories` 收紧(口径向 `/update` 看齐):`content` 要 trim 非空(面板是人工输入,空正文只可能是误操作)、`document` 提前 400、scope/sensitivity 不接受。写入准入(`write-admission.js`)对面板路径不生效——它无会话身份即放行——但路由仍显式处理 `action === "denied"` → 422,免得日后面板带上会话身份时静默吞掉拒绝。回归锁六条进 `test/api-routes-gaps.test.js`(①鉴权 401 且 GET 落 404;②六种 400 校验且库内零写入;③201 created 且 `source` 为 `panel`;④200 merged 且 `count() === 1`;⑤`denied` → 422 且理由随行;⑥落库异常 500),面板侧另有一条 `test/client.test.js` 锁「入口真的挂在树上」(菜单项确实调 `setCreateOpen(true)`、弹层确实被条件挂载、`onCreated` 确实刷新列表并选中新行、请求体逐字只有 type/title/content/importance/tags、表单不得长出 scope/sensitivity 输入框、类型下拉不得含 `document`)。评审期补的两处:**成功路径不得重置 `busy`**——提示要停 900ms 才关窗,那段时间按钮若重新可点,第二次同 type+title 会落进同一行并由 `appendContent` 把正文再追加一遍,提示还会从「已新建」翻成「已并入」,一次误触就写坏一条记忆;因此整个弹层只允许错误路径重置 `busy`,按钮文案也随之改成 `busy && !msg`,免得写完了还写着「保存中…」。另把字段锁从「整包里包含这些词」改成在 `CreateDialog` 源码切片上匹配真实请求体写法(在整包上断言 `clientSource.includes("title")` 恒真,等于没锁)。两侧都做了变异检验:面板/后端逐条把锁打回坏状态都转红(含「菜单项变哑」「弹层摘掉」「成功路径放回 setBusy(false)」「按钮文案只看 busy」「表单长回 scope」「不 trim title」「打错路由」这类只有肉眼才能发现的退化)。 - **面板存储回收入口(issue #386 第 4 条)**:库文件只增不减,回收一直只能靠命令行(`dsh-mneme-maintenance`)或独立服务的数据面,面板侧连「现在能收多少」都看不见。状态页新增「维护」分区与「存储回收」卡:挂载即 GET 一次预估,摆出文件体积、空闲页、本次会清掉多少条输入快照与多少条归档向量;改窗口后必须点「重新估算」,数字与输入框不一致时执行钮保持禁用——对不可逆动作来说,确认态里摆着另一个窗口的数字是最坏的一种错位。执行是**两步确认**:第一次点击只进确认态(换成仓库既有的实心红 `mneme-btndangerconfirm`),确认态里把窗口与 VACUUM 状态一并锁死显示,第二次点击才发出请求,请求体逐字 `{dryRun:false, confirm:true, olderThanDays:Number(days), vacuum}`。**VACUUM 默认关**:它会短暂独占数据库(别的进程此时写入会 `SQLITE_BUSY`),属可选动作,默认开等于每次回收都替用户独占一次库。**VACUUM 失败不得渲染成干净的成功**:两个 UPDATE 各自提交且不可逆,VACUUM 只负责把文件缩回去,半成功态单独出文案说明「清理已完成、不可逆、重跑幂等」,并且无论成败都露出回执号(缺失时明写「未记录」)——不可逆动作没有回执,出事时无法对账。面板侧不使用 `window.confirm`(宿主 webview 里不可靠),确认门是渲染出来的分支而不是阻塞弹窗。后端新增路由 `GET/POST /api/dsh-mneme/maintenance/reclaim`,底下就是 `src/maintenance.js` 那一份与 CLI、standalone 共用的实现,**未改动数据面**(`src/api-standalone.js`、`src/maintenance.js` 零改动):GET 恒为只读预估;POST 默认 dry-run,要真执行必须带 `dryRun:false` + `confirm:true`,否则回 400 `confirm-required` 并**附上那份 dry-run 报告**(确认的依据就是报告里的数字,且报告必须描述「将要执行的那个动作」——带了 `vacuum` 就得看到会 VACUUM 的那份,否则用户照着「不 VACUUM」的估算按下去)。两个方法都要鉴权:这是不可逆内容丢弃的入口,不能有例外。窗口解析在两侧**刻意不同口径**:GET 是 query 字符串域,只收十进制整数字面量(`/^\d+$/`),**不用 `Number()`**——它把空串、空数组、纯空格都化成 0,而 0 在这里的含义是「清掉全部快照」,最坏解释绝不能从一个畸形输入静默拿到;POST 收 JSON 数字域,字符串 `"3"`、`-1`、`3.5`、`true`、`{}`、`[]` 一律 400(`NaN` 不在坏输入列表里:JSON 表达不了它,`stringify` 会把它变成 `null`,而 `null` 与缺省同义、落默认窗口)。边界值 0 两侧都合法。`invalid-json` 与 `invalid-body` 分开报(前者查调用方序列化坏了,后者查把数组当 body 发了),不用会吞成 `{}` 的 `parseBody`。报告**不加包装层**,字段就在顶层——面板与 standalone 两个客户端必须看到同一个字段路径。接线:`createApi` 增第 9 个参数 `maintenance`,`src/index.js` 把实例构造前移到 webServer 块之前并同时传给面板与 standalone(构造晚了面板路由拿到 `null`,功能「看起来加了」但每次都回 503;少传则如实报 503 `maintenance-unavailable` 而不是把 `undefined.reclaim` 落成崩溃,既有 14 处不传第 9 参的调用照旧工作)。回归锁 14 条进 `test/api-routes-gaps.test.js`(鉴权、GET 只读且畸形窗口不落最坏解释、POST 默认 dry-run 且三个改库原语一个都没被调用、确认门回带同窗口报告、执行真清且行数不变只置列、非布尔 vacuum、invalid-json 与 invalid-body 分开、POST 数字域、缺依赖 503、抛错映射 500 且 logger 留住原始错误文本、非 GET/POST 落 404、报告无包装层、执行时 VACUUM 真的跑、确认报告复述调用方要的窗口),面板侧 8 条进 `test/client.test.js`(32 个 i18n 键中英成对且分区真的渲染、路由是 in-process 那条而非数据面、请求体逐字含确认门且全卡只有一处 POST、无 `window.confirm`、卡片真的挂在状态页维护分区下、成功态只在确认门另一侧且 stale 时禁用、VACUUM 失败与回执如实渲染、VACUUM 默认关且非 2xx 带出服务端 error 串且成功后复位确认态)。**独立验证**:第三方复核方独立重做了实现者报的全部 19 条后端与 12 条面板变异(全部 RED),并**额外找出 5 条契约硬要求没被任何锁钉住的覆盖洞**(最严重的是「VACUUM 默认关」——危险默认没有任何测试守住),已逐条补锁并同样做变异检验转红;另有一处诚实记录:去掉 GET 内层 catch 后**只断言状态码抓不住**(外层 catch 也兜 500),必须靠注入 logger 断言原始错误文本留住才算真锁住。**评审期(PR #410)另修三处**:① `olderThanDays` 补**上界** `MAX_RECLAIM_WINDOW_DAYS = 3650`(与 `llmAudit.retentionDays`、`recallRetentionDays` 同值),GET 与 POST 两侧统一——整数校验挡不住量级,`1e20` 也是整数,而 `maintenance.reclaim` 会拿它算 `now() - days * 86400000` 再 `toISOString()`,超出 `Date` 有效范围就抛 `RangeError`;GET 侧还要额外挡 `Number.isFinite`,否则 400 位十进制字面量化成 `Infinity` 会被当成畸形输入静默落回默认窗口 7——调用方要一个巨大窗口,实际只清了 7 天外的东西,且报告上写着 7 看不出异样。② `confirm-required` 分支里那份 dry-run 报告挪进 `try/catch`:它在 `readBody` 的回调里计算,外层 try 那时已经退栈,抛出去就是一个没人接的 rejection——调用方拿到的是「一个字节都没回」,而不是执行路径给出的 500 `internal`。③ 面板「未记录」文案改成「未记录(审计未启用或回执写入失败)」:`writeReceipt` 返回 null 有两种成因(审计被关、或回执写库失败只 warn),旧文案把后者说成前者,而这行出现在不可逆操作的结果区,用户会照它去对账。面板同时补上本地窗口上界(超界直接提示窗口不合法,不发请求,免得用户把 400 看成网络故障)。补锁三条并逐条变异转红(GET/POST 两侧量级、confirm-required 抛错映射 500、文案必须同时点出两种成因)。 ## ⚠️ 行为变更 - **`dreamMaxTokens` 默认值维持 131072,并补上「调小反而更贵」的实测依据(#135 成本审计)**:审计的原始假设是「把默认从 131072 降到 8192~16384 能省 token」,实测否定了它,故**默认值不动**,只把结论与证据写进 `config.js` 注释与文档。三条依据:① 这个键是**上限**不是计费口径(流式按实际用量计费,与 `sleepMaxTokens` 同口径),压上限只在真撞顶时生效,而近 10 天 88 个成功轮的 output 中位 24022、**73.9% 超过 16384**;② 撞顶的后果不是「少烧一点」而是**整轮失败**(截断 → `extractJsonArray` 得 null → `no json array in llm output`),且调度器只有 `ok` 轮才刷新基线(`src/dream.js:905-915`,刻意设计「失败必须能立刻重试」),下一次写入用**同一批输入**重跑;按 133 个有真实用量的成功轮逐轮折算净总量:cap=8192 **+49.7%**、cap=16384 **+54.1%**、cap=24576 +43.3%、cap=32768 +15.9%、cap=49152 +6.5%、98304 起为 0;③ 真正的大头它治不到——把「时间就近」配上的 133 次成功调用的决策正文(merge/update 的 content+title,剥离代码自动补的隐式 keep)按中英分估约 419,487 token,同一批调用输出合计 3,543,111 token,**正文约占 11.8%**(换算方法敏感性区间 8.2%~22.0%,逐轮中位 12.1%),其余约 88% 是推理与 JSON 骨架,病根是推理与正文共享 `max_tokens` 单一池子,能治它的是 `dreamReasoningEffort` 与模型选择。**共享预算这件事在插件侧没有接口可分离**:宿主(DSH)侧确有分离机制(`DEFAULT_THINKING_BUDGETS`、`adjustMaxTokensForThinking()`、`clampThinkingBudgetToAnswerRoom()` 保证正文至少留 1024 token),但**仅 Anthropic Claude 路径默认生效**;chat-completions 网关(volcano-engine / deepseek / glm 均在此列)须由路由 profile 开 `compat.supportsThinkingTokenBudget` 并指定 `compat.thinkingTokenBudgetField`(上游端点还须认这个字段,asar 注释明写 "Off by default"),且两者都是 **profile 级**配置、插件 `ctx.llm.stream()` 无法 per-request 传递——故本次只把这条约束与前置条件写进注释(含「需要旧行为的库显式设回 131072」,以及显式调小仍然允许、但需知悉净成本方向)。**用户可见行为零变化**(默认值与上限都没动)。 - **新增 `dreamMaxMergePerRun`(默认 40):单轮 merge 决策数量闸(#135 成本审计派生)**:merge 是巩固里**唯一回吐正文**的动作——每条 merge 都要求模型重新写出合并后的 content(实测 2113 条 merge 平均 550 字符/条),而 keep / archive / conflict 都不带正文,所以单轮 merge 数是「输出里正文占比」唯一可控的杠杆;此前 update(2) / create(5) / archive(8) 都有上限,唯独 merge 没有。形态与现有三道**完全一致**:全局信号、`dreamSkipInvalid` 也不豁免、超限整单拒绝(run 记 failed)。默认 40 的校准依据(只读探针:全库 246 轮 / 近 10 天 88 轮 `dream_runs.decisions`):每轮 merge 数全库 p50 7 / p90 15 / p99 28 / max 39,近 10 天 p50 9 / p90 20 / p99 39 / max 39 —— 40 即观测分布上沿(历史命中 **0/246**),与 archive 闸默认 8 恰为其 p90=max 是同一校准法:面向未来的尾险护栏,**不是成本削减工具**(现有三道闸在实测分布下命中率均为 0.0%,同一性质)。刻意保守的理由是代价不对称:整单拒绝 = run 记 failed = 基线不刷新 = 同输入重跑,一次误伤约等于该轮成本 ×2;而探针里 merge≥15 的大轮正文/显式决策比例都正常(10~22%),没有失控特征。想用覆盖换成本的用户可自行调低(面板可调,`1`–`200`)。 ## 🐛 修复 - **归档净增速率改用精确归档时刻(issue #275 第二批)**:`memories` 表新增可空列 `archived_at`,`GET /api/dsh-mneme/recall-stats` 的 `archive` 块与面板「记忆复用」卡的归档净增速率从**代理口径**升级为**精确口径**。代理口径取行的 `updated_at`(`setArchived` 翻标志位时刷它),偏差方向是**偏高**:对已归档行做一次普通 `memory_update`,或对同一行重复调 `setArchived(…, true)`,都会被算成「本窗口新进归档」。精确口径下 `setArchived` 只在「活跃 → 归档」那一次落值(单条 `UPDATE` 的 `CASE WHEN ? = 1 AND archived = 0`,`archived` 取更新前值,无 TOCTOU 窗口),取消归档清 `NULL`,**重复归档不刷新**——重复归档不是新一次归档,刷新它等于把旧毛病换个入口复活。`updated_at` 的既有语义**刻意不动**(面板排序、快照哈希、CAS 都读它),两列职责分开:`updated_at` = 最后一次写,`archived_at` = 归档时刻。存量归档行**不回填**:用 `updated_at` 回填等于把被推翻的代理口径固化成看不见的脏数据,留在 `NULL` 至少是「已知的不可知」;读侧 `toRow` 把 `NULL` 读作 `undefined`(与 `sensitivity` / `occurred_at` 同款),新增 `archive.undated` 单独计数这些行,**不计入 `addedInWindow`**(迁移当刻窗口内的存量归档行同样是 `NULL`,既不能证明在窗口内也不能证明在窗口外——用 `updated_at` 顶替是走回代理口径,按 0 充数是把「不知道」伪装成「没有」)。「入库即归档」(`store.save({archived:true})`)落值 = 入库时刻,否则 `archived=1` 而 `archived_at=NULL` 会落进存量行语义、白丢一条当下发生的归档;`update()` 的 patch 不能写该列(否则给精确口径开了绕过幂等/清空语义的后门)。回归锁 `test/store.test.js` 六条(legacy 库迁移建列且存量行留 `NULL`、重开幂等不重复 ALTER 不动已有值、归档落时刻/取消清空、`updated_at` 既有语义不被动、`save({archived:true})` 时刻 = `created_at`、`update` 忽略 patch 里的 `archived_at`)+ `test/recall-stats.test.js` 六条(按 `archived_at` 取时刻、**核心回归锁**:归档后 `memory_update` 刷 `updated_at` 不得算窗口新进且先断言前提成立、取消/再归档落新时刻、重复归档幂等、存量 `NULL` 计 `undated` 不顶替、口径升级不波及其他四项指标)。变异检验三条:把读侧退回 `Date.parse(m.updated_at)` → 3 条转红;写成 `m.archived_at ?? m.updated_at` 兜底 → `undated` 那条转红;去掉 `archived = 0` 条件让重复归档刷新时刻 → 幂等那条转红。 - **存储回收的两个 HTTP 入口收口到一份口径(issue #411)**:面板路由 `/api/dsh-mneme/maintenance/reclaim` 与 Bearer 数据面 `POST /maintenance/reclaim` 底下是同一份 `src/maintenance.js`,但 HTTP 外壳一直是**整段复制**的。于是 #410 修好面板侧之后,数据面还停在旧形态:窗口没有上界(`1e20` 也是整数,`maintenance` 拿它算 `now() - days * 86400000` 再 `toISOString()` 抛 `RangeError`——而它发生在 `readBody` 的回调里,外层 `try` 那时已经退栈,调用方拿到的是**一个字节都没回**,不是 500),确认门里那份 dry-run 报告同样没兜错。现在口径(错误码字符串、默认值、报告形状、`MAX_RECLAIM_WINDOW_DAYS = 3650`、确认提示、warn 文案)只写在新的 `src/reclaim-http.js` 里,两个入口都调 `parseReclaimQuery` / `parseReclaimJson` / `readReclaimFields` / `reclaimReply`,各自只留鉴权与路径。**顺序也是契约的一部分**:`invalid-json` → `invalid-body` → 503 `maintenance-unavailable` → 字段校验——端点整个没接线时报「窗口不合法」是误导(调用方会去改窗口,改完照样 503),所以两侧都补了锁钉住这个先后。防复发不靠注释(段头那句「改这里时那边一起改」正是漂移发生的地方),靠 `test/reclaim-http.test.js` 的静态锁:`invalid-older-than-days` / `invalid-vacuum` / `confirm-required` / `maintenance-unavailable` / `storage reclaim failed` / `MAX_RECLAIM_WINDOW_DAYS` 这六个字面量在 `src/api.js` 与 `src/api-standalone.js` 里**一个都不许出现**,同时两个文件必须真的 import 共用模块并调用它——光删字面量不接线等于把功能删了。新增测试 14 条:`test/reclaim-http.test.js` 13 条(解析、校验、答复形状、抛错、防分叉),`test/maintenance.test.js` 1 条从**数据面自己的端口**打真 HTTP(窗口上界、确认门报告复述被问的窗口、勾了 VACUUM 就真透传),另给面板侧与数据面的 503 各补一条顺序断言。11 条变异逐条打回坏状态全部转红(含「把口径复制回数据面」「数据面不再调共用答复」这两条防分叉锁本身)。 - **面板文字对比度全面达标 WCAG AA(issue #178)**:面板有 270 条 `color` 声明,此前 **70 条**在浅色或深色主题下达不到 AA(正文 4.5:1、大字号 3:1;浅色 65 条、深色 5 条,最差的两条 `.mneme-statfoot` 只有 1.26 / 1.64)。根因是面板直接拿宿主的**标签层** token 当正文色(`label-tertiary` 50 处、`label-dimmed` 4 处):这些是给分隔线/占位符用的低对比层,不是正文层。另有三处**图形**同病(不在文字口径内,按 3:1 复核):热度条与热度点的冷档用 `label-dimmed` 压在热度轨道上只有 1.14:1、关系图连线描边 1.26:1,等于把三档热度画成同一个看不见的灰。修法不是逐条换颜色,而是在四个渲染根上定义**面板自己的语义色层**:`.mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry{…}` 六条 `--mneme-*`(`fg-dim` / `graphic-dim` / `accent` / `danger` / `warn` / `ok`),每条都用 `color-mix()` 从宿主 token 混出——`fg-dim` = `label-primary` 60% + 透明(浅 4.94 / 深 6.30),其余同法。**四个根一个都不能少**:`.mneme-overlay` 与 `.mneme-topentry` 是 `createPortal` 出来的独立子树,`.mneme-triggerwrap` 挂在宿主 sidebar footer 插槽,只挂 `.mneme-x` 的话这三个根拿不到变量、静默退回旧色。顺带修掉 **5 个幻影 token**(`--dsw-alias-state-error` 19 处、`state-warning` 14 处、`state-success` 3 处、`border` 2 处、`state-warn` 1 处):这些名字宿主从未定义过,一直靠 CSS 兜底字面量在跑,宿主改配色时面板不会跟——现全部换成真名(`state-error-primary` / `static-amber-600` / `border-l1` 等)。另需分清:`label-dimmed`(4 处)与 `label-tertiary`(50 处)**都是真实存在的宿主 token**,问题不在名字而在用法——宿主把它们定位成分隔线/占位符的低对比层,面板却拿来当正文与图形色。三处 knockout 配色一并修:`.mneme-conflict-primary` 浅色主题下白字压品牌蓝 `#4176e6` 只有 4.23:1,底色换成 `--mneme-accent`(浅 5.32 / 深 7.67);`.mneme-entrybadge` 深色主题下 `bg-layer-1` 压 `#c33` 只有 3.06:1,底色换成 `--mneme-danger`;`.mneme-btndangerconfirm` 底色同步换成 `--mneme-danger`,并把写死的 `#fff` 换成 `var(--dsw-alias-bg-layer-1)`——它旧的写死白字只在旧底色上侥幸过关,底色一变浅就会在深色主题掉到 2.67:1。回归锁 `test/contrast.test.js` 五条:①把宿主 token 的浅/深取值快照硬编码进测试(CI 上没有宿主包,读不到 asar),逐条重算全部 `color` 声明的对比度,两主题都必须过,**解析不出的颜色记失败而不是跳过**(否则改成 `var(--mneme-unknown)` 就能静默溜过);②面板引用的每个 `--dsw-*` 必须在快照表里存在(幻影名回归锁);③语义色层的选择器必须恰好是那四个根(少了根主锁照样全绿——主锁只算颜色,不知道变量有没有继承到);④图形色(`background` / `stroke`,非 `color`)单独按 3:1 复核——热度条、热度点、打卡点、关系图连线都不在主锁的视野里;⑤虚线关系边不得靠 `opacity` 区分(无 `memory_id` 的边同时命中 `.mneme-gedge` 与 `.mneme-gedge-dashed`,后者的 `opacity:.7` 把 50% 描边压到浅色 2.28,而图形锁只读单条规则、看不见这种叠加)。变异检验七条:改淡 `fg-dim`、一处正文用回宿主标签色、塞回幻影名、渲染根少挂一个、热度冷档改回宿主 `label-dimmed`、一条 `color` 改成解析不出的 `var(--mneme-unknown)`、虚线边加回 `opacity:.7`,七条锁分别转红。 ## 🧹 工程 - **benchmark 诚实性写进 CONTRIBUTING(中英双语)**:本仓的调研材料里混着自报数字与元数据矛盾(竞品 README 的「token 降 72%」「1372× 吞吐」;`NOASSERTION` 许可证却挂 MIT 徽章)。新增一条纪律:只引可复现的数字;引用他人的 benchmark 必须标注**自报**还是**独立复现**;无法核验的声称按宣传处理、不得进我们的文档;借鉴设计读 README 就够,借鉴代码先读 LICENSE 原文——`NOASSERTION` + MIT 徽章等于「未定」,不等于 MIT。 ## [0.8.15] - 2026-10-10 ## 🆕 新增 - **面板暴露后端已有能力(issue #386 第一批)**:三张只读卡补齐「后端已实现、面板未暴露」的信息盲区。① 状态页「库内一览」新增「Markdown 镜像」健康卡,读 `GET /api/dsh-mneme/health` 展示 `mirror.status`(ok/degraded/unknown)、未落盘提示、`last_error` 三码(磁盘满 / 权限不足 / 同步失败)的人话映射与上次尝试/上次成功时间——面板侧与后端同为 **fail-closed**:请求失败或 unknown 一律显示「未知」,绝不显示「正常」(镜像写失败此前在库内完全静默);② 设置「连接与安全」新增「独立服务 `dsh-mneme-serve`」卡(一行示例命令 + 复制,并提示它与外部访问 API 默认抢同一端口、令牌同源);③ 同组新增「MCP 服务 `dsh-mneme-mcp`」卡(可复制的 `.mcp.json` 挂载片段,数据面走外部访问 API 的 Bearer)。三张卡纯展示,不做进程启停/守护/端口探测,不加配置键;示例命令必须带 `-p @modusensus/dsh-mneme`——bin 名与包名不同,裸 `npx` 会去装一个不存在的同名包(`test/client.test.js` 有字面量锁)。 - **记忆库落点检测告警(issue #368)**:`memoryDir` 落在宿主 profile 作用域(形如 `…/profiles/<名字>/…`)时,启动打一条可操作告警——#218 末楼记录过 profile 被清理导致整库连带丢失的实际事故。判定是纯路径段匹配(跨平台分隔符、大小写不敏感,只认显式 `profiles` 段,不做「看起来像用户数据」的猜测:漏报可容忍、误报等于狼来了)。**只检测不搬迁**:不改行为、不动数据、不加配置键、不进 settings 白名单(路径配置不是行为开关,与 `documentDir` 同口径),存量用户零迁移。根 README 中英成对补「记忆库落点建议」节。 - **注入前判定 `preInjectGate`(issue #380,E12 D1 裁决落地,opt-in 默认关)**:每帧注入候选出池后、进入 system prompt 前,一次**池级** LLM 调用判出「会向本次请求注入意见/立场」的记忆(判定协议与 E12 实验 protocol-preinject.txt 同源;E12 实测判定+过滤把谄媚率 26.9%→2.5%,且 MemGauge 三阶段考卷实测写入/巩固/检索预算三面对内容毒均无防线——这是唯一过预注册判据的机制)。两级语义复用 #254 writeAdmission:`enabled` 观察档(判定完成落 `llm_audit_logs`,真实 token 用量 + 被标记记忆 id,先看真实负载的意见占比分布)、`enforce` 缓存命中帧真滤除(E12:判定+标记比过滤差 +17.6pp,判定结果**绝不进模型上下文**)。宿主注入渲染是同步回调 → 判定走 async prefetch + cache 同步消费(queryVectorCache 同款),冷缓存首帧降级为原样注入、下一轮同 key 渲染生效;判定失败原样注入 + `degraded` 审计行(审计行**无条件落**,无默认模型路由时 `model_id` 落 `unknown` 占位),防线故障永不阻塞注入;连续降级熔断一个冷却期(判定器系统性坏掉时不逐帧花钱,到期自动恢复)。审查加固(#382 评审):判定与滤除移进 `service.injectCandidates` 的 `touchRecalled` **之前**(与 strictScope 同纪律——留在调用方做时,被滤掉的记忆照样刷温时钟并落 `mode='inject'` 曝光账,反馈环会把闸门想压下的意见记忆重新顶上来),`recall_runs` 的「实际注入集」随之只吃滤后集合;**整池被标记**时放弃本帧 enforce(fail-open:记忆正文可影响判定模型,一条指令型记忆不该让整个记忆块静默失明),形态即审计行的 `n_flagged == n_pool`;`clear()`(dispose 钩子)后置 disposed、在飞判定不回写缓存;缓存命中刷新序(LRU)。pin 池(#249 逐字保真)与 graphHint 线索行豁免;候选映射 `m#` 局部序号(E13b 实测 UUID 回显脆弱);缓存键 = 查询 + 候选 `id:内容` 集(轮换/新记忆/内容被就地修正即重判);`llmAudit` 关闭时本闸不运行;lightMode 强制关(`applyLightModePreset` 支持点分键)。白名单 + 计数锁 +2 + 文档(CONFIGURATION.md 新节)。 ## ⚠️ 行为变更 - **`dreamMergeGuard` 默认值 `false` → `true`(issue #339 / E8)**:巩固 merge 护栏自 v0.8.13 引入时是 opt-in 默认关,现改为默认开——合并对象命中长保留类型(与 archive 护栏同表:preference/pattern/rejected_solution/constraint/pitfall)的 merge 决策整条跳过,`dreamSkipInvalid`(默认开)下被跳条目进 `dream_runs.skipped`、run 记 degraded。依据是它引入时就已写明的机制:archive 护栏只挡 archive 不挡 merge,而 merge 的「更精炼摘要」恰是约束失真的主通道——E8 实测巩固损耗里 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉;同类压缩管线的实测也显示安全规则被同速压缩后一轮只剩 53%、五轮只剩 10%,且全程无报错信号。需要旧行为的库显式设 `dreamMergeGuard: false`(面板可关)。 ## 🐛 修复 - **面板 `memory_runtime verify` 的假失败:验证跑到包内去找模型(#387)**:`embedModelCacheDir` 的约定是「空 = `~/.dsh/mneme/models`」(README / CONFIGURATION.md / LOCAL_MODEL.md 同口径),local-embedder 与 reranker 也都按 `String(x ?? "").trim() || 默认` 解析;但 `tools.js` 的 verify 分支把这个值原样交给 `verifyPayload`,而默认 engine 的 `if (cacheDir)` 让空串等同于「不设置 `env.cacheDir`」——transformers.js 于是转去找包内相对路径,报错指向 `node_modules/@huggingface/transformers/models/Xenova/...`。真实嵌入用的是用户目录那份(所以检索一切正常),只有面板验证在包内找,报出 `functional: false` 的假失败;CLI(`scripts/mneme-runtime.mjs`)自己解析了默认目录,于是同一份运行时「CLI 通过、面板失败」。修法是把解析收进 `src/runtime/verify.js` 的默认 engine(新增导出 `resolveModelCacheDir`),让「验证与真实推理用同一份模型」成为模块自身的不变量——任何调用点传空值都不会再验错地方(收在调用方只能修掉这一处)。顺带补同一调用点的第二处信息缺失:verify 此前不传 `model`,恒探默认小模型 `Xenova/bge-small-zh-v1.5`(与 `localEmbedModel` 默认值同字面量,因此只有换过模型才暴露),用户换模型时那份结论不是他实际使用的那份,现在按 `config.localEmbedModel` 探(空值仍落默认,默认配置逐字节不变)。回归锁四处:默认 engine 省略 / 空串 / 纯空白 cacheDir 都落用户级默认目录、显式目录不被默认值顶掉、`cacheDir` 与 `model` 沿线透传、以及走面板同一条路的端到端用例(`memory_runtime {action:"verify"}` → `verifyPayload` → 默认 engine,假 transformers 入口记录它收到的模型与目录);兜底做变异检验(把 `resolveModelCacheDir` 还原成旧行为时三条用例变红)。 - **事务内创建的记忆实体抽取被静默丢弃(#372)**:`scheduleEntityExtraction` 在事务内直接 return(注释写 deferred,实际 `transaction()` 从不补跑),而自动蒸馏的全部写入都裹在 `service.transaction()` 里——`session:*` 来源的记忆自 0.8.x 起 100% 抽不到实体(报告者库内数据:9-25 后 337 条自动蒸馏记忆 0 抽取 0 审计)。修法:事务内入队、COMMIT 成功后由事务本体补跑(回滚路径丢弃,绝不对已回滚的行抽取);`saveWithDedupe` 的合并分支补触发抽取(此前并入的新内容永不进实体面;`saveAttr` 按 (entity_id, attr_key) 先失活再插入,重抽幂等);`writeAudit` 在 LLM 路由解析失败(modelId 为空)时对非 ok 状态补一条 warn——消灭 #108 同款「零实体零日志」盲区。报告者(lqs50)三条主发现全部对源码坐实;其建议的「finally 里补跑」已修正为仅 COMMIT 成功后补跑。 ## 🧹 工程 - **code-scanning 依赖告警清零(`sharp` 0.35.4 → 0.35.5)**:GitHub code-scanning 上仅剩的一条 open 告警是 `sharp@0.35.4` 命中 `GHSA-wq5f-xc86-pv6w`(CVE-2026-96889,severity high,修复版 **0.35.5**——随包把上游 librsvg 带到 2.63.2)。它在 mneme 里不是直接依赖,而是 `@huggingface/transformers` 声明的 `^0.35.4` 传递依赖(本地嵌入运行时构建面,npm 用户装不到)。只改 lockfile 不够:`runtime-manifest.json` 把 `sharp` 与 23 个 `@img/sharp-*`(libvips 1.3.3)连 tarball 与 sha512 一起钉死并分发给用户,而扫描器看不见 JSON 清单,漏洞版本会继续发出去。故 lockfile 与清单同步升到 `sharp 0.35.5` + `@img/sharp-* 0.35.5` + libvips `1.3.4`(同一 `^0.35.4` 区间内,无 API 变更)。可达性本身很低——mneme 只做文本嵌入与重排,没有任何 `sharp()` 调用(不解码 SVG),RCE 还需 glibc Linux 且要攻击者提供 SVG;修的理由是升级免费且清单面向最终用户。`npm audit`(含 dev 与 `--omit=dev` 双口径)0 vulnerabilities;全量测试 1561/1560 pass/0 fail/1 skip。 - **npm 包 `files` 补发 `docs/`,`allowScripts` 对齐 lockfile**:README 有 9 处相对链接指向 `docs/*.md`,而 `files` 白名单不含 `docs`——这些设计文档在 npm 页面全 404(GitHub 上正常)。白名单加入 `docs`(8 个 md 随包发布)。同处清掉另一笔陈旧账:`allowScripts` 还写着 `onnxruntime-node@1.24.3` 与 `sharp@0.34.5`,实际锁文件是 `1.30.0` / `0.35.5`,而 sharp 0.35.x 已不带 install 脚本——按 `1.30.0` 保留 onnxruntime 的 postinstall 白名单、删除 sharp 条目(#384 明确欠的后续)。 ## [0.8.14] - 2026-10-06 ## 🐛 修复 - **strictScope 硬过滤被图召回绕过(#371,#17 A3 的图召回分支)**:`injectCandidates` 的硬过滤原本长在 entityRecall 图召回合并**之前**——entityRecall 本身不做 scope 门控,被过滤出局的显式他域行会被图召回重新打上 `graphHint` 带回候选池,`graphInjectHint` 开启时作为 [检索线索] 行进入注入块(宿主自动注入与 standalone `/context` 同受暴露;为 #370 端点做可达性评审时由 CodeRabbit 发现)。修法是把硬过滤移到图召回合并之后、pin 池之前——顺带保证越权行进不了 pin 池;A2 软加权语义(auto / 存量他者保留可见)不受影响。暴露面 = `strictScope` + `entityRecallEnabled`(`graphInjectHint` 决定是否泄入注入文本)多开关同开,默认全关。回归锁配变异检验(还原旧过滤位置测试变红)。 ## 🆕 新增 - **standalone API `GET /context`(issue #370,#363 承诺)**:把宿主注入管线开放为端点——Mneme-Bridge 这类网页端桥接可以删掉自建的 pins 启发式了。`GET /context?q&topK&threshold&agent_scope&workspace_scope` 返回 `injectCandidates` 全语义(优先级分层 / 编码门控 / heat / scope 软加权 / hybrid 语义路 / pin 前置)+ 画像 + 规则的结构化聚合 `{profile, rules, items, pinnedCount}`,不含渲染文本(语言 / 时间前缀 / 热记忆是宿主会话概念,调用方自渲染)。与宿主注入的两点 HTTP 面差异:查询向量即时嵌入——首次调用即走语义路,不等异步 prefetch(宿主注入渲染必须同步);不做跨轮轮换(daemon 无会话状态,调用方自管)。`threshold` 是 importance 阈值(默认 3,与 `/search` 的相似度 threshold 同名不同义);scope 两参全缺 = 不解析(软加权与 strictScope 硬过滤都不触发,与宿主会话身份取不到时同构),任一给出则缺维按 fail-closed。recall_runs 记账随 `injectCandidates` 内建(#217 曝光型访问事件,`mode='inject'` 与检索命中同表分账)——宿主注入同样记,端点自动同口径。`/search` 顺手补 scope 透传(`searchMemories` 本就收参、8790 此前没有入口;不传参数行为逐字节不变);`createStandaloneApi` 新增可选 `embedder` 注入(宿主与 `dsh-mneme-serve` 各传 semantic 装配的同一实例),缺省时 `/context` 降级规则 + BM25 档,嵌入失败同理绝不 500。宿主外部访问与 daemon 同享该路由;测试锁形状(四键)、空 q 规则档、threshold 语义、pin 前置、scope 硬过滤、嵌入接线与失败降级,daemon 侧全链路含 recall_runs 分账锁。 - **daemon 向量检索(#365,#363)**:`dsh-mneme-serve` 的 `/search` 接入完整语义管线——embedder/reranker 装配与 boot 自动回填从 `index.js` **纯搬移**至 `src/semantic.js`(宿主与 daemon 共用同一份,调用时序契约原样;`backfillMissingEmbeddings` 经 index.js barrel 再出口,测试调用方零改动),daemon 侧新增 `createVectorIndex` 接线与 `--embed` 参数:`local`(默认,自管 runtime/嵌入模型缺失时经 `provisionRuntime` download 档自动取件,可用 `DSH_MNEME_RUNTIME_TARBALL_DIR`/`DSH_MNEME_RUNTIME_MIRROR` 换离线/镜像来源;失败降级关键词并打可操作日志)、`ollama`、`openai`(读宿主面板 vector-config)、`off`。向量轴有注入假 embedder 的回归锁;`createServeRuntime` 因此转为 async、语义键默认值在 `daemonSemanticCfg` 逐键锚定 config.js。 - **独立服务 daemon(`dsh-mneme-serve`,#363)**:mneme 现在能在 DSH 宿主之外常驻——`src/serve.js` 的 `createServeRuntime` 用最小装配(store → settings → mirror → service → maintenance → standalone API,每步锚定 index.js 装配行号)把数据面跑成独立进程,第三方集成(网页端桥接等)不必为挂载记忆库而保持 DSH 开机。第一期刻意无 LLM:巩固(autoDream)与蒸馏结构性不在 daemon 内,这是与宿主「单写者」的机械保证,不靠用户自觉。token 与 DSH 面板/CLI 共用同一 kv 凭证,端口/主机解析链与外部访问一致;`createStandaloneApi` 新增 `strictPort` 选项——daemon 的配置端口被占即报错退出而非顺延(第三方把 URL 写死,静默换端口等于坏),不传该选项的宿主旁路行为不变。`/search` 照常落 recall_runs,第三方检索的复用统计不缺数。多进程共存(daemon 与宿主同库互写互读)有专门回归锁;已知限制(双进程去重竞态、镜像双写、版本偏斜)见 docs/DAEMON.md。 - **heat 注入侧拆独立开关(`injectHeatEnabled`,opt-in 默认关;issue #218)**:heat 乘进注入排序在真实年龄混合的库上会饿死老约束(对照实验里 importance-only 排序的注入遵从显著更优,现行量级表现为拟合参数),注入侧因此拆出独立开关:默认关时 `heatEnabled` 开启后热度字段 / sleep 保护 / 面板投影照常,但注入排序乘数恒 1(与 heat 关闭逐字节一致,平价锁);显式开启恢复「层内乘 heat」的旧序(老用户回滚口)。召回侧时钟(touchLastAccess)与 sleep 降级联判不受本键影响。开关纪律照旧:config schema 与 settings 白名单成对注册,旗标计数锁同步。 ## 🧹 工程 - **发布准备脚本在 CRLF 检出上不再假成功(`scripts/release-prep.mjs`)**:该脚本用 `/^(# Changelog\n\n)/` 匹配 CHANGELOG 文件头,而 Windows 检出是 CRLF——正则命中不了,`replace` 退化成空操作,**脚本却照样打印 `✓ … 占位节`**,`git status` 里看不出任何异常(CI 跑在 ubuntu 是 LF,所以只有本机发版会中招,v0.8.13 那次即如此、最后靠人工补的占位节)。规则抽成 `dsh-mneme/scripts/changelog-prep.mjs` 的纯函数:行尾两种都吃、插入内容跟随原文件行尾、带 BOM 也认;匹配不上则如实回报 `header-not-found`,入口**报错退出(exit 1)**而不是假打印成功。配 6 条回归测试(LF / CRLF / BOM / 幂等 / 回报契约 / detectEol)。 - **standalone API 的 `/status` 版本号不再硬编码**:该端点此前返回模块内的字面量常量,发布时不会随 `package.json` 一起 bump——手上的 0.8.13 实例对外报的是 `0.7.12`,第三方集成据它判版本会读到假数据。现改为复用 `version-check.js` 的 `PACKAGE_VERSION`(运行时读 `package.json`,宿主侧 `/api/dsh-mneme/status` 走的就是这条),测试断言同步改成锁 `PACKAGE_VERSION` 而非字面量——旧断言把常量钉死,正是「发布漏 bump 而 CI 全绿」放行的原因。 ## [0.8.13] - 2026-10-03 ## 🐛 修复 - **密钥 / PII 判据三处加固(`src/sensitive-scan.js`,PR #356,均为合并后独立复审发现)**:① **回溯有上界**——`email` 的 local part 与 `connection_string` 的 scheme 都作用在含 `.` 的字符类上,缺上界时每个起点都要一路重扫到结尾才失败(O(n²)),而判据在写入路径上同步跑:实测 34KB 点分链 0.6 秒、120KB 对抗串 23.5 秒(email 18.6s + 连接串 4.1s),等于把写入卡死;加上界后同输入约 60ms,边界取 RFC 5321 给 local part 的 64。② **赋值型规则左边界不再排除 `_`**——环境变量名正是拿 `_` 当分隔符,原写法让 `DB_PASSWORD=` / `MY_API_KEY=` / `MYSQL_PASSWORD=` 这类「前缀_关键词」整类漏放(只有恰好落在行首的 `API_KEY=` 能中);放宽后 #332 的 26 条语料仍全绿,挡误杀的仍是那道占位符守卫。③ **身份证档补校验位**(GB 11643 / ISO 7064 MOD 11-2)——原先只有银行卡档有 Luhn,18 位纯数字(订单号 / 内部编号)先被身份证规则命中,等不到银行卡那条的校验。三处都配了回归测试,并做过变异检验(改回原写法各自变红)。判据仍在 `sensitiveScanEnabled` 默认关之后,线上行为不变。 - **strictScope 硬过滤被 `entity:` / `attr:` 前缀检索绕过(#17 A3 的漏网分支;issue #357 / PR #358)**:`searchMemories` 里这两条前缀路在**函数入口**就 return,而硬过滤写在**后半段**的融合池上——早返回的路根本走不到,于是显式标注为他者 scope 的记忆换这两个前缀就能原样读出,而且是**满分**返回(连 A2 的 ×0.5 降权都没有)。暴露面是 `scopeEnabled` + `strictScope` + `entitySearchEnabled` 三者同开(默认全关),而 `entity:` 正是 #24 图谱线在推的语法。修法:scope 闸抽成 `gateByScope()` 单一实现,融合池与两条前缀路三处共用,让「过滤点写在哪」不再漂移(同 #349 把阈值口径收进 `activeStoreSize()` 的理由);闸门插在 `topK` 截断**之前**(先 filter 后 slice)——反过来会让出局的候选占掉名额,`topK=1` 且首位出局时直接返回空数组;同时闸门必须在 `touchRecalled` **之前**,否则出局的行仍会被刷回温时钟、并在被动确认开启时 bump 关联边,命中反馈落到了调用方本不该看见的行上。`strictScope` 关(默认)时逐字节不变;他 scope 行的 A2 软加权要不要一并补到这两条路,按 #339 的口径另行决定。 ## 🆕 新增 - **写入边界的密钥 / PII 判据(`sensitiveScanEnabled`,默认关;PR #354)**:写入准入(#254 第 1 级)此前只跑空白 / 噪声两类判据,密钥 / PII 那一档按设计留了注入点而没实现。现在补上 `src/sensitive-scan.js`——纯确定性、零 LLM,先认形状再认关键词(赋值型规则带占位符守卫,所以「把 API key 放进环境变量」这类讨论句不报)。命中落审计 `metadata.deny.reason='sensitive'` + `kind`(密钥 / PII 分档,便于先看分布再决定放行策略),审计位与 #332 定的形状一致。开关分层:本键只决定「这类判据参不参与」,命中之后是仅告警还是真拦仍由 `writeAdmission.enforce` 决定(默认仅告警、拦截 opt-in)。回归样本集(10 条密钥 + 4 条 PII 正样本、12 条负样本)原样跑真判据:正样本不漏、负样本不误杀。 - **能力说明补 scope 声明规则(`memory_save` 工具描述;PR #355)**:`memory_save` 的 `workspace_scope` / `agent_scope` 是必填面,此前的指引只讲「该不该写」,没讲「写给谁看」。现在在 `memory_save` 的工具描述尾部补一条判断规则:只在记忆确实只属于一个 workspace / 一个 agent 时才标注,否则两个都留空(未标注 = 处处可见,是安全的默认)。写进工具描述而不是总则——它是单工具的判据,总则那五条讲的是「何时查 / 何时写 / 何时 no-op」,加第六条会把单工具语义抬成全局纪律。#249 第二批;注入时机(N0 能力说明 / N1 分池 / N2 尾声提醒)未动工。 ## 🧹 工程 - **发版说明与流程收尾**:CHANGELOG 条目去掉非公开单号与实验代号,并新增 `### 贡献者 / Thanks` 小节;CONTRIBUTING 补两条发版惯例。 ### 贡献者 / Thanks - **@heptaspirit** — 写入边界的密钥 / PII 判据(PR #354)与 `memory_save` 的 scope 声明指引(PR #355)。 ## [0.8.12] - 2026-10-01 ## 🐛 修复 - **巩固阈值基线跨重启持久化(issue #89 基线半边)**:#291 只把时间闸门 `lastRunAt` 从 `dream_runs` 恢复了回来,阈值基线 `baseline{count,chars}` 仍是调度器闭包里的内存变量,进程重启归零。库里记忆远多于阈值(默认 10 条 / 5000 字符)时,重启后第一条写入(`notifyWrite` → `dreamHook` → `maybeSchedule`,更新类写入同样计入)就让 `overBase` 成立——**阈值被整个绕过**,直接开跑一整轮巩固。0.8.11 实测一天四次重启,其后第一轮巩固发生时窗口内只新增 6 / 7 / 0 / 8 条记忆(1427 / 1488 / 0 / 1509 字符),每轮约 16 万 tokens(consolidate ≈7.5 万 + 总览 ≈8.8 万,`llm_audit_logs` 实测)。现在成功轮把「本轮结束后的活跃库规模」写进 `dream_runs.store_count` / `store_chars`(新增两列,幂等迁移,两列同时有值或同时为 NULL),调度器构造时按 `run_type='auto'` 播种;认不出行(新库、升级后还没跑过成功轮、老行两列为 NULL)就退回零基线——**不假装知道基线**,行为与升级前逐字节一致。阈值口径抽成 `activeStoreSize()` 单一实现,`shouldTrigger` 与落库共用:两处各写一份过滤条件的话,重启播种读到的基线会与运行期判定漂移(一个把 document 算进去、一个不算),而这种漂移只在重启后才显形。run 自报的基线与审计行是同一份数字,进程内刷新改用它,两条路径不再各算一次。 - **蒸馏 JSON 崩溃窗口的 salvage**:实测 10-20% 的蒸馏窗口输出了 含记忆条目的完整 JSON 数组,只因中段一处语法错误(如杂散引号)被 `parseSummaryJsonResult` 整窗拒收——失败路径的生产行为是游标不推进 + 温度 0 重试同文同错 = 该窗口记忆静默丢失, 无任何用户可见信号;再蒸馏场景(摘要再当输入)崩溃率翻倍。修复:解析失败时做括号配对 双遍扫描(字符串感知 + 盲扫,去重合并)截出完整的顶层对象逐个 parse,救活的条目走正常 白名单校验后照常写库、推进游标;审计 `metadata.json_salvaged: true` 留痕。截出 0 条不视为 「显式空数组」,窗口保持可重试;lib-smoke 的三类退化输入行为不变。 - **工具结果新旧两种形状都收(PR #353)**:蒸馏采集 `tool/result` 事件只认旧形状(载荷与 `isError` 装在 `data.message.content` 的 `type: "tool-result"` 块上),而 DSH 0.2.x 起内核直接投递 `role: "tool"` 消息本身(`content` 只有 `type: "text"` 块、`isError` 挂在消息上)——本机 14 个 v4 会话实测 `tool/result` 1095/1095 全是新形状,工具输出与工具报错**被整批静默丢弃**。现在两种形状都收,下游 `textOf` / `isError` / `trim` 一律不动。感谢 @dustinmoon78。 ## 🆕 新增 - **图谱锚定层 Activation Anchoring(issue #24 块1)**:在 `entityRecallEnabled` 的「查询命中实体 → 挂联记忆进融合池」之上再加一层级联——命中实体作锚定种子(`graphSeedCap` 封顶,默认 12),沿 `entity_relations` 按 `graphCascadeDepth`(默认 2)向邻居扩散,邻居挂联的记忆以跳数配额权重(1:2:3-hop = 0.5/0.3/0.2)补位进池,恒低于种子直达分(attr 1.0 / relation 0.9)。级联 BFS 逐层懒取邻居(>1 跳真正可达),邻接查询双向绑定(方向写反的关系行不丢)。默认关=检索行为与 #219 单跳轴逐字节一致(关档连种子裁剪都不生效)。新增三键 `graphAnchoringEnabled`/`graphSeedCap`/`graphCascadeDepth` 注册进 feature_flags 白名单,面板可启停;lightMode 强制关。 - **边权重演化 Weight Evolution(issue #24 块2)**:`entity_relations` 加 `weight`/`source` 两列(幂等迁移,存量行按 manual 1.0 认账)——建边来源决定初值(manual/confirmed 1.0、llm 0.4、tag 0.3),LLM 抽取自动建的边标 `source='llm'`;演化走 `bumpRelationWeight`(只加不减、封顶 1.0,单条 `UPDATE ... MIN(1.0, weight + ?)` 原子抬升,负/非有限增量在存储边界拒绝)。新增两键 `graphWeightEnabled`/`graphWeightDelta` 注册进 feature_flags 白名单,面板可启停;lightMode 强制关。触达侧接线见块4(被动确认)。 - **关联提示防幻觉 Injection Budget(issue #24 块3)**:图召回候选进注入时默认只参与排序、不改变注入块构成(`graphInjectHint` 关,保守档);开启后以固定句式「[检索线索]」标注、并在首个线索行前加引导语(链路信息非事实断言),条数由独立预算 `graphInjectBudget` 约束——既不受 document 预算约束、也不挤占 `maxItems` 槽位。线索行不进 pin 池(pin 会在保守档下把它放回块内,绕过开关)。新增两键注册进 feature_flags 白名单;lightMode 强制关。 - **被动确认 Passive Confirmation(issue #24 块4)**:把「人工认可/删除/忽略」的确认门槛收敛成例外管理——正常触达(记忆被召回/注入)即视为对挂联关系边的被动确认,`store.getRelationsByMemory` 反查关联边后 `bumpRelationWeight` 抬一格(只加不减、封顶 1.0,自激回路由封顶遏制)。`graphPassiveConfirm` 与 heat 是独立闸门(`heatEnabled=false` 时仍可演化边权),且受块2 总闸 `graphWeightEnabled` 约束——两键同开才生效,单开通道键绕不过「演化默认关」。仅异常路径暴露给用户复核的复核 UI 留后续块。新增键注册进 feature_flags 白名单;lightMode 强制关。 - **`dreamMergeGuard`(opt-in 默认关)**:巩固 merge 护栏——合并对象命中 长保留类型(与 archive 护栏同表:preference/pattern/rejected_solution/constraint/pitfall)的 merge 决策整条跳过。实测巩固损耗里 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉 (archive 护栏只挡 archive 不挡 merge,「更精炼的摘要」恰是约束失真的主通道)。 `dreamSkipInvalid`(默认开)时被跳条目进 `dream_runs.skipped`、run 记 degraded;关闭时整单拒绝。 dream 与 sleep 两条链路同一判据;白名单 + 计数锁 +1。 ### scope - **A2 软加权补齐到注入通道**:此前 ×0.5/×1.25 只作用于 searchMemories, 实测「explicit 标注 + 软档」的注入集与无标注逐条相同(80/80)——自动注入这个主泄露面上 软档形同虚设,而文档本就承诺「关闭时全部为软隔离」。现 `scopeEnabled` 开启且会话至少一维 可解析时,注入规则路比较器在层内数值积乘 `scopeMultiplier`(priority 档位不动)、selectiveInject 相似度重排乘同乘数;未激活时乘 1,排序与改动前逐字节一致。strictScope 硬过滤保持在软加权 之前,与检索侧叠加顺序一致。 ## 🧹 工程 - **基线持久化回归集 9 条,全量测试 1441 → 1450**:无种子对照组与播种组同库同写入(一个仍触发、一个被拦下——锁的正是这个差异,否则播种被忽略时两条断言同真同假)、阈值照常累积(+10 条仍触发)、run 自报基线优先于现场计算(运行后再长 15 条仍不触发)、成功轮落库 / 失败轮不落、sleep 行不进 auto 基线、半个基线被拒、老库补列幂等、非法种子(半个 / 负数 / 非整数 / 非对象)一律按「没有基线」处理、收尾取证失败既不反噬审计行也不写假基线、`index.js` 接线源码锁。**变异检验**:把播种改成忽略种子(`toBaseline(baselineSeed)` 恒为空)→ 2 条变红;把「成功轮落基线」分支短路 → 1 条变红。 - **宿主开发依赖对齐 0.2 线(PR #344)**:`peerDependencies` 自 #121 起就声明覆盖 0.2.x,`devDependencies` 却停在 `^0.1.0-rc.6`(0.x 语义 = `>=0.1.0-rc.6 <0.2.0`,永远装不到 0.2 线)——**声明支持 0.2、CI 却从没跑过 0.2**,而桌面端 DSH NEXT 内置内核已是 0.2.0-rc.1。开发依赖对齐 0.2.0-rc.1,lockfile 与 CI 真落在声明覆盖的版本上。 - **brace-expansion 钉到 5.0.12(PR #345,CVE-2026-102276/102277/102278)**:osv-scanner 三条告警(两条 high 栈耗尽 DoS + 一条 medium 二次时间 DoS)全落在 devDependency 链的同一包上——实际打不着(不被仓库自身 import),但 lock 合规仍要清;overrides 抬 pin 号即清零,`npm audit` 双口径 0 vulnerabilities。 - **测试数口径收拢到共用模块(PR #346)**:双 README 的测试数以五种形状重复(1 徽章 + 4 条命令注释),而两条写入路径各存一份替换规则——release-prep 只覆盖徽章 URL、`badge:sync` 才覆盖全部,**自动路径反倒成了漂移来源**(v0.8.11 发版后徽章 1437、注释停在 1431);替换规则收进共用模块,两条路径单一实现,下次发版不再各改一半。 - **CI 加宿主版本兼容腿(PR #348)**:新增 `test-host-legacy` 作业,把 `peerDependencies` 声明的 0.1.x 两段(0.1.0-rc.8 / 0.1.7-rc.2)各真跑一遍全量测试——此前这两段一直「声明支持却没人验证」,与 #344 是同一类缺口的两侧。 ### 贡献者 / Thanks - **@dustinmoon78** — 蒸馏采集兼容 `role:"tool"` 形状(PR #353,新旧形状都收),修复新内核下工具输出与工具报错被整批静默丢弃。 ## [0.8.11] - 2026-09-30 ## 🐛 修复 - **侧边栏入口不再盖住「新会话」图标(PR #338)**:宿主 0.2 的侧边栏收起时只把开关(`left:12px`)与「新会话」(`left:48px`)固定在 Windows 标题栏上,而记忆入口沿用了同一个 left —— 两者完全重叠,宿主的「新会话」被压在下面,左上角只剩两颗图标。现在入口用一条更具体的规则排到「新会话」右侧 84px,并让 `--dsh-windows-menu-start` 顺延到 120px、「应用 / 编辑」菜单右移让位;这段覆盖只在收起且入口确实挂上时生效(`:has([data-sidebar-collapsed=true]):has([data-plugin-entry=…])`)。真机确认收起态为 `[开关][新会话][记忆] 应用 编辑`,无重叠。 - **冻结冲突裁决后,入口红点立刻消失(PR #338)**:裁决成功时冲突队列本就广播 `mneme:conflicts-changed`,但只有状态卡订阅了它,侧边栏红点漏订——只能等 60s 兜底轮询才更新,用户反馈「消失得很慢」。现在红点订阅该广播立即重取,打开面板时也重取;计数收进模块级单例,两处入口(portal 与 footer 回退)共享一条轮询(原先各起一条且互不同步),并发请求去重,最后一个订阅者离开即停表。60s 兜底轮询保留:后台 autoDream 自己产生/消解冲突时没人广播,靠它浮现。 ## 🆕 新增 - **记忆入口改为低调面板行(PR #338)**:展开态入口 portal 进宿主 `nav.panelList`,套用 `panelRow` 的实时类名与图标/标题槽位类名,成为「插件 / 自动化任务」之后的第三个面板行。此前它套用「新会话」按钮的类名,展开后与「新会话」长得一模一样(同款高亮大按钮)——根因是**宿主 CSS 后注入,同特异性时宿主赢**,插件自己的配色覆盖从未生效。类名一律从活元素读(哈希前缀随宿主构建变),并剔掉激活态修饰类 `panelActive`,免得入口跟着宿主当前选中的面板常亮。 - **自绘 mneme mark,替换借来的「归档」图标(PR #338)**:不再用宿主的 `IconArchiveOutline*`——「归档盒」与记忆库里的归档动作是同一个符号,用户看不出这是记忆插件;而且宿主图标名换代时(#287:`Outline20` → `OutlineRegular` / `OutlineMedium`)只认一代就会把 undefined 交给 `h()`,落成 React #130 把整个 slot entry 崩掉。改为自绘螺旋(记忆痕迹 / 召回回路),规格逐条对齐宿主:16 栅格、`fill:none`、`stroke:currentColor`、描边 1;路径是 6 段三次贝塞尔拟合(与逐点采样版逐像素一致),源码里只有 200 字符。四处入口(浮层标题栏 / footer 回退 / 侧边栏 portal / better-sidebar tab)共用同一颗。 - **插件列表的图标与文案(PR #338)**:`package.json` 声明 `icon`,宿主 `iconOf()` 只收包内相对路径、realpath 不许跑出包目录、仅 SVG/PNG/JPEG/WebP、≤256KiB,命中后内联成 data URI;`assets/icon.svg` 自带品牌蓝 `#4176e6`(宿主 `--dsw-static-deepseek-500`),因为列表把它渲染成 ``、继承不到 `currentColor`。列表标题与描述改读包内 `locale/en.json` 与 `locale/zh.json`(英文 `mneme`、中文 `mneme 记忆库`),`exports` 放行 `"./locale/*"`——少了这一行,宿主用 ESM resolver 读不到字典(`ERR_PACKAGE_PATH_NOT_EXPORTED`)且**静默**回落成裸包名 `@modusensus/dsh-mneme`,文件乖乖躺在包里也永不生效。 ## 🧹 工程 - **面板守卫净增 6 条,全量测试 1431 → 1437**:收起态不遮挡(锁 84px / 120px 两条规则与 `:has` 守卫)、`panelActive` 不外溢、badge store **真执行**时序用例(用 `new Function` 抠出源码里那段 store,注入受控 `apiFetch`/`setInterval` 断言请求去重与定时器引用计数)、mark 自绘且不再探测宿主图标名、列表图标的声明/文件/同源漂移校验、locale 的 exports 门槛与字段形状。红点用例做过**变异检验**:把 `listeners.size === 1` 改成 `> 0`,两条用例立刻变红("still one timer for both entry buttons"),确认测试不是空转。 ## [0.8.9] - 2026-09-28 ## 🐛 修复 - **V4 写入准入兼容(issue #326,PR #327)**:全部 16 处会话消息写入点(蒸馏、注入续接、巩固、 睡眠、实体抽取、连通性探测)的 `source.kind` 从裸 `"plugin"` 改为生产者自有 kind `"plugin:dsh-mneme"`(`plugin` 字段保留)。DSH 0.1.7-alpha.1 起的 V4 写入校验把 kind 缺失/空串/恰好等于 `"plugin"` 的消息整条拒绝 (`format v4 message requires a producer-owned source kind`):0.8.8 在新版宿主上 压缩边缘触发时当前轮直接报错(continuity 注入点,实测见 #326),且该次抢救因边缘 消费即删不会重试;其余 LLM 管线写入点同批统一,防宿主后续收紧时扩大受害面。 读侧过滤(`kind === undefined || kind === "user"` 白名单)不受影响,历史库里的旧形状 事件无需迁移。新增静态形状锁(`test/llm-message-source.test.js`)防止新增写入点回退 到裸 kind。 ## 🆕 新增 - **面板状态页与设置页重排版(PR #328)**:状态页九张卡拆成「库内一览 / 后台运转」两个扫读分组, 多指标卡片(记忆分类 / 实体分类 / 记忆复用 / 注入预览)从「·」串联的说明长句改成一行 一条的指标行(label 左、数值右),热度分布卡改三档堆叠条 + 分档计数——原「热门数」 大数字易误读成总量;注入预览的 `maxItems=` / `threshold=` 裸键名改白话标签。 设置页重组为「个性化 / 记忆引擎 / 搜索 / 连接与安全 / 帮助」五个分组(运行模式提前到 功能开关之前、向量搜索并入搜索组、API Token 与外部访问 API 相邻为「连接与安全」), 全部小节统一 boxed 卡片样式;功能开关说明全面白话化(document 记忆 / 严格隔离 / 高峰时段等),逐行「重启 DSH 后生效」后缀收敛到卡片级一处——生效语义不变 (feature_flags 仍是启动时合并,见 `src/index.js`)。意见与反馈入口重做:图标行 + 副标说明 + 版本 footer(兼当「关于」收尾)。补 `memory.tab.summary` 中文标签 (此前状态卡直接漏出英文 "summary")。 - **状态卡网格排布规整化(PR #328)**:概览区三卡改 `auto-fit` 恒铺满(行尾不再留洞);「记忆复用 / 注入预览」在宽容器下各跨两列补齐引擎区的奇数行,跨列仅在 ≥2 列容器生效 (container query 守卫,单列下撤掉避免隐式轨道撑爆布局);向量卡在 2 列档位跨满整行。 - **面板星形全线统一(PR #328)**:详情抽屉编辑态的重要度下拉(文本星号,角尖锐且无法与 SVG 星 同形)改为 SVG 星形按钮行(实心/描边与查看态同一规则,radiogroup + 逐星 aria-label); 重要度筛选 chip、图谱侧卡提及数、注入预览的行内星号统一走共享 `StarGlyph` (同一份 Lucide 路径)。`test/client.test.js` 星形守卫同步收紧为「全文零文本星号」。 ## [0.8.10] - 2026-09-29 ## 🐛 修复 - **注入路径 4 处截断点切开 emoji 代理对 → 会话级 400(issue #334,PR #335)**:UTF-16 `slice` 切在代理对中间会留下孤立高位代理项,序列化成非法 UTF-8,DeepSeek API 对每个请求回 400;畸形文本随注入消息被永久写进会话历史,此后每一轮都带着它(换话题/重启都不恢复)。新增 `safeSlice`(尾字符是高位代理即丢弃),热上下文查询/回复、lastUserQuery、记忆条目 4 处截断点全部换用;回归测试以「孤立代理项不存在」锁语义,配阴性对照。 - **恢复 v0.7.11 误删的对话开始时间注入(issue #34,报告 #333,PR #335)**:`injectTimePrefix` 于 v0.7.2 引入(issue #34),v0.7.11 面板大改版时随一次批量删除静默消失、CHANGELOG 无登记(同刀误删的 `escapePromptVars` 已随 #162/#165 恢复,本函数漏了)——用户按 CHANGELOG 开着开关实际什么都不会发生。本次按 v0.7.2 语义原样恢复:键名/默认值(false)/格式 `[当前时间: YYYY-MM-DD 周X HH:MM]` 不变,存量 feature_flags 恢复即生效;per-session 闩锁保证每个新会话只注入一次。 - **saveWithDedupe 透传显式 epistemic_status(trustEpistemicWeighting 写入通路缺口)**:创建路径此前丢弃显式字段,store 回退内容标记推断(中文正则)——英文/无标记内容一律落 subjective,EPISTEMIC_WEIGHTS 对整池均匀 ×0.7,`trustEpistemicWeighting` 的重排对工具主路径写入的记忆**整体空转**(v0.4.5 起消费者在、写入通路断;patch 路径 store.update 本来就通)。E 系列四臂实验现场抓到(重排臂与对照臂注入序逐条相同)。修复后 undefined 保持推断行为(存量零变化),显式值优先(契约同 resolveEpistemicStatus);回归测试锁「observation 加权前后分数不变——字段丢失则全体缩放当场红」。 ## 🆕 新增 - **写入准入第 1 级:确定性硬拒(issue #254,PR #332,贡献者 heptaspirit)**:saveWithDedupe 写入前对内容做确定性扫描——空白/噪声行(blank)与密钥/PII 形状(sensitiveScan:AWS Key、GitHub PAT、JWT、私钥块、邮箱、手机号、身份证)直接拒收不入库;`writeAdmission.enforce` 默认关(第一阶段只计量),审计行打 `gate=level1` 落 `llm_audit_logs` 供真实流量回看;被拒内容不进去重键、不占写入预算,审计回填跳过被拒行。sensitiveScan 语料全用 GitHub 官方文档示例值(运行时拼接成完整形状,源码与提交历史零真实凭据)。 ## 🧹 工程 - **supersede 旧值探针进 recall benchmark(anti-update,E6/#280 链,PR #330)**:benchmark-recall 的 TEST_CASES 支持可选 `forbidden` 字段(forbidden id 进 top-K 即记 `forbiddenHit` + LEAK 标记),seed 复刻 applySupersede 落库状态(loser archived=1 + superseded-by 注记)。新增 `test/recall-anti-update.test.js` 6 例钉成回归锁:四路默认检索对归档行是**排除**不是降权、取代是归档不是删除、`includeArchived` 显式口子仍在。依据 StatemenBench anti-update probes。测试 1405 → 1411(当轮)。 ## [0.8.8] - 2026-09-27 ## 🆕 新增 - **蒸馏思考强度设置项(issue #315,PR #316)**:`summarizeReasoningEffort`(`off`/`low`/`medium`/`high`/`none`,默认 `none` = 不发送字段、行为不变)。思考型模型蒸馏时推理烧光输出预算、总结为空,配 `off`/`low` 可封顶推理;档位被拒自动去字段重试一次(`withEffortFallback` 共享、拒收判别式 `EFFORT_REJECT_RE` 提单一来源);面板「功能开关 → 自动总结」下新增档位下拉。 - **压缩边缘双落点(issue #249 N3,PR #314)**:`continuityRescueEnabled`(默认关)——宿主压缩前抢救「正在做什么」:连续性提案落新表 `continuity_proposals`(`(session_id, kind)` 唯一、满 200 弃新)+ 同一份快照追加为序列末尾插件消息(压缩摘要器只看对话内容)。三字段确定性抽取、全程不调模型;降级为持久规则不算失败。新增回归 14 条(`test/continuity.test.js`),文档见 [docs/CONTINUITY.md](docs/CONTINUITY.md)。 - **错峰队列镜像到巩固(issue #239 第 4 项,PR #320)**:`dreamPeakHours` + `dreamPeakMaxDeferMinutes`(默认 120)——高峰不调模型,skip 审计 + 择时补跑,baseline 不刷新(攒到非高峰一次大 run);时段解析三件套抽零依赖模块 `src/peak-hours.js`,顺带消除 #316 引入的 dream↔summarize 循环依赖。新增回归(`test/dream-peak-hours.test.js`)。 - **autoDream 连续失败退避(issue #292,PR #322)**:`autoDreamFailureBackoff`(默认关 = 行为逐字节不变)——开启后有效最小间隔 = `dreamMinIntervalMinutes` × 2^连续失败数,成功清零,封顶 30 分钟(只拦增长、不压小用户配的大基数)。计数内存态,跨重启冷却由 #291 持久化负责。新增回归 5 条(`test/dream-failure-backoff.test.js`)。 - **配置说明一页(issue #290,PR #322)**:新增 [docs/CONFIGURATION.md](docs/CONFIGURATION.md)——以 `src/config.js` schema 为唯一正本,147 键全覆盖:按功能面分组,每键给默认值 / 作用 / 开启后果与冲突;lightMode 联动键逐行标注。两个 README 文档索引各加一行。 ## 🧹 工程 - **GitHub issue 模板三件套(PR #321)**:bug / feature YAML forms(双语、环境字段对齐历史高质量报告)+ config.yml(空白 issue 关闭、问答引导 Discussions、漏洞引导私密通告);feature 模板内置实现口径自查与 AI 辅助披露。 - **双 README 重复徽章行去重(PR #319)**:9-24 合并解冲突复制出的 tests 徽章与 `npm test` 注释重复行(根 README 三处、包内两处)手工去重——`badge:sync` 全文替换只会一起刷新、永不自愈。 ## [0.8.7] - 2026-09-24 ## 🐛 修复 - **DSH 0.1.7 系宿主上面板入口整块消失(issue #287 / #309,PR #310)**:宿主 `dsh-client-ui-primitives` 在 0.1.7-alpha.1 起把图标命名从「像素后缀」(`IconArchiveOutline20`)换成「字重后缀」 (`OutlineRegular` / `OutlineMedium`)且不留旧名别名,`lib/client.js` 取到 `undefined` 交给 `h()` 渲染,落成 React #130,宿主 slot 把崩溃的 entry 整条摘除——侧栏「记忆」入口 与记忆库面板一起消失(0.8.5/0.8.6 均受影响,跨 Windows/macOS 实测一致)。修复:运行时 按旧名 → `Regular` → `Medium` 顺序探测,全缺时降级为无图标而非崩溃;0.1.6 系旧宿主 链首命中不丢图标。回归守卫锁链序、裸常量不得直达 `h()`、4 个渲染点全过助手。 感谢 chengxinshengglj-png(#257/#258/#287 三份高质量报告 + 本修复)、idoall(0.1.7-rc.1 macOS 复现与 0.8.6 未带修复的拆包证据)、idonweb(五版本 primitives 拆包对拍钉住断点)、 lqs50(Windows 复现与控制台日志)。 ## 🆕 新增 - **存储无损回收维护入口(issue #275 第一批,PR #307)**:新增 `src/maintenance.js` 与 `dsh-mneme reclaim` 子命令(standalone 数据面 `POST /maintenance/reclaim`)。两项零价值 判断、零条数变化的回收:`dream_runs.input` 按保留窗口(默认 7 天)置空(run 的骨架、 LLM 决策原文与 receipt 一律留住、永不删行);归档行向量置空(检索 SQL 恒带 `archived = 0`、 按定义不可达)。刻意不挂启动路径、不接定时器、不开 `auto_vacuum`:这是不可逆的内容丢弃, 只由人显式触发(默认 dry-run,带 `--apply` 才执行,`--vacuum` 单独指定)。报告口径按 VACUUM 前后体积量,列文本大小只作上界;执行留一行 receipt。取消归档时服务端重新排队嵌入, 回收不是单程票。实测(活库副本:177 个 run 的输入快照 + 295 行归档向量,磁盘足迹 69.5 MiB → 50.8 MiB,VACUUM 含 checkpoint 1.2 s)与代价见 `docs/STORAGE.md`。 - **升格吸收的 evidence 随之归档 + 归档侧第五指标(issue #275 拍板 5,PR #312)**: `memory_document` 注册成功后同一事务把被吸收的原子 evidence 行翻归档(只翻标志位、 内容与审计全留、可还原;`keep_evidence_active: true` 可退出;pinned 池 constraint / preference 永不自动归档;document / summary 两类自有生命周期不吸收)。重注册新版文档时 认回自己此前吸收过的 evidence 行(窄口径:别的文档引用照旧拒绝、捏造判据不变)。 `recall-stats` 新增 `archive` 块(归档总行数 / 窗口内净增 / 日均速率 / 按内容哈希可压掉 行数),面板「记忆复用」卡同步展示;判据用内容哈希而不用向量近重复——回收动作本身会清 归档行向量,指标不能建在会被自己回收掉的数据上。 - **写入准入第一阶段:只计量、不拦截(issue #254,PR #305 + #311)**:新增 `src/write-admission.js`。三个确定性测量点随每次会话内新建行落进 `llm_audit_logs` (一行一个新建行,全程旁路、任一步失败只 warn、绝不反噬写入): ① g1 会话写入预算——`session_key` 可空列 + 索引,按会话直接计数即得「会话内新建条数」分布; ② g2 同话题冷却——话题锚只收机械可判的两类(issue/PR 引用、文件路径),重复时附 `metadata.g2`(topic + gap_ms)即得「同话题重写间隔」分布; ③ dup 内容哈希——`memories.content_hash` 派生列(归一化 NFKC/小写/去标点/空白折叠后取 sha256,存量回填 + 索引 + 五个写入口全重算),归档行与已遗忘行一并纳入去重候选集, 命中区分活区 / 归档 / 遗忘三个出口分字段落盘——「出口止体积、不止重复」的另一半由 去重候选集兜住。 口径收口:只在新行上取测量点(并入已有行不算);pinned 类型不进预算也不进基准,仍照记 一行(穿透频率可观测);无会话身份的写入(dream / summarize / import / organize)不进 预算;`llmAudit.enabled=false` 时一行都不写。不装阈值、不加拦截分支,N 与 X 等真实分布 再定(2026-09-23/24 拍板)。 - **MCP server 拆出独立包 `mneme-memory`(讨论 #300 双包方案第一批,PR #302)**:根目录 新增 `mcp/` 包目录(bin 名 `mneme-mcp`),零依赖单文件从 `dsh-mneme/bin/` 迁出——工具面、 渲染与 standalone API 数据面完全不变,插件包内旧 bin `dsh-mneme-mcp` 原样保留(向后 兼容,已部署挂载零迁移)。新增 env 别名 `MNEME_URL` / `MNEME_TOKEN`(与 `DSH_MNEME_*` 同级、后者优先);跨包平价回归测试锁新包工具定义与 `src/tools.js` 逐字一致。 `mneme-memory@0.1.1` 已上 npm 并带 `mcpName` 字段(官方 MCP Registry 发布准备,PR #304)。 - **MCP 生态收录配套(PR #306 / #308)**:`mcp/server.json` description 压到官方 Registry 校验的 100 字符上限内;新增 Dockerfile 与 `glama.json`(Glama 目录的 running-server 检查用,`slow-stack/mneme` 已通过 Glama 提交与徽章检查)。 - **recall_runs 审计回执附带 per-source 检索信号(PR #299)**:`searchMemories` 的 recall 回执里每个 candidate 附 `signals`——keyword/vector/bm25/entity 四路的融合前原始分。 纯加字段:无 schema 迁移、无新配置开关、不改任何排序行为。动机:自适应融合加权的收益 已被消融实验锚定(AssoMem, arXiv 2510.10397),权重画像要靠这组逐路分数才能在真实 工作负载上算。 ## 🧹 清理 - **README 版本历史瘦身**(PR #301):包 README 移除「最近版本亮点」逐版本大表与逐小版本 路线图表(~140 行),压缩为指向 CHANGELOG 与 GitHub Releases 的短节 + 一行进化链—— 版本说明以 Release 为唯一事实来源,日后发版不再需要同步改 README。新增「用在其他 AI 工具里(MCP)」速查节(根 README 双语):六客户端最小挂载配置表。移除过时文档: `docs/devlog/`(6 篇 v0.1.x 开发日志)、`docs/MIGRATION.md`(迁移幂等自动执行)、根目录 `IDEA.md`(未跟踪草稿)。 - **仓库更名 slow-stack/dsh-mneme → slow-stack/mneme**(讨论 #300 拍板,PR #303):旧链 GitHub 自动 301,协作者零操作;源码内活引用(徽章图片源、package.json 元数据、运行时 issue 链接等 30 处)同步清扫。npm scope `@modusensus/` 不随仓库改名而变。 ## [0.8.6] - 2026-09-23 ## 🆕 新增 - **Sleep Mode 与实体抽取接入 LLM 审计(issue #250,#286)**:三条从未记账的后台 LLM 链路补进 `llm_audit_logs`——sleep 冲突裁决与模式挖掘(`dream/sleep.js` 有自己的一份 `streamText` 副本, 此前漏接 `onUsage`,现按 #242 同口径读 `chunk.usage`)与**每次写入记忆都会触发**的实体抽取 (适配器此前没有 `service`,根本无记账能力,补 `service`/`config` 入参)。新增 `operation_type`: `sleep_conflict` / `sleep_pattern` / `entity_extract`,`trigger_source` 记 `sleep` / `entityExtract`。 不新增配置键——三条链路共用 `llmAudit.enabled` 一个闸门;审计写失败只 warn、绝不反噬功能本体; 顺带修审计诚实性(流式成功但输出无 JSON 时记 `status='error'`,与 dream_runs 不再自相矛盾), README 审计节覆盖面改准(autoDream 实为三次调用)。测试 1277 → **1285**。 ## 🐛 修复 - **autoDream / sleep 的节流与冷却时刻跨重启持久化(issue #89 连发根因)**:两个调度器的 `lastRunAt` 只活在内存里,进程重启即归零——最小间隔 / 冷却闸对新实例放行,重启后立刻连发 (#89 Sample A/C 实测:横跨重启边界的 8.7 / 23.1 分钟间隔连发)。修复走审计表:`dream_runs` 本来就逐 run 落库(failed/degraded 也算 run),新增 `store.lastDreamRunAt(runType)` 读回最近 一次开跑时刻,构造 dream 调度器(`run_type='auto'`)与 sleep 调度器(`run_type='sleep'`)时 注入种子——零 schema 迁移、零新配置键。审计行 `created_at` 同时改为记录**开跑时刻**而非完成 时刻(run 耗时不应计入下一轮间隔窗口)。回归测试 +5(审计读回与 run_type 过滤、dream/sleep 重启闸、index.js 接线源码锁 ×2)。 - **本地嵌入对 BGE 系用错池化(静默偏差,不报错,#285)**:`LocalEmbedder.embed()` 对所有本地模型硬编码 `pooling: "mean"`,但 BGE 系(含默认的 `Xenova/bge-small-zh-v1.5`)是按 **CLS** 训练的——模型自带的 `1_Pooling/config.json` 明确写着 `pooling_mode_cls_token: true` / `pooling_mode_mean_tokens: false`, 官方 README 亦为「select the last hidden state of the first token」+ L2 normalize。此前每次嵌入都用了 非训练口径的池化,向量系统性偏移、检索排序受损,且因为不抛错而完全不可观测。 新增 `localEmbedPooling`(`auto` 默认 = 按模型族判定,BGE → `cls`,其余 → `mean` 保持既有行为; 也可显式钉 `cls` / `mean`)。池化同时进 `modelHash`:默认 `mean` 保持历史指纹形状(未受影响的索引 无需重建),`cls` 独立成指纹 ⇒ 既有 mean 空间的索引会被索引一致性闸门判失配并自动重建。 测试 1275 → **1277**。 ## [0.8.5] - 2026-09-21 ## 🆕 新增 - **工具暴露开关:`disableMemorySearch` / `disableMemoryArchive` + 工具描述的调用纪律(默认关=行为不变)**:跨会话记忆已由注入每轮带上,`memory_search` 只在「注入块里没有、需要深挖」时才值得一次串行往返(生成参数 → 执行 → 回填 → 再生成),`memory_archive` 是整理动作、正常会话很少需要——轻量/慢模型对「何时该调」判断弱,容易顺手每轮调一遍。两个开关把对应工具直接从注册表摘掉(模型看不到就不会调,比在描述里劝更可靠),走 feature_flags 白名单、面板可启停=线上回滚开关;默认关即工具全暴露,隐藏仅对全新会话生效(live patch reload 下宿主不会反注册已注册的工具)。同批收紧两份常驻文案:`memory_search` 描述点明「相关记忆每轮已注入,只在注入块没有所需内容时才搜」,`memory_archive` 描述补「只在用户要求或条目确已过时时归档,不要中途主动整理」。 - **agent 主动整理接口(issue #231):dryRun 比对报告 → 判断 → apply,全程留审计**:内聚块 `src/organize.js`,service 层只做依赖注入 + barrel 出口(`service.organize`,一个入口带 mode 参数——`{ mode: "dryRun" | "apply" }`)。按维护者口径只做功能本体:**不进工具列表、不加独立 opt-in 开关**,#249 到位时只差「注册工具 + 注入指引」一步。① `dryRun({ candidates, agent_scope?, workspace_scope?, sensitivity? })`:逐条与库内**同类型同 scope** 的行比对(精确层 = 标题归一后相等;向量层 = `MIN_SIM 0.92`,与 document 的 C2 档和 `findSessionDuplicate` 的 vector 档同源,不在第三个地方发明阈值),产出 `verdict: exact | near | new` 与命中行(含相似度),**不写记忆表**、只落一行 `dream_runs`(`run_type='organize'`);行扫描与向量读取按 type 缓存,候选硬上限 50 条(整理不是批量导入)。② `apply({ run_id, decisions })`:`save`(走 `saveWithDedupe`,复用常规写路径的镜像/通知/重嵌入语,不另起一套 epilogue)/ `discard`(只进回执)/ `archive`(**筛除 = 归档,绝不物理删除**),整批一个事务;apply 回执行经 `outcome.dry_run_id` 指回它所依据的那份报告,审计可还原「报告 → 判断 → 落地」三步,receipt 走 `buildReceipt`——与 dream 同一格式,`parseReceipt` 可解。三条硬规则都是「宁可什么都不做」形态:dryRun 不写库;apply 必须引用一次真实 dryRun(没有比对过的候选一律不落库,堵死「跳过报告直接写」的绕过路径);筛除只归档。宽容形态同仓库红线 4:单条非法候选/决策跳过 + 应用合法子集 + run 记 `degraded`(逐条明细进 `skipped` 列),基础设施级错误记 `failed` 并原样上抛——绝不虚报 ok。`document` 候选在 dryRun 即被拦(唯一铸造口是 `registerDocument`,#230)。`dream_runs.run_type` 的注释补第三档 `organize`(列本身无需迁移)。测试 1220 → **1228**。 - **注入形态(issue #249 第一批):能力说明 + 约束/偏好分池逐字保真**:两个 opt-in 键,默认关/零,默认档下注入块与既有行为逐字节一致。① `injectGuidanceEnabled`(默认关)——把「怎么用记忆」的判断指引落到两个零注入成本的位置:`memory_search` / `memory_save` 的工具描述尾部各追加一句判断指引(工具描述常驻、不进每轮上下文),以及一段 order 150 的系统提示段(常量文本、`[dsh-mneme memory]` 前缀——常驻段内容必须同会话内稳定,否则每轮变化会作废其后的前缀缓存;宿主不提供 section seam 时静默跳过,能力说明仍落在工具描述上,不算失败)。指引只加在「何时不该用」真有歧义处:`memory_list` / `memory_get` / `memory_update` 的触发是机械的,`memory_register_document`(#230 已内建 `Use for … lookups`)与 `memory_runtime`(已自带 provision 成本告诫)不重复;跨工具的克制判断进总则段——第 5 条点明可逆替代品(`memory_archive` 隐藏、`memory_forget` 只停注入,两者均可恢复)与**不可逆**的 `memory_delete`,这是全 guide 里唯一有数据损失后果的一句(回归测试锁它在场)。指引写**英文单一正本**(新内聚块 `src/guide.js`):注入指引的三条参照实现(ACP 的 `ACP_SYSTEM_PROMPT` + `HOW_TO_COMPRESS_RULES`、mnemon 的 `ROUTING_GUIDANCE`、宿主压缩摘要规则)全为英文,仓库既有先例也是工具描述硬编码英文,而 `memory.language` 管的是「生成出来的记忆内容与块内标题」,与本模块是两件事——故不并入 `STR`、不做 zh/en 双写(双写只会让两份文本日后漂移)。② `pinnedInjectBudget`(0–5,默认 0 = 关闭)——约束/偏好类进独立 pin 池:不进相关性竞争(取满预算后前置到块内排序之前)、不参与跨轮轮换(也不进轮换历史——每轮固定出现的 pin 若记进去只会占满轮换窗口、挤掉情景候选的新鲜度)、逐字保真(不受 `injectContentMaxChars` 的常规截断,只受 2000 字硬顶:逐字不等于无界,一条超长约束若无上限会每轮把常驻段吃满,超顶照旧带截断提示)。独立预算的意义是 pin 不占 `maxInjectedItems` 名额、也不会把当前任务需要的情景候选挤出去;超预算条数在块内如实标注「另有 N 条未展示」,绝不静默。选路统计经可选出参 `pinnedStats` 透出,`injectCandidates` 的「返回数组」契约不变。待维护者拍板(PR 内说明,本批次不自行决定):`constraint` 同属 `CODING_MEMORY_TYPES`,非编码任务里已被 `codingGate` 滤掉、pin 池同样拿不到它。测试 1220 → **1228**。 - **注入预览(issue #179)**:面板状态页新增「注入预览」卡——展示最近一帧 prompt 组装实际注入了什么:条目构成(类型/标题/重要性/字符数)、hot memory 与总体积、生效参数(maxItems / threshold / 自适应条数 / scope / 轮换抑制)。实现走旁路快照:`src/inject.js` 在真实渲染路径上缓存同一份候选与最终文本(`getInjectionSnapshot()`,不二次检索、零额外开销),`GET /api/dsh-mneme/inject-preview` 只读透传,无快照(autoInject 关闭 / 新会话 / 旧宿主)整卡退化为「暂无预览」不猜;注入器卸载即清空快照,不跨生命周期存留。与 #182 的「极简模式注入关闭」提示卡同区呈现,状态页至此覆盖注入可观测性两端:为什么没注入(minimal 压制)+ 注入了什么(本卡)。另含 #178 无障碍批次一(面板 aria-live 播报网络 / 状态卡语义标题 / 弹层焦点圈 / 冲突按钮可区分标签 / ego 图摘要,对比度审计待宿主主题联调)。 - **document 型记忆——agent 产长文档入库为指针行(issue #230)**:写入权分离——全文归 agent(管线零读零写零改),库里只存摘要 + doc_path + evidence 三样;新工具 `memory_register_document({path,title,summary,tags,importance,evidence})` 作唯一铸造口(内聚块 `src/document.js`,service barrel 出口):注册校验(~ 展开后绝对路径、存在 + 非空常规文件、evidence 与库求交——合法子集落库 + `evidence_degraded` 系统标记、全捏造整单拒绝);C2 vector 档比对(minSim 0.92 复用 #127 档语义;同 doc_path/同标题 = 出新版显式 supersede——旧行归档 + `[superseded by ]` 指针注记 + content_history 存旧摘要,旧文件不删;仅向量近重复而路径标题都不同 = 拒绝并指路,不越 C1 矛盾检测替 agent 裁决);store 新增 doc_path 列(幂等迁移),toApiList 条件透出 doc_path(普通行 DTO 逐字节同形),memory_get 渲染亮出文件路径;`documentMemoryEnabled`(默认关,白名单 + lightMode 强制关,面板「记忆增强」组第三处落位)+ `documentInjectBudget`(1–5 默认 2);注入档位合并拍板(#164 评审线,#230 内一次落地):叙述条(source=narrative)从纯按需解禁进注入落次优先档(受 dreamNarrativeEnabled 约束,语义/检索路径不变),document 摘要行同档 + 独立预算封顶(超预算跳过由后续候选补位),dream/sleep 五个候选池排除 document(互不代管),heat 免疫(λ=0,指针行不衰减);写入权分离守卫双保险(saveWithDedupe / updateMemory 拒绝铸造或改入 document,standalone API 数据面给 400 `document-requires-register`),MCP 六件套 memory_list 枚举平价同步 +document(注册工具不进 MCP,接口面归 #231)。 - **总览(dream_summarize)独立路由与输入硬上限(issue #258)**:`dreamSummaryProvider` / `dreamSummaryModel`(默认空 = 沿用巩固路由,行为逐字节不变)——consolidate 有 `dreamMaxSnapshotSize` 窗口而总览输入为全库无界,两者 ctx 需求差数倍却强制共用 dream 路由:`dreamProvider` 指向小 ctx 模型时总览当场 `CONTEXT_WINDOW_EXCEEDED`(实测 120,969 > 32,768 tokens),指向大 ctx 模型则 consolidate 的卸载收益归零;`dreamSummaryMaxInputs`(默认 0 = 不设上限,0–100000 可调)>0 时按 `updated_at` 倒序保留最新 N 条(与 consolidate 窗口同一排序口径),总览口径脚注的条数随实际输入变化。三键 schema 与 settings 白名单成对落位,面板可调。 - **注入命中留痕与注入命中率(issue #217 增量,2026-09-19 口径确认)**:注入终选集落一行 `mode='inject'` 审计(candidates 存实际注入条目,跟随 `recallRecordDefault` 不设新配置键;`heatEnabled=false` 时照写——留痕与消费解耦);recall-stats 新增注入口径(轮数 / 注入条数 / 槽位填充率 `slotFillRate`,注入候选计入 Top-N 与僵尸零曝光判定);面板「记忆复用」卡追加注入段(窗口内无注入行时省略)。 - **蒸馏的成本感知级联与有界检查点(issue #239,第一批)**:两个 opt-in 旋钮,默认 `0` = 行为逐字节不变。① `summarizeMinWindowChars`——**蒸馏前的零 LLM 预判**:窗口内可蒸馏文本不足阈值时直接跳过 LLM 调用(纯规则判定、无模型参与;被挡下的窗口照常消费游标,否则每个 `turn/end` 都会重评同一段短文本),skip 原因写进 `llm_audit_logs`(`status='skipped'` / `error_message='window-too-small'`)——此前只有最小间隔一档留痕,「这一轮为什么没蒸馏」基本不可观测;② `summarizeMaxRunsPerSession`——每会话最多发起多少次蒸馏,只统计**真正发起过** LLM 调用的 run(被预判挡下的窗口不占额度),aborted 调用按既有口径回滚,游标刻意不消费(预算恢复或重启后仍能蒸馏到该窗口)。两键在 `config.js` schema 与 `settings.js` 整数白名单成对落位,面板可调;第 4 项(错峰队列)与第 5 项(注入侧不确定性召回)留后续批次。 - **注入条数的查询自适应(issue #239 第 5 项)**:`injectUncertaintyAdaptive`(默认关)。开启后:确定性强的话题把注入条数收缩到一半(下限 1),模糊话题(回指/时间线索,或极短查询)维持 `maxInjectedItems` 上限——**只做单向收缩**,绝不越过用户配置的上限。依据是不确定性驱动的读路径(模糊多召回、确定少召回),但「注入相关却带偏生成」的内容可能比不注入更糟(两处独立出处),所以宁可少注入也不新增这个风险面。判据只看查询本身、零额外检索(先探针检索等于白付一次 `fuseRecall`);拿不到查询时维持现状不猜。纯函数 `needsBroadRecall` / `adaptiveInjectBudget` 落在 `src/search/adaptive.js`(与既有的查询自适应向量阈值同源),可单测。 - **蒸馏错峰队列(issue #239 第 4 项)**:`summarizePeakHours`(本地时间时段串,逗号分隔、支持跨零点,**可带星期前缀** `mon-fri` / `1-5` / `sat,sun`,省略 = 每天;如按高峰计费的供应商可写 `mon-fri 08:00-12:00,14:00-18:00`)+ `summarizePeakMaxDeferMinutes`(顺延上限,默认 120 分钟)。命中高峰时不调模型——只登记一行 `status='skipped'` / `error_message='peak-hours'` 审计并择时补跑;**游标刻意不消费**,窗口在高峰期间继续累积,非高峰一次蒸馏(批量比逐轮碎蒸更省)。被上限截断后到点仍处高峰则由 `bypassPeak` 照跑,长高峰不会把蒸馏饿死。跨零点段按「开窗那天」认星期(`mon-fri 23:00-06:00` 的周六凌晨仍算高峰);任一写法非法则整串按「未配置」处理——排程是省钱手段,绝不该因为写错格式把蒸馏停掉。时钟与定时器可注入(房型同 `dream/sleep.js`),补跑定时器 `unref` 且 `dispose` 时清理。429 自适应退避沿用既有 `distillRateLimit*` 路径,本批次不重复造。 ## 🐛 修复 - **旧版 service 缺游标 API 时降级为内存游标,不再打断每轮蒸馏(#274 回归修复)**:第三方宿主用旧版 service 构造时没有 `setDistillCursor`,此前直接抛错、一轮都蒸不了——改为记一条 warn 后降级为内存游标(#274 之前的行为):本进程内不重复蒸馏,重启后的窗口重放由 `saveWithDedupe` 的 (type,title,scope) 三元组兜底;**方法存在但抛错仍向上传播**,那是「写失败须回滚」的恰一次语义,两条语义各带回归锁。 - **蒸馏游标持久化:重启不再重放历史窗口(issue #229)**:新增 `distill_cursors` 表(幂等迁移,按 `session.id` 记最近成功消费的事件序),游标只向前推进——较小 seq 不覆盖已存进度;记忆写入与游标推进同一事务,LLM 失败 / 解析失败 / 中止 / 记忆写入失败 / 游标写入失败任一处出错都保留窗口、回滚并如实记失败审计(此前游标只在内存里,进程重启后历史窗口被重复蒸馏)。 - **能力说明第 5 条写全 `memory_forget` 的副作用面,`memory_save` 尾句收短(#249 文案精确性)**:原文只写「`memory_forget` 只停注入」,比工具实况窄——它实际让条目从注入、检索结果与列表三处消失,写窄会让模型低估其影响面;改为与 `memory_archive`(从列表 / 检索 / 注入 / 巩固四处隐藏)同口径,回归测试锁完整短语在场。 - **运行时完整性判据从未生效:状态词对不上,而且结论根本没读(issue #268)**:`verifyPayload` 只认 `integrity.status === "sha512-matched"`,而下载通道在 `mneme-runtime.json` 里记的是 `{status: "verified", checked, detail}`(`src/runtime/download.js`,自 #133 起)——这份结论一喂进来就恒判 `ok:false`,接线即系统性判失败;更根本的是两个生产调用方(`memory_runtime` 的 verify 分支、`scripts/mneme-runtime.mjs` 的 `runVerify`)都只传 `cacheDir`,**这一层在生产路径上从未运行过**:清单里明写的 mismatch 也被静默放过(红测试证实)。附带第二处形状违约:`loader.js` 把清单里的**对象**直接填进 `describeLocalRuntime` 里声明为 `string|null` 的 `integrity` 字段,经免鉴权的 `/api/dsh-mneme/semantic` 外发,CLI `status` 还会把它打印成 `[object Object]`。修法收成一处:新增 `layout.js` 的 `recordedIntegrity()` 归一清单字段的两种形态(对象 / 缺失),判据与投影都只调它——`verifyPayload` 在调用方未显式传结论时读 `describePayload` 已解析的清单,判据同时认 `verified` 与 `sha512-matched`,并把显式 `unverified` 与「没传」同判(原先一个 `ok:true`、一个 `ok:false`);不一致的结论(含清单里记下的 mismatch)照样判失败。 - **睡眠冲突/模式阶段的输出预算可配(issue #257)**:`src/dream/sleep.js` 冲突消解与模式发现两处 `maxTokens: 2048` 硬编码提为 `sleepMaxTokens`(默认 8192,schema + 整数白名单成对落位,面板可调)。实测依据(报告者 llama.cpp 环境):默认档 24 对裁决需 2097 token,恰好压在 2048 边界(53 次运行 48 败 5 胜的「间歇性失败」指纹);`sleepActionSet: full` 六分支实测需 6967(3.4 倍越界)——该档位自 #126 引入起从未跑通过。流式计费按实际用量,调大不增加成本。 - **审计记账改读 `chunk.usage`,token 不再恒为 0(issue #242)**:dsh-llm 的 StreamChunk 契约把用量嵌在 `{type:"usage", usage:TokenUsage}`(TokenUsage = inputTokens / outputTokens / …),chunk 顶层没有 token 字段——dream / summarize 的审计读取把整个 chunk 当用量对象,input/output 恒为 undefined,审计行落 0(实测 7 天 49 次 success 调用 token 全 0,面板「LLM 消耗」长期显示 0)。改读 `chunk.usage ?? chunk`,`?? chunk` 兜底兼容用量平铺在顶层的替身(嵌套 + 平铺双形状回归测试)。 ## 🧪 工程 - **CI 与徽章口径收敛**:tests 徽章改为本地手工对齐(`npm run badge:sync` 自跑全量取套件总数),ci.yml / release.yml 里的 badge job 全部撤除——github-actions[bot] 推不进受保护的 main(GH006),徽章不再由 CI 自动刷;新增安全扫描三件套(gitleaks 全历史密钥扫描 + PR 依赖审查 + OSV 提醒级兜底,warn-first 起步);仓库 slug 由 modusensus 迁移至 slow-stack(npm scope 与包名不变)。 - **依赖告警清零(GitHub code-scanning 四条 open)**:`@huggingface/transformers` 4.2.0 → **4.3.0**(其 sharp 依赖声明升至 `^0.35.4`,消掉 sharp 的两条 high——path 处理与 DoS),`package.json` overrides 的 `adm-zip` 0.6.0 → **0.6.1**(消掉 adm-zip 的 high + medium 各一条;两者均处 devDependency 链——本地嵌入运行时构建面,npm 用户装不到)。连带项:runtime-manifest 闭包在 sharp 0.35 下新走到无 `os` 约束的 `@img/sharp-wasm32` 及 freebsd/webcontainers 两个 WASM 回退包(Node 构建从不 import),按 onnxruntime-web 先例加入 `scripts/build-runtime-manifest.mjs` 的 EXCLUDED 并重生成清单;`test/runtime-manifest.test.js` 的 payloadId 断言由硬编码版本号改为取生成器输出本身(锁格式不锁版本,升级不再碎)。`npm audit`(含 dev 与 --omit=dev 双口径)0 vulnerabilities;全量测试 1243/1242 pass/0 fail/1 skip。 - **全工具矩阵的「DTO 键集 ⊆ output schema」系统性断言(issue #195)**:#184(memory_get 内联 schema 漏声明 v0.8.1 的 scope 来源三键 → 任何被标注过的行都过不了 in-process 校验)此前只有单点回归护住 `memory_get` 一个工具,换一个工具、换一个键,同类事故可以原样重演。新增 `test/tools-dto-schema-matrix.test.js`,四层断言各管一段:① 9 个工具每个可安全触达分支的**真实 execute 返回值**过生产同款校验器 `validateJsonSchemaValue`(不写手抄期望值);② DTO 唯一产地 `toApiList` 在全形态(极简 / 敏感度 / 事件时间 / 单维与全量 scope 标注)下的输出 ⊆ `MEMORY_ITEM_SCHEMA`,并**反向**要求声明里的每个键都被至少一种形态真实产出(死声明会在下次增键时暴露);③ 全部工具 schema 的结构不变量(闭合、required ⊆ properties、每项带 type——否则前两层会因校验器形同虚设而静默失效);④ 负例锁:注入未声明键**必须**报错。护栏自证:两次变异测试(删共享 schema 一个键 / 给 memory_get 塞手抄小副本)分别让 2 条与 3 条断言转红。`memory_runtime` 的 provision(联网下载)与 verify 命中载荷(真实加载模型)不在单测内驱动,由 ③ 兜底声明合规。 - **supersede 旧值回归集(anti-update 探针,E6/#280 链)**:`scripts/benchmark-recall.js` 的 TEST_CASES 支持可选 `forbidden` 字段——forbidden id 出现在 top-K 即记 `forbiddenHit`,legacy/fused 与三融合配方全配置判定,CLI 报告加 LEAK 标记;seed 复刻 applySupersede 的落库状态(loser `archived=1` + 正文 superseded-by 注记,无专属取代列)。新增 `test/recall-anti-update.test.js` 6 例,把三件事钉成回归锁:四路默认检索对归档行是**排除**不是降权(谁把排除改成降权立刻红)、取代是归档不是删除(行还在、未遗忘、注记在)、`includeArchived` 显式口子仍在(排除发生在检索层而非行消失)。依据 StatemenBench anti-update probes。测试 1198 → **1204**。 ## 🏗️ 工程 - 致谢:heptaspirit(#247 注入命中留痕 + #267 agent 主动整理接口 + #277 能力说明文案精确性)、davidekingsss(#248 审计记账修复 + #253 审计边界测试)。 ## [0.8.4] - 2026-09-19 ## 🆕 新增 - **面板标注「极简模式下注入按宿主设计关闭」(issue #182)**:`GET /api/dsh-mneme/inject-status`(只读)返回 `{autoInject, agentPreset, suppressed}`——preset 探测走 `session/event` 钩子记最近会话头(注入回调在 minimal 下被宿主整体压制,#175 定论,不能作检测源;读法与 scope.js 同源),`suppressed` 需同时满足 autoInject 生效值开启 + 观测到 `agentPreset === "minimal"`;面板状态页在 suppressed 时渲染提示卡(含解法:标准模式 / `agent-presets.default: standard` / AGENTS.md 过渡),standard 会话与 preset 未知的宿主零渲染零打扰。不新增配置键。 - **记忆复用统计端点与面板卡(issue #217)**:`GET /api/dsh-mneme/recall-stats?window=30`(整数天数,1-365 钳制,缺省 30)只读聚合——Top-N 召回(按窗口内 recall_runs 候选计数,join memories 补 type/source,已删记忆 type=null)、僵尸记忆率(活跃且窗口内零曝光,豁免期 7 天单独报数)、覆盖度标注(earliestRunAt / 扫描超上限 truncated);纯读聚合独立成模块 `src/recall-stats.js`(service.js 过 2000 行参考线,barrel 出口调用方零改动);面板状态页新增「记忆复用」卡(自门控,窗口内无回执整卡不渲染)。注入命中率与「入池未中」零召回语义(B)待注入留痕口径拍板后接入。 - **heat 广义指数衰减 + 注入排序热度乘数(issue #218)**:衰减式由幂律 `H=1/(1+λΔt)^α` 换为广义指数 `H=exp(-λ·Δt^β)`(FadeMem v2 Eq4,维护者拍板选型;幂律在 Wixted & Ebbesen 1991 / Rubin & Wenzel 1996 / FSRS 有支持但不在 #164 论文集内,留待真实负载回放两族 A/B);`heatGlobalAlpha` → `heatGlobalBeta`(0.5–2,默认 1.0,默认关期间零迁移成本),λ=0 免疫位语义不变;`heatEnabled` 补进面板「记忆增强」组(feature flags 白名单第三处落位,面板可启停=回滚开关);开启后注入排序规则路在优先级层内乘 heat——同级内乘数,priority 分层与 `order=chrono` 分页序不动,关闭时排序逐字节一致;touch 回温 / sleep 热联合判定 / 前端热度投影沿用存量路径。 - **stdio MCP server——记忆六件套进入任意 MCP 客户端(issue #181,PR #214)**:`bin/dsh-mneme-mcp.mjs` 零依赖 stdio MCP server(JSON-RPC 2.0 换行帧,不引 SDK,Node ≥ 20 全局 fetch);工具面 memory_save / memory_search / memory_list / memory_get / memory_update / memory_delete 六件套与 `src/tools.js` 逐字对齐、平价回归测试锁漂移;配置沿用 CLI 约定(`DSH_MNEME_URL` / `DSH_MNEME_TOKEN` > `~/.dsh-mneme/cli.json` > 默认 8790);配套 api-standalone 补齐六件套数据面(PUT /memories/:id 字段补丁、POST /memories 透传 sensitivity/occurred_at/显式 scope、GET /memories 与 /search 补 include_archived 与 occurred_from/to)。 - **图召回轴——实体挂联记忆并入检索融合池(issue #219,PR #222)**:`entityRecallEnabled`(默认 false,feature_flags 白名单可启停,lightMode 强制关)开启后检索融合池三源扩四源——store 新增 `findEntitiesMentionedIn` / `getLinkedMemoryIds` 反查原语 + `idx_relations_memory` 索引,fuseRecall 的 blend / rrf / minmax 全配方参与;实体轴与 BM25 同为确认/回填信号(we 并入 wb 后共用 0.3 回填权重),永不主导语义排序,失败降级空数组;keyword 模式契约不变(纯文本路径不吃实体轴)。 - **冷启动——从仓库文件反向构建初始记忆(issue #220,PR #223)**:`src/bootstrap.js` 确定性解析、零 LLM 必有产出——package.json scripts / README 概览 / CONTRIBUTING 规范 / CI 工作流清单 / 顶层目录树(跳过产物目录),git 仓库追加近期提交主题(execFile 5s 超时,非仓库静默跳过);幂等骑 saveWithDedupe 的 (type,title,scope) 去重 + `_overwrite` 原地刷新,重跑零重复行,产物 source='bootstrap';standalone API 新增 POST /bootstrap(Bearer 门内,dir 显式必填)。 - **注入截断上限可配 + 截断不再静默(#164①,PR #225)**:`injectContentMaxChars`(默认 300=既有行为,60–4000 可调)+ 面板整数档;截断尾部带提示——上限/原长/全文 `memory_get` 指引(BUDGET_EXCEEDED 原则,agent 永远拿得到取全文的路径),双语;块预算 `Math.max(1500, 上限+600)` 随上限放大,调大单条上限不被旧 1500 闸卡死;`_full_content` 压缩注入路径保持逐字不加提示,短正文零变化。 - **dream 总览升级常驻状态条——状态叙述 + 快照口径脚注(#164 对齐,PR #227)**:dreamSummary prompt 由泛化总览改为「当前状态」叙述(在做什么/最近变化/明显走向,只陈述有据事实),作为唯一常驻注入的 summary tier 0;内容尾部带快照口径脚注(整理后条数 + run 片段 + 日期),常驻答案的生成口径永远可查;`_overwrite` supersede 语义与标题不变(dedupe 键稳定,跨版本平滑)。 - **叙述条——按主题合成叙述 + evidence 证据链(#164 对齐,PR #228)**:`dreamNarrativeEnabled`(默认 false,白名单 + lightMode 强制关)+ `dreamNarrativeMinCluster`(2–20,默认 3);`src/dream/narratives.js` 纯函数 clusterByTag(共享 tag 主题簇,≥K 门槛、降序、cap 3/轮)+ intersectEvidence(模型 evidence 与簇成员求交,捏造 id 剔除、交空回落全簇);dream 新增 generateNarratives 阶段单次 LLM 调用按簇合成叙述(source=narrative、`_overwrite` 原地刷新、标题=叙述: 确定性键跨 run 稳定),失败降级零条不反噬主流程;store 新增 memories.evidence JSON 列(幂等迁移);注入候选排除 source=narrative(按需检索,常驻位只留 dream 总览)。 - **路由旧值显式标记 + 行级提示与测试说明(issue #191,PR #213)**:巩固/睡眠/实体抽取的级联下拉刻意保留不在列表里的旧值(不静默丢配置),但此前与正常选项无差别展示——切 Provider 后残留的旧 model id 照常保存,分不清「改错了」还是「还没生效」。旧值收起即带「(不在可用列表)」标记,路由行给 ⚠ 提示(改选,或点「测试连通性」当场验证;改动保存后重启 DSH 生效);标记只在适配器列表非空时判定(防误报),旧值保留保持无条件(列表为空不静默丢已存值);「测试连通性」接上 modelTestHint 作说明行,与结果行同槽位二选一。不新增配置键。 ## 🐛 修复 - **连通性测试透传 reasoningEffort(issue #215,PR #216)**:面板「测试连通性」三条路由各传各的档位键(dreamReasoningEffort / sleepReasoningEffort / entityExtractionReasoning),'none'/未配置省略字段,与后端 src/api.js 同口径——此前测试按钮不发档位,sleep 无回退重试时档位被拒只能等真实 run 才暴露;服务端零改动。 - **按事件序增量蒸馏会话窗口(PR #226)**:summarize 由「整轮转录」改为按会话事件 seq 游标增量蒸馏——只读上次成功游标之后、当前 turn/end 之前的事件;解析失败 / 流失败 / 中止不推进游标,下次仍重试同一窗口;同批记忆写入收进 `service.transaction` 原子提交(第 N 条失败不残留前 N-1 条),最后一条写入 + 解析全成功才提交已消费 seq;`lastRunAt` 节流改到真正发起 LLM 调用时才占。 - **summarize 补齐工具结果与子会话交付蒸馏(PR #232)**:tool/result 与 tool/code-dispatch 改读新消息形状(`data.message.content` 的 tool-result 块与 `data.content`,isError 判定)——旧字段废弃后工具结果一度静默消失、pitfall 根因蒸馏断供;新增子会话交付蒸馏(agent-message / subagent-settled / agent/inbox/spliced 三类消息去重进入转录),agent 委派结果不再只进工具槽;空数组语义收紧——模型明确判断无内容(合法空数组)才消费窗口,非空但全部无效不推进 seq 游标。 ## 🏗️ 工程 - **复杂度自查收尾——实体召回与冷启动 5 处瘦身(PR #224)**:对 #222/#223 diff 的过度设计自查落地——bootstrap SKIP 集合去掉 .v2c、readHead 删无人覆盖的 limit 参数;findEntitiesMentionedIn 去掉等于默认值的实参、fuseRecall 删唯一调用方恒传的 entity/we 默认值、we 并入 wb(实体轴与 BM25 共用 0.3 回填权重,等 #217 数据说话再议独立权重)。 - 测试 1160 项全绿(较 0.8.3 新增 81 项:MCP stdio 帧级 / 实体召回 / 冷启动 / 注入截断 / 增量蒸馏 / 叙述条 / 常驻状态条 / 复用统计 / heat 广义指数 / 极简模式注入状态)。 - 致谢:heptaspirit(#213 路由旧值标记 + #216 reasoningEffort 透传)、z2Ace0107(#226 增量蒸馏 + #232 工具结果与子会话蒸馏)。 ## [0.8.3] - 2026-09-17 ## 🆕 新增 - **注入位跨轮轮换 `injectRotationTurns`(issue #205,PR #206)**:同一会话里相邻轮次反复注入同一条记忆——注入窗口只看当前轮,转出去的条目下一轮又原样转回来。新旋钮 `injectRotationTurns`(默认 0 = 关,行为不变):记住最近 N 轮已注入的条目避免原地重复,同查询的工具轮不推进、不转自己;状态按 sessionId 分桶(FIFO 上限 32),轮换集合随 `injectCandidates` 传递(Philia-FY 报告 + A/B 验证)。 - **轮换窗口随候选池扩容(PR #208,#205 补测)**:注入候选池在语义 / bm25 / merged / 规则四处各有 `maxItems` 级硬上限,`injectRotationTurns` 开大后轮换窗口被池子卡死——条目出不了箱。四处统一扩容 `poolSize = maxItems × (轮换窗口 + 1)`(下限 200),旋钮开多大、池子就够多大(报告者读码定位 + 数学闭合验证;旧测试恰好按 maxItems×2 构造,测不到这个盲区)。 - **模型文件下载断点续传与重试 `resilientModelDownload`(issue #194,PR #207)**:本地嵌入 / 重排模型的下载一次中断就从头再来,大文件 + 不稳网络下反复白费。env.fetch 弹性层:HTTP Range / If-Range 断点续传、416 / 偏移失配自动重置重下、单写者锁 + 降级直通(不支持 Range 的源照常工作)、空闲看门狗回收半死连接;默认开启,设置页有开关与白名单双语说明(heptaspirit 报告 + 实现)。 ## 🏗️ 工程 - 测试 1079 项全绿(较 0.8.2 新增 18 项:跨轮轮换 / 候选池扩容 / 断点续传与重试路径)。 - 致谢:Philia-FY(#205 报告 + A/B 复测 + 池子盲区定位)、heptaspirit(#194 报告 + 实现)。 ## [0.8.2] - 2026-09-16 ## 🐛 修复 - **memory_get 输出 schema 与共享 DTO 同源(issue #184,PR #186)**:内联 schema 副本漏掉 v0.8.1 的 scope 来源三键,`additionalProperties: false` 下任何被标注过的行都过不了 in-process 校验;改回与 memory_search / memory_list 复用同一份 item schema 并导出防漂移护栏(lujfsd 报告 + 三段式取证)。 - **LLM 消息补 source 字段(issue #189,PR #190,社区 PR)**:dsh-llm 契约要求每条消息携带 `source`,mneme 手拼的 13 处消息缺该字段——遇到会读 `message.source.kind` 的第三方 provider 适配器时,请求在序列化阶段(0–8 ms、零网络请求)抛 TypeError,dream / summarize / sleep / 实体抽取四条管线全灭;内置 dsh-llm-deepseek 不读该字段所以长期潜伏。13 处全部补 `{ kind: "plugin", plugin: "dsh-mneme" }`,system 消息一并补齐(heptaspirit 报告 + 修复 + 严格桩回归测试 + A/B)。 - **sleep 计时器撞 CD 不再丢轮(issue #187,PR #192)**:idle 计时器到点时若 `sleepMinIntervalHours` 未满,整轮被静默丢弃且无人重排——「CD 到期」成了没有监听者的时刻,要等下一次写入才救回来。改为按「剩余 CD / 剩余静默」更晚者重排 + 构造即挂表(Philia-FY 报告,注入时钟最小复现)。 - **本地重排器单 logit 头恒 0.5(issue #188,PR #193)**:`sigmoid(l1 - l0)` 只对双 logit 头成立,`Xenova/bge-reranker-base` 这类单 logit 交叉编码器的全部分数被压成常量 0.5——不抛错、日志照常 ready,重排静默失效。按 `cols` 分支:单 logit 取 `sigmoid(logit)`(双 logit 逐字节等价不变);恒分批 warn 一次的绊线;顺带把 `embedModelMirror` 死配置接成 transformers 下载镜像(嵌入 + 重排双侧)、重排层补 dtype 默认 q8(新配置 `rerankDtype` 可回 fp32)、README 配置表补行(AoooooE 报告 + 三条补充全部有实测数据)。 - **连通性探测改发 user 消息(PR #197)**:0.1.6-alpha.1 起 system-only 对话被官方 API 整单拒绝(适配器把 system 抽成顶层参数),面板「测试连通性」502。探测改为一条 user 消息(保留 source 标注),回归断言锁定 role 与 source。 - **首轮注入跑偏——BM25 同步兜底(issue #198,PR #199)**:注入渲染是同步的(宿主 systemPrompt 不支持异步 text),查询向量只能异步 prefetch 给下一轮——首轮必 miss,落到静态排序后被老的高重要性 preference 占满全部槽位,当前话题的 decision/project 进不来。首轮(无向量、无缓存召回)改用 BM25 词法召回领位(纯进程内、零等待),语义命中时行为不变;优先级链:向量 > 缓存召回 > BM25 > 静态排序(lqs50 报告,根因行号级准确)。 ## 🛠️ autoDream 宽容路径(issue #104 三方向闭环) - **coverage 不足降级为 degraded(PR #200,方向 1)**:`dreamMinExplicitCoverage`(默认 0.5)护栏把提示词自己要求的「挑重点」输出整单拒绝——提示词硬性规则明文「无问题的条目无需输出」,实测官方路由 + 强模型 81 轮 failed 全是 coverage(10%–49%)、另一位报告者把门槛降到 0.3 后覆盖率仍 13%–22% 全线不达标。合法子集照常应用 + 未提及条目隐式 keep,降级理由随 `outcome.degradations` 落库;防洗白核心保留——全部决策被跳过、合法子集为空时仍整单拒绝(零幸存护栏);`dreamImplicitKeep: false` / `dreamMinExplicitCoverage: 0` 可恢复旧严格行为。 - **archive 类型护栏 + 批量上限(PR #201,方向 2)**:长保留类型(preference / pattern / rejected_solution / constraint / pitfall)的 archive 决策必须命中「重复/过时」类理由(中英词表,宁宽勿误拦),「价值判断」式大扫除单条跳过记 skipped(明细进 `dream_runs.skipped`,run 记 degraded)——conflict 裁决的败者归档不受影响;新配置 `dreamMaxArchivePerRun`(默认 8)与 update 上限同类的全局闸门;提示词中英同步加固。`allowCrossTypeMerge` 维持 false(288 条 skipped 明细里 94% 跨类型 merge = 护栏正确工作,跨类型意图的正确出口是 `sleepActionSet: full`)。方向 3(失败审计明细)此前已由 #137 + `dream_runs.skipped` 列闭环。 ## 🚀 性能(issue #202 第一批,PR #203) - **检索路径三处固定成本**:`updated_at` 索引入 schema(幂等迁移,ORDER BY 不再走临时 B 树);`all()` 显式列清单排除 embedding 列(5k 行少搬 ~9.4 MB,toRow 本就不输出向量);`searchVector` 重写为 id 列查询 + 解析缓存(FIFO 上限 4000)+ Top-N 主键回表,`getParsedEmbedding` 共享读路径(vector-index / 语义去重同源),失效点覆盖 setEmbedding / update / compareAndUpdate。5k 行 / 1000×512 维向量实测:`all()` 231.6 → **50.9 ms**,`searchVector` ~140 → **16.3 ms**(lengduan 报告 + 量化脚本;第二批 TEXT→BLOB 迁移另行跟进)。 ## 🆕 新增 - **standalone API 补 profile / rules 只读路由(issue #180,PR #185,社区 PR)**:8790 独立 API 对齐 DSH 内工具的画像 / 规则读取(根路径 GET /profile → {profile}、GET /rules → {rules},Bearer 同款),MCP server v1(#181)的前置(z2Ace0107 从认领到 PR 不足一小时)。 ## 🏗️ 工程 - 测试 1061 项全绿(1060 pass + 1 环境 skip;较 0.8.1 新增 35 项:source 契约四管线 / CD 重排 / 单 logit 两路 / BM25 兜底 / coverage 降级 / archive 护栏 / 向量缓存与失效 / memory_get schema 护栏)。 - 致谢本轮社区贡献者与报告者:z2Ace0107(#185)、heptaspirit(#190 及 #104 数据)、Philia-FY(#187)、AoooooE(#188)、lqs50(#198)、lujfsd(#184)、lengduan(#202 报告与 #104/#135 数据)、483218131(#104 数据)。 ## [0.8.1] - 2026-09-15 ## 🆕 新增 - **归属显式声明与人工纠偏(issue #170 三步,PR #171/#172/#173)**:记忆归属从「自动标注一把抓」升级为三段式可控回路——① per-dim 来源列落库 + memory_save / update 新增 scope 参数 + 面板详情抽屉可编辑(显式声明的底座,#171);② sleep 跨 scope 相似对不再自动裁决,停车进冲突队列人工判定,纠偏回路与 #166 队列打通(#172);③ `strictScope` 硬过滤只认显式声明——自动载体标注降为纯软加权(保留可见),硬隔离与软降噪语义分离(#173,4.3 已确认的行为口径)。 - **版本自检(PR #176,#174 后续)**:独立插件自报新版本——`GET /api/dsh-mneme/version-check` 只读路由(运行版本 vs npm registry latest)+ 设置页偏差横幅(仅 outdated 渲染,up-to-date / ahead / 查询失败全部零渲染零打扰);registry.npmjs.org https + 精确 host 白名单(构造性排除本机/内网地址)、TTL 1h 缓存、5s 超时、全失败静默返 null;文案带 pnpm 钉子警示(安装时指定过版本的常规升级不跨 minor/major,#174 报障者的实际成因)与市场 ~1 天收录延迟说明(zh/en)。 ## 🏗️ 工程 - 测试 1026 项全绿(1026 pass + 1 环境 skip;较 0.8.0 新增 38 项:scope 显式声明 / 纠偏队列 / 版本比对与白名单拒绝矩阵)。 ## [0.8.0] - 2026-09-13 ## 🆕 新增 - **作用域隔离(issue #17 批次 A,PR #153/#155/#156/#157 + #163)**:记忆按 agent 与 workspace 双维标注(agent_scope / workspace_scope),检索加权(本会话命中 ×1.25、他 scope ×0.5 保留可见),opt-in `strictScope` 硬过滤贯通检索/注入/列表/单取四路(身份解析不到 fail-closed 只见未标注行);`scopeEnabled` / `strictScope` 默认关、进面板设置页「作用域隔离」组;去重键扩展 +agent_scope +workspace_scope +sensitivity——跨作用域同标题不再物理合并;memory_save 新增 sensitivity / occurred_at 参数;面板新增「作用域」徽章与详情抽屉四行(scope 标注在 web 会话全空的修复含于 #163)。 - **occurred_at 时间维度(PR #155)**:memories 新增事件发生时间列(区别于入库时间 created_at),occurred_from / occurred_to 闭区间过滤贯通 memory_search / memory_list 与搜索融合池;未标注行回退 created_at,存量数据可比。 - **冲突集中处理(PR #166)**:conflictFreezeEnabled 冻结的矛盾对新增人工出口——GET /api/dsh-mneme/conflicts 队列接口 + POST /conflicts/resolve 人工确认(盖章审计 + dream 同款处置:保留方追加已否决注记、另一方归档,CAS + 事务 + 幂等同源);状态页新增冲突队列视图(双方并排对比 + 保留 A/B / 仅标记已处理)。 - **自定义斜杠命令提交 Agent(PR #152,issue #151)**:斜杠命令的指令内容经 agent.followup 真正提交给模型执行,不再只是 UI 提示;声明 input.hint;无 followup 的宿主回退旧行为。 ## 🐛 修复 - **注入边界花括号转义恢复 + hot memory 跳过 reasoning(PR #165,issue #162)**:v0.7.4 的 escapePromptVars 在 v0.7.11 重构中被整块误删(0.7.32 受影响)——恢复三处注入出口的转义;hot memory 的 textOf 跳过 reasoning part,模型思考里出现的 `{{.Server.Version}}` 类文本不再让会话永久卡死;恢复 `escapePromptVariables` 配置(默认 true)。 - **scope 标注 web 会话全空(PR #163)**:scope 解析器先读 requestHeader()(EpochHeader 请求级路由头)遮蔽了 session.header——改为 session.header 优先;v0.8.0 发版前 web 实测发现并修复。 ## 🏗️ 工程 - **autoDream 溯源致谢(PR #158)**:README 增补理念来源与致谢章节(Claude Code Auto Dream / Sleep-time Compute 论文 arXiv:2504.13171 / cc-haha)。 - 988 项测试全绿(988 pass + 1 环境 skip;较 0.7.32 新增 72 项:作用域隔离全链路、冲突集中处理、注入转义回归等)。 ## [0.7.32] - 2026-09-12 ## 🆕 新增 - **记忆/提示语言选项(#124)**:新增 `memory.language`(zh/en)配置,prompt 提示词、注入与镜像语言可选,中文用户不必再依赖默认英文镜像。 - **自管本地推理运行时(#131:PR #132 + #133)**:transformers 改为可选 peerDependencies + 三档取件(收编 / 本地 .tgz / registry)——运行时依赖不再强制捆绑安装,宿主可按需选择自管或沿用插件内置。 - **summarize 节流、产出上限与同会话去重(#127,PR #143)**:新增最小触发间隔(节流)+ 每会话产出上限 + 同会话写入端去重,长会话不再反复蒸馏同一主题。 - **dream 候选集向量驱动 hybrid 档(#125,PR #147)**:巩固/睡眠的候选集召回新增 hybrid 档(关键词 + 向量语义混合),配置项与设置面板同步接入。 - **sleep 冲突动作集扩展(#126,PR #146 + #149)**:`sleepActionSet` 新增 `full` 档——supersede(取代:赢家正文干净、输家归档并附「已被取代」注记)/ differentiate(双留 + 差异注记)/ update / merge / conflict / keep 六分支;默认 `conflict`,**零行为变化**;被跳决策带 phase 名落库 `dream_runs.skipped`(审计可见);PR #149 补档位白名单、supersede 预填、索引维护等 6 处 review 修复。 - **sleep 校验路径接入 skipInvalid 宽容策略**:sleep 冲突校验现遵循 `dreamSkipInvalid`(默认 true),从「一票否决」变宽容——与 autoDream/consolidation(#104 方向)对齐;需严格一票否决可设 `dreamSkipInvalid: false`。 ## 🐛 修复 - **注入助手内容读取(#129,PR #136)**:assistant 正文改从 `data.message.content` 读取,适配宿主消息结构。 - **决策前缀唯一解析 + 失败路径校验明细落库(#135,PR #137)**:8 位 UUID 前缀与整串校验对齐;失败轮次校验明细不再静默丢弃。 - **effort 未配置自动最低档 + `dreamMaxTokens` 默认抬至 131072(#135,PR #138)**:默认档位不被模型支持时自动降最低档,思考模型不再因预算不足产生空体。 - **质量过滤 importance 豁免 + 系统信号标签不被 update 抹掉(#135,PR #139)**:低 importance 豁免与信号标签保留,避免合法记忆被质量门误伤。 - **semantic ready 区分「未配置」与「初始化中」+ 索引覆盖度降级告警(#135,PR #140)**:embedder 不可达不再与「未配置」混淆,覆盖度不足时降级并告警。 - **vector boot 回填不被模型指纹短路 + 只补活跃行(#128,PR #141)**:重启回填不再因指纹不匹配整体跳过,且只补活跃记忆行。 - **degraded 轮跳过明细落库 `dream_runs.skipped`(#104,PR #142)**:降级轮被跳的决策带原因落库,审计不再只记「run degraded」一个笼统状态。 ## 🏗️ 工程 - **#127 fail-safe 边界补覆盖(#144)**:审计写入器抛错不打断蒸馏、aborted run 回滚间隔打点(下一次被接纳)、候选查询异常降级 undefined、去重不可用时仍落库不丢条目。 - **runtime 测试 import 归一 src/ + 覆盖补齐**:runtime 测试从 `lib/` 切到 `src/`(c8 排除 `lib/**`,原统计把 runtime 覆盖低估到 65%),并补 embedding 成功/降级路径、`reindexMissing`、`defaultEngine` 假 transformers 驱动等 9 项用例——`src/runtime` 65.7% → 96.0%,总覆盖回到 **92.7%**(embedding 100%、verify 97.9%、index 71.6%)。 - 916 测试全绿(新增 sleep 冲突动作集 15 项 + summarize fail-safe 4 项 + embedding/runtime 覆盖补齐 9 项)。 ## [0.7.31] - 2026-09-11 ## 🐛 修复 - **peerDependencies 宿主版本声明不匹配中间预发布版本(#121)**:原 `^0.1.0-rc.6` 按 node-semver 预发布元组规则不匹配 `0.1.5-rc.1` 等中间预发布版本(当前 dsh 用户安装会 ERESOLVE),也违反 awesome-dsh-plugin 的 peer-range 预发布分支规范。改为显式三段式预发布分支 `>=0.1.0-rc.6 <0.2.0 || >=0.1.5-rc.0 <0.3.0 || >=0.2.0-rc.0 <0.3.0`:覆盖当前全部已发布 0.1.x(含 `0.1.5-rc.1`),未来 0.2 预发布留显式分支不静默排除,0.3 以上待验证后另行放开。 ## 🏗️ 工程 - 745 测试全绿(新 peer 范围实测匹配 dsh `0.1.5-rc.1`、`0.1.0-rc.6`、`0.2.0-rc.x`)。 ## [0.7.30] - 2026-09-10 ## 🐛 修复 - **状态页「向量索引」卡片改读 `/semantic`(issue #118)**:旧实现读带鉴权的 `/vector-config`——无存储 token 时 401,卡片显示「加载失败」;且其 `enabled` 只反映 OpenAI 兼容外部服务,ollama/local 模式恒显「未启用」。改读开放的 `/semantic`(覆盖全部 embedder 提供方 + 索引统计),新增「初始化中(embedder 不可达,正在重试)」与「已索引 N / M 条」回填进度两种显示;清理不再使用的 `vectorOn` i18n 死键。 - **legacy OpenAI 兼容 embedder 显示「Object · 0D」**:`createEmbedder` 返回对象字面量,`constructor.name` 为 "Object",且进程首次嵌入成功前 `dimension` 为 undefined——卡片渲染成「Object · 0D」。embedder 显式携带 `name: "OpenAI"`,`/semantic` 的 `embedProvider` 优先读 `embedder.name`;客户端维度回退索引元数据 `index.dimension`。 - **「已索引 N / M 条」分子大于分母**:`store.embeddedCount()` 不过滤归档/遗忘记忆(如实测库 269 条含 246 条归档),而 `count()` 默认只数活跃记忆(36)。embeddedCount 补齐 `forgotten = 0 AND archived = 0`,与分母同口径。 - **embedder init 失败不再永久降级(issue #118)**:Ollama 等异步初始化 embedder 在启动时不可达(服务未就绪)原先一次性判死为关键词搜索、只能重启恢复。改为有界重试(共 5 次:初始 + 4×15s),期间搜索降级关键词,重试耗尽才降级并告警;插件卸载清理重试定时器。`OllamaEmbedder` 暴露 `ready` 生命周期位,`/semantic` 新增 `ready` 字段(无 embedder 为 null、初始化中 false、就绪 true;legacy 无 `ready` 属性视为就绪)。 ## 🏗️ 工程 - 745 测试全绿(新增 `/semantic` ready 三态与显式显示名优先断言、embeddedCount 剔除归档/遗忘断言、`OllamaEmbedder.ready` 生命周期断言)。 ## [0.7.29] - 2026-09-10 ## 🐛 修复 - **实体抽取静默失败(#108/#109)**:实体抽取的 LLM 路由契约缺陷——`provider`/`model` 未显式配置时兜底读取默认路由选择有误,且 reasoning effort 被模型拒绝后无重试路径,导致抽取静默失败或结果为空。修复:`streamEntityText` 抽取为可测试导出的 `createEntityStreamAdapter` 工厂(显式 `provider`/`model` 优先,缺省兜底读 `agentDefaultModel.currentSelection`);配置的 `reasoningEffort` 被模型拒绝时自动去掉重试一次(与 autoDream/sleep 同一降级策略);新增实体抽取思考强度配置(`entityExtractionReasoning`)。 ## ✨ 新增 - **面板实体抽取控件**:设置页新增实体抽取 `provider`/`model`/思考强度三个控件,与巩固/睡眠一致的级联下拉 + 连通性测试。 - **「帮助与反馈」卡片**:设置页底部新增反馈入口——GitHub issue 预填(自动附插件版本 + 平台)+ 邮件 `work@modusensus.space` + 浏览已知问题去重;配套新增 `GET /api/dsh-mneme/info`(返回插件版本,供反馈环境信息)。 ## 🏗️ 工程 - 744 测试全绿(新增 effort 拒绝重试 / 显式路由优先 / info 版本一致性 / 反馈卡片源码断言 用例)。 ## [0.7.28] - 2026-09-09 ## 🐛 修复 - **连通性测试 `reply` 恒空(思考模型)**:`POST /api/dsh-mneme/test-model` 的最小调用 `maxTokens` 仅 16,思考模型(如 deepseek-v4-flash / glm-5.3-flash)的推理过程即可将其耗尽,正文一个字都吐不出来——`reply` 恒为空,「真的答了 ok 而非只报通」对思考模型不成立。提到 1024(按实际用量计费,手动按钮无放大成本)。 - **连通性测试按钮串扰**:测试结果状态为巩固/睡眠两组共享单份,点任一「测试连通性」按钮两组同时进入「测试中…」、互相禁用、结果行双份显示。拆为巩固/睡眠各持一份,互不影响。 - **睡眠路由显示「被重置」**:面板挂载时初始化草稿的键名单漏 `sleepProvider`/`sleepModel`——后端一直保存正常,但重挂载(切页签/退出重进)后下拉恒显「跟随默认路由」,看起来像设置丢失。补键后正常回填(历史已保存的值无需重填)。 ## [0.7.27] - 2026-09-09 ## 🐛 修复 - **v0.7.26 端点遗漏补齐(发版树缺端点分支)**:v0.7.26 的 CHANGELOG/Release 已宣告 `llm-providers` / `test-model` 两个设置面板端点,但承载它们的 `fix/dream-effort-trap` 分支未合入 main——发版树里面板「级联下拉 + 测试连通性」调用会 404。本版将该分支 rebase 到 main 后合入(PR #100),端点实际落地;与 main 已合内容重复的提交(docs 及 effort 陷阱修复等价补丁)rebase 时自动丢弃。 ## [0.7.26] - 2026-09-09 ## 🐛 修复 - **记忆巩固 `UNSUPPORTED_REASONING_EFFORT` 根治(defaultEffort 陷阱)**:harness 在调用方省略 effort 参数时注入 `reasoning.defaultEffort`(模型声明的最低档),若该默认档位本身不被模型支持(如火山 coding 适配器声明 `defaultEffort="low"` 而模型只收某固定档位),则任何重试策略都无效——换 effort 或去掉 effort 都会被同一个理由拒绝。修复:新增 `resolveDreamEffort`,在发流**前**经 `ctx.llm.resolveModelInfo()` 探测路由模型支持的 effort 档位,再发送受支持的档位—— - 配置档位(`dreamReasoningEffort` / `sleepReasoningEffort`)被支持 → 原样发送; - 配置档位不被支持 → 自动换用模型声明的 `defaultEffort`(若也支持)或首个支持档位,并 `logger.warn` 留痕; - 模型声明无 reasoning 能力 → 省略 effort 字段(发送普通补全请求); - `resolveModelInfo` 查询失败 → 按配置原样发送(fail-open,不因探测失败卡死巩固)。 - 影响面:autoDream(`dream.js`)与 sleep(`dream/sleep.js`)两条路径共用该 helper。 - **设置面板巩固/睡眠模型字段选型提示**:`dreamProvider`/`dreamModel`/`dreamReasoningEffort`/`sleepProvider`/`sleepModel`/`sleepReasoningEffort` 6 字段补 `.description()`——引导选非思考模型(思考模型可能烧光 token 预算返回空体导致巩固失败),并说明 effort 不支持时会自动换档。 ## ✨ 新增 - **巩固/睡眠模型连通性测试(Web 设置面板配套后端)**: - `GET /api/dsh-mneme/llm-providers`:宿主侧 LLM provider/model 发现(`ctx.llm.listProviders()`/`listModels()`,逐 provider best-effort),返回 `{ providers: [{ provider, models: [{ id, name }] }] }`,无 `ctx.llm` 时 501;**密钥仅存宿主侧,不经插件侧、不入本端点**。 - `POST /api/dsh-mneme/test-model`:`{ provider?, model?, reasoningEffort? }` 连通性探测——最小流式调用(maxTokens 16)实测模型真实可通。**空 body = 按巩固路由解析**(`dreamProvider/dreamModel > agent 默认模型`,解析不出返回 400 `no-route`);部分输入(有 provider 缺 model)400 `missing-provider-or-model`;成功 200 `{ ok:true, durationMs, modelId }`(+`reply` 预览),失败 502 `{ ok:false, error, durationMs, modelId }`;requireAuth 鉴权。 ## 🏗️ 工程 - **测试清理**:`client.test.js` 移除 identity `.replace()`(CodeQL `js/identity-replacement` 告警 #1,测试死代码)。 - 732 测试全绿。 ## [0.7.25] - 2026-09-09 ## 🆕 新增 - **`memory_get` 工具(第 8 个模型工具)**:按 id 读取单条记忆完整正文,用于 memory_search / memory_list 命中后读取全文。 - **`memory_search` / `memory_list` 输出内容预览**:render 现在嵌入命中条目标题、ID/type/importance 元数据与正文预览(不再只输出「命中 N 条」计数)——宿主只透传 render 文本时,模型也能直接读到记忆内容。 - **记忆巩固模型选型引导**:config 注释与 README 增加巩固模型分类声明(非思考模型 / 思考模型差异与建议),设置面板换巩固模型有据可依。 ## 🐛 修复 - **`memory_get` 执行崩溃:`userExecute is not a function`**:memory_get 的 execute 被误嵌套进 output 属性内(defineTool 拿不到 options.execute),模型调用必报错;此前测试只数工具名未执行该工具,714 全绿掩盖。已移回 defineTool 顶层 + 补回归测试。 - **工具重复注册(DSH live patch reload)**:热补丁重载同一 registry 时 `tool "memory_search" is already registered`——新增按 registry 的 WeakMap 工具名去重,重复跳过并告警。 - **Standalone API 端口冲突(EADDRINUSE)**:多 DSH profile / 实例共用默认端口时 API 永久不可用——新增 listenWithRetry(最多 20 次递增 + 最终落 OS 分配端口 0),实际绑定端口回传调用方。 - **client inject 声明与实际消费不一致**:声明 `["slots","locale","layout","connection"]`,实际只消费 slots/locale——对齐为 `["slots","locale"]`。 - **better-sidebar 集成加固**:peer 依赖 `*` → `^0.18.0`(仍可选);registerTab 前 getTab 查重跳过重复 tab;注册失败告警并降级回原生侧边栏入口,不再直接报错。 ## 🏗️ 工程 - 718 测试全绿(+4 回归测试:memory_get 执行 / 缺失 id、memory_search / memory_list render 内容嵌入)。 ## [0.7.24] - 2026-09-09 ## 🐛 修复 - **DSH Desktop 插件树加载崩溃(v0.7.23 回归):`cannot get property "webServer" without inject`**:v0.7.23 把 webServer 从 inject 声明中去掉想支持 headless,但 cordis 4 的 ctx 是 Proxy——**直接访问未在 inject 声明的属性会抛错而非返回 undefined**,`if (ctx.webServer)` 守卫因此失效;且去掉 inject 后 cordis 不再等待宿主 webServer 服务就绪,Windows 桌面端重启即插件树加载失败。修复两处: - **恢复 webServer 到 inject 声明**:cordis 等宿主服务激活后再 apply,路由注册不落时序(回退到 v0.7.22 已知正确行为)。 - **apply/register 守卫改 `ctx.reflect.get`**(cordis 免 inject 读取,未提供返回 undefined 不抛错):即使未来把 webServer 移出 inject 支持 headless,apply 也安全跳过 API 注册而不崩。 - **真实 cordis + dsh-host-webserver 插件本地实测**:API 路由真实注册(GET /api/dsh-mneme/list → 200 JSON)、未知路径 404 JSON、headless 无 webServer 时静默不激活。 - **回归测试(真实 cordis Context 两种宿主形态)**:有 webServer 时插件 boot 且注册 exact 路由 / 无 webServer 时不抛错——直接覆盖本次崩溃根因,防止再犯。 ## 🏗️ 工程 - 714 测试全绿(+2 回归测试)。 ## [0.7.23] - 2026-09-09 ## 🐛 修复 - **记忆沉淀「反复失败」根治:consolidation 合法空数组 `[]` 不再误判失败**:CONSOLIDATION_PROMPT 明确允许「无问题的条目无需输出」,模型在记忆库无冗余时输出合法 JSON `[]`(**不是**空体/截断)——`validateDecisions` 却硬判 `decision list must be a non-empty array` → runDream 整单 failed、审计表反复记失败,**记忆越健康越容易「失败」**。与 sleep 空模式(skipped no-op)语义不一致,且**与模型无关**(任何遵守 prompt 的模型都会踩中,ChatGPT/Claude 同样)。修复:对空数组显式短路 `{ok:true, applied:0}` no-op(不可简单放行——隐式 keep 的覆盖率检查 0%<50% 会拦截)。Linux 本地真实 LLM(火山 coding OpenAI 端点)实测验证:无冗余场景 `ok:true applied:0 summary:true`,审计记 ok。 - **空体修复第二段:`dreamMaxTokens` 默认 8192→32768**:思考型模型(v4 系)默认推理全开,8192 预算被 reasoning 烧光正文为空 → 默认预算翻四倍给推理留余量;上限 131072 不变,非思考模型实际用量远低无成本影响;设置面板可调。 - **skipInvalid 恢复代码 splice 残留 bug(v0.6.9 原版就带,issue #89 回归路径)**:`decisions.splice` 守卫用长度相等代理「内容一致」——当「被跳非法决策数 == 隐式补齐 keep 数」时长度回等但内容已变,被跳决策残留进 apply/audit(重复 claim 的 merge 会被照样应用)→ 改无条件 splice。由本次新增的 skipInvalid 全量测试揪出。 ## 🏗️ 工程 - **skipInvalid 恢复路径全量测试补全**(12 条:逐原因跳过 + 全局闸门交互 + runDream e2e)。 - **合法空数组 e2e**:consolidation 返回 `[]` → run ok / applied 0 / summary 照跑 / audit+receipt 记 ok。 - ~~**webServer 可选化**:inject 声明去掉必填 webServer(headless/无 UI 宿主兼容),API 注册改运行时守卫。~~ ⚠️ **失败已回退**:cordis 4 直接访问未注入属性抛错(`if (ctx.webServer)` 守卫失效)+ 去掉 inject 后 cordis 不再等待宿主 webServer 服务 → **桌面端插件树加载崩溃**,见 [0.7.24] 修复。 - **712 测试全绿**。 ## [0.7.22] - 2026-09-09 ## 🐛 修复 - **恢复 v0.6.9 的 skipInvalid 宽容校验路径(issue #89 回归,v0.7.11 重写丢失)**:v0.6.9(issue #26)实现的 `dreamSkipInvalid`(单条非法决策跳过 + 合法子集应用 + run 记 degraded)与 `allowCrossTypeMerge` 开关在 v0.7.11 近重写时丢失,README Fail-safe 一节承诺的宽容行为自此与代码错讹;qwen3.8-flash 等弱模型决策合规抖动下 4/4 全败,整单拒绝白烧 LLM 调用。恢复三件: - **`decisions.js` 原样移植 skipInvalid 双轨结构**:单条错误进 local,skipInvalid 时记入 skipped 并从 decisions 原地剔除、不 claim 任何 id;严格模式(不传开关)行为不变,sleep 路径维持严格。 - **`allowCrossTypeMerge` 显式放宽跨类型合并**;全局上限/覆盖率下限仍整单拒绝(刷爆上限=模型坏了,不是轻微 schema 漂移)。 - **`dream.js` 透传 config 开关**:skipped 非空时 run 状态如实记为 degraded(ok:true 保持基线刷新语义),跳过明细进 warn 日志。 - **autoDream 最小触发间隔(issue #89 请求 2)**:新增 `dreamMinIntervalMinutes`(0-10080,默认 0=不限);`minIntervalMs` 节流——失败/degraded run 也占用间隔(节流目的正是防失败调用连发),间隔内触发静默跳过。 ## 🏗️ 工程 - 三个新 feature flags(`dreamSkipInvalid` 默认 true / `allowCrossTypeMerge` 默认 false / `dreamMinIntervalMinutes` 默认 0)入白名单,**34 键**。 - 696 测试全绿(+8:v0.6.9 五用例移植 + degraded/严格 e2e + 节流 + 白名单计数)。 ## [0.7.21] - 2026-09-08 ## 🐛 修复 - **autoDream/sleep 的 effort 回退在流式路径上是死代码(v0.7.16 补的 catch 式回退不生效)**:dsh-llm rc.1 的 `adapterStream` 把 adapter 阶段异常(含 `resolveCallWithInfo` 抛出的 `UNSUPPORTED_REASONING_EFFORT`)转成终态 error finish chunk,不再向上抛出;`streamText` 丢弃 `chunk.reason.failure`,导致配置 `dreamReasoningEffort`/`sleepReasoningEffort` 后巩固/睡眠请求被 provider 即时拒绝(tok=0、duration≈0)时,catch 式回退永远不触发,`llm_audit` 只记笼统的 `llm stream aborted or errored`。修复三处: - **`streamText`(dream.js 与 sleep.js 两份)捕获 finish-chunk 失败原因**:新增 `describeStreamFailure` 把 `chunk.reason.failure` 归一化为 `{code, message}` 并导出。 - **`withEffortFallback` 增加 `getStreamError` 访问器**:结果为 `undefined` 且原因匹配 `reasoning effort` / `UNSUPPORTED_REASONING_EFFORT` 时去掉 effort 重试一次(流式路径与 catch 路径同等对待)。 - **`runAuditedLlm` 支持 `spec.streamError`**:audit 行 `error_message` 携带真实原因(如 `llm stream aborted or errored (UNSUPPORTED_REASONING_EFFORT: ...)`);`run.error` 保持稳定的 `"llm failed"` 不变(对外契约不动)。 ## 🏗️ 工程 - 688 测试全绿(+3:dream/sleep 流级 effort 拒绝触发去 effort 重试;非 effort 流失败不盲目重试且真实原因进审计行)。 ## [0.7.20] - 2026-09-08 ## 🆕 新功能 - **heat 热度模型回归(issue #87 社区反馈)**:v0.7.11 近重写时与 Wiki-Link/tag 等一并被移除的 v0.7.0「自进化记忆 heat 热度模型」完整找回: - `src/heat.js` 纯函数模块恢复:幂律衰减 `H=1/(1+λΔt)^α` + per-type 差异化半衰期(`TYPE_DECAY`,preference/pattern/summary 免疫 λ=0) - 配置恢复:`heatEnabled` / `heatGlobalAlpha` / `heatTypeDecay` / `sleepHeatThreshold` / `recallRecordDefault` / `recallRetentionDays` - **sleep 降级热联合双保护**:phaseDemotion 需「时间窗冷 + heat<阈值 + importance<5」才降级,免疫类型永不因 sleep 降级;ref 语义恢复 `last_accessed_at ?? created_at`(updated_at 不算访问) - **touchRecalled 门控**由 `heatEnabled` 接管;**实体热投影**回归(ego 图谱节点 heat 字段 + 前端 nodeRadius/fillOpacity 随热度缩放) - **recall_runs 默认记录**恢复(`recallRecordDefault: true`) - **阶段二前端(验收清单完成)**:/list 在 `heatEnabled=true` 时逐条下发 heat 投影(`heat.js` 纯函数,λ=0 免疫类型恒 1.0;字段缺省前端自动隐藏);HeatBadge 三档徽章(Lucide flame + 整数百分比:热≥66% 橙 / 温 33-66% / 冷<33% 弱化藏百分比,卡片页脚与详情抽屉共用);状态页热度分布卡(采样最近 200 条三档统计,自门控);**order=heat 页内排序**——热度是运行时投影无存储序、SQL 排不了且分页切片后页内非全局序,走前端页内排:卡片网格对已加载条目按 heat 降序、时间树保持 chrono 不受影响;「热度优先」chip 自门控,时间线视图开启时自动切卡片视图 ## ⚠️ 行为变更(验收清单落地) - **heatEnabled 默认关**(`false`):v0.7.12 起用户已习惯无 heat 行为,默认开=全员行为变更。默认关时 sleep 降级退回纯时间分层(v0.7.12 行为),touch 零写入。 - **feature_flags 白名单接线**:`heatEnabled` 加入 `FEATURE_FLAG_BOOLEANS`(现成 30 键机制的 31 键)——面板可启停 = 线上回滚开关,首次线上事故无需回滚版本。 - **lightMode 联动关停**:`heatEnabled` 加入 `LIGHT_MODE_OFF`,轻量模式不开热计算。 - **sleep 降级审计暴露**:`/api/dsh-mneme/dream-status` 返回 `run_type` + `demotion {demoted, archived}` 计数(数据来自 runSleep 已写入 dream_runs 的 `decisions.demotion`)——状态页工作动态可展示「降级 N 条 / 归档 M 条」,自动改记忆留痕。 - **updated_at ⊥ last_accessed_at 契约**:heat/sleep ref 永不 fallback 到 `updated_at`(只用 `last_accessed_at ?? created_at`);heat 是运行时投影不写盘;heat 加权不进入 `order=chrono`(记忆库分页/月份树/无限滚动稳定序不受影响)。 ## 🐛 修复 - **better-sidebar 软集成无 bs 环境启动失败(issue #88,用户打不开文件)**:根因是模块级/manifest 的 `inject: ['betterSidebar']` 声明被 loader 硬等待——未安装 `dsh-better-sidebar` 的环境整个 entry pending(`'1 entry did not activate'` → Failed to load plugins)。改为 dsh-server-deck 同款双模式:外层入口零 inject 立即激活(独立模式保底),tab 注册挪进 `ctx.plugin({ inject: ['betterSidebar'] })` 内层动态子插件由 cordis 原生等待——bs 未装时内层 fiber 永远 INACTIVE,静默无害;模块与 manifest(`package.json dsh.client.inject`)同步移除。 ## 🏗️ 工程 - 685 测试全绿(heat 后端 +15、阶段二前端 +2:/list heat 投影契约 + client HeatBadge/分布卡结构断言;better-sidebar 修复改结构断言锁定内层子插件模式) ## 📝 历史补记与勘误(issue #87 全仓审计) - **删除版本勘误**:v0.7.11(发布提交 `ce4658e`)近重写时移除了一批 v0.6.x/v0.7.0 实验功能(Wiki-Link、tag 系统/目录/tag 加权、user/fact 分层类型、前缀 id 解析、/stats 与 /directory 端点、heat、内置面板 client.js 内联面板)——此前本 CHANGELOG 与 README 中英 archival note 把删除归因于 v0.7.12,已统一勘误。v0.7.11 与 v0.7.12 同日发布,v0.7.12 相对其父提交未删除任何文件。 - **静默丢失补记**(此前全文档零记载,本次如实补录,**不找回**): 1. **provenance 出生会话溯源(`session_id`)**:v0.6.1 引入,v0.7.11 连测试一并删除;`source` 字段与 `epistemic_status` 来源分级仍保留 2. **会话生命周期(`session_disposed_at` 软隐藏)**:v0.6.0 引入,v0.7.11 从存活的 store/service 静默剥离;README 死文档与死配置键(`sessionLifecycleEnabled`/`showSidebarTrigger`)已清理;留待 issue #47 无痕会话重做 3. **normalizeDecisions 决策字段归一化**:v0.5.3 引入,v0.7.11 删除,功能由「提示词硬规范字段名 + extractJsonArray 解包」取代 ## [0.7.18] - 2026-09-08 ## 🆕 新功能 - **better-sidebar 软集成(生态第一步)**:声明 `betterSidebar` inject + optional peer `dsh-better-sidebar`,运行时软探测注册「记忆库」tab(id `dsh-mneme:memory`,复用记忆库/实体/状态/设置四视图,10×1s 重试兜服务时序);未安装时安全跳过,独立入口(侧边栏 sheet)完整保留。`.mneme-x` 加 container-type:inline-size,`@container ≤640px` 隐藏卡片/时间线 seg 防窄容器重叠。双模式真机实测:无 bs 环境回退完好,有 bs 环境经「+」菜单进入四视图正常。 - **记忆库沉淀/归档筛选**:新增 `GET /list?deposited=only`——沉淀视图 = receipt_chain 的 merge/update live verdict(record_id 即保留/更新目标)∪ source="dream" 直写,total 同过滤,conflict 不算沉淀;筛选栏「来源」分组新增「沉淀」「已归档」chip,可与类型/重要性/时间筛选叠加(deposited ∩ archived 组合有效)。 ## ✨ 优化 - **状态页仪表盘化**:沉淀/归档长列表收敛为「小页预览 + 服务端 total + 查看全部」,点击跳转记忆库并预置对应筛选——查询收敛到有搜索与分页的浏览视图,状态页保持轻量。 - **详情抽屉「恢复」**:归档记忆在抽屉内一键回主列表,与「归档」操作对称。 ## 🧪 测试 - deposited 视图组合过滤(∪ source=dream、∩ archived=only、默认列表不受影响)+ client 结构断言(chip/跳转/恢复/服务端 total);667 测试全绿。 ## [0.7.17] - 2026-09-08 ## ✨ 优化 - **侧边栏入口持续对齐宿主**:入口按钮的类名此前在挂载时刻快照一次,宿主/皮肤异步改写「新会话」按钮类名后会停留在旧类上(宽度/对齐失配)。改为 MutationObserver 监听类名变化持续同步,并加 `width:100%` 与宿主行同宽;去掉自作的 label 左对齐覆盖(`flex:1; text-align:left`),交还原生居中。 - **重要性星级改为星形图标**:内联 Lucide v1.42 star 路径数据(morphicons 官方配套的数据包;插件运行时禁止 require 第三方库,沿用既有内联惯例),新增 `ImportanceStars` 实心/空心星形组件,替换详情抽屉与卡片页脚的文本 ★(编辑态 `