- Startseite
- Plugins
- Gedächtnis
- dsh-workspace-recollection
dsh-workspace-recollection
reaxlll/dsh-workspace-recollection
Sitzungsübergreifendes Langzeitgedächtnis für DeepSeek Harness, nach Workspace getrennt und zwischen Sitzungen geteilt. Im dauerhaften Prompt stehen nur oberflächliche Kernpunkte; lange Texte werden über segmentweise Indizes gefunden und nur das passende Segment abgerufen. Die tiefen Originale (damalige Gedanken, Befehle, Schlussfolgerungen) bleiben lokal in einer Vektordatenbank und werden bei Bedarf geholt. Rauschfilter und Zeitkanal; kein Netzwerk (lokales Embedding-Modell), keine Änderung am offiziellen Kern.
Installation
dsh plugin --profile web add github:reaxlll/dsh-workspace-recollectionREADME
dsh-workspace-recollection 🧠
给 DeepSeek Harness 的跨会话长期记忆:按工作区隔离、跨会话共享。
DSH 的会话一关,上下文就散了。这个插件把「做过什么、当时怎么决定的、那个坑是什么」沉淀下来, 需要时再分层、按需地取回 —— 会话开始时只注入一行提示(告诉模型「你有记忆、去查」),用哪层取哪层。
核心是分层检索:想先看「大概有什么」就调 memory_index 拿目录(浅层要点,置顶在最前);条目很长时,中间还有一层
段级引索 —— 先定位到「哪一段」,只把那一段取回来;深层原文(当时的思考、命令、结论)
留在本地向量库里,真要细节时才取。实测一条 1.5 万字的记忆取回一段约 1200 字,省 92%~97%。
快速开始(安装插件)
以下命令都在你的命令行终端(PowerShell / CMD / bash 等)中运行。
# ① 前置要求:确认 Node.js 已安装
node -v
# ② 安装 DSH 启动器与 pnpm(已装可跳过;装完请重新打开终端)
npm install -g @deepseek-ai/dsh pnpm
dsh --version # 验证 dsh 命令可用
# ③ 安装本插件:--profile 填你实际在用的那个
# 桌面应用(Electron 版)→ desktop;dsh web → web
dsh plugin --profile desktop add dsh-workspace-recollection
装完重开桌面应用(dsh web 用户则重启 dsh web)才生效 —— 运行中的进程持有内存里的旧插件。
之后点会话头部的「🧠 记忆」(或设置页里的「记忆」)就能打开记忆面板。
兼容性:本插件在 dsh
0.2.0-rc.2下开发并测试(dsh --version可查看你的版本)。 建议使用相同版本;其他版本如遇问题欢迎反馈。
从源码安装(clone 本仓库后)
本插件是纯 JavaScript、无构建步骤 —— clone 下来就能直接挂载:
git clone https://github.com/reaxlll/dsh-workspace-recollection.git
dsh plugin --profile desktop add file:/path/to/dsh-workspace-recollection
改源码后重开应用即可(宿主半侧改动必须重启;浏览器半侧刷新页面即可)。
首次启动会自动装好嵌入模型
语义检索需要一个本地嵌入模型(Xenova/bge-small-zh-v1.5,512 维,ONNX 量化版约 23MB)。
不用你动手:插件启动时会检查它是否在位,缺了就自己从镜像(hf-mirror.com,失败自动回退
huggingface.co)逐个文件拉下来,下完自动加载 —— 实测 24MB 约 18 秒。
之后运行时零联网,也不调任何云端 API。
网络不通或下载失败也不会影响使用:memory_recall 自动退回关键词检索,其他功能照常;
面板状态条上的「加载模型」可随时重试。
💬 反馈与发版节奏
欢迎提 issue 与 PR —— bug、体验问题、优化想法都行。 可以随意对插件进行修改因为作者很可能没空维护,也对应了dsh那句,想要啥自己做(^_^) 但动手前请先翻一眼 提交记录, 确认不是已经修掉的同款问题。
- 小修不单独发版:不影响使用的小 bug 与优化会累计到下一个版本一起发。
所以「
main上已经修了」不等于「你装的这版已经包含」——latest没更新时,以main的提交记录为准。 - 报 bug 时请带上:插件版本、
dsh版本、操作系统、复现步骤(有日志或截图更好)—— 直接用 issue 模板 会把这些列好。 - 想改代码?先看 CONTRIBUTING.md:代码地图、三条硬规矩(纯函数要配单测 / 阈值必须用真实回放来定 / 约束行为要用代码而不是提示词)、常见改动配方,以及一份提 PR 前的检查清单。
插件功能
- 跨会话记忆:记忆按会话 header 的
cwd分桶 —— 一个工作区一份,这个工作区里的所有会话共享同一份记忆 - 分层检索:浅层要点目录(
memory_index,按需)→ 段级引索(定位到段)→ 深层原文(按需取整条),三层都有工具,够用就停 - 一行提示:默认只往 system prompt 里放一行「你有其他会话的记忆,通过 memory_index 查看目录,详细善用 memory_recall」—— 40 字换回「存在感」(旧数据升级时自动从零注入改过来);目录、要点、原文全部走工具按需取
- 时间轴按你的时区显示:首次安装时检测一次本机时区并记下来,会话时间、记忆日期、时间通道都按它格式化
- 噪音过滤:
memory_recall的结果自动过四路信号(绝对底线 / 硬特征 / 相对长尾 / 平局),跑题的别冒充「找到了」;被筛掉的永不静默丢弃,折成一行id + 分数留着你复查 - 时间通道:问「10月6日」「九天前」「昨天」这类问题时,按本地日历日直接筛出那天的条目,并给出涉及到的会话 id(可顺着读会话原文)
- 排序分层:深层分是靠正文块「撞」出来的,容易虚高 —— 只有要点分够高、或命中 query 的硬特征、或能被段级引索复核出「实词」的,才承认它的深层分
- 本地嵌入 · 零联网:
bge-small-zh-v1.5(512 维,ONNX q8)跑在本机,不调任何 API;加载约 35ms、编码约 2ms/条 - 模型自己装:启动时检测嵌入模型是否在位,缺了就自动从镜像拉取(约 23MB,实测 18 秒),下完自动加载
- AI 压要点:写完记忆直接走人,后台队列用
ctx.llm.stream把长正文压成一句话要点(也可自己给,或手写) - 会话导入:把本工作区的历史会话一键收进长期记忆(一条记忆 = 你当时说的话 + 思考原文 + 用到的工具 + 结论)
- 管理台:会话头「🧠 记忆」与设置页「记忆」两个入口打开同一块全屏面板 —— 状态条 / 按工作区浏览记忆 / 新增编辑删除置顶锁定 / 预览注入 / 检索测试 / Memory.md(预览 + 编辑手记区块) / 改工作区归属 / 重载数据 / 重建向量;面板本体是自包含页面
/api/session-memory/page,也能用系统浏览器开成独立窗口(拖到第二块屏) - 工作区改名自动认领:记忆按路径分桶,目录一改名就认不上了 —— 所以每个工作区另存一份跟路径无关的身份(
git remote+ 包名 作强指纹,顶层条目名作弱指纹),新旧匹配且旧路径已不存在时自动认领,连带搬向量、重写Memory.md;判不准就不搬(宁可视而不见,也不把两个工作区的记忆混在一起) - 不碰内核:只用官方插件 API(
systemPrompt.section/tools.register/webServer.register),前后端走/api/session-memory/*
分层结构
| 层 | 位置 | 角色 |
|---|---|---|
| 事实源 | $DSH_HOME/session-memory/memory.json | 每条 = text(深层原文,可长)+ summary(浅层一句话) |
| 索引 | <工作区>/Memory.md | 工作区里的镜像文件,写的是浅层要点;默认只注入一行提示(面板可切 hint / index / full / off);底部「手记」区块原样保留 |
| 向量 | $DSH_HOME/session-memory/vectors.json | 每条两份向量:s(浅层,检索定位)/ d(深层,兜底补齐) |
| 段索引 | 不落盘,运行时现算(lib/segments.js) | 把正文按 ## 切成段,用纯 CPU 的 BM25 关键词定位到段号 |
检索流程(为什么省 token)
memory_recall("那个编码的坑") → 浅层向量检索 → 噪音过滤 → 只回「要点 + id + 相似度」
↓ 条目很长时(清单里标了「长文」,上万字的会话导入都是)
memory_locate({ id, query: "zstd" }) → 段级 BM25 定位 → 只回「段号 + 小标题 + 字数 + 命中词 + 预览」
↓ 挑好段号
memory_detail({ ids, segments: [2, 7] }) → 只取这两段切片
浅层文本短、语义集中,定位更准也更省;深层只在真要细节时才付 token; 段级引索让「真要细节」也不必整条取回 —— 一个字都不多花。 浅层向量缺失时自动回退;嵌入不可用再回退关键词检索。
取用顺序(写进了工具描述与每次工具返回的末尾):按需逐层、够用就停 ——
① memory_recall 拿要点 → ② 要点不够就用要点里的关键词 memory_locate 拿段号、memory_detail({segments}) 取切片 →
③ 浅层和切片都还不够(要当时的思考、命令全文)才 memory_detail({ids}) 整条兜底。别为了「完整」把三层都调一遍。
九个工具
| 工具 | 作用 |
|---|---|
memory_write | 写记忆:text 写完整深层内容,summary 可选(省略则 AI 总结) |
memory_index | 目录:置顶在前、其余按更新时间,带分页 / 标签 / 期望词过滤 —— 不知道库里有什么时先调它 |
memory_recall | 浅层语义检索(带时间通道),只回要点 + id;自带噪音过滤(filter / min_score 可覆盖;被略去的折成一行 id+分数) |
memory_locate | 段级引索:在已命中的那条里按关键词定位段号,只回段号/小标题/字数/命中词/预览 |
memory_detail | 按 id 取深层完整原文;给 segments: [段号] 就只取那几段切片 |
memory_search | 关键词精确检索(不走向量) |
memory_forget | 删除(两份向量一并清;锁定条目删不掉) |
memory_import | 把本工作区的历史会话导入长期记忆(一条用户消息 = 一条记忆) |
session_read | 读历史会话原文/摘要的正门(不带 id 列会话;mode:"raw" 取指定轮次原文含思考) |
手工解压
$DSH_HOME/sessions/下的 zstd 会话文件已被插件在工具层硬拦(tools/pre-executedeny)—— 不是靠提示词自律,而是那条路走不通。要走正门用session_read。
时间轴与时区
时间戳在磁盘上一直是 ISO(UTC),给人看的那一层统一走 lib/localtime.js:
- 首次安装检测一次:插件第一次创建
memory.json时读一次本机时区(IANA id,例如Asia/Shanghai), 存进memory.json的timeZone字段。之后一直用它 —— 不会因为换时区设置就让历史时间整体漂移。 - 检测失败有兜底:拿不到 IANA id 时按当前偏移合成
UTC+08:00,永远有一个可用值。 - 哪些地方按它显示:
session_read的会话列表与轮次时间、记忆条目日期(memory_detail/memory_search)、 时间通道(memory_recall里问「九天前」)、Memory.md的「更新于」。 - 时间通道也按它算「今天」:问「昨天」时按你记下来的时区的日历日筛,而不是按 UTC 的日期。
- 想看当前生效值:
POST /api/session-memory/status的timeZone(stored/detected/label)。
为什么不在每次显示时现算:一条三个月前的记忆该按当时的钟点显示。 这条也是被实测教训出来的 —— 旧代码用
toISOString().slice(11, 16)显示时间, 结果本地 19:40 在界面上变成 11:40,差整整八小时。
检索噪音过滤
要解决的问题:memory_recall 原本是「排序后取 top-K」,没有任何过滤。
导入大批历史会话后,无关条目照样挤进前几名 —— bge 的相似度整体偏高(无关也能到 0.5+),
绝对阈值不可信;而单一分数也切不干净:实测同主题查询里,无关条目的分甚至高于相关条目。
做法(lib/noise.js,纯函数零依赖,可脱离 DSH 单测)—— 四路信号,从硬到软:
| 信号 | 判定 | 为什么 |
|---|---|---|
| ① 绝对底线 | top1 < 0.45 → 一条都不留,明说「没有明显相关的记忆」 | 纯噪音查询的 top1 实测落在 0.40 附近 |
| ② 关键词硬特征 | query 里的专有名词 / ASCII 词 / emoji,在要点或标签里一个都没命中 → 略去(分数多高都不留) | 跨主题噪音靠这个切,分数上看不出来 |
| ③ 相对长尾 | 低于 top1 × 0.88 且绝对差 > 0.08 → 略去 | 砍掉明显跟不上的尾巴 |
| ④ 平局 | 前四条挤在 0.05 跨度内 → 只留前三条 | 没有谁真正胜出时,后面的往往正是跑题的 |
两条反误杀的护栏(都是实测踩出来的):
- 硬特征只查要点 + 标签,不查正文:两万字的会话正文里随便提过一句「PowerShell」就算命中, 会把「配置某个工具」这种跑题条目留下来。
- 只在 query 给出两个以上硬特征时才开硬切:单个硬特征(如「emoji」)会误杀真相关的条目 (它讲了这件事,只是没写那个词)。另外硬特征命中的条目豁免③的长尾规则(主题对上号了,分低一点也留)。
可覆盖:memory_recall({ filter: "auto" | "off" | "strict", min_score: 0.5 }) ——
off 回到旧行为看原始排名,min_score 是显式下限(一律生效、不豁免)。
阈值集中在 lib/noise.js 顶部常量,调完跑 npm test(用例里的分数直接取自回放,是「阈值有没有被改坏」的回归网)。
排序分层:深层分要有「资格」
深层的排序分是正文分块取最大值得来的 —— 块越多,撞上一个高分块的期望越高, 于是长文天然占便宜,无区分度的查询也会被刷出虚高的分(实测里三条八竿子打不着的长文霸占了前三名)。
所以深层分现在要有资格才被承认,三条任意一条成立即可:
- query 的硬特征(专有名词 / ASCII 词 / emoji)命中了这条的要点或标签;
- 要点分本身够高(≥ 0.45,与绝对底线同语义:连底线都够不着 = 这条在要点层没有落点);
- 通过段级引索的二级验证 —— 用 BM25 在这条正文里复核,且命中的词里至少有一个是「实词」。
第 3 条的门槛是文档频率(词出现在多少条记忆里):中文 bigram 的泛词毫无区分度 (实测「什么」出现在 98 条里、「10」122 条、「日」60 条),拿「命中 ≥2 词」当判据时 8 条跑题条目里 7 条都能蒙混过关;限到「至少一个词的文档频率 ≤ 15」之后,跑题组 0/8 通过、 真相关组 6/6 通过。三条都不成立的条目会被降级(退回要点分排序,结果里标「要点弱命中」)—— 它不会被丢弃,但不再用正文撞出来的高分冒充相关。
段级引索层
要解决的问题:memory_recall 只能定位到「哪一条」,memory_detail 一给就是整条正文(上限 2 万字)——
记忆越长取一次越贵,而里面常常只有一小段是真正要找的。
做法(全部本地 CPU:零依赖、零联网、零模型、确定性;代码在 lib/segments.js,可脱离 DSH 单测):
| 步骤 | 做法 |
|---|---|
| 切段 | ## 小节是唯一可靠的语义边界 → 按标题切「节」;一节短于 900 字就整节成一段,长了才按 600 字在空行/句末断开,每片带 节名 (i/n) 序号;不足 40 字的碎屑节(如一行代码围栏)并入前节,标题写回正文保证信息不丢 |
| 抽词 | 中文 bigram + ASCII 词元 —— 不引 jieba;路径、命令、常量名原样保留(MAX_TEXT_CHARS、pnpm、/srv/app/deploy) |
| 打分 | 段级 BM25(k1=1.2, b=0.75);含 _ - . : / \ 的精确形态词加权 ×2.2(这类词命中率极高,也正是记忆里最常搜的东西) |
| 返回 | 只回段号 / 小标题 / 字数 / 命中词 / 命中位置的上下文片段(围绕命中的词取,不是固定截开头),正文一个字都不回 |
性能与效果:切段 + 全量定位在几百条量级只要几十毫秒(纯 CPU); 用每条记忆自己的要点当 query 回查,能全部命中到自己的正文段;1.5 万字的记忆取 2 段 ≈ 1200 字。
三个坑(都已处理):
-
空行不是硬边界:会话导入的「当时的思考」内部全是空行,按空行切会把它剁成几十个没名字的碎片 (实测 1.5 万字 → 64 段、段名全丢)。→ 先按标题切节,节内不打散。
-
段数超上限时必须合并尾部,不能截断:一份 1.2 万字的部署命令清单有几十个小节,截断会静默丢掉后半截 (实测覆盖率 81.8% → 91.5%)。另外覆盖率不到 100% 是正常的 —— 段里不含小标题行与
---分隔线, 标题信息在段号的 label 里。 -
预览不能固定截段开头:关键词常落在段的中后部(实测一条 532 字的段里,
font_*.png/click_font/ 路径全在中后段),而预览只截前 80 字,于是模型看到的是开头那句闲聊 —— 它据此判断「词碰上了、内容没对上」,直接跳过memory_detail,白白浪费一次已经成功的定位。 → 预览改成围绕最先出现的命中词取上下文(前 36 / 后 76 字,两端补省略号)。
边界:短条目(不足 900 字)整条就是 1 段,切片对它没意义 —— 直接用 memory_detail 取全文即可。
时间通道
「九天前我问了什么」「10月6日做了什么」这类问题,向量检索答不了 —— 记忆的各层文本里从来没有时间戳,日期型 query 在向量层没有任何落点(只会靠正文块零散撞词, 把语义相近但时间无关的长文刷上来)。
所以 query 里带日期或相对时间时,直接走结构化过滤:按本地日历日筛出那天的条目,按发生时间列出,
并把涉及到的会话 id 一并给出(顺着 session_read 就能读回那天的对话原文)。认这些形态:
| 形态 | 例子 |
|---|---|
| 绝对日期 | 2026-10-06、2026年10月6日、2026/10/6 |
| 月日 | 10月6日、10月6号(晚于今天则算去年) |
| 相对日 | 今天、昨天、前天、大前天 |
| N 天前 | 9天前、九天前、十五天前(中文数字也认) |
⚠️ 时间字段的可信度是分来源的:只有助手自己写的条目(
source.origin为agent/panel), 时间字段才代表「事件发生时间」;从旧环境导入的存档,时间字段是落库时间(那批条目全挤在导入那一天), 拿它做时间反查会把整批一起捞出来 —— 所以这类来源不参与时间反查。
AI 要点总结
- 可以不写 summary 直接写记忆:后台队列自动用
ctx.llm.stream压成一句话(实测 600~900ms)。 - 也可以直接给 summary(写入方刚做完事,最清楚要点)——状态记作
manual。 - 总结失败不阻塞写入:规则摘要兜底,状态标
failed,面板可一键重跑。 - 默认路由
deepseek-official / deepseek-flash,可在面板或memory.json的summaryProvider/summaryModel改。 - 面板「测试 AI 总结」一键验证路由与凭据。
会话导入
面板的「导入会话」按钮:列出该工作区的历史会话 → 勾选 → 预览 → 导入。
每条记忆 = 你当时说的话 + 思考原文 + 用到的工具 + 结论;深层原文直接存、不过 LLM(会话正文的量级不是模型能吞的),
要点交给 AI 总结队列。导入的条目带 source.origin === "import",和普通记忆各记各的「已导过」,重复导入走更新分支、不会翻倍。
注入策略(面板四档)
- 一行提示(默认,v2.7 起):只注入 40 字 ——「你有其他会话的记忆,通过 memory_index 查看目录,详细善用 memory_recall。」目录、要点、原文全部走工具按需取
- 关闭:完全不注入 —— 目录用
memory_index、要点与原文用检索工具按需取 - 索引:
Memory.md(浅层要点)+ ★ 置顶记忆的要点,每轮进 system prompt - 全量:所有条目的要点逐条注入
v2.6 曾经默认「关闭」,实测的代价是记忆没有存在感:模型看不到任何与记忆相关的字眼,
遇到「之前做过什么」时会直接去翻磁盘文件,而不是先 memory_recall。
v2.7 起改回「一行提示」:常驻只留一句提醒「你有记忆」,其余仍然按需取。
旧数据会在首次启动时自动迁移(v3 的 index/full → v4 的 off → v5 的 hint),只迁一次 ——
之后你在面板里改档位不会被再重置。
标了 meta: true 的条目(施工/状态记录之类,生来就会过期)只留在库里可检索,不进目录。
工作区改名怎么办(自动认领)
记忆是按路径存的,目录一改名,路径就变了。原来的结果是:新路径是一个空工作区,旧记忆还在库里躺着没人找。
现在每个工作区都有一份跟路径无关的身份:
- 强指纹 =
git remoteURL +package.json的name—— 一个仓库搬到哪都是它 - 弱指纹 = 顶层条目名(排序后取前 30 个)—— 没有 git 也没有 package.json 的工作区只能靠它
新建工作区之前先试着认领:
| 情况 | 行为 |
|---|---|
| 强指纹一致 且 旧路径已不存在 | 自动认领(连带搬向量、重写 Memory.md) |
| 弱指纹一致 且 旧路径已不存在 | 自动认领(记 migratedFrom,可追溯) |
| 旧路径还在 | 只提示,不搬 —— 可能是复制出来的副本 |
| 多个候选身份相同 | 一律不动,等你在管理台点名 |
判不准就不搬:两条工作区的记忆混在一起,比「暂时看不见」糟得多。手动搬家在管理台里(workspaceMove,合并而不是覆盖,按 id 去重)。
界面与管理台
| 入口 | 位置 | 用途 |
|---|---|---|
| 右上角「🧠 记忆」 | 会话标题栏 | 打开全屏记忆面板;徽章显示当前工作区的条数 |
| 设置页「记忆」 | 系统设置 | 同一个面板的入口 |
两个入口打开的是同一块全屏面板:左边所有工作区、右边条目管理,按 Esc 或点空白处关掉,会话还在后面没被卸载。
面板本体是一个自包含页面 GET /api/session-memory/page,用 iframe 载在应用内 —— 不跳系统浏览器。
这点是刻意设计的:跳浏览器依赖默认浏览器存在、依赖 127.0.0.1 不被防火墙拦、依赖用户认得 dsh-app:// 这种协议,
打包给别人用时任何一环断了都表现为「点了没反应」。想要真正独立的窗口(拖到第二块屏、跟对话并排),面板里点「独立窗口」——
由 host 侧 openConsole 起系统浏览器打开同一个页面。
面板里能做的事:
- 状态条:嵌入模型状态 ·
浅层 N · 深层 N / 总数· 待总结数 · 注入模式 · 「重建索引」「重新总结」「加载模型」「重载数据」 - 按工作区切换(跨工作区浏览,不用切会话):列表浅层要点为主,深层原文可折叠(带总结状态)
- 新增 / 编辑 / 删除 / 置顶 / 锁定;条目多了可以按标签筛
- 三个页签:预览注入 / 检索测试 / Memory.md(预览 + 编辑手记区块)
- 工作区管理:看身份指纹与认领记录、改归属(
workspaceMove)、一键重建向量
HTTP 接口
GET /api/session-memory/page—— 管理台页面(自包含 HTML,不依赖 DSH 前端运行时;改assets/console.html刷新即生效,不用重启)POST /api/session-memory/<method>—— body{ sessionId, input },返回{ ok: true, value } | { ok: false, error: { message } }
status clientReady clientError list create update remove setEnabled setInjectMode
clear setBudget setSummaryRoute setTimeouts reindex resummarize recallTest warmup testSummary
readMemoryFile writeManualBlock
importList importPreview importRun importProgress(历史会话导入)
workspaceEntries workspaceMove workspaceRemove claimInfo refreshIdentity(跨工作区 / 认领)
reloadStore(从磁盘重读记忆与向量,不重启) openConsole(起系统浏览器开管理台) diag(排障:请求来源与本机地址)
嵌入模型与向量化
检索不靠关键词,靠语义向量 —— 所以「那个编码的坑」能找到「PowerShell 写出 BOM」。这一层是怎么跑的:
模型:Xenova/bge-small-zh-v1.5(512 维,ONNX q8,约 23MB),落在
$DSH_HOME/session-memory/models/Xenova/bge-small-zh-v1.5/。首次启动自动拉取,之后零联网。
实测:加载 ~35ms、编码 ~2ms/条、上千条 × 512 维暴力余弦 ~1ms —— 这个量级不需要向量数据库,
JSON + 内存暴力检索就够,还省掉一个原生依赖。
写入记忆时(memory_write,或会话导入)—— 同一条记忆编出两种粒度的向量:
- 要点(summary)单独编码成一个浅层向量
s:它短、语义集中,就是这条记忆的「墓志铭」; - 正文(text)按 380 字分块(尽量在换行 / 句号处断开),每块编码成一个向量,单条最多 24 块,
到上限时末块承载结尾,保证头尾都被索引 ——
vectors.json里d是一串 base64(每个元素是 512 维 float32 的原始字节)—— 比写成数字数组小 37%、落盘快约 4 倍。
取出记忆时(memory_recall):
- 把 query 编码成同一个 512 维空间里的向量;
- 对每条记忆算两个分:浅层分 = 与要点向量的余弦;深层分 = 与它全部正文块的余弦里最大的那个;
- 取更相似的那一层作为排序依据 —— 浅层通常赢(定位准又省),只在正文后半段出现过的细节靠深层兜住; 深层分要有资格才被承认(见上「排序分层」),否则退回要点分;
- 排序后过一道噪音过滤,只把「要点 + id + 相似度」交回去 —— 向量本身不占 token,
真要原文再用
memory_locate/memory_detail分层取回。
一句话:向量决定「哪一条、哪一段值得看」,token 只花在真正要看的那部分上。
超时:AI 总结 45s(summaryTimeoutMs)、嵌入 120s(embedTimeoutMs),范围 3s~10min。
总结超时用 AbortController 真中断;嵌入超时只是不再等待。
降级链(每一层都兜得住,不会因为嵌入不可用就瘫掉):
模型缺失 / 加载失败 → memory_recall 退回关键词检索;某条浅层向量还没算出来(刚写入、要点待总结)→
该条只按深层分参与排序;单独一条完全没向量 → 它不会进 memory_recall,但 memory_search 仍找得到。
兼容性
- DSH 版本:在
0.2.0-rc.2下开发并测试;0.1.x不兼容(插件 API 有变动) - 操作系统:Windows / macOS / Linux 均可(纯 JS + N-API 预编译的 onnxruntime,与平台 Node 的 ABI 无关)
- Node.js:
>= 22.12.0(node:sqlite等新 API 的底线;实际运行在 DSH 的进程里) - 浏览器:面板走 DSH 官方 slot 机制,Chromium 内核浏览器均可
- 体积:插件自带
@huggingface/transformers+ onnxruntime 约 150MB(随 profile 的node_modules走), 装的时候 pnpm 报Ignored build scripts: onnxruntime-node可以无视(N-API 预编译,ABI 无关,实测可用); 嵌入模型额外约 23MB(首次启动自动下载到$DSH_HOME/session-memory/models/) - 规模上限:单工作区 500 条;深层正文上限 20000 字;浅层要点上限 200 字
数据安全
memory.json / vectors.json 里都记着版本号,读入时按版本决定行为 —— 下面三种情况都不会让你的记忆悄无声息地消失:
| 情形 | 行为 |
|---|---|
| 版本一致 | 正常读写 |
| 数据版本比插件更新(先用了新版插件,又换回旧版) | 只读保护:照常可读可检索,但绝不写回 —— 写回去会丢掉新字段;日志与面板都会说明原因 |
| 数据解析失败(文件损坏、手工改坏) | 先把原文件自动备份到 backups/,再按空库启动 —— 而不是静默显示「没有记忆」 |
| 版本更旧且备有迁移步骤 | 迁移前自动备份,然后逐级升级 |
另一个实例正在写(同一个 $DSH_HOME 开了两份 DSH) | 单实例写锁:拿不到锁就进只读保护 —— 两边都是「全量读改写」,同时写会互相覆盖。想写入请先关掉那个实例;确认它已异常退出的话,删掉 write.lock 再重启即可 |
备份落在 $DSH_HOME/session-memory/backups/,文件名形如 memory.json.2026-10-16T13-18-41.bak(可排序即时间序),每个数据文件最多保留 5 份。
写锁落在 $DSH_HOME/session-memory/write.lock;崩溃残留的锁会被自动接管(先看持有者 pid 还在不在,再看时间戳兜底)。
迁移链目前只有一条 vectors v2 → v3(向量从「数字数组」改成 Float32 base64:体积 -36%、落盘 CPU -78%);下次再改数据结构时,往 lib/migrate.js 的链里加一条函数,老用户打开插件就会自动升级。
已知边界
- 只按工作区(会话 header 的
cwd)分桶,没有跨工作区全局层 - bge-small-zh 的相似度整体偏高(无关也能到 0.4+):排序可信、绝对阈值不可信 —— 这也是噪音过滤要四路信号的原因
- AI 总结是额外一次模型调用,失败降级到规则摘要,不会堵住写入
- 记忆是单个 JSON(
memory.json+vectors.json),没有增量存储;几百条量级无感,上千条需要重建索引 - 工具层守卫只在全新会话的第一个动作出手:会话里已经有过对话轮次就不拦(读不到会话结构时按「有上下文」处理,宁可少拦)
- 工作区认领靠指纹,指纹只能证同、不能证异:同一个仓库的两份克隆会被判成「旧路径还在」而不搬,这种情况用管理台的「改归属」
- 中英混排会让段级 BM25 的长度归一化失真:抽词对英文按词切、对中文按 bigram 切,同一段字数下 token 数差好几倍 (实测真实数据里 79% 的多段记忆,段间 token 密度差 ≥2 倍,中位数 3.58 倍),于是英文段被系统性高估。 试过改用「字符数归一化」:能修好语言偏差,但对长段的惩罚更重,净效果打平 —— 729 个真实查询实测,91.2% 的 top-1 不变,变化的 8 个里新旧各胜一半。所以没改; 真要解决得按语言分组各自归一化,等真实使用中出现具体坏 case 再说。
项目结构
├── lib/
│ ├── index.js 宿主半侧入口(插件定义 + 9 个工具 + HTTP 路由 + 注入)
│ ├── client.js 浏览器半侧(记忆面板界面)
│ ├── store-logic.js 存储层纯逻辑:排序 / 超限裁剪(置顶必须留下)/ 批量删除 / 工作区键
│ ├── sessions.js 历史会话的读取与切段(会话导入与 session_read 的地基)
│ ├── noise.js 检索噪音过滤 + 排序分层
│ ├── segments.js 段级引索:切段 + BM25 定位
│ ├── lexical.js 词法通道:中文 BM25 + RRF 融合(与向量通道并列)
│ ├── rerank.js 交叉编码重排(可选,默认关)
│ ├── discriminants.js 判别串抽取与命中加分
│ ├── supersede.js 覆盖链:新条目取代旧条目,旧的不删只降权
│ ├── workspace-id.js 工作区身份指纹(强 / 弱,跟路径无关)
│ ├── workspace-migrate.js 改名认领判定(够不够格搬、搬哪去)
│ ├── timehint.js 时间提示解析
│ ├── localtime.js 时区检测与时间显示(时间轴都走这里,不用 toISOString)
│ ├── lock.js 单实例写锁的状态判定
│ ├── migrate.js 存储版本判定、迁移链、备份命名与保留
│ ├── vector-codec.js 向量 Float32 ↔ base64 编解码、v2→v3 迁移
│ └── model.js 嵌入模型清单与下载地址
│ (以上除 index / client 外都是纯函数模块:零依赖、可单测)
├── tests/ node --test:245 条用例(分数取自真实回放)
├── types/ 外部模块的类型声明(供 tsc --checkJs 使用)
├── assets/icon.svg 插件图标
├── assets/console.html 管理台页面(自包含 HTML,改完刷新即生效)
├── assets/mindmap.svg 工作原理思维导图(README 用,图上文案是手写的)
├── assets/architecture.svg 会话生命周期与检索判断链(README 用)
├── cordis.patch.yml 包自带 patch(安装后自动进层栈)
├── CONTRIBUTING.md 贡献指南(改代码前先看)
├── .github/ issue / PR 模板 + 两个 CI job(单测 / 类型检查)
├── tsconfig.json tsc --checkJs 的配置(拿真类型校验 JSDoc)
└── package.json
数据落点:$DSH_HOME/session-memory/(memory.json / vectors.json / models/),
外加每个工作区根目录的 Memory.md。
开发
npm test # 245 条单测:零依赖、几百毫秒(不需要装任何东西)
npm run typecheck # tsc --checkJs:拿真类型校验 JSDoc(需要先 npm install)
CI 有两个 job:单测(Node 22 与 24 各跑一遍)与类型检查。 改代码之前请先翻 CONTRIBUTING.md —— 里面写了代码地图、三条硬规矩(纯函数必须配单测 / 阈值必须用真实数据回放 / 约束行为要用代码而不是提示词),以及最容易踩的坑。
许可
Ähnliche Plugins
hindsight (coding-agents)
vectorize-io/hindsight
OpenViking (dsh-memory-plugin)
volcengine/openviking
MemOS (memos-local-plugin)
memtensor/memos
ReMe (dsh)
agentscope-ai/reme