dsh-keepsake
English | 中文
DSH(DeepSeek Harness)的跨会话记忆插件:对话里聊过的事自动记下来,下次对话自动带上——你不用每次重复交代背景。附带一个本地「记忆管理」面板,记了什么、删了什么、人设怎么设,全程可见可改。
- 插件版本:0.3.1(以
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/v0.3.1.tar.gz
目录
这是什么
DSH 每次新会话都是从零开始:你的工作背景、这个项目的决策历史、上周聊定的口径,全都得重新说一遍。dsh-keepsake 把这件事自动化:
- 自动提取——每轮对话结束,插件自动判断这轮有没有值得记的事(用户偏好、项目决策、长期有效的结论……),有就落盘成本地 Markdown 文件;
- 自动注入——之后每一轮对话,把已记下的内容以四段形式(人设 / 全局记忆索引 / 项目使用说明 / 项目记忆索引)作为一条插件自有的用户消息注入会话,模型据此"记得"你;
- 面板管理——DSH 设置页多一个「记忆」区块,从这里打开记忆管理面板:浏览/编辑/删除/导入导出记忆条目、编辑人设与称呼、开关各路记忆、看提取管道健康状况。
记忆分两层:
- 全局记忆(跨所有对话生效):工作背景、用户个人背景、当前关注、近期动态(自动滚动清理,默认保留 30 条 / 30 天)四个对话分类,外加长期记忆;
- 项目记忆(按工作区路径隔离):每个项目一个独立目录,记这个项目的决策与状态。
功能介绍
以下每条都有源码依据(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"可回退到旧的系统提示词注入)。这条消息由四段组成,模型据此"记得"你:
| 注入段 | 模型看到什么 | 预算 |
|---|---|---|
| 人设 | 「用户叫你小助手」「希望你称呼他为朋友」+ 你编辑的补充人设正文 | 400 行 / 16k 字符 |
| 全局记忆索引 | 四分类 + 长期记忆的条目索引;近期动态带年龄与过期倒计时(如「3 天前 · 27 天后过期」) | 每类 22 行(超限保最新),总 120 行 / 12k 字符 |
| 项目使用说明 | 记忆目录在哪、索引怎么读、怎么按你的要求直写/删除记忆 | 固定短文 |
| 项目记忆索引 | 该项目 MEMORY.md 索引(每行指向一个记忆文件,模型按需 Read) |
200 行 / 25k 字符,超限保最新丢最旧并附 WARNING |
- 三层开关(面板「设置」页,改动下一轮对话即生效):
| 开关 | 关掉后的效果 |
|---|---|
| 对话记忆(总开关) | 四段注入全灭 + 自动提取全停;关掉期间的轮次不积累,重开后不回补 |
| 项目记忆 | 项目使用说明/项目索引不再注入,项目类记忆写入被过滤(计数可见) |
| 全局自动提取 | 不再自动写全局记忆;已有的全局条目照常注入 |
- 防自反馈:注入的记忆消息带
source.kind = "plugin:dsh-keepsake"标记;提取侧按此标识把自家注入的消息从「用户散文」里排除(既不计入 skip 门槛、也不进提取输入),不会出现"模型把注入的记忆又记回记忆"的自食循环。
语义召回(可选)
默认关闭。开启 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 才随该地址发送)。
- 自带模型(provider-agnostic):插件不绑定任何嵌入模型或厂商——只要你的服务提供 OpenAI 兼容的嵌入端点(绝大多数本地部署都提供),填上
embeddingBaseUrl+embeddingModel即可,本地自选模型(EmbeddingGemma、bge-m3 等)同样适用。embeddingProvider只是显示标签,配不配都行。 - 失败显式降级:端点超时/报错时本轮不召回(不阻塞、不臆造),计数进
/state.recall。
记忆管理面板
入口:DSH 设置页「记忆」区块(门牌)→「打开记忆管理」。打开方式自动三级降级(新窗口 → 内嵌 iframe → 整页跳转),面板主题自动跟随宿主深浅色。门牌本身也是速操台:总开关、「DSH 的名字」「对我的称呼」两个输入框(防抖自动保存)、状态摘要(全局 N 条 · M 个项目)、提取失败红条 + 「详情」深链。

上图为设置页示意(使用示例数据渲染)。记忆管理面板的条目管理见文首截图。
面板(/dsh-keepsake/panel/,仅本机可访问)包含:
- 四个对话分类页 + 长期记忆页:条目卡片流(标题 / 描述 / 文件名 / 更新时间 / 正文),支持——
- 浏览与检索:长期记忆页有即时过滤(标题 / 描述 / 正文);近期动态条目带年龄徽标;
- 编辑:标题(60 字)、描述(60 字)、正文(20000 字符上限),实时字符与 UTF-8 字节计数;
- 删除(确认弹窗)、新增条目;
- 源码模式:整类序列化成一个文本直接编辑,保存 = 先自动备份再整类重建;
- 重置:清空整类(先自动备份);
- 导入:支持上传
.json/.md文件(≤2MB)或直接粘贴,三种格式(JSON 结构化逐条 / Markdown 按##拆条 / Markdown 整段);先预览将要写入的条目(与现有内容冲突或批内改名的标红),确认后才落盘(落盘前自动备份);批内同名自动-2/-3避让。
- 人设页:左右分栏(markdown 编辑 + 实时预览),内置简洁 / 专业 / 幽默 / 伙伴 / 自定义五个预设模板,一键套用;「恢复默认」回到出厂模板(先自动备份);上限 100KB。人设是官方系统提示的补充而非替换,对所有回复生效。
- 称呼页:「DSH 的名字」「对我的称呼」两个输入框(各 40 字上限),防抖自动保存,与门牌输入框是同一份数据。
- 项目记忆页(只读概览):按工作区列出各项目的条目数与最后更新时间;可单项目导出 JSON。项目记忆由对话自动维护,不提供手工编辑。
- 设置页:三层开关;管道健康卡与提取模型卡(见下节);数据卡(导出全部 JSON / 清空全部记忆——逐类自动备份,人设与称呼保留);备份列表(最近 20 份,逐份可「恢复此备份」,恢复前还会先备份当前状态)。
- 损坏可见:
settings.json/addressing.json/persona.md解析失败时,门牌与面板显示红条「解析失败(已按缺省运行)」,点任意开关 / 重新保存对应内容即可重写修复;人设 / 称呼的损坏档在覆盖写前自动备份*.corrupt-<ts>,原数据不失访(settings.json无备份、改开关时直接重写修复)。
管道健康卡与「立即重试提取」
设置页的「自动提取管道」健康卡让你一眼看出记忆提取是否健康:
- 三态灯:绿 = 最近无失败(折叠为一行摘要);黄 = 近期有过失败但已自动恢复;红 = 当前连续失败(同因连击数);灰闪 = 正在提取。连击 > 0 期间每 5 秒自动轮询刷新,侧栏「设置」项同步显示红色连击徽标。
- 失败分桶计数:按原因(无路由 / 无 cwd / LLM 调用 / 解析 / 落盘 IO)分桶,只显示非零桶。
- 最近 5 条失败详情:时间 / 原因 / 模型 / 错误摘要,配合失败日志路径(
<storageRoot>/extraction-errors.log)排障。 - 错过区间提示:「seq X 之后的轮次尚未记录,恢复后自动补提」——告诉你哪些对话可能漏记了。
- 「立即重试提取」按钮:对仍有未提取区间的会话真的发起一次 LLM 提取(有确认弹窗,明示消耗少量 token);点击后按钮进入 busy 态,面板每 2 秒轮询状态最多 60 秒,恢复即提示「提取已恢复」。10 秒最小间隔,提取在途时返回 409。
- 「复制诊断信息」按钮:一键复制当前状态摘要,可直接粘贴到 issue。
- 提取模型卡:如实展示提取路由(显式配置的
provider/model,或「未指定(跟随会话路由)」)、fallback 换道配置、最近一次提取用车(路由 + 相对时间)、嵌入模型(语义召回所用;未配置时显示为未启用)。
失败韧性:重试、分类、落盘留痕
提取是后台静默发生的,所以失败处理必须显式可见:
- 可取消退避重试:单次提取最多 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全量备份到<storageRoot>/backups/<时间戳>/; - 滚动保留最新 20 份,防无界累积;
- 半备份可见且拒恢复:备份采用两段式落盘(先
complete: false,拷完改true),中断的半备份在面板标「不完整」且拒绝恢复; - 恢复也有后悔药:恢复备份前先把当前状态再备份一份(
pre-restore),恢复中途失败时响应会带上这份备份 id 指路自救。
快速开始
安装
两种渠道任选(零运行时依赖,全程无需 npm install):
① GitHub tag tarball(推荐) —— 从仓库 tag 直接安装,钉死版本:
dsh plugin --profile web add https://github.com/axuan-1314/dsh-keepsake/archive/refs/tags/v0.3.1.tar.gz
# 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/<新版本号>.tar.gz
# 本地克隆渠道:git pull 后无需重装(软链生效)
- 数据不受升级影响:记忆、开关、人设、备份全部落在
~/.dsh/memory/——在插件包目录之外,卸载 / 重装都不碰它(除非你手动删除该目录)。数据与代码分离是刻意设计。 - 升级后需重启:替换已安装的包后,进程要重启才会加载新 JS 模块(web profile HMR 可自动重载;desktop 重启应用;headless 下次启动)。
- 只读保护(防降级弄坏数据):数据目录里的
keepsake-format.json记录存储格式版本(见 FORMAT.md)。若数据由更新版本的插件写入(例如先装 0.4 又退回 0.2),旧版本会自动转为只读——不再记录新记忆、面板写入被拒(写端点返回 503),并在门牌与设置页「管道健康卡」显式报警;升级插件后自动恢复。这是为了不让旧代码按旧规则误改新格式数据。
验证生效
- 装好后开一个新会话,正常聊几轮有实质内容的话(整句散文,而非单词命令);
- 检查落盘:看
~/.dsh/memory/projects/<slug>-<hash16>/memory/是否出现MEMORY.md与记忆文件,或全局分类目录(如~/.dsh/memory/global/work-profile/)是否出现条目; - 检查注入:新开一轮对话,让模型复述"你记得关于我/这个项目的什么",能引用出记忆内容即注入生效;
- 也可直接查 API:
GET /dsh-keepsake/api/state(本机 loopback)看counts(全局条目数 / 项目数)与extractionFailures(应为全零); - 打开 DSH 设置页 →「记忆」区块 →「打开记忆管理」,面板各页应能正常浏览。
注意:desktop / headless profile 改 cordis.patch.yml 等 entry config 后需重启对应实例才生效(web profile 由 HMR 自动生效);而面板里的三层开关、称呼、人设走 ~/.dsh/memory/settings.json,宿主每轮读取,下一轮对话即生效,无需重启。
配置
cordis entry config(见 cordis.patch.yml):
| 字段 | 默认 | 说明 |
|---|---|---|
enabled |
false(代码缺省关,fail-closed) | 总开关;cordis.patch.yml 显式 enabled: true,安装即启用且开关可见于 YAML |
injectChannel |
"message" |
注入通道:"message"=durable 用户消息通道(默认,全预设生效);"section"=旧四段 systemPrompt 注入(一键回滚) |
injectMaxChars |
60000 | 消息通道注入总预算(字符):召回块与主块共享此预算,主块为召回块让位;超限按优先级整段丢弃并附可见 WARNING |
storageRoot |
~/.dsh/memory |
记忆总根(测试/迁移用,一般不动) |
provider / model |
空(跟随会话路由) | 提取调用的模型路由;成对出现,否则报错。缺省时自动捕获本会话路由(request/header 优先,assistant/message 的 source 兜底,同 session-title 策略) |
minUserWords |
3 | skip 条件的散文词数下限 |
timeoutMs |
120000 | 单次提取调用超时 |
maxOutputTokens |
4096 | 提取输出上限 |
maxIndexLines / maxIndexChars |
200 / 25000 | 注入截断预算 |
maxManifestFiles |
200 | manifest 文件数上限 |
maxPromptChars |
48000 | 增量 transcript 截断 |
maxOps |
20 | 单轮最多写/删条数 |
recentKeepCount / recentKeepDays |
30 / 30 | 近期动态滚动清理:最多保留条数 / 最大年龄天数 |
retryAttempts |
3 | 提取 LLM 调用总 attempt 数(含首次) |
retryBackoffMs / retryMaxDelayMs |
500 / 2000 | 指数退避首档与上限(jitter ±20%;上游带 Retry-After 时优先、钳 ≤3×上限) |
fallback |
[](纯重试不换道) | 换道路由 [{provider, model}],≤3 条、去重、不得撞主道。主道 attempt 链耗尽后逐条换道(同 attempt 链),成功记 fallbackUsed 且最近用车指向该道;未配置则与纯重试一致 |
embeddingModel |
空 | 你的嵌入模型标识。开启 recallEnabled 后必填——它同时是向量空间的缓存身份(换模型=整库重嵌)。填你端点上的模型名即可,如 EmbeddingGemma、bge-m3、text-embedding-3-small |
embeddingProvider |
空 | 可选,纯面板显示标签,不参与调用,不要求与 embeddingModel 成对(如 local / lmstudio) |
recallEnabled |
false | 语义召回总开关;开启需配 embeddingBaseUrl + embeddingModel(缺一启动即抛,fail loud) |
embeddingBaseUrl |
空 | 你的嵌入端点,任意 OpenAI 兼容服务即可(本地或远程):POST <url>,体 {model, input},返回 {data:[{embedding}]}(也容忍 {embedding} / {embeddings} 两种本地常见形态)。仅接受 http(s)。示例:本地服务 http://127.0.0.1:1234/v1/embeddings,vLLM / TEI / llama.cpp 等同形;远程如 https://api.openai.com/v1/embeddings |
embeddingApiKey |
空 | 端点鉴权(可选;有则带 Authorization: Bearer) |
recallTopK / recallMaxTokens |
5 / 1200 | 单轮召回条数上限 / 召回块 token 预算(CJK 感知估算) |
recallMinScore |
0.35 | 余弦相似度阈值([0,1],越界即抛) |
recallTimeoutMs |
8000 | 单次嵌入调用超时。实测:本地 CPU 嵌入服务独立调用 ~0.6s,但宿主进程内竞争下可达数秒——故默认留足余量;端点真死会连接被拒秒退,只有慢端点才吃满(超时按失败降级:本轮不召回) |
recallTotalBudgetMs |
20000 | 单轮召回整体时间预算(毫秒)。嵌入可能分批串行请求,单请求超时(recallTimeoutMs)不足以约束整轮耗时,故整体封顶;超出后本轮按已缓存部分召回、下次续跑(不再让一轮卡满 N×单请求超时) |
失败分类:按内核 failure.code 判可重试(EMPTY_RESPONSE/RATE_LIMIT/SERVER/TIMEOUT/TRANSPORT),AUTH/INVALID_REQUEST/QUOTA 等确定性失败立即摊牌;code 缺失才退保守 message 正则。
配置严格性:未知 config 键即抛错(带 did-you-mean 建议),非法值即抛——fail loud,不静默吞错配。
数据与隐私
- 数据全在本地:所有记忆、设置、备份、日志都落在
~/.dsh/memory/(自有目录,卸载不影响其他组件); - 提取走会话路由:提取调用复用你当前对话的模型通道(
ctx.llm.stream),没有独立的云端上传路径; - 失败日志不含会话内容:
extraction-errors.log只记时间 / pid / 原因 / 路由 / 错误摘要;诊断口debug.log仅在DSH_KEEPSAKE_DEBUG=1时开启; - API 与面板仅本机可访问:全部端点先过 loopback 守卫(仅 127.0.0.1 / localhost,校验 Host 与 Origin),DSH 反代鉴权是第一层。
数据布局:
~/.dsh/memory/
├── settings.json # 三层开关(面板可改,每轮读取)
├── extraction-errors.log # 提取失败日志(常开,>512KB 轮转两档)
├── debug.log # DSH_KEEPSAKE_DEBUG=1 时的诊断口
├── embeddings.json # 语义召回条目向量缓存(开启 recallEnabled 时生成)
├── global/ # 全局记忆
│ ├── persona.md # 人设(出厂默认模板起步)
│ ├── addressing.json # 称呼 {dshName, userAlias}
│ ├── work-profile/ user-profile/ current-focus/ recent-activity/ long-term/
│ │ └── MEMORY.md + <kebab-name>.md # 每类:索引 + 条目
├── projects/<slug>-<hash16>/memory/ # 项目记忆(按工作区隔离)
│ ├── MEMORY.md # 索引:- [Title](file.md) — 一行钩子(≤150 字符)
│ └── <kebab-name>.md # 记忆文件:frontmatter name/description/metadata.type
└── backups/<ts>/ # 自动备份(滚动保留最新 20 份)
slug:workspace basename 净化(小写、[^a-z0-9._-]+→-、截 48、空则project)hash16:绝对路径(win32 转小写)sha256 前 16 hex- 记忆文件格式(frontmatter / 索引行 /
[[name]]互链 / 跨 agent 互认约定)见 FORMAT.md
回滚与卸载
- 停功能不卸包:把 profile 的
cordis.patch.yml(或 bundle patch)里enabled改为false。生效方式分 profile:web profile 由 HMR 自动生效;desktop/headless 改 config 后需重启对应实例(桌面端重启应用;headless 本就每次启动新进程)。不存在"改完即热生效"的统一行为,以上为各 profile 的实际生效路径。若只想临时停用记忆功能,用面板的总开关即可(下一轮对话生效,无需重启)。- 只回退注入通道(不停功能):把
injectChannel改为"section",回到旧的四段系统提示词注入;同样按上面的 profile 生效路径。 - 只关语义召回:把
recallEnabled改为false(或删除),回到"仅索引、无自动召回",不再产生任何嵌入调用。
- 只回退注入通道(不停功能):把
- 整包卸载:
dsh plugin --profile <name> remove dsh-keepsake。 - 清数据:删
~/.dsh/memory/(自有目录,不影响其他组件)。
FAQ
Q1:改了配置 / 关了开关,怎么没生效?
分两层。cordis.patch.yml 里的 entry config(provider/model、预算等)改完后,web profile 走 HMR 自动生效,desktop / headless 需重启对应实例。而面板里的三层开关、称呼、人设不走这套——它们写在 ~/.dsh/memory/settings.json,宿主每轮对话前读取,下一轮对话即生效,不受重启限制。
Q2:设置页红条「自动提取失败 N 次」是什么?怎么办?
这是提取管道在报障(典型场景:免费池限流 429)。长这样:门牌/面板出现红条,健康卡灯变红、显示连击数、失败分桶、最近失败详情和「seq X 之后的轮次尚未记录」的错过区间提示。插件会自动重试(3 次 attempt 指数退避)并在此后每轮自动补提;如果长期不恢复,到设置页健康卡点「立即重试提取」,或看 <storageRoot>/extraction-errors.log(健康卡上有路径),或在 cordis.patch.yml 给 dsh-keepsake 显式配一组更稳的 provider/model。
Q3:手动编辑条目/人设时粘贴错了并保存,能找回吗? 不能——单条覆盖写(保存条目、保存人设、改称呼)是直接替换旧文,没有编辑历史(有意不修,见已知限制)。有自动备份的是批量破坏路径(重置 / 导入 / 恢复备份),单条覆盖前请自行先导出兜底。
Q4:刚聊的事为什么没被记下来? 依次排查:① 该轮没有 ≥3 个词的用户散文(如只有单词命令)会被 skip——这是降噪设计;② 你(或模型)在该轮直接写入了记忆目录,直写轮会跳过自动提取;③ 提取失败(看红条/健康卡);④ 三层开关被关了。任何情况下都可以在面板手动新增条目补上。
Q5:我的对话内容会被上传到哪?
记忆数据全部落在本地 ~/.dsh/memory/。提取调用本身走你当前会话的模型路由(和你对话用的是同一条通道,发给它的是增量对话摘录 + 现有记忆索引);失败日志只记错误摘要,不含会话内容。API 与面板仅本机 loopback 可访问。
Q6:「立即重试提取」为什么提示间隔过短? 该按钮有 10 秒最小间隔节流(真调 LLM 的端点必须有闸;进程内计时,重启插件会重置);提取在途时会返回 409「已有提取在进行中」。稍候再点即可。
工作原理
内核机制视角(实现语义,供二次开发/排障参考):
- 触发:订阅
session/event,在turn/end(data.reason.kind === "completed")后触发提取(进程内单飞 + latestPending 合并:running 期间新触发只留最新边界)。 - 提取:
ctx.llm.stream单次调用——输入为增量 transcript 摘录 + 现有记忆 manifest(mtime 降序前 200,最近小文件附正文)+ 全局四分类索引现状,输出为Nothing to save.或一个{"ops":[...]}JSON(ops 带 scope:project缺省 /global:<category>)。 - 落盘:由插件(非模型)写文件到
~/.dsh/memory/projects/<slug>-<hash16>/memory/或全局分类根,文件名经三道闸(包含性判定 + 黑名单段 + Unicode 控制符剥除);MEMORY.md索引自动合并维护;recent-activity的时间戳由宿主盖章(不信模型报的时间);近期动态滚动清理在启动时 + 每次全局写入后收口。 - 注入:默认走 durable 用户消息通道——每轮第一个 step 在
agent/pre-step求值四段(4990 人设 / 4995 全局记忆索引 / 5000 项目使用说明 / 5001 项目MEMORY.md索引),拼成一块带<memory-context>包裹的 user 消息注入(块首一行免责/署名:记忆是参考上下文、非任务指令;subagent 会话不注入);与视野内已有注入内容相同则跳过(内容变化或 compaction/rewind 把旧注入挤出视野时才补发),总预算injectMaxChars(召回块计入其中,主块先为召回块让位)。injectChannel: "section"时回退为四段ctx.systemPrompt.section注入(空段自动丢弃;索引读取有 mtime 缓存,缓存键含项目维度,防同 size+mtime 串档)。 - 语义召回(可选,
recallEnabled):开启后每轮以当前用户消息为查询,对记忆条目做嵌入余弦 top-k(recallTopK/recallMinScore/recallMaxTokens),命中的条目以<memory-recall>块经消息通道注入;条目向量增量缓存于<storageRoot>/embeddings.json(仅新/改条目重嵌),查询每轮嵌入一次。端点超时/失败/未配置 → 本轮不召回(显式降级,/state.recall可观测);记忆文本会发往embeddingBaseUrl(默认关=不配置就不出本机,指向本地端点即不出本机)。 - skip 双条件:无 ≥3 词用户散文;本轮已有对记忆目录的直写(write/edit 工具落 memoryRoot)。skip 也推进 cursor。
- cursor 推进纪律:run 结算 'ok'(成功或 skip)推进到快照边界;'error' / abort 不推进,下轮合并重提。cursor 为进程内状态不持久化;插件中途挂载/恢复的会话只提取存活期间新完成的轮次,不回扫历史。
- 关停纪律:插件卸载时先有界 drain 在途提取(headless one-shot 依赖这段在退出前把记忆写完),超时才 abort 兜底。
- 六条规格边界:① 去抖合并+cursor 推进 →
lib/state.js;② 写入边界(仅 .md、包含性强制、模型永不直接碰盘)→lib/extract.js;③ 文件名安全 →lib/filename.js;④ 注入预算 →lib/manifest.js;⑤ 触发门槛 →lib/transcript.js;⑥ 防自反馈(提取 request-only + 注入消息按source.kind过滤)→lib/index.js/lib/transcript.js。
可观测与运维
- 提取失败计数:
/dsh-keepsake/api/state的extractionFailures(按原因分桶 + 同因连击 streak +retried重试 attempt 计数 +recent[5]最近失败详情);门牌/面板红条显示连续失败。结构性失败(无路由/无 cwd)连续 5 次后显式放弃该区间(error 日志),不再无限重试积压。 - 观测面:
/state另含lastFailedRange(未恢复失败区间)/extractionInFlight/lastAttemptAt/lastUsedRoute(最近一次提取用车)/extractionConfig(含 fallback)/embeddingConfig(嵌入召回配置);面板设置页有「管道健康卡」承载以上全部。 - 注入面(v0.2.0):
/state的injection字段(count/lastChars/totalChars/resends/droppedSegments/renderFailures/lastAt);面板健康卡附一行注入摘要(注入次数 / 最近字符 / 丢段 / 渲染失败)。渲染失败显式计入renderFailures(不静默丢)。 - 召回面:
/state的recall字段(hits命中注入次数 /items累计条目 /chars累计字符 /misses无命中 /errors嵌入或检索异常 /lastAt)。 - 失败落盘日志:每次提取失败追加
<storageRoot>/extraction-errors.log(只记摘要:时间/pid/原因/路由/错误摘要,不含会话内容;>512KB 轮转两档)。不依赖DSH_KEEPSAKE_DEBUG,常开——上游限流停摆场景里运行态零痕迹的补丁。单进程单写者前提;跨进程共享 storageRoot 不保证 interleave 安全。 - 手动补提:
POST /dsh-keepsake/api/extract/retry(面板「立即重试提取」按钮)→ 202 已触发 + 经/state观测结果;在途 409;10s 最小间隔(进程内计时,重启插件重置)。复用 scheduler 单飞,无并发双跑。 - 损坏可见:settings.json / addressing.json / persona.md 解析失败=按缺省运行 +
corrupt标记 + 界面红条;覆盖写前损坏档自动备份*.corrupt-<ts>。 - debug 日志口:
DSH_KEEPSAKE_DEBUG=1时写<storageRoot>/debug.log(>1MB 轮转两档 debug.log.1)——Electron 宿主 console 不回传时的全量诊断通道。 - 备份策略:reset/import/restore 前自动全量备份到
backups/,滚动保留最新 20 份;半备份(meta 无 complete)标记 incomplete 且拒绝恢复。
生态内相关项目
同作者的另一个 DSH 插件,与本插件互不依赖,可分别独立安装:
- dsh-usage-stats —— DSH 桌面版「使用统计」插件:设置页新增使用统计分区(五格摘要条 / 52 周 Token 热力图 / 今日·近 7 日·近 30 日趋势折线 / 模型用量环形图),全本地 JSONL 落盘、零上传。
两者都只读写各自的本地数据目录,装不装另一个都不影响本插件工作。
开发与测试
npm test # node --test,346 个测试(含防漂移检查 test/client-source.test.mjs)
npm run check # 语法快检 lib/*.js(跨平台 node -e 循环,cmd/bash 均可跑)
- API 冒烟:
tools/api-walkthrough.sh对一次性测试实例把全部 REST 端点走一遍(默认拒绝执行,须显式WALKTHROUGH_CONFIRM=yes;会清空目标实例的记忆数据)。 - 参与贡献:开发环境、提交约定与 PR 流程见 CONTRIBUTING.md;版本变更记录见 CHANGELOG.md;后续方向与「有意不做」清单见 ROADMAP.md。
模块结构:lib/index.js(入口:订阅/路由捕获/重试链/注入)、lib/inject-channel.js(注入块拼装/预算裁剪/来源判据)、lib/state.js(提取状态机)、lib/extract.js(输出解析 + scoped 落盘)、lib/transcript.js(转录渲染与 skip)、lib/manifest.js / lib/filename.js(索引与文件名安全)、lib/global-store.js(全局记忆 CRUD/备份/恢复)、lib/settings.js(三层开关)、lib/metrics.js(失败计数/失败分类/退避/注入面)、lib/errorlog.js(失败日志)、lib/api.js + lib/panel.js + lib/panel-logic.js(REST API / 面板供给 / 面板纯逻辑,双端共享进检查)、lib/client.js(设置页门牌)、web/panel.html(面板单文件前端)。
已知限制
- 规格 taskType 过滤未移植:上游规格按任务类型(interactive/fork/…)过滤提取资格;本插件对所有会话生效,依赖 skip 双门槛(无 ≥3 词散文 / 直写轮)兜底抑制噪声。
- 相对路径 file_path 的直写判定偏保守:
transcript.js对 write/edit 的file_path以 memoryRoot 为基解析相对路径,恰好落在记忆目录内的相对路径会判为"直写轮"而 skip 提取——方向保守(多 skip、不漏存),安全。 - 单次 LLM 调用(非提取子任务 loop 形态,其 API 面在 DSH 未实证):更新老记忆时模型只见 manifest 描述 + 最近小文件正文,不见全部旧文;由 manifest 附正文(≤3000 字符 × 最近 10 文件)缓解。
- 防自反馈与检索:提取调用为 request-only 输入(不 append 事件);注入的记忆消息带
source.kind = "plugin:dsh-keepsake"标记,提取侧按此排除(不计 skip 门槛、不进提取输入)——注入内容会出现在会话记录流中,但不会被记回记忆。记忆正文无自动检索,靠模型按索引主动 Read(与上游规格同构)。 - desktop/headless 改 config 需重启实例(见回滚节),无统一热生效。
- 无 webserver 的 profile 不挂载 API/面板:插件的硬依赖只有「提取用的 LLM」与「section 模式的 systemPrompt」;在无 webserver 的 profile(如 headless)也能加载并正常做记忆注入,只是不挂载 REST API 与面板(
/dsh-keepsake/...不可访问)。 - 单条覆盖写无编辑历史(有意不修):PUT persona / PUT entry 直接替换旧文——粘贴错内容保存即丢旧文;批量破坏路径(reset/import/restore)有自动备份,单条覆盖请自行先导出兜底。
- 4995 全局索引每次求值全量读条目(有意不修):近期动态条目渲染读取条目正文(只用 22 行预算),桌面规模无感;若条目数暴涨可加 mtime 缓存。
- 注入块在会话内可能累积:记忆内容变化时补发新的注入消息、旧块不删(已提交历史不可改),仅靠 compaction 收敛;单块受
injectMaxChars约束,/state.injection.resends可观测累积速度。若长期高频变化,后续可考虑「同 form 只保最新」。 injectMaxChars是注入总预算(含召回块):开启召回时主块为召回块让位,两段合计不超过该值;可见截断提示行也计入预算(仅当预算小到连包裹与提示行都装不下时才允许轻微越界)。- 防自食只覆盖自家注入:提取侧按
source.kind只排除plugin/plugin:*;宿主官方agent-instructions、runtime-context等注入的 user 消息仍按普通用户内容对待(既有行为,非本版引入;本版保证的是"不把自己的注入记回去")。 - 回声抑制是标签级:提取输入会剥掉自家
<memory-context>/<memory-recall>包裹块(防模型原样回声被记回);若模型是"意译"引用而非原样带标签,剥离不生效——注入消息本身的排除靠source.kind兜底,但意译文本不可辨。 - 语义召回按轮嵌入查询:开启后每轮对当前用户消息嵌一次(单请求超时
recallTimeoutMs,整轮总预算recallTotalBudgetMs,超时即本轮不召回);条目向量增量缓存不重复嵌入,但首次开启会一次性嵌入当时全部条目。默认关,未配置端点时不产生任何调用。 - 召回块的会话内累积:召回结果变化时补发新块、旧块不删(已提交历史不可改),仅靠 compaction 收敛;
/state.recall累计chars/items可观测。未做 supersede(与主注入同一取舍)。端点连续失败会熔断 60s(期间跳过召回,避免每轮阻塞)。
下一版方向与有意不做的清单见 ROADMAP.md。
No comments yet. Be the first to write one.