axuan-1314/dsh-keepsake

dsh-keepsake

DSH 跨会话记忆插件:本地 markdown 落盘、自动提取+注入、零依赖 + 管理面板。Local-first cross-session memory plugin for DSH (DeepSeek Harness).

待分类agent-memorydeepseek-harnessdsh

dsh-keepsake

English | 中文

DSH(DeepSeek Harness)的跨会话记忆插件:对话里聊过的事自动记下来,下次对话自动带上——你不用每次重复交代背景。附带一个本地「记忆管理」面板,记了什么、删了什么、人设怎么设,全程可见可改。

  • 插件版本:0.6.4(以 package.json 为准;变更记录见 CHANGELOG.md)
  • 适配宿主:DSH 0.2.0-rc.2(web / headless / desktop 三个 profile 均可安装);package.json 声明了 DSH peer 范围,安装/启动时宿主会做版本兼容检查,不匹配会让用户显式确认后才装
  • 分发:GitHub 仓库 tag tarball 渠道(+ 本地 clone 作开发);升级方式见「升级」(DSH 插件不支持自动更新)
  • 数据位置:全部在本地 ~/.dsh/memory/,不上传任何远端;API 与面板仅本机(loopback)可访问
  • 前置条件:已安装 DSH(web / headless / desktop 任一 profile);Node ≥ 18;零运行时依赖(无需 npm install)
  • 许可:MIT License(见 LICENSE)

设计参考同类 agent 的项目记忆子系统;v1 采用单次 LLM 调用简化提取。

记忆管理面板:分类条目管理

上图为界面示意(使用示例数据渲染)。最快上手(完整步骤见快速开始):

dsh plugin --profile web add "https://github.com/axuan-1314/dsh-keepsake/archive/refs/tags/v<版本号>.tar.gz"   # 版本号见 Releases

目录

这是什么

DSH 每次新会话都是从零开始:你的工作背景、这个项目的决策历史、上周聊定的口径,全都得重新说一遍。dsh-keepsake 把这件事自动化:

  1. 自动提取——每轮对话结束,插件自动判断这轮有没有值得记的事(用户偏好、项目决策、长期有效的结论……),有就落盘成本地 Markdown 文件;
  2. 自动注入——之后每一轮对话,把已记下的内容以六段形式(人设 / 运行时用户画像 / 运行时工作记忆 / 全局记忆索引 / 项目使用说明 / 项目记忆索引)作为一条插件自有的用户消息注入会话,模型据此"记得"你(运行时的两段需开启「运行时记忆」,默认关;关闭时为空段、不产生输出);
  3. 面板管理——DSH 设置页多一个「记忆」区块,从这里打开记忆管理面板:浏览/编辑/删除/导入导出记忆条目、编辑人设与称呼、开关各路记忆、看提取管道健康状况。

记忆分两层:

  • 全局记忆(跨所有对话生效):工作背景、用户个人背景、当前关注、近期动态(自动滚动清理,默认保留 30 条 / 30 天)四个对话分类,外加长期记忆(归档区——v0.6.0 起不再进注入索引,见「运行时记忆层」);
  • 项目记忆(按工作区路径隔离):每个项目一个独立目录,记这个项目的决策与状态。

功能介绍

以下每条都有源码依据(lib/ 与 web/),按"用户看到什么 / 能得到什么"叙述。

自动记忆:会话结束自动提炼,下次对话自动带上

  • 无需手动操作:每一轮对话正常结束(turn/end 且 reason.kind === "completed")后自动触发一次提取判断;纯寒暄、命令式短句(不足 3 个词的用户散文)这类没内容可记的轮次自动跳过,不白花 token。
  • 增量提取:插件为每个会话维护提取进度(cursor),只提炼新完成的轮次;提取失败时进度不推进,下一轮自动连本带利重提(自动补提),不会漏记也不会重复记。
  • 提取走会话路由:默认复用你当前对话所用的模型路由(与 session-title 同款捕获策略),不单独配置也能跑;也可以在 cordis.patch.yml 显式指定更稳的 provider / model。
  • 跨会话六段注入:之后每轮对话,插件把记忆作为一条插件自有的用户消息注入会话(默认走 durable 消息通道,对所有预设——包括 complete:true 的极简预设——都生效,这是 v0.2.0 对旧版"极简模式静默失忆"的根治点;injectChannel: "section" 可回退到旧的系统提示词注入)。注入受双预算约束:字符口径 injectMaxChars(默认 30000)与 token 口径 injectMaxTokens(默认 24000,CJK 感知估算;0 = 关闭 token 预算、只用字符口径)——超限按优先级整段丢弃,两个上限同时生效。这条消息由六段组成(人设与两段运行时为锁定段,不进整段丢弃序列;只有全局索引 / 项目说明 / 项目索引可被整段丢弃),模型据此"记得"你:
注入段模型看到什么预算
人设(锁定段)「用户叫你小助手」「希望你称呼他为朋友」+ 你编辑的补充人设正文400 行 / 8k 字符(v0.6.0 起由 16k 下调)
运行时用户画像(锁定段)开启「运行时记忆」后:由 user-profile 等条目编译的 USER.md(定长紧凑画像)≤ 4096 字节(编译期配额)
运行时工作记忆(锁定段)开启「运行时记忆」后:由工作背景 / 当前关注 / 项目条目编译的 MEMORY.md≤ 10240 字节(编译期配额)
全局记忆索引四个对话分类的条目索引(长期记忆不再进注入索引,v0.6.0 起);近期动态带年龄与过期倒计时(如「3 天前 · 27 天后过期」)每类 22 行(超限保最新),总 120 行 / 12k 字符
项目使用说明记忆目录在哪、索引怎么读、怎么按你的要求直写/删除记忆固定短文
项目记忆索引该项目 MEMORY.md 索引(每行指向一个记忆文件,模型按需 Read)200 行 / 25k 字符,超限保最新丢最旧并附 WARNING

运行时两段在「运行时记忆」关闭时为空段(不出现在注入里),此时注入内容与旧版四段等价(唯一差异:长期记忆索引行自 v0.6.0 起不再注入)。

  • 三层开关 + 运行时记忆(面板「设置」页,改动下一轮对话即生效):
开关关掉后的效果
对话记忆(总开关)六段注入全灭 + 自动提取全停;关掉期间的轮次不积累,重开后不回补
项目记忆项目使用说明/项目索引不再注入,运行时两段也一并停注(保守口径);项目类记忆写入被过滤(计数可见)
全局自动提取不再自动写全局记忆;已有的全局条目照常注入
运行时记忆(默认关)不编译、不注入运行时两段;全局索引照旧全量渲染(不做去重)
  • 防自反馈(按「机器来源」排除):注入的记忆消息带 source.kind = "plugin:dsh-keepsake" 标记;提取侧按统一的机器来源判据,把任意插件(不限本插件)注入的消息、以及 instruction / summary / tool 等系统性消息,都从「用户散文」里排除(既不计入 skip 门槛、也不进提取输入),不会出现"模型把注入的记忆又记回记忆"的自食循环。真人消息(含无来源、steer / followup)永远算数;语义召回的查询文本同样只取真人消息。

运行时记忆层(可选,默认关)

把条目文件编译成两份定长的紧凑记忆(USER.md 用户画像 + MEMORY.md 工作记忆),随每轮注入——不用等模型按索引主动 Read。默认关闭(runtimeEnabled),在面板「设置」页打开后下一轮生效。

  • 机制一句话:条目 →(编译)runtime/<scope>/USER.md / MEMORY.md → 注入;真源永远是条目文件,产物可删可重建,手工改产物会被下次编译覆盖。
  • 配额:USER.md ≤ 4096 字节、MEMORY.md ≤ 10240 字节(UTF-8 字节口径,含文件头声明与告警行),是编译期常量(面板与配置都不提供调高)。pinned(pinned: true)条目优先入选、永不淘汰;合计超配额时不截断、不静默——清单 status=overQuota、面板红灯、注入块内可见告警行。
  • 装不下的条目不静默消失:顺序装填装不下即停,被淘汰的条目记进清单 evictCandidates[],产物末尾留一行 [runtime evicted: N entries …] 提示;同时只要该分类还有条目没进产物,这个分类就继续留在全局索引里(去重按分类粒度,不是整类抹掉)——所以任何一条记忆至少有一个可见入口。
  • 项目条目也去重:进了产物的项目条目不再在项目索引里重复出现一遍;reference 类型(不进产物)照旧留在索引。
  • 淘汰走「候选 + 确认」:编译只把装不下的条目登记为候选,绝不搬文件;你在面板「运行时记忆」页勾选后点「执行归档」才真正搬进 global/long-term/(归档 ≠ 删除)。
  • 归档可逆:搬动前对每个文件做手术式快照(backups/evict-<ts>/,保留最近 10 份),并追加持久审计 evict-audit.jsonl(时间 / 原位置 / 新位置 / 快照 / 操作者);面板「从归档移回」按条目里记录的出处(originProject / originType / originSessionId)落回原位并重编。这三个出处字段在面板编辑归档条目时原样保留——编辑一次不会让条目变成移不回去。也可手工把文件搬回原目录——路径集变化会自动触发重编。审计写入失败时本轮停止继续搬运并如实回报「已归档 N 条 / 未处理 M 条」,不会假装什么都没发生。
  • 预算:persona 段上限 8000 字符(≈12000 tokens)+ runtime 两段常驻约 14336 字节(≈7170 tokens)≈ 19170 < injectMaxTokens 24000;字符口径 8000 + 4779 ≈ 12779 < injectMaxChars 30000。injectChannel: "section" 模式下同样承载这两段(插在人设与全局索引之间)。
  • 面板位置:面板左侧「运行时记忆」页签(scope 可切全局/各项目;只读预览、用量条、候选清单、「重新编译」、「从归档移回」,以及条目列表上的「标为关键 / 取消关键标记」——即 pinned 切换)。
  • 如何回滚:把「运行时记忆」开关关掉(或删掉 settings.json 里的 runtimeEnabled)即回到不注入 runtime 两段的旧形态,不编译、不写盘;已产生的 runtime/ 目录可整目录删除,不损失任何记忆。

内容脱敏:密钥类内容进不了记忆与提示词

对话里偶尔会出现密钥、令牌、内网地址这类不该被记下来的内容。脱敏默认开启(redactionEnabled,默认 true),在两道位置拦截:

  • 第一道(进提取提示词之前):会话摘录渲染时(用户消息 / 助手回复 / 工具调用 / 工具结果四处)先脱敏再截断——顺序刻意为先脱敏后截断,防止截断把密钥切成两半导致漏检。模型看到与提取用的摘录都是脱敏后的文本;
  • 第二道(落盘之前):提取产出的写入/删除操作在写记忆文件前再过一道脱敏(纵深防御),处理条目正文的字符串内容字段与文件名(name)——密钥被写进文件名时会落盘、进索引、随每轮注入,比正文里的更持久,故一并脱敏;文件名命中后会追加源名短指纹([redacted]-<8位十六进制>)保证不同条目不会撞名。路径分隔、sha1 与操作类型不动。命中数计入提取运行日志的 redacted 字段(有命中才写),面板「最近提取记录」可查。

内置 7 类固定模式(清单固定、不做用户自定义正则——防正则回溯拖垮宿主进程):

  1. 私钥块:-----BEGIN ... PRIVATE KEY----- 到 -----END ... PRIVATE KEY----- 整块;
  2. Authorization / Proxy-Authorization 头里的 Bearer / Basic / Token 凭据;
  3. 独立出现的 Bearer / Basic 令牌(16 字符以上);
  4. 常见厂商密钥形态:sk- 前缀、GitHub PAT(ghp_ / gho_ / ghu_ / ghs_ / ghr_ / github_pat_)、AWS Access Key(AKIA)、Slack 令牌(xox)、Google API Key(AIza)、JWT(eyJ 三段式);
  5. URL 内嵌凭据:协议://用户:密码@主机 保留协议与主机、凭据部分换占位符;
  6. 敏感键值赋值:password / secret / token / api_key / private_key / credentials 等键名后跟 : 或 = 的值;
  7. 内网 IPv4 地址:10.x.x.x / 172.16-31.x.x / 192.168.x.x。

命中的内容替换为占位符 [redacted];对已脱敏文本重复执行不再变化(幂等)。不想用时可配 redactionEnabled: false 完全关闭,回到不脱敏的旧行为。局限见「已知限制」。

语义召回(可选)

默认关闭。开启 recallEnabled 并配好嵌入端点(embeddingBaseUrl / embeddingModel)后,每轮对话会按你当前的问题语义检索最相关的记忆条目(嵌入余弦 top-k),以 <memory-recall> 块注入——不必等模型按索引主动 Read。条目向量按内容增量缓存,只有新写/改动过的条目才重新嵌入。

  • 默认关:不配置端点就不产生任何外部调用(记忆文本不出本机);指向本地端点(如 http://127.0.0.1:1234/v1/embeddings)则记忆文本也不出本机。
  • 面板里配置:设置页「嵌入服务」卡直接填端点 / 模型 / API Key,带「测试连接」按钮;保存后下一轮对话即生效,不必改 cordis.patch.yml、不必重启(面板配置存于 ~/.dsh/memory/settings.json,优先于 entry config)。
  • 测试连接不转发已存密钥:已保存的 API Key 只发往它自己配置的那个端点;若「测试连接」的请求指向别的地址,不会把已存密钥带过去(只有请求体里当场填的 Key 才随该地址发送)。
  • 密钥不会被重存顶掉:保存嵌入配置时,Key 输入框留空 = 保留已存密钥(只改端点/模型等其余字段时无需重输);「清除已存 Key」按钮(仅已设 Key 时出现)= 显式清空;输入新 Key = 覆盖。
  • 自带模型(provider-agnostic):插件不绑定任何嵌入模型或厂商——只要你的服务提供 OpenAI 兼容的嵌入端点(绝大多数本地部署都提供),填上 embeddingBaseUrl + embeddingModel 即可,本地自选模型(EmbeddingGemma、bge-m3 等)同样适用。embeddingProvider 只是显示标签,配不配都行。
  • 配置不全 = 显式降级,插件照常起:recallEnabled: true 但缺 embeddingBaseUrl / embeddingModel(或 injectChannel 不是 message)时,插件不再启动失败——召回被显式关闭(零嵌入调用),面板与门牌出红条按【当前生效配置】每轮现算着指路:缺哪个键、去面板「嵌入服务」卡补(要同时打开同卡的「开启语义召回」开关——开关预填为关,只填端点不打开开关的话红条会一直提示到打开为止)、通道问题只能改 cordis.patch.yml(面板救不了);配置齐了但开关没开也有一条专门的「打开开关」红条,绝不静默不召回。红条里的 settings.json 路径按 /state.storageRoot 现算(自定义了数据目录的用户看到的是真实路径)。用户补齐配置后不重启即恢复(诊断每轮现算)。其它功能(对话记忆提取与注入)完全不受影响。端点 URL 不是 http(s) 属于「配错」而非「配不全」,仍然启动即抛。
  • 失败显式降级:端点配齐但超时/报错时本轮不召回(不阻塞、不臆造),计数进 /state.recall,面板与门牌同时出红条(连续失败 3 次后自动暂停召回 60 秒)。

记忆管理面板

入口:DSH 设置页的记忆区块(下称门牌)→「打开记忆管理」。打开方式自动三级降级(新窗口 → 内嵌 iframe → 整页跳转),面板主题自动跟随宿主深浅色。门牌本身也是快捷操作区(速操台):总开关、「DSH 的名字」「对我的称呼」两个输入框(防抖自动保存)、状态摘要(全局 N 条 · M 个项目)、提取失败红条 + 「详情」深链。

设置页:三层开关与提取管道健康卡

上图为设置页示意(使用示例数据渲染)。记忆管理面板的条目管理见文首截图。

面板(/dsh-keepsake/panel/,仅本机可访问)包含:

  • 四个对话分类页 + 长期记忆页:条目卡片流(标题 / 描述 / 文件名 / 更新时间 / 正文),支持——
    • 浏览与检索:每个分类页与长期记忆页都有即时检索(标题 / 描述 / 正文即时过滤,每页 50 条分页);近期动态条目带年龄徽标;
    • 编辑:标题(60 字)、描述(60 字)、正文(20000 字符上限),实时字符与 UTF-8 字节计数;
    • 删除(确认弹窗)、新增条目;
    • 源码模式:整类序列化成一个文本直接编辑,保存 = 先自动备份再整类重建;
    • 重置:清空整类(先自动备份);
    • 导入:支持上传 .json / .md 文件(≤2MB)或直接粘贴,三种格式(JSON 结构化逐条 / Markdown 按 ## 拆条 / Markdown 整段);先预览将要写入的条目(与现有内容冲突或批内改名的标红),确认后才落盘(落盘前自动备份);批内同名自动 -2 / -3 避让。
  • 人设页:左右分栏(markdown 编辑 + 实时预览),内置简洁 / 专业 / 幽默 / 伙伴 / 自定义五个预设模板,一键套用;「恢复默认」回到出厂模板(先自动备份);上限 100KB。人设是官方系统提示的补充而非替换,对所有回复生效。
  • 称呼页:「DSH 的名字」「对我的称呼」两个输入框(各 40 字上限),防抖自动保存,与门牌输入框是同一份数据。
  • 项目记忆页(只读概览):按工作区列出各项目的条目数与最后更新时间;可单项目导出 JSON。项目记忆由对话自动维护,不提供手工编辑。
  • 回收站页 / 快照页(见「写入可逆性」):被删条目与批前留档的旧文,可按条或按单个文件恢复。
  • 运行时记忆页(见「运行时记忆层」):scope 切换、USER.md / MEMORY.md 只读预览、用量进度条(越界红显)、淘汰候选勾选 +「执行归档」、「重新编译」、「从归档移回」。默认关;未开启时页面给一句引导。
  • 设置页:三层开关 + 运行时记忆开关;管道健康卡(含「补提」区,见「会话补提」)与提取模型卡(见下节);记忆整理卡(见「记忆整理」);保留策略(回收站保留天数 / 快照保留份数 / 近期动态保留天数三个输入,下一轮对话生效);数据卡(导出全部 JSON / 清空全部记忆——逐类自动备份,人设与称呼保留);备份列表(最近 20 份,逐份可「恢复此备份」,恢复前还会先备份当前状态)。
    • 损坏可见:settings.json / addressing.json / persona.md 解析失败时,门牌与面板显示红条「解析失败(已按缺省运行)」,点任意开关 / 重新保存对应内容即可重写修复;损坏档在覆盖写前自动备份 *.corrupt-<ts>,原数据不失访。settings.json 损坏时除了保留原档(改名 settings.json.corrupt-<ts>),还会从影子档 settings.last-good.json(每次成功写入同步刷新)恢复用户的开关,恢复不了才落默认——不是简单丢弃重写。设置值不合法被纠正落默认时,设置页会出黄条「N 项设置值不合法已回默认」并列出字段名(/state.settingsDiagnostics.coercedFields),重存设置可修正。

管道健康卡与「立即重试提取」

设置页的「自动提取管道」健康卡让你一眼看出记忆提取是否健康:

  • 三态灯:绿 = 最近无失败(折叠为一行摘要);黄 = 近期有过失败但已自动恢复;红 = 当前连续失败(同因连击数);灰闪 = 正在提取。连击 > 0 期间每 5 秒自动轮询刷新,侧栏「设置」项同步显示红色连击徽标。
  • 失败分桶计数:按原因(无路由 / 无 cwd / LLM 调用 / 解析 / 落盘 IO / 重试 / 换道 / 思考档降级)分桶,只显示非零桶。
  • 思考档降级提示:该路由不支持所配思考档时(宿主以 UNSUPPORTED_REASONING_EFFORT 拒绝),插件按模型默认档重发一次并在健康卡与门牌各显示一行「思考档降级 N 次」——这趟额外往返此前只进日志,用户既不知道发生了、也无从判断要不要换档位。
  • 截断标记:「最近提取记录」里 written 行若带 truncatedParse(输出被 max-tokens 腰斩但仍解析成功),标签附「(输出被截断,可能少存)」——这是"看起来正常的成功",不标出来永远不知道那次可能少存了几条。
  • 去重口径覆盖率:/state.extractionLog.coverage 给出 distinct 区间数(distinctFailedRanges / distinctWrittenRanges / distinctAbandonedRanges)与区间级成功率 writtenRangeRate。区间口径,不是轮次口径(宿主不提供总轮次数,故不编造);failed 计数下降 ≠ 记忆变多,只有 distinct 区间数说得清。
  • 最近 5 条失败详情:时间 / 原因 / 模型 / 错误摘要,配合失败日志路径(<storageRoot>/extraction-errors.log)排障。
  • 错过区间提示:「第 X 轮对话之后的内容尚未记录,恢复后自动补提」——告诉你哪些对话可能漏记了;配套「补提」区可逐条重跑(见「会话补提」)。
  • 「立即重试提取」按钮:对仍有未提取区间的会话真的发起一次 LLM 提取(有确认弹窗,明示消耗少量 token);点击后按钮进入 busy 态,面板每 2 秒轮询状态最多 60 秒,恢复即提示「提取已恢复」。10 秒最小间隔,提取在途时返回 409。
  • 注入预算占用:健康卡的注入摘要显示 token 口径占用「最近 ≈X tokens / 上限 Y」(injectMaxTokens 为 0 时不显 token 段、退回字符展示);数据源是 /state.injection 的 lastTokens / totalTokens。
  • 「复制诊断信息」按钮:一键复制当前状态摘要,可直接粘贴到 issue。
  • 提取模型卡:如实展示提取路由(显式配置的 provider/model,或「未指定(跟随会话路由)」)、fallback 换道配置、最近一次提取用车(路由 + 相对时间)、嵌入模型(语义召回所用;未配置时显示为未启用)。

提取运行日志与跨重启续提

此前"聊了几轮、记忆里看不出任何反馈"的根因是:成功 / 零产出 / 跳过都不留痕,失败详情只活在进程内存(DSH 一重启归零),失败区间跨重启就永久丢失。本版补上持久观测面:

  • 提取运行日志(durable 观测面):每次提取结算向 <storageRoot>/extraction-journal.jsonl 追加一行——written(写 / 删了几条)/ zero-ops(模型无可记、或有 op 无可落盘)/ skipped(直写记忆、无有效用户发言、总开关关、格式只读)/ failed(与失败分桶同名的病因)/ abandoned(同一结构性病因连续失败过多,显式放弃该区间、游标已推进不再重试)。追加串行化防并发写坏,行数超 2000 自动轮转留尾 1000;写日志失败只告警、不阻断提取。它不是记忆真源,删除不损失任何记忆(代价是丢掉观测历史与续提边界),见 FORMAT.md 第七节。
  • 面板「最近提取记录」:设置页健康卡新增折叠清单(时间 + seq 范围 + 结果标签),每轮"为什么没记"有据可查。"未恢复区间"红条改双源——重启后回落盘侧未恢复清单,不再隐身;结构性失败被显式放弃的区间也会在盘上留痕(面板「已放弃 N 段」提示)。
  • 跨重启续提:每会话提取覆盖边界(cursor)随运行日志持久化,DSH 重启后重开旧会话从上次边界续提,失败区间不再因重启永久丢失。取舍:从没提取过的会话仍不回扫历史——插件的提取由"会话继续产生新轮次"驱动,对一段从未提取过的历史不会主动回补,需该会话继续对话才会接着提。

提取触发门槛:打分门、空闲延迟与每会话上限

提取要消耗 token,触发侧有三道成本闸门(全部默认开启,各有独立回滚开关):

  • 打分门(extractGateEnabled,默认开;阈值 extractGateMinScore,默认 6):提取前对这批轮次打分——分数 = 用户词数 + 2×工具种类数 + 2×真人轮次数,分数低于阈值就不调模型、直接记 skipped/thin-evidence(运行日志留痕、进度照常推进)。用户词数沿用既有散文口径(空格词数 + 中文按每 2 字折 1 词);带工具调用或多轮真人发言的会话轻松过门,纯寒暄薄轮不再白花一次 LLM 调用。
  • 空闲延迟(extractIdleDelayMs,默认 3000 毫秒;0 = 立即提取):一轮结束后不立刻起跑,等 3 秒空闲再跑——期间新完成的轮次合并进同一批,突发连续对话只触发一次提取。定时是固定时刻而非滑动窗口,持续对话中也会到期;会话关闭/插件退出时挂起中的提取立即收口执行(进既有有界 drain),不丢最后一轮。
  • 每会话上限(extractMaxRunsPerSession,默认 50;0 = 不限):单个会话的自动提取次数到顶后,后续触发记 skipped/session-cap 并推进进度,不再调模型——病态循环轮(模型反复空转)不会无限烧钱。只约束自动链路,面板手动补提不受限。

会话补提:跨重启把失败区间读回来重提

v0.4.1 让失败区间跨重启看得见(运行日志持久留痕),v0.4.2 把它推进到补得上:

  • 面板「补提」区:设置页健康卡内嵌补提清单,列出未恢复 / 已放弃的提取区间(时间 + seq 范围 + 结果标签),逐条可点「补提」重跑;清单来自提取运行日志,跨重启可见。
  • 自动收口(autoRecoverEnabled,默认开):除手动按钮外,插件还会在三个时机自动补提失败区间——① 会话关闭时(session/disposed,只补该会话);② 每轮对话完成后(turn/end,全局清扫);③ 插件装载后数秒(启动清扫,跨重启续提闭环)。规则:
    • 只自动补未恢复的失败区间(kind=failed);已显式放弃的区间(kind=abandoned)绝不自动重试——"放弃"就是放弃;
    • 同一区间两次自动补提之间有冷却间隔(autoRecoverCooldownMs,默认 300000 毫秒 = 5 分钟),防止宿主读不回的区间被每个触发面反复白读;
    • 自动补提与手动按钮共用同一把单飞锁(同一时刻只有一次补提),手动与自动天然互斥、绝不并发双跑;
    • 宿主未提供会话持久化服务时自动补提静默不跑(能力缺失不是故障;手动按钮此时如实返回 503)。
  • 前提与如实降级:补提要能读回会话内容,依赖宿主提供会话持久化服务(本机宿主 0.2.0-rc.2 起内置);旧版宿主没有该服务时,面板如实提示"本机补不了",接口返回 503(E_HOST_UNSUPPORTED)——不假装补过。
  • 范围纪律:补提区间必须整体落在会话可读范围内;超出可读范围(含超大会话读回截断后的尾部)的区间显式拒绝、继续留在清单里,绝不把没读到的部分按已补提记账、静默丢记忆。
  • 异步与观测:POST /api/sessions/recover 即回 202、后台执行(同一时刻只有一次补提,在途再点返回 409),结果经 /state.recover(inFlight / lastAt / lastOutcome / lastSessionId / autoEnabled / autoAttempts / lastAutoAt / lastAutoTrigger)观测;清单数据面是 GET /api/sessions/pending。

记忆整理:重复、陈旧与索引对账建议

设置页新增「记忆整理」卡,只出建议、不自动改动你的记忆:

  • 重复建议:正文高度重叠、跨分类同名、标题相近且正文重叠的条目对,附相似度与判断依据——只提示,留哪条由你决定,不自动合并。
  • 久未更新提醒:默认 90 天未更新的条目列表;修改时间缺失的条目如实标「年龄不可知」,不拿 0 天冒充。
  • 索引健康与「修复索引」:把 MEMORY.md 索引与磁盘对账(悬挂行 = 索引指向已不存在的文件、漏登记 = 磁盘有条目但索引没这行、空分类),一键修复只增删索引行、绝不删条目文件;改动前先把索引原文存进快照(可在「快照」页找回),留档失败即拒绝本次修复。
  • 端点:GET /api/maintenance/suggestions(建议清单)、POST /api/maintenance/repair-index(响应 fixed 为已应用动作明细数组、fixedCount、unchanged)。

失败韧性:重试、分类、落盘留痕

提取是后台静默发生的,所以失败处理必须显式可见:

  • 可取消退避重试:单次提取最多 3 次 attempt(含首次),指数退避 500ms 起步、2s 封顶(jitter ±20%);上游带 Retry-After 时优先采用(钳制防恶意长等)。插件卸载/进程退出时退避立即中断,不白等。
  • 失败分类:按内核 failure.code 判可重试(EMPTY_RESPONSE / RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT);AUTH / INVALID_REQUEST / QUOTA 这类确定性失败立即摊牌,不浪费 attempt。
  • 换道兜底(v0.2.0):主道 attempt 链不可恢复失败后,按 fallback 配置逐条换道(换道同样走完整 attempt 链);换道成功计入 /state 的 fallbackUsed 并把最近用车指向该道。未配置 fallback 时行为与纯重试完全一致。
  • 失败落盘日志:每次提取失败追加 <storageRoot>/extraction-errors.log——常开,不依赖任何环境变量;只记摘要(时间 / pid / 原因 / 路由 / 错误摘要),不含会话内容;超过 512KB 轮转两档。
  • 结构性失败显式放弃:无路由 / 无 cwd 这类重试不可能自愈的失败,连续 5 次后显式放弃该区间并记 error 日志,不无限积压。
  • 单条容错:模型输出里个别畸形操作跳单处理(好条目照常落盘);单条 IO 失败隔离上报,不阻断整批。

自动备份

所有批量破坏性操作都有后悔药:

  • 自动全量备份:重置分类 / 确认导入 / 恢复备份 / 重置人设 / 清空全部之前,自动把当前全局记忆(global/,含人设与称呼)+ settings.json + 项目记忆(projects/)全量备份到 <storageRoot>/backups/<时间戳>/;
  • 滚动保留最新 20 份,防无界累积;
  • 半备份可见且拒恢复:备份采用两段式落盘(先 complete: false,拷完改 true),中断的半备份在面板标「不完整」且拒绝恢复;
  • 恢复也有后悔药:恢复备份前先把当前状态再备份一份(pre-restore),恢复中途失败时响应会带上这份备份 id 指路自救。

写入可逆性:快照与回收站

自动提取的写入不是一锤子买卖——项目侧与全局侧各自留了后路,面板里可查可回滚:

  • 批前快照(项目侧):一批里要覆写或删除的旧文,落盘前先留档到 backups/surgical/<batchId>/(按 surgicalKeep 保留最新 30 份)。面板「快照」页可整份恢复或只恢复某个文件,恢复后自动对账 MEMORY.md 索引。
  • 回收站(延迟删除):删除不再硬删,而是先进 trash/<entryId>/ 落副本、确认副本落稳后再删原件,按 trashKeepDays 默认保留 30 天。面板「回收站」页可单条恢复、单文件恢复,也可清除单条 / 按保留天数清理 / 全清(清空=真正删除)。此路同时覆盖自动提取产生的删除与面板手动「删除条目」(DELETE /entry);而分类重置(clearCategory) 与 近期动态滚动清理(pruneRecentActivity) 仍是直接删除、不进回收站(分类重置前另有整类自动备份)。
  • 写后读校验(项目侧 + 全局侧):落盘后回读校验 frontmatter 四字段与正文;校验失败回滚旧文(纯新增无旧文可回滚时移除脏档、进回收站),项目侧并留标本于快照 failed/。全局侧写路径同样回读校验。
  • 硬事实守护(项目侧):覆写把既有硬事实(数字、版本号、十六进制标识、字节尺寸)弄丢时留痕告警并计 hardfactDrops;hardfactPolicy 设为 block 则回滚该 op 并计 hardfactBlocks(与写后校验失败 verifyFailed 分列,不混计;默认 warn:只告警不阻断)。判据局限如实说明:检出口径是「同类型硬事实在新文里一个都不剩」或「被极近的值顶掉(疑似誊写错误,如 269 记成 267)」——frontmatter、索引行与日期 / 倒计时等动态注记不参与比对,而正文里同类数字很多时,整行删除也可能检不出;warn / block 的策略语义不变。
  • 写路径三道闸(项目侧):覆写(整段替换)只在带对期望 sha1 时才发生——覆盖 op 的 sha1 字段与当前文件不符即拒绝该 op(并计计数器 expectedSha1Mismatches),防基于过期内容误覆写;不带 sha1 的「盲写」不会覆盖旧正文,而是降级为追加(保留旧正文,新内容接在分隔线后)而不是整段覆盖;提取落盘抢批锁(<storageRoot>/extract.lock),锁冲突时阻断破坏性操作。
  • 覆盖前自动快照(全局侧):自动提取改写全局记忆既有条目前,先把旧文留档到 backups/surgical/;快照失败则跳过这次覆盖、原文保持不动(宁可漏一次更新,不可丢旧文)。面板手动保存(改条目)同样先留档:留档失败直接拒绝保存(503,旧文一字未动),成功响应带 preSaveSnapshotId,可在「快照」页找回被顶掉的旧文。
  • 恢复前也留档:从快照 / 回收站 / 整份备份恢复前,先对恢复目标处已存在的冲突文件留快照。整份备份恢复(restoreBackup)走「旧档改名 retired-* → 换入」流程,换入失败会把旧档改名回原位(自动回滚;回滚也失败则保留 retired-* 目录供手工恢复)。而快照 / 回收站恢复(restoreSurgicalSnapshot / restoreTrash)不自动回滚——它们按条循环写文件,中途失败就地停下、不改回原状;但恢复前的留底快照仍在盘上,API 的失败响应会带上 preRestoreSnapshotId 与提示,指向它供你手工找回被覆盖的旧版。恢复端点与提取落盘共用同一把批锁,不会边恢复边写入。
  • 健康卡可见:设置页「自动提取管道」健康卡多一行安全面摘要(回收站条数 / 写后校验失败 / 硬事实拦截 / 硬事实丢失告警 / 盲写降级追加 / sha1 不符拒写 / 批锁冲突 / 快照失败——非零才显示);侧栏也会显示回收站与快照条数。其数据源 /state.safety 共十个字段:盘上三项(surgicalBackups / lastSnapshotAt / trashPending)每次请求实时现算,进程内七项计数(verifyFailed / hardfactDrops / hardfactBlocks / expectedSha1Mismatches / blindWriteDowngrades / lockConflicts / snapshotFailures)重启清零。

真源只有 *.md 与 MEMORY.md:backups/ 与 trash/ 是可安全删除的副本目录(删掉不损失当前记忆,只失去回滚能力)。见 FORMAT.md 第七节。

三项安全面设置写在 ~/.dsh/memory/settings.json(与三层开关同一份文件,不是 entry config——写进 cordis.patch.yml 会被拒)。其中 surgicalKeep / trashKeepDays 与近期动态保留天数 recentKeepDays 可在设置页「保留策略」卡直接改(下一轮对话生效);hardfactPolicy 有意不给输入控件(拨到 block 会把合法更新一起回滚,用户会看到「我改的记忆自己变回去了」),面板保留策略卡有一段指路文案说清现值、两值语义与改法(路径按 /state.storageRoot 现算),改文件仍是手工、下一轮生效:

设置默认说明
surgicalKeep30批前快照保留份数;超过的最旧快照由写入路径自动清理
trashKeepDays30回收站条目保留天数;过期的由写入路径与面板「清理过期」清掉
hardfactPolicywarn硬事实丢失策略:warn=只告警落日志;block=回滚并计 hardfactBlocks

手改取值规则(面板走 PUT /settings 同语义):未提及的键保留现值;surgicalKeep / trashKeepDays 显式给了非法值(非正整数 / 负数 / 非数值)时按默认值生效;hardfactPolicy 给白名单外的值时保留现值(手工写坏文件被读到时回 warn)。

快速开始

安装

两种渠道任选(零运行时依赖,全程无需 npm install):

① GitHub tag tarball(推荐) —— 从仓库 tag 直接安装,钉死版本:

dsh plugin --profile web add "https://github.com/axuan-1314/dsh-keepsake/archive/refs/tags/v<版本号>.tar.gz"   # 版本号见 Releases
# profile 可选 web / headless / desktop

② 本地克隆(开发 / 自用) —— 源码改动即时生效,适合改代码:

dsh plugin --profile web add "<克隆到本地的仓库路径>"   # 或 git URL

装完验证 entry 入树:dsh --profile web --dump-config。

安装即启用:cordis.patch.yml 显式 enabled: true(代码缺省关,fail-closed),开关状态在 YAML 里可见。

改代码 / 换版本后要跑新代码:web profile 由 HMR 自动重载;desktop / headless 需重启对应实例(桌面端重启应用;headless 每次启动天然取最新)。本地克隆渠道是 link: 软链,源码改动无需重新安装。

升级

DSH 的插件管理器不支持自动更新(官方文案:升级 = 先卸载再装新版)。所以升级 = 用新版本重跑一次安装命令:

# tag tarball 渠道:升级 = 用目标版本号重跑一次(版本号见 Releases 或 CHANGELOG.md)
dsh plugin --profile web add "https://github.com/axuan-1314/dsh-keepsake/archive/refs/tags/v<新版本号>.tar.gz"
# 本地克隆渠道:git pull 后无需重装(软链生效)
  • 数据不受升级影响:记忆、开关、人设、备份全部落在 ~/.dsh/memory/——在插件包目录之外,卸载 / 重装都不碰它(除非你手动删除该目录)。数据与代码分离是刻意设计。
  • 升级后需重启:替换已安装的包后,进程要重启才会加载新 JS 模块(web profile HMR 可自动重载;desktop 重启应用;headless 下次启动)。
  • v0.6.0 升级须知(行为变化,务必过一眼):① 长期记忆(long-term)不再进注入索引(归档条目对所有会话不可见,提取 prompt 仍可见);② 人设段上限 16000 → 8000 字符;③ injectMaxTokens 默认 16000 → 24000;④ 新增「运行时记忆」开关,默认关(不开则注入内容与旧版等价,只是少了 long-term 索引行);⑤ 存储格式版本 1 → 2,退回旧版会自动只读。细节见 CHANGELOG.md 的 0.6.0 条目。
  • 降级前先备份:数据目录里的 keepsake-format.json 记录存储格式版本(见 FORMAT.md)。v0.6.0 起 FORMAT_VERSION=2(v0.3.0 → v0.5.0 期间一直为 1):新增可选 pinned 与 long-term 的 origin 三字段,属可选增量、无数据迁移。由此——从 v0.6.0 退回 v0.5.0 或更旧版本时,旧版读到 v2 标记会自动转只读(写端点 503 + 门牌/健康卡报警,读取仍放行),这是防「旧版覆写剥掉 pin」的兜底;而 v0.4.0/v0.5.0 退回 v0.2.x 时格式闸不会触发,旧版可以正常读写同一份数据,但它没有 v0.4.0 的写入安全面(批前快照 / 回收站 / sha1 覆盖闸 / 写后读校验),其自动提取的覆写是直接替换。降级前建议先做一次全量备份(设置页数据卡「导出全部」,或直接拷走 ~/.dsh/memory/)。
  • 降级到 v0.6.4 及更早版本前,先删掉自己显式加过的两个 config 键:extractReasoningEffort(本版本新增的键)与 maxOutputTokens: 0(v0.6.4 的校验要求正整数,0 会被拒)在旧版 resolveConfig 里分别是「未知键」与「非法值」,而 apply() 第一步就调 resolveConfig 且无 try/catch——旧版会装载即失败,不是降级运行。cordis.patch.yml 出厂配置里这两个键默认就是注释状态(代码缺省已等价于「0 + 不下发」),只有你自己显式取消注释时才需要手动删掉。

验证生效

  1. 装好后开一个新会话,正常聊几轮有实质内容的话(整句散文,而非单词命令);
  2. 检查落盘:看 ~/.dsh/memory/projects/<slug>-<hash16>/memory/ 是否出现 MEMORY.md 与记忆文件,或全局分类目录(如 ~/.dsh/memory/global/work-profile/)是否出现条目;
  3. 检查注入:新开一轮对话,让模型复述"你记得关于我/这个项目的什么",能引用出记忆内容即注入生效;
  4. 也可直接查 API:GET /dsh-keepsake/api/state(本机 loopback)看 counts(全局条目数 / 项目数)与 extractionFailures(应为全零);
  5. 打开 DSH 设置页 →「记忆」区块 →「打开记忆管理」,面板各页应能正常浏览。

注意:desktop / headless profile 改 cordis.patch.yml 等 entry config 后需重启对应实例才生效(web profile 由 HMR 自动生效);而面板里的三层开关、称呼、人设走 ~/.dsh/memory/settings.json,宿主每轮读取,下一轮对话即生效,无需重启。

配置

cordis entry config(见 cordis.patch.yml):

字段默认说明
enabledfalse(代码缺省关,fail-closed)总开关;cordis.patch.yml 显式 enabled: true,安装即启用且开关可见于 YAML
injectChannel"message"注入通道:"message"=durable 用户消息通道(默认,全预设生效);"section"=旧 systemPrompt 注入通道(一键回滚;v0.6.0 起同样承载六段,含 runtime 两段)
injectMaxChars30000消息通道注入总预算(字符口径):召回块与主块共享此预算,主块为召回块让位;超限按优先级整段丢弃并附可见 WARNING。v0.4.2 起默认由 60000 下调(块首取代声明 / 免责两行固定开销 + 节流收敛,见「工作原理」);需要更大预算时显式配置即可覆盖
injectMaxTokens24000消息通道注入总预算的 token 口径(CJK 感知估算;含召回块,主块让位;与 injectMaxChars 双上限同时生效,取更严者)。0 = 关闭 token 预算,退回纯字符口径。v0.6.0 起默认由 16000 上调:为运行时记忆层留预算(persona 8000 字符 ≈12000 tokens + runtime 两段常驻 ≈7170 tokens ≈ 19170)。token 估算是近似值(CJK 字 ≈1.5 token/字、其余 ≈0.25 token/字),非精确分词
redactionEnabledtrue内容脱敏总开关(见「内容脱敏」):提取提示词与落盘写入两道位置的密钥类内容替换为 [redacted];false = 完全不脱敏,回旧行为
storageRoot~/.dsh/memory记忆总根(测试/迁移用,一般不动)
provider / model空(跟随会话路由)提取调用的模型路由;成对出现,否则报错。缺省时自动捕获本会话路由(request/header 优先,assistant/message 的 source 兜底,同 session-title 策略)
minUserWords3skip 条件的散文词数下限
extractGateEnabledtrue提取触发打分门开关(见「提取触发门槛」);false = 关门,过薄轮次也会照旧调模型提取(旧行为)
extractGateMinScore6打分门阈值:分数 = 用户词数 + 2×工具种类数 + 2×真人轮次数,低于该值记 skipped/thin-evidence 跳过提取(不调模型、进度照常推进)
extractIdleDelayMs3000提取空闲延迟(毫秒):轮次结束后延迟等待、合并突发轮次为一次提取;0 = 立即提取(旧行为)。上限受定时器最大延迟约束,超限启动即抛
extractMaxRunsPerSession50每会话自动提取次数上限:到顶后后续触发记 skipped/session-cap 并推进进度(不调模型);0 = 不限。只约束自动链路,手动补提不受限
autoRecoverEnabledtrue自动补提开关(见「会话补提」的「自动收口」):会话关闭 / 轮次完成 / 启动清扫三个时机自动补提未恢复的失败区间;false = 关闭自动补提,只保留面板手动按钮。宿主无会话持久化时自动链路静默不跑
autoRecoverCooldownMs300000同一失败区间两次自动补提尝试的最小间隔(毫秒),防宿主读不回的区间被反复白读;上限受定时器最大延迟约束
timeoutMs120000单次提取调用超时
maxOutputTokens0提取输出上限。0 = 跟随模型(不发 maxTokens,由宿主套模型默认值;推荐);>0 时照旧作为硬上限。显式 4096 会让带思考的模型把预算全花在思考上(实测 88 条 llm-error 里 81 条是 kind=max-tokens)
extractReasoningEffort不下发(缺省)提取调用的思考档:"minimal" / "low" / "medium" / "high"(非法值启动即抛,带允许值提示)。缺省不下发(options 里不出现该键,跟随模型默认档):宿主按模型能力声明校验档位,不被支持的档位会在 provider I/O 前被拒(UNSUPPORTED_REASONING_EFFORT),