Repository navigation
Releases: axuan-1314/dsh-keepsake
Release list
v0.7.1 — 纠错与检索闭环 + 面板可读性根治
本版要点
v0.7.1 是 v0.7.0 之后的一轮功能补充与稳定性收尾,共 52 项改动。核心是让记忆「能被纠错」「能被查到」「能被整理」三件事真正闭环,并把面板的可读性问题从根上修掉。
新增功能
- 纠错闭环:给「这条记忆记错了」一条正式通道。以前记错只能等下一轮提取碰巧覆盖,常留下两条互相矛盾的事实;现在可以说「这条错了,正确的是 X」——对话里用
/memory fix,或在面板检索后点「纠正这条」。改动有快照,可回滚。 /memory命令:不用开面板就能关键词检索记忆,支持列最近、查命中位置、纠正、遗忘。- 项目知识页:把一个项目的记忆压缩成 5 页稳定视图(组件地图 / 核心概念 / 约定与模式 / 关键决策 / 进行中),手动生成、不自动烧钱。真源仍是条目,删掉可重建。
- 空闲整理(默认关):空闲时让模型合并重复、清理过期的项目记忆。模型只提案,落盘仍走批锁、快照、写后校验,删除进回收站。
- 轮次活动条:把提取 / 注入 / 整理 / 纠错四面观测按时间窗归并成一条时间线,面板新增「最近活动」页。
修复重点
- 面板在浅色底上不可读:面板把宿主背景 token 原样搬进来却不给实色底。壁纸插件把背景改成半透明白后,白字压在洗出的中灰上实测只有 3.10:1(可读性标准 4.5),卡片和正文底 1.00:1 完全同色、没有边界。现在面板自带不透明实色四档,文字与边框仍跟随宿主主题。
- 顺带纠正了一个错误设想:曾以为「把半透明合成到实色上」能解决,复算证明不行——对比度只由那层白的透明度决定,与垫在下面什么颜色无关。
- 纠错总是「改一半」:写盘侧要求带上当前文件的内容指纹才允许覆盖,而纠错拿不到这条指纹,于是每次都退化成「保留旧文 + 追加新文」,盘上新旧事实并存、下一轮全进了上下文。现在纠错读到原文时一并算出指纹,真正做到了替换。修复前后实测:同一诉求原先留下 8080 与 9090 两条,现在只剩 9090。
- 召回端点配错会让整个插件起不来:原先只要「开启语义召回」但没填端点,插件启动即抛错,连核心的记忆提取与注入一起废掉。现在插件照常加载,语义召回被显式关闭,面板出红条点名缺哪个键、去哪补;补齐配置后不重启即恢复。
- 注入预算不够时索引整段消失:全局索引与项目索引同时放不下时会被整段丢弃,模型看不到记忆目录。现在先降级为紧凑索引(分类计数 + 最新 N 行 + 路径),实在装不下才丢。
- 记忆列表「最近的」不最近:原先按固定分类顺序灌满上限,后面的分类新写入的条目永远不出现。现在全局按修改时间排序后再截断。
- 检索不认全角与 Unicode 变体:正文写
PNPM、查pnpm命中 0 条;café的两种规范化形态互查同样 0 条。现在两侧统一做兼容性归一化,片段仍从原文切出。 - 知识页落盘会留半新半旧:末页写入失败会留下「新页 + 旧清单」。现在改成两阶段提交,任一阶段失败整体回滚。
- 另有十余项观测面与错误回报的收尾:被打回却没说明的改动、按条目看历史会误报「没改过」、整理被挡住时完全静默等。
测试
测试从 1253 增至 1378(+125),skip 归零。新增面板配色可读性的结构检查(对比度数学 + 双主题达标 + 背景不消费宿主 token),以及纠错闭环的端到端接线检查。多处改动做了变异验证——把实现改坏,对应用例确定变红。
安装 / 升级
dsh plugin --profile web add "https://github.com/axuan-1314/dsh-keepsake/archive/refs/tags/v0.7.1.tar.gz"--profile 换成 headless 或 desktop 亦可。零运行时依赖,不需要 npm install。无数据迁移、无配置键变更。
完整逐条记录见 CHANGELOG.md。
v0.7.0 — 召回故障可见化 + 中止分类 + 输出预算逃逸
[0.7.0] - 2026-10-24
召回端点故障可见化 + 硬事实守护指路 + 在途中止分类 + 零正文失败路径。 本批由三个模块并行开发、随后经三路独立检查(测试有效性 / 自伤路径 / 终端用户视角)发现 2 条实现 P0、2 条检查 P0 与若干 P1,逐条修复后收口。先交代三件对账事实,防止后续翻案:
- 方案 v2 §4.1(放弃闸重设计)的前提不成立,本批不动放弃闸。 重启后 journal 里 0 条
no-route(级联已断),唯一残留失败族是ABORTED(见修复③)。 - 「max-tokens 复发」被证伪。 此前把重启后一条
llm-error误读成kind=max-tokens——逐字节核验原文是ABORTED;最后一条真正的 max-tokens(11:22:48Z)早于插件重启(13:32:50Z)。注意插件经 npm link 开发安装、直连本仓工作树,代码生效看重启时刻,不看提交时刻。 - 但「零正文 max-tokens」这个失败模式本身是真的(上一批之前 81/88 条 llm-error 都是它),且机制已在本机网关实测坐实:思考 token 与正文共享同一个
maxTokens预算、思考链先跑(max_tokens=64→ reasoning 258 字符 / content 0 字符),丢reasoningEffort救不了(不传档位模型照样思考)。本批修的是它的失败路径——有界、不重演上一批。
修复
- 在途中止按「执行结果」分类(本机真实故障,本批含金量最高的一件)。本机
extraction-errors.log13:28/13:37 两条llm-error … ABORTED:插件callLlmText的单次超时计时器(默认 120s)controller.abort()后,宿主把它映射成code=ABORTED——不在可重试族 → 一次判死;1291-seq 的大区间(越长越有价值)最容易被它吃掉。修法两条,判据全是执行结果、不猜错误形状:① 计时器超时 → 按controller.signal.reason === timeoutError补挂宿主同形code=TIMEOUT(既有可重试族)→ 链上退避重采样;② dispose 在途(卸载打断在途 attempt)→runAttemptChain捕获处先查lifetime.signal本尊 → 按aborted收场,不进llm-error桶、不推失败连击(是我们在卸载,不是模型失败)。 - 零正文
max-tokens:显式配小上限的用户有界逃逸;跟随模型不进重试链(成本纪律)。显式配了maxOutputTokens > 0时零正文 → 丢掉该上限、改跟随模型重发一次(唯一真正改变预算的手段,实测同一 prompt 不下发上限时正文正常产出),只逃逸一次,且重发拿到正文才计数(escapeRetried在成功返回点记,发起不计——此前检查抓过"逃逸失败时健康卡同时显示『提取失败』与『重发成功』"的意图当结果)。刻意不给它开链级重采样:实现版曾自造code=OUTPUT_BUDGET_EXHAUSTED进RETRYABLE_FAILURE_CODES,权衡后移除——跟随模型(本机 25 万 token 预算)仍零正文在实践中不可达,为它重采样最坏是retryAttempts × fallback 条数次满预算调用(实测retryAttempts=10+ 3 条 fallback = 84 次),拿真金白银换一个几乎不存在的场景。RETRYABLE_FAILURE_CODES保持宿主DEFAULT_RETRYABLE_CODES五项镜像(有测试钉住集合本体)。 - 召回端点配错:从「启动即抛、整个插件起不来」改为「插件照常加载 + 面板/门牌红条指路」(方案 v2 §4.2 降级落地,不加开关)。原实现:
resolveConfig对「recallEnabled: true但缺embeddingBaseUrl/embeddingModel、或injectChannel不是message」直接throw,而apply()第一行就调它——用户没填一个可选功能的端点,代价是连核心的对话记忆提取与注入一起起不来。现在拆成两类:- 静态配置类:不再抛错。
recallEnabled被显式置 false(运行时零嵌入调用,不是"开着但每轮白撞"的 fail-open),诊断经/state.recallDiagnostics透出,红条文案(L.recallDisabledNoticeText,面板/门牌/启动日志同一出处)三岔口径、全部按当前生效配置每轮现算:①缺端点/模型 → 点名键名 + 面板「嵌入服务」卡 + 必须同时打开「开启语义召回」开关(检查 P0:开关预填 false,用户照旧文案填完端点后红条让位、召回静默不工作);②injectChannel不是 message → 只能改cordis.patch.yml(明说面板改不了;用户补齐端点后红条改为只报通道问题,不再谎报缺键——检查 P0);③配置齐了但开关没开 → 显式switchOff红条(两路检查各自独立抓到的 fail-open:旧rescuedBySettings只看端点/模型不看开关)。诊断每轮现算的副产品:热恢复——用户补齐配置后不重启即恢复(有测试)。仍然启动即抛的只有"配错"(URL 非 http(s)、键类型非法),与其它配置键 fail-loud 同口径。 - 运行时故障类(端点连不上/超时/HTTP 错):熔断语义一字未改(3 次 → 冷却 60s),只补可见性——
/state.recall.errors非零时面板与门牌出红条(与静态类互斥显示),文案注明「只影响语义召回,其它功能不受影响」。
- 静态配置类:不再抛错。
- 硬事实守护策略(
hardfactPolicy)面板指路(方案 v2 §4.3,按既定设计只加文案不加控件):设置页「保留策略」卡一段说明——守护什么(正文里的数字、版本号、十六进制标识、字节尺寸这类写错一个字符就可能跑不通的内容)、当前值(warn/block 各自意味着什么;脏值如实标「不认识(按 warn 生效)」,不冒充现值)、去哪改(settings.json 的hardfactPolicy键,下一轮生效),并如实说明为什么不给控件(拨到 block 会把合法更新一起回滚,用户看到「我改的记忆自己变回去了」)。检查后修正两处:路径不再写死~/.dsh(storageRoot可配,改由/state.storageRoot现算,自定义路径的用户不再被指错路);删掉「当前值 warn——warn:」的自我复述。 - 红条指路路径动态化(同上口径):召回红条里的 settings.json 路径同样由
/state.storageRoot现算,缺省才回落~/.dsh/memory/settings.json;/state.recallDiagnostics增requested字段(entry 是否请求过召回),单独透传的recallRequested死字段移除。
测试(1126 → 1152,+26)
- 零正文逃逸(3 条 + 集合钉 1 条):显式上限 + 两趟零正文 → 逃逸重发成功落盘且
escapeRetried=1;跟随模型 + 零正文 → 恰好 1 次调用(不逃逸、不进重试链——变异:把自造 code 同时加回集合与补挂 → 必红);逃逸那趟仍零正文 → 恰好两趟后 llm-error 且escapeRetried=0(结果口径);RETRYABLE_FAILURE_CODES集合本体 deepEqual 宿主五项(防将来混入自造 code)。 - 在途中止分类(2 条,全走真
apply()/ 真 REST 面):①在途超时 →retried≥1(真实重采样)、失败摘要带 TIMEOUT(变异:摘掉 TIMEOUT 补挂 → 必红);②dispose 在途(走补提路径触发——scheduler.dispose 会先有界 drain 在线提取,在线路径的 abort 永远落在两次 attempt 之间,只有补提不经 scheduler 才能确定命中"attempt 中途被打断")→llm-error桶不增、journal 不记 llm-error(变异:摘掉 dispose 检查 → 必红)。变异期间抓掉两个假测试:假流"永不 yield 的死流"对 abort 信号无动于衷、extractSession 永远挂在 for-await 上,dispose 路径根本没走到——改为挂起至 signal abort 后抛宿主形状 ABORTED。 - 召回降级(
test/recall-degrade.test.mjs,14 条;全走真入口):缺键不抛 + 插件照常加载 +/state诊断形状;只缺 model / 通道 section 各自精确;有候选前提下零嵌入调用(检查 P1:无条目时 fail-open 也不会发请求,断言无区分力——现在先createEntry再断言 0);面板红条渲染级(含自定义 storageRoot 路径 + switchOff 岔 + 不得谎报缺键);门牌兜底表驱动逐字一致(含 switchOff);运行时故障端到端 + 推行锚;熔断常量 3 次/60s 未动;边界(配错仍抛 / 没请求过不出红条 / 配齐无诊断);switchOff 显式红条(变异:摘掉 switchOff 岔 → 2 条红);热恢复(补开开关不重启即恢复)。 - hardfact 指路(2 条渲染级):真实行模板求值(warn/block/缺失/脏值 + 自定义 storageRoot 路径 + 路径参数兜底);无控件判据升级为「卡内白名单外一律违规」(检查 P1:旧判据只认带
keepHardfact的标签,radio/checkbox 可绕过)。 - 推行锚补强(检查 P1:只锁 const 行锁不住"算了但没 push"):
escapeLine与recallFailureLine的 push 行各有测试。
v0.6.5 — 提取韧性三修 + 观测面补齐
[0.6.5] - 2026-10-23
提取韧性三修:输出预算跟随模型 + 显式降思考档 + 失败不再自删路由 + 补提路径复原路由;外加「证明修复真的生效」的三项观测能力。 本机 extraction-errors.log 309 行里 llm-error 88 条,其中 81 条是 kind=max-tokens;且 llm-error → no-route 转移 75/88——一次模型侧抖动被两条缺陷接力放大成结构性失败链。修完四件止血后有三个观测缺口导致无法验证修复是否真生效,本批补齐。无新增依赖,FORMAT_VERSION 仍为 2。
修复
- 输出预算固定 4096 吃光带思考模型的输出:
maxOutputTokens语义改为 0 = 跟随模型(新默认),且为 0 时不下发maxTokens键(宿主resolveCallFor只在maxTokens === undefined时材料化模型默认值,传 0 会被当成 0 预算)。cordis.patch.yml同步改为 0。 - 从不显式降思考档:新增 config
extractReasoningEffort(minimal|low|medium|high,默认low,非法值启动即抛且提示允许值)。此前提取调用只传maxTokens、从不传reasoningEffort,宿主因此套用模型默认最高档,单次思考就把预算吃满、正文零输出。 kind=max-tokens一次判死:callLlmText改为{ text, truncated }返回形状;max-tokens且已累积正文时不再直接抛错,而是带truncated标记交回调用方先尝试解析——解析成功即照常落盘并记新观测键truncatedParse(进journal.shapeRun白名单,跨重启可重放),解析失败才记parse-error(并显式标注是否截断所致)。正文零输出仍判llm-error。- 失败自删路由 → 级联 no-route:
llm-error后不再delete st.route,保留 last-good 路由;「中途换模型可自愈」改由新增的routeStale标记承担——失败后到达的assistant/message.source允许覆盖旧路由(request/header本就无条件覆盖)。 - 补提路径 100% no-route:
createSessionAdapter现在扫描读回事件复原路由(routeFromEvents:request/header优先、assistant/message.source兜底、同一来源取最后一条),落在适配对象的新字段recoveredRoute上;extractSession路由解析加入第三级session.recoveredRoute。字段名刻意带recovered前缀,不与宿主Session的既有属性面冲突。 - 宿主能力校验的边界:宿主对不支持的思考档会在 provider I/O 前以
code=UNSUPPORTED_REASONING_EFFORT拒绝(非思考模型完全没有 reasoning 元数据)。callLlmText命中该 code 时打一条 warn 并不带档位重发一次,同时把该路由记进进程内reasoningUnsupported表,后续同路由不再下发——降级可见、不静默、不无限重试。
观测能力(三项缺口:修完了却证明不了生效)
- token / 成本观测(P0):
callLlmText此前只认text-delta与finish,把宿主的{type:"usage", usage:{inputTokens, outputTokens, totalTokens, cacheReadTokens?, cacheWriteTokens?}}chunk 直接丢弃——于是maxOutputTokens从 4096 抬到模型默认(本机 250000,61 倍)之后,单次输出上限放大 61 倍、成本可能被带飞而完全看不见。现在每趟(含降档重发第二趟)收口时记账,新面/state.extractionFailures.extractionTokens={ input, output, total, calls }。口径:calls记真实发起的调用次数(含降档重发那趟、含失败途),宿主没发 usage chunk(旧适配器)时 token 记 0 但calls仍计数——「调了几次」与「花了多少 token」分开报;total取宿主totalTokens(含 cache 读写,缺失才回落input + output),不自己凑数。 /state去重口径(P0):新增/state.extractionLog.coverage={ distinctFailedRanges, distinctWrittenRanges, distinctAbandonedRanges, writtenRangeRate, writtenRangesTotal, failedRangesTotal, abandonedRangesTotal, scope: "range" }。这是验收的唯一判据:修复生效后「不再反复白撞同一区间」会让failed计数下降,但那不等于记忆变多——只有 distinct(sessionId, from, to)区间数说得清。口径如实标注为区间口径不是轮次口径:宿主不提供总轮次数,故writtenRangeRate=written / (written + failed + abandoned)(区间级成功率),分母不含 skipped/zero-ops(无区间成败语义),也不编一个轮次数。*RangesTotal是不去重行数,与 distinct 并排,行数/distinct即「平均每区间结账几次」。journal 侧进程内累计 + 重放同源(runsring 只有 30 条且会被轮转裁掉,从 ring 现算必少算);parseReplayLine/shapeRun白名单语义未动(不新增盘上字段,纯读侧统计)。- 截断与降档上用户面(P1):
truncatedParse > 0的written行标签附「(输出被截断,可能少存)」(extractionRunLabel);reasoningDegraded > 0时健康卡与门牌各显示一行「思考档降级 N 次(该路由不支持所配档位…)」(新增纯函数reasoningDegradedText+EXTRACTION_BUCKET_KEYS增桶);面板最近失败表的 reason 列改走L.reasonLabelOf(...)与测试中文化口径统一(检查点名的遗留:此前直接esc(r.reason),面板上会露no-route/llm-error这类英文枚举)。门牌侧同步内联兜底(reasoningDegradedTextFallback),双端逐字一致。
测试
- 新增
test/extract-route-resilience.test.mjs(12 条):预算缺省 0 且 options 无maxTokens键、思考档缺省/显式/非法、UNSUPPORTED_REASONING_EFFORT降级重发一次且同路由不再白撞、截断可解析即落盘 +truncatedParse跨 journal 实例重放仍在、半截 JSON 走parse-error、零输出仍判死、llm-error后同会话下一轮不产生no-route、失败后新source能顶替旧路由、routeFromEvents优先级、补提经真 REST 面发起真调用且路由来自读回事件。全部走真apply()入口/真 REST 面,未复刻接线。逐条做过拆改验证(摘掉对应修复 → 必红)。 - 新增 token/成本面测试(6 条):
test/observability.test.mjs3 条(累计/两次累加/cache 时 host total 优先、脏 usage 钳零 + 无 usage 只计 calls、快照拷贝不可变)+test/extract-route-resilience.test.mjs4 条端到端(假 llm 发 usage chunk → 真 REST/state快照input=100/output=500/total=600/calls=1;两次调用累加;降档重发第二趟也计;旧适配器无 usage chunk 不抛错且calls仍计 1)。 - 新增去重口径测试(5 条):
test/journal.test.mjs3 条(3 条同区间重复失败 + 1 条不同区间 + 2 条成功 →distinct=2而行数 4;跨重启重放不缩水 + 同区间再记 3 次 distinct 不动行数 +3;按会话分键 + 零结算rate=null不编数)+test/api.test.mjs2 条(同构造经真 REST/state;ringMax=2下跨重启一致)。 - 新增用户面测试(6 条):
test/panel-logic.test.mjs2 条(truncatedParse截断标 + 与落盘失败并存不互顶、非 written 不带标;reasoningDegradedText含「降级」且零值/旧宿主 null)+test/panel-drift.test.mjs3 条(渲染级:把面板真实行模板抠出来求值,reason 列不得命中/\b(no-route|llm-error|parse-error|io-error|no-cwd)\b/;降级摘要接线;截断标记随行标签出)+test/client-source.test.mjs1 条(reasoningDegradedTextFallback` 表驱动与实现逐字一致 + 双端消费锚)。 - 拆改验证(逐条「摘修复 → 必红 → 还原 → 绿」):删
callLlmText的 usage 分支 → 4 条 usage 测试红;把 distinct 改成行数统计 → 4 条 coverage 测试红;删truncatedParse附标分支 → 2 条红;EXTRACTION_BUCKET_KEYS去掉reasoningDegraded→ 1 条红;面板 reason 列回退esc(r.reason)→ 渲染级测试红;门牌降级文案与 panel-logic 漂移 → 防漂移测试红;面板降级摘要改接别的函数 → 2 条红。测试总数 1104 → 1123。
v0.3.1
变更
- 全局记忆索引保最新(每类 22 行 / 2200 字符、总 120 行 / 12k 字符),五类都能进注入,写满预算后新记忆不再消失
- 新增
recallTotalBudgetMs(默认 20000ms):单轮召回整体时间预算,分批嵌入逐批增量落盘 injectMaxChars为含召回块的注入总预算,主块为召回块让位- 无 webserver 的 profile 也能加载并做记忆注入
修复
- 嵌入「测试连接」不再把已存 API Key 转发到请求指定的任意地址
- 召回块去重比对全部历史块,杜绝逐轮累积
- 索引行文件名与磁盘逐字节一致;同批 write+delete 不再留悬挂索引行
- 显式非法文件名明确拒绝;空标题兜底文件名为确定性命名
- 存储格式标记损坏先留证再补写;合法 JSON
null请求体返回 400
v0.3.0 — 分发与升级支持
dsh-keepsake v0.3.0 — 分发与升级支持
面向"其他用户如何安装、以及以后如何升级"补齐闭环。DSH 插件不支持自动更新,本版把这件事做成可用流程。
新增
- 分发渠道:GitHub tag tarball 直装(本版起可用)。README 补齐安装与升级步骤。
- DSH peer 兼容声明:
package.json声明peerDependencies(@deepseek-ai/dsh-client-ui-slots@^0.2.0-rc.2),宿主在安装/启动时据此做版本兼容检查(不匹配需用户显式豁免)。 - 存储格式版本闸(防降级弄坏数据):存储根新增
keepsake-format.json记录格式版本;数据被更新版插件写入时(标记高于当前),本版本自动转只读——拒绝写端点(503)、跳过滚动清理与临时清扫、召回缓存不落盘、提取按 skip 处理(不落盘、cursor 推进);读取仍放行。/state.format暴露状态,门牌与设置页「管道健康卡」显示只读告警。
安装
dsh plugin --profile web add https://github.com/axuan-1314/dsh-keepsake/archive/refs/tags/v0.3.0.tar.gz
# profile 可选 web / headless / desktop升级
DSH 插件不支持自动更新(升级 = 先卸载再装新版)。把 URL 里的 v0.3.0 换成新版本号重跑上面的命令即可。数据不受影响——记忆、开关、人设、备份全部落在 ~/.dsh/memory/,在插件包目录之外,卸载重装不碰;升级后需重启进程加载新代码(web 可由 HMR 重载)。
兼容性
- 适配宿主:DSH
0.2.0-rc.2(web / headless / desktop 三个 profile) - 运行环境:Node ≥ 18;零运行时依赖
变更
- README(中/英)新增「升级」一节与「分发」摘要;
FORMAT.md新增第七节「存储格式版本」;ROADMAP.md新增「分发与升级」。 - 门禁:332 项测试全绿(新增 9 项)。