From dfc075fc7b2e6cb9d4f0c076530053c8728ddb03 Mon Sep 17 00:00:00 2001 From: modusensus Date: Fri, 30 Oct 2026 04:01:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs(changelog):=20=E6=8A=8A=20v0.9.4=20?= =?UTF-8?q?=E4=B9=8B=E5=90=8E=E6=94=92=E7=9A=84=E4=B8=9C=E8=A5=BF=E5=86=99?= =?UTF-8?q?=E8=BF=9B=20Unreleased=EF=BC=88v0.9.5=20=E5=8F=91=E7=89=88?= =?UTF-8?q?=E5=89=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release-prep 只搬运不写内容:[Unreleased] 里现在只有 6 条,而 v0.9.4 之后合了 9 条 PR。照原样发版,release notes 里连「分享包」这个本版主题都不会出现。 补齐的是:分享包四车道(#386 A–D,作为新增的主条)、#505 去重候选窗口(含那条表达式 索引的实测取舍)、#515 模式扫描的政策轴位置、#518 设置页目录、#514 极简预设告示搬页。 措辞沿用 #503/#504 那两条的形状:先说现象,再说为什么这么修,数字与判据留在条目里。 分享包那条刻意没有写「面板已在宿主实测」——车道 D 目前只有静态源锁,docs/SHARE.md 自己写着「不是浏览器里看过」。真机四步跑完之后再由定稿的人决定要不要补那句读数。 --- dsh-mneme/CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index fac099cc..5f79bdf6 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -6,6 +6,12 @@ - **记忆溯源有了读出口:`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:真重复放行、只有理由不同类不放行、同类型但只写「主题相近」仍拦。 @@ -14,6 +20,8 @@ - **一条只错在分类词上的窗口,被当成「模型没按格式写」反复重放(#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 ## 🆕 新增 @@ -558,7 +566,6 @@ - **能力说明第 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` 兜底兼容用量平铺在顶层的替身(嵌套 + 平铺双形状回归测试)。 From 4115e99a235587bd7942f57cd04ec9b25e72e0d1 Mon Sep 17 00:00:00 2001 From: modusensus Date: Fri, 30 Oct 2026 04:35:30 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(changelog):=20=E8=A1=A5=E4=B8=8A=20#50?= =?UTF-8?q?7=20/=20#508=20/=20#513=20=E4=B8=89=E6=9D=A1=E5=A4=96=E9=83=A8?= =?UTF-8?q?=20PR=20=E7=9A=84=E6=9D=A1=E7=9B=AE=E4=B8=8E=E8=B4=A1=E7=8C=AE?= =?UTF-8?q?=E8=80=85=E5=B0=8F=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对着 issue 编号核缺条目是不够的——得对着 PR 作者核。v0.9.4 之后 z2Ace0107 合了四条 PR,之前只有 #509 有条目,另外三条(#508 的 tier 档位信号 / #507 与 #513 的违规去重键 两步)一个字都没提,而 #508 还是个 feat。 同时按 CONTRIBUTING 的形状补上 ### 贡献者 / Thanks 小节:@z2Ace0107(四条 PR,写明 分别是什么)、@strange2000(#493 现场报告,#501/#502 两半都由它展开)、 @SternChiri(#505)、@zingreqe(#514)、@youli42(#518)、@niangnniang(#521)。 写代码的与报问题的分开写,谁做了什么一句到位——只列用户名等于没谢。 小节随 [Unreleased] 一起被 release-prep 整段搬进新版本节,不需要发版时再手搬一次。 --- dsh-mneme/CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index 5f79bdf6..24156a68 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -8,6 +8,8 @@ - **把本机记忆打成一个包发给同样装着 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。 +- **`/context` 的响应多了一个 `tier` 档位信号:嵌入降级不再对调用方静默(#383,实现由 @z2Ace0107 贡献,PR #508)**:独立服务的 `/context` 原先只有 `profile` / `rules` / `items` / `pinnedCount` 四键,而语义召回依赖的嵌入一旦降级,返回的 `items` 只是换了算法来源、形状完全一样——调用方看不出这次是「语义档」还是「规则档」,也就没法在自己那边决定要不要重试或提示。现在第五键 `tier` 三值:查询串为空 → `unknown`(语义路未启用,不宣称档位)、查询向量非空 → `full`(嵌入真的成功了)、否则 → `degraded`(因为降级才走规则档)。三个取值都是 fail-closed 的:**判不出来报 `unknown`,不报 `full`**——把「不知道」写成「正常」是最省事的谎言,也正是这条 issue 要消灭的那种静默。 + ## 🎨 面板 - **设置页加了一栏粘顶分组目录,一键跳到各分类(#518)**:设置页的卡片攒到需要滚半屏才找得着一个开关,而用户要的「一个分类直接跳到某一节」在这里有个前提——`.mneme-set` 自己是整块滚动的容器。目录由**分组标题那一张表**生成(五个标题与五个 chip 从同一个 map 里产出,不手写第二份清单:两份清单迟早会漂,而漂了的目录会把人领到不存在的锚点),标题在渲染的同时把自己注册成跳转目标。粘顶是因为不粘顶的目录滚两下就飞出视野,那不叫方便跳转;跳转目标自带 `scroll-margin-top`,否则点一下正好把那个分类标题停在目录底下——「跳过去了」和「看得见」是两件事。窄屏收成横向滚动而不是换行:换行会让粘顶条吃掉半屏高度,还会连带把上面那个偏移弄失准。`nav` + `aria-label` + `focus-visible` 是键盘与读屏那一半,用降透明度区分禁用态会把文字压到 AA 以下。 @@ -22,6 +24,17 @@ - **同标题的记忆不再被当成新记忆写第二份:去重候选取的是「该类型最重要的 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)——否则配额调大之后无界重取每次巩固都会触发,等于把省下来的那一半又花回去。 + +- **M.O. 的前置违规去重键会塌缩:没有 `run.at` 的 run 被并成一条,补上时间戳之后又被并回去(#461,@z2Ace0107 的两步修复 PR #507 / #513)**:`suggestRetirements` 用 `violationSeen` 记「同一轮里同一条违规只报一次」,键以前是 `${run.at ?? ""}|${step.id}|${need}`,而这个集合**跨整本账本共享**——于是所有没有时间戳的 run 的同 `(step, need)` 违规共用一个键,只剩第一条被报出来。`run.at` 存在时行为逐字节不变,缺的正是最需要看数据的那批。修法分两步:**#507** 用位置索引兜底(`#${i}`),并把「缺失」的判定从 `?? ""` 扩到含空串(`run.at = ""` 以前会当成合法时间戳参与拼键);没拿 `run.id` 兜底是因为 run 对象**根本没有 `id` 字段**——`normalizeRun` 只产出 `{ at, by, triggeredBy, steps }`,位置索引是这里唯一现成的判别项。**#513** 补的是 #507 合并之后复盘指出的遗漏:匿名指纹原先只取 step 的 `id`,于是两条都缺 `run.at`、其余全同、只在 `steps[0].at` 上不同的 run 仍是两次**不同的观测**却共键——那是同一类键塌缩的反面形态(该分开的被并了),所以指纹补进 `step.at` 等结构化字段,并把 `null` 与缺失区分开写进 JSON。 +### 贡献者 / Thanks + +- **@z2Ace0107** — 本轮四条 PR:`/context` 的 `tier` 档位信号(#508,#383)、记忆溯源读出口 `GET /memory/provenance`(#509,#421,含「409 表示用户关了、503 表示装配坏了」这套状态码口径)、M.O. 违规去重键的两步修复(#507 与 #513,#461)。 +- **@strange2000** — #493 的现场报告与对照数据:蒸馏把转录当成「该我回话了」导致同一窗口确定性失败、重试越试越贵。#501(五处 prompt 未围栏)与 #502(`type` 枚举不对称)这两半都是从这份报告展开的。 +- **@SternChiri** — #505 的报告与复现条件:`saveWithDedupe` 的去重候选是「该类型最重要的 100 行」,库一大就静默不再合并同标题记忆。 +- **@zingreqe** — #514 的现象与定位线索:在 DSH 极简模式下开启人格定义却不生效。结论是宿主侧 minimal 预设压制(非插件缺陷),而「解释只长在状态页」是这一版面板改动的原因。 +- **@youli42** — #518 的功能请求:设置页攒到要滚半屏才找得着一个开关,希望有分类索引能直接跳。 +- **@niangnniang** — #521 的报告:`dreamMergeGuard` 默认开之后 merge 护栏与 archive 护栏不对称,合并提议被整类跳过、提示词却还在催合并。 + ## [0.9.4] - 2026-10-21 ## 🆕 新增