diff --git a/dsh-mneme/CHANGELOG.md b/dsh-mneme/CHANGELOG.md index efe8d436..fc72cb95 100644 --- a/dsh-mneme/CHANGELOG.md +++ b/dsh-mneme/CHANGELOG.md @@ -2,26 +2,36 @@ ## [Unreleased] -## ⚠️ 行为变更 - -- **`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`)。 +## [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 ## 🆕 新增 diff --git a/dsh-mneme/package-lock.json b/dsh-mneme/package-lock.json index 10a81c0e..88c96d04 100644 --- a/dsh-mneme/package-lock.json +++ b/dsh-mneme/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modusensus/dsh-mneme", - "version": "0.8.15", + "version": "0.8.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modusensus/dsh-mneme", - "version": "0.8.15", + "version": "0.8.16", "license": "MIT", "dependencies": { "morphicons": "^1.7.1" diff --git a/dsh-mneme/package.json b/dsh-mneme/package.json index f824f9a2..6c2b39f6 100644 --- a/dsh-mneme/package.json +++ b/dsh-mneme/package.json @@ -1,7 +1,7 @@ { "name": "@modusensus/dsh-mneme", "description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 9 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel", - "version": "0.8.15", + "version": "0.8.16", "license": "MIT", "repository": { "type": "git",