Repository navigation
v0.9.5
🆕 新增
-
记忆溯源有了读出口:
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。 -
/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 以下。 -
宿主「极简模式」为什么不生效,现在写在用户点开关的那一页(#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_count46 轮全部 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:393sleep 链路),改一处两处都受益。回归测试 +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: "<reason 截 60 字>")可能夹带记忆正文、也可能被截断成没有右引号(那样按引号配对的替换根本剥不掉),而/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_compress18:32 成功、18:35 又失败,而失败行的metadata里raw_has_bracket = true、raw_len2574/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_consolidate9 次失败、输入 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)——否则配额调大之后无界重取每次巩固都会触发,等于把省下来的那一半又花回去。 -
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 预设压制(非插件缺陷),而「解释只长在状态页」是这一版面板改动的原因。
-
@niangnniang — — #521 的报告:
dreamMergeGuard默认开之后 merge 护栏与 archive 护栏不对称,合并提议被整类跳过、提示词却还在催合并。