dsh-cross-workspace-memory
Verifieddsh-cross-workspace-memory · v0.1.2 · MIT
Cross-workspace long-term memory for DeepSeek Harness: one machine-local store shared by every workspace, a compact index injected at session start, and a searchable tool for the details.
Install
dsh plugin add dsh-cross-workspace-memory Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-cross-workspace-memory · 跨工作区长期记忆
English · 简体中文
换一个工作区,不用再从零介绍一遍自己。
dsh-cross-workspace-memory 给 DeepSeek Harness 加一层本机共享的长期记忆:偏好、事实、项目档案(路径 / 命令 / 约定)、决策理由(选 A 不选 B 及当时的约束)、上次做到哪。所有工作区共用一份存储,条目带项目标签,检索时优先本工作区。
一、它为什么长这样(先说一件反直觉的事)
这个插件的起点是「换个工作区就丢记忆,缓存命不中,白花钱」。实测之后结论要分开看,而分开看决定了插件的形状:
| 现象 | 实测 | 含义 |
|---|---|---|
| system prompt 跨工作区是否相同 | 相同。里面没有工作区路径;cwd 是靠 runtime-context 以历史消息形式注入的,位置在用户首条消息之后 |
缓存前缀本来就是跨工作区可复用的,这部分没有损失 |
| 换工作区真正丢的是什么 | 对话里攒下的知识与状态:这次踩了什么坑、为什么这么定、改到哪了 | 这才是要补的东西 |
| 上下文块什么时候进历史 | 宿主发现上下文文本变了,就把 runtime-context 再追加一条(旧的不删)。实测 16 回合的会话:time-context 追加 14 次、runtime-context 追加 3 次 |
追加在尾部,前面整段的缓存照旧命中;但每追加一次,那段文本就要多付一次 |
由此得到三条设计约束,写死在 lib/store.js 与 lib/index.js 的注释里:
- 注入必须放在 prompt 尾部(
systemPrompt.context,order 9000,排在runtime-context/skill-catalog/time-context之后),绝不碰 system prompt 头部。 - 注入文本必须确定性:同一份存储状态渲染出的字节永远相同,只允许绝对短日期(
10-19),禁止「3 天前」「刚刚」这类会自己漂移的措辞。 - 注入文本在一次会话里必须基本不动:所以自动记的进度条目里,本会话自己刚写的那几条不进索引(它们就在对话里,对模型零信息量,却会让文本每轮变一次),进度条目在索引里也最多占 3 行。
违反任意一条,插件就会变成它本来要消灭的那种浪费。
二、三种喂回模型的方式
1. 会话开头:注入一份精简索引
一行一条,带类别、项目、日期:
<memory 跨工作区记忆 · 3 条 · 本机所有工作区共享>
跨工作区共享的长期笔记;细节用 memory 工具检索(索引只是摘要,别凭它猜细节)。
- [项目] plugin · dsh-cross-workspace-memory 插件:存储在本机 ~/.dsh/memory/memory.json(10-19)
- [决策] plugin · 选择 JSON 文件而非 storageDomain:storageDomain 需要 zod 做值校验(10-19)
- [进度] plugin · 装好插件并在 desktop profile 里验通(10-19)
提示:memory search / save / update / forget / list / diagnostics
- 预算默认 1600 字符(约 600 token),超出按类别权重截断 ——
preference>decision>project>fact>todo>activity。 - 空库时整块不注入,一个字都不花。
- 会话中途写入的新记忆不会挤进当前会话的索引:本会话自己写的进度条目被排除,其余条目也是在会话开始时就渲染好的——所以要读细节还有工具。
2. memory 工具:按需检索全文、读写
| 你想干什么 | 参数 |
|---|---|
| 回忆「上次做到哪 / 当初为什么这么定」 | {"query":"打包 为什么 用 electron"} |
| 记一条 | {"text":"构建用 pnpm build;打包走 electron-builder --dir 先看结构","kind":"project"} |
| 改一条 | {"id":"m3f8a1c07","text":"..."} |
| 删一条 | {"action":"forget","id":"m3f8a1c07"} |
| 看看都有什么 | {"action":"list","kind":"decision"} |
| 自检注入是否生效 | {"action":"diagnostics"} |
kind 支持英文与中文别名:preference/偏好、decision/决策、project/项目、fact/事实、todo/待办、activity/进度。
检索是中文整句友好的:英文按词切,中文按二字组,所以「打包为什么慢」能命中「打包用了 electron-builder」这类条目,不需要精确措辞。
3. 自动捕获:每轮结束记一条进度
每个 turn 结束写一条 activity:「用户这句话的第一句」+「用了哪些工具、各几次」+「碰了哪些文件」。于是新开一个工作区,第一条索引就写着上个工作区做到哪。
- 每个项目只保留最近 3 条(
keepActivities),不会把索引撑爆。 - 子代理(subagent)会话默认不记,避免噪声。
- 落盘有 1.5 秒合并窗口(同一毫秒内多次写入只落一次)。
自动捕获记的是**「做了啥」,不是「学到啥」**。真正的结论("这个 API 的返回值不是 lossless JSON","选了 A 因为 B 在沙箱里起不来")请用 memory save 显式写 —— 这类判断只有模型自己在场时知道。
三、安装
plugin_manager install_bundle target = dsh-cross-workspace-memory
从 npm 拉包。装完新开一个会话,索引就会出现在开头。
为什么包名叫 dsh-cross-workspace-memory
dsh-memory 这个名字在 npm 上已被占用(bbnopromo 于 2026-08-13 发布了同名包,用途类似:SQLite FTS5 + 三个工具 + recall 段落)。为了不撞名、能发布,本包改用现在的名字;但 bundle 的 id 保持 dsh-memory 不变,所以从旧名升级是原地替换,配置和记忆文件都不用动。
打包与分发(维护者)
node test/store.test.mjs # 17 pass / 0 fail
npm pack # → dsh-cross-workspace-memory-<version>.tgz(10 个文件,约 42 kB)
# 校验:解包后在 package/ 里再跑一次测试,确认 tarball 自包含
tar -xzf dsh-cross-workspace-memory-0.1.2.tgz -C /tmp/x && (cd /tmp/x/package && node test/store.test.mjs)
npm publish --dry-run # 先看清单
npm publish # 本包 unscoped,默认 public;registry 为官方源
npm view dsh-cross-workspace-memory version dist.tarball # 发布后核对
新包名第一次发布会先看到一个占位版本。 registry 会先建
0.0.0-stage(packument 里stub: true,README 是 "Temporary package placeholder for staged publishing"),真实版本进入审核,通过后自动转正并把latest指向它。本例从npm publish到npm view … version能查到0.1.0隔了约 3 分钟,期间只看得到0.0.0-stage—— 别当失败处理,更别急着改版本号重发。若长时间停在占位版,用npm stage list看有没有待批的 staged release(npm stage view / approve / reject <stage-id>)。
发版顺序建议:改 package.json 的 version → 更新本节数字与 CHANGELOG(如有)→ 测试 → npm publish → git tag vX.Y.Z && git push --tags。readme 会随包一起发布到 npm 页面,所以先改 README 再 publish,否则 npm 上看到的是旧文案(npm 不允许覆盖已发布版本,改文案得再发一个补丁版本)。
2FA 会让无 TTY 的发布直接失败。 账号开了两步验证(
auth-and-writes)时,npm publish必须过一次交互式验证:在真终端里它打印认证链接或提示输入 6 位码;在 CI / 管道里则直接报EOTP,而且链接会被打码成***(防泄漏),连复制出来代点都做不到。两条可行路:npm publish --otp=<6位码>,或者建一个勾了 Bypass 2FA 的 granular token(注意:classic token 自 2025-11 起已不能创建、旧 token 已全部吊销;带 bypass 的 GAT 目前仍能直接发布,npm 计划 2027-01 收回这个能力,之后应改用 trusted publishing / staged publishing)。
也能发布到 GitHub Packages(只作镜像,安装方同样需要 token):
# 包名需改为 @<github账号>/dsh-cross-workspace-memory
npm publish --registry=https://npm.pkg.github.com
四、配置项
写在 cordis.patch.yml 的 config 里,全部可省略:
| 键 | 默认 | 说明 |
|---|---|---|
dir |
~/.dsh/memory |
存储目录(DSH_HOME 有值时跟它走) |
capture |
true |
是否自动记录每轮进度 |
maxIndexChars |
1600 |
会话开头注入的字符上限 |
keepActivities |
3 |
每个项目保留几条进度 |
maxItems |
400 |
总条目上限,超了先删最老的进度条目,再删低权重类别 |
五、存储格式
单文件 ~/.dsh/memory/memory.json:
{
"version": 1,
"items": [
{
"id": "m3f8a1c07",
"kind": "decision",
"text": "选择 JSON 文件而非 storageDomain:它需要 zod 做值校验",
"tags": ["dsh", "storage"],
"project": "D:/dshproject/plugin",
"projectName": "plugin",
"created": 1761000000000,
"updated": 1761000000000,
"source": "manual"
}
]
}
- 写入是 tmp + rename 的原子替换,不会写坏。
- 文件坏了(JSON 解析失败)不会让插件崩:
loadError记下来,工具照常回话。 - 多实例(同时开两个 dsh)各自持有内存副本,下次写入前先重读文件,内容变了就按
id合并,updated大的胜出;删除不传播 —— 单机单人的场景下,这比加锁便宜得多。这里刻意不用 mtime 判断变化:时间戳分辨率不足时连续两次写会拿到相同 mtime(Windows 上就踩到过),按内容比才可靠。
六、已知边界
- 索引不是实时的:本会话里
memory save写进去的东西,要等宿主重新装配上下文才可能出现在索引里(通常要新开一轮/新会话);当前会话直接用memory search读,别等索引。同理,本会话自己的进度条目被排除在索引外 —— 这是刻意的(见第一节约束 3)。 - 只记本机会话:不跨机器同步。想同步就把
memory.json放进自己的同步盘,并把dir指过去。 - 中文检索按二字组:够用但不是分词,太短的查询(一个字)会命中偏多。
- 索引是摘要:故意只放一行一条。凭索引猜细节是这类插件最常见的误用 —— 工具描述里已经写明「索引只是摘要,要细节就 search」。
- 改完代码要重载:
plugin_manager set_plugin target="include:dsh-memory" enabled=false再true(只改lib/*.js的话,热重载不会自己发生;ESM 模块有缓存)。
七、开发
node test/store.test.mjs # 17 个用例,约 100ms
别用
node --test:本机沙箱下node:test默认走 pipe 子进程,会spawn EPERM。
lib/store.js—— 纯逻辑,零宿主依赖(可脱离 DSH 单测)。lib/index.js—— Host 侧:注入 + 工具 + 自动捕获。@deepseek-ai/dsh-tools只能由 DSH Host 进程解析,纯 node 进程 import 不了,所以测试只覆盖 store。tools/session-stats.mjs—— 解压~/.dsh/sessions/下的多帧 zstd 日志,统计各会话的回合数与上下文追加次数(本文第一节那些实测数据就是它跑出来的)。
八、延伸阅读
- 给 DeepSeek Harness 做一层跨工作区记忆 —— 完整的过程记录:怎么用会话日志证伪「换工作区就丢缓存」、注入位置为什么决定生死、6 个踩过的坑、以及发布时撞的三个钉子。
九、许可
MIT © 2026 Urtaome0211
选它的理由很实际:这是个零依赖、几十 KB 的小工具,MIT 是 DSH 插件生态里最常见、也最不给人添麻烦的选择——随便商用、随便改、闭源再分发都行,只要保留版权声明;真有人想把它并进 GPL 项目,也仍然兼容。如果要拿去做什么,不用先问我。
十、贡献
欢迎提 issue 和 PR。两条硬约束请先看一眼,它们不是风格偏好,是设计的根:
- 不许把注入挪到系统提示词或用户消息前——前缀一变,prompt 缓存全废,插件存在的意义就没了。
- 注入文本必须逐字节确定(绝对日期、稳定排序、排除本会话自己写的内容)。这条靠单测兜着:
test/store.test.mjs里有「两次渲染完全相同」和「不含相对时间措辞」两个用例,改renderIndex时它们会先叫。