带纵深加固的 dsh 长期记忆插件。两层存储(项目级 `.dsh-memory/` 与用户级 `$DSH_HOME/memory/`),每层就是一个可手改、可 diff 的纯 JSON 文件,条目与标签均有上限。写入原子(唯一临时文件 + rename,旧版本进入有上限的 history/ 备份),跨进程由带过期回收的 per-tier 锁串行化;磁盘 revision 校验拒绝丢失更新,绝不静默覆盖其他写入者。记忆文本以显式 untrusted-data 边界包装(伪造包装标签写入时剥除)以削弱记忆内容中的提示注入,加载时校验拒绝越界存储,tier 目录外的符号链接逃逸以 MEMORY_STORE_ESCAPE 拒绝。不做内容启发式过滤、不加确认步骤,无头自动巩固流程不受影响。安装:dsh plugin --profile web add github:RocoFish/deepseek-memory。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:RocoFish/deepseek-memory
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 中文
DeepSeek Harness 的长期记忆 插件,以可安装的社区 bundle 形式交付。
这里的"记忆"是少而精的沉淀,不是对话记录索引:库里只存有限条关于用户和项目的 短事实,并且整库注入每一次请求。正是这个"有界"消掉了检索问题——不需要 embedding 服务、不需要排序、也不会在会话历史里留下陈旧快照。
记忆库同时也是一个信任边界:库里的文本是模型写入的,之后又会读回提示词。 本插件对此是认真的——写入原子化且加锁,记忆文本以"数据"而非"指令"的身份注入, 越界或伪造的内容被拒绝而不是悄悄截断。详见下文安全模型。
功能
| 部件 | 行为 |
|---|---|
| 两层记忆 | user($DSH_HOME/memory/)记"这个人是谁";project(<cwd>/.dsh-memory/)记这个仓库的约定与决策。 |
| 工具 | memory_add、memory_update、memory_delete、memory_list、memory_search、memory_status。 |
| 注入 | 一个 systemPrompt 区段,其 text 是每次装配时求值的 provider。不往会话历史里写任何东西。 |
| 轮末整理 | 由一个受限 child agent 只通过记忆工具审阅刚结束的那一轮。 |
| 明文存储 | 库是可读 JSON,可手改、可 git diff;每次改动保留上一版。 |
安装
dsh plugin --profile web add github:RocoFish/deepseek-memory
没有构建步骤,也不需要你放行任何脚本——因为本仓库把编译产物 lib/ 提交进了
版本控制,从 git 源安装直接拿到编译好的插件。这是一个有意的取舍:把构建产物
放进版本控制并不常见,代价是维护者每次推送前要重新构建一次;换来的是每个使用者都不必先读
一遍 pnpm 的构建脚本报错、再手工编辑 pnpm-workspace.yaml。一个要给别人装的插件,安装
就得是一条命令。
也就是说,本包刻意不声明 prepare 脚本。 pnpm 判断一个 git 依赖的构建脚本是否需要
放行,靠的是 package.json 里有没有这个条目——在执行之前判断,且与里面写了什么无关。
所以一个"lib/ 已存在就直接退出"的条件式 prepare 照样会要求放行,这一点是实测出来的,
不是推断。真正能去掉这一步的做法就是:提交 lib/,并删掉那个条目。prepublishOnly 仍然
会在发布 npm 包时构建,那条路径不影响任何使用者。
贡献者克隆仓库后没有 lib/(也可以自行删除),用 npm install && npm run build 构建即可。
装完重启该 profile。
社区收录
DSH 的社区插件索引就是 GitHub 主题
dsh-plugin——"the dsh-plugin topic gathers
third-party plugins that extend it"。在仓库上设置这个主题即可出现在索引里;npm 的
keywords 里也列了 dsh-plugin,两条路都指向这里。
配置
每个键都能在 profile 的 cordis.patch.yml 里针对 memory 这一行覆盖:
- id: memory
config:
maxEntries: 80
consolidation: always
| 键 | 默认 | 含义 |
|---|---|---|
user |
true |
启用用户级记忆层。 |
project |
true |
启用项目级记忆层。 |
maxEntries |
50 |
每层最大条数。 |
maxEntryChars |
500 |
每条最大字符数。 |
injectChars |
4000 |
单次请求注入的最大字符数。 |
tools |
true |
注册记忆工具。 |
consolidation |
gated |
off、gated 或 always。 |
minTurnsBetweenRuns |
1 |
同一会话两次整理之间的最小已结束轮数。 |
keepHistory |
true |
在 history/ 里保留上一版。 |
consolidationSubagentProvider |
in-process |
整理 child 运行的 subagent provider 名。注意这是 provider 名,不是模型路由。 |
consolidationRouteProvider |
— | 退化路径的 provider 路由,仅在没挂 subagent provider 时使用;省略则复用会话自己记录的路由。 |
consolidationRouteModel |
— | 与上面配对使用的模型 id。 |
consolidationTimeoutMs |
120000 |
单次整理运行的墙钟上限。 |
整理是怎么工作的
每一轮都花一次模型调用太浪费——绝大多数轮次里没有任何值得长期保留的东西。闸门是一段
本地、确定性的扫描,只认真正会改变长期记忆的情形:明确要求记住、纠正、长期偏好、
决策、约束。它故意偏向漏触发:漏掉的一次整理会被之后相关的一轮补上;而一个过度
积极的整理器最终会开始编造记忆。想要"每轮必整理"的字面行为,设
consolidation: always。
闸门放行后,整理以一个受限 child agent 运行:
toolFilter: { allow: [memory_*] }把其余所有工具从 child 的提示里移除,并在 执行层拒绝——所以无论对话内容怎么诱导,它都读不到也写不了你的仓库;persona给它一份"只管整理记忆"的职责说明,仅对这个 child 遮蔽部署 persona;outputSchema让它返回一份结构化回执;- 它不继承父会话历史,所以整理永远不会撑大主对话的上下文。
child 通过本插件自己的工具写记忆,所以每一条上限和每一条纠错文案照旧生效。这也正是 插件不相信 child 自我报告的原因:它比对运行前后的库 revision,把"声称改了但库 没动"当作失败。
当部署没有挂载 subagent provider 时,插件退化为一次直连模型请求 + JSON 改动集,由插件 逐条校验、限制单次条数、按层原子应用。child 路径严格更好;退化路径的存在是为了让没有 subagent 的部署也能整理。
安全模型
长期记忆插件必须把"记忆本身成为攻击面或损坏源"的情况挡在外面。main 上目前
成立的保证:
- 抗注入包装。每条记忆文本注入时都被包在显式的
<untrusted-data>边界内, 并带有"这是数据,不是指令"的头部;伪造的包装标签在写入时即被剥除——记忆条目 无法自我升格为系统指令,被记住的内容永远不会被当作命令执行。 - 拒绝而非截断。加载时的校验对越界存储(条目数量/大小、标签数量/长度)直接 拒绝,而不是悄悄保留一个坏掉的前缀。
- 原子化、串行化的写入。提交运行在 per-tier 锁文件之下(陈旧锁 30 秒回收), 锁内复检磁盘 revision,写盘用唯一临时文件 + rename——一次被打断的写入绝不会 留下半个文件。
- 拒绝丢失更新。如果本会话加载之后另一个进程提交过同一层,写入会以
MEMORY_STORE_CONFLICT被拒绝,而不是静默丢掉对方的条目。 - 一切有界。history 备份按数量(20 个)和字节(5 MiB)双重封顶;资源型配置 项硬性钳制到上限。
- 刻意没有的:内容启发式的注入过滤(可被绕过,且贩卖虚假安全感),以及用户 确认步骤(整理器本就设计为无头自动运行)。
存储格式
{
"format": 1,
"tier": "project",
"revision": 3,
"updatedAt": 1790000000000,
"entries": [
{
"id": "m1a2b3c4",
"text": "构建用 tsc,不用打包器。",
"tags": ["build"],
"createdAt": 1789990000000,
"updatedAt": 1790000000000,
"origin": "consolidator"
}
]
}
id 由文本派生,所以同一个事实重复添加是幂等的,改写也保留身份。format 比当前
构建新的文件会被拒绝而不是猜着读。
项目层是给用户在自己的仓库里提交用的;不想共享就往项目的 .gitignore 里加
.dsh-memory/。
设计说明
有三条约束塑造了这个实现,改动前值得先知道。
1. 没有万能的 plugin 消息来源。 MessageSourceMap 是可合并扩展的联合类型,
每个生产者在自己的模块里声明自己的 kind。写 { kind: 'plugin', plugin: 'x' }
——旧版本接受的形状,而且在 harness 自己的测试夹具里到处都是——在 0.2 下通不过类型
检查,且在执行运行时强制的版本上会在日志边界抛
format v4 message requires a producer-owned source kind。本插件在
src/source.ts 里声明自己的 kind。而注入刻意完全不用消息注入:
systemPrompt 的 provider 不可能在历史里留下陈旧副本。
2. 轮次边界是 agent/status → idle,不是 agent/turn-stopping。 后者是
serial 且被 await 的监听器,在那里整理会给每一轮都加上模型延迟。agent/status
是 emit,所以记忆工作发生在轮次已经关闭之后,没有任何东西在等它。
两层记忆的位置从哪里来
项目层取自会话自己记录的目录:session/event 的载荷里带着 header,其中 cwd 是经过
校验的绝对路径,也是唯一永远正确的来源。它通过 getter 按需解析,所以宿主切换项目时不用重启
插件也会跟随,并丢弃过期缓存;apply 时不缓存任何东西。
两个实测事实让这是唯一站得住的选择。第一,ctx.get('cwd') 在任何试过的宿主上都不是服务
——即使服务已注入也返回 undefined,所以那行查找从一开始就是死代码。第二,process.cwd()
描述的是宿主怎么被启动的,不是哪个项目打开着:同一个打包应用,一次启动得到
D:\WSL\DSH Workspace,另一次得到它自己的 profile 目录。信任它会把项目记忆静默写进应用
自己的配置目录——在项目里看不见,在别处又具有误导性。
用户层按下面的顺序解析,实际用到的来源由 memory_status 报告:
| 顺序 | 来源 | 为什么 |
|---|---|---|
| 1 | DSH_HOME |
直接命名 harness home |
| 2 | DSH_PROFILE_DIR 上溯两级 |
应用公布的是 <home>/profiles/<name>,home 在上两级 |
| 3 | USERPROFILE + .dsh |
Windows 标准 home 加 harness 自己的目录名 |
| 4 | HOMEDRIVE + HOMEPATH + .dsh |
路径被拆开的 shell |
| 5 | dshHome 服务 |
以服务形式而不是环境变量公布的宿主 |
| — | 否则禁用 | 绝不回退到工作目录 |
第 2–5 条存在的原因是桌面应用的宿主主进程不携带 DSH_HOME:从同一个应用里派生的沙箱
shell 有这个变量,而宿主自己的启动环境没有,于是工具在 GUI 里报告该层缺失。回退到工作目录
依然被拒绝——那会把长期个人事实散落到宿主恰好启动的目录里,包括插件自己的检出,从而可能
被提交进仓库。
两层遵循同一条原则:定位不成立的层保持关闭,并明说。memory_status 会显式报告被关闭
的层,而不是省略它——否则"这一层缺失"和"这一层是空的"在输出里完全一样。
3. 插件在运行时是自包含的,所以 SDK 只是类型依赖。
defineTool、参数 schema 编译、参数校验、createUserMessage、输出 schema 检查,全部在本包
内实现,而不是 import 进来。SDK 包仍然声明着——作为 peerDependencies 表达版本一致性、作为
devDependencies 供类型检查和等价比对测试使用——但 lib/ 里没有任何一处 import 它们,这正是
profile 安装能工作的前提。详见下文"产物自包含"。
4. ctx.get 只在 apply 时读一次。 与其说是要绕开的约束,不如说是要记住的事实:解析器
只读一次服务,所以插件需要的每个服务都必须列进 inject,不可能之后再发现。
结构接口 + 编译期一致性校验
插件对自己通过 ctx 触及的服务(SubagentConsolidator、工具注册表形状)声明的是窄结构
接口,而不是 import SDK 的服务类。运行时依赖面因此是零:产物 lib/*.js 不含任何裸说明符,
由 tests/packaging.test.ts 静态与动态双重把关。
结构接口有可能和真实接口悄悄漂移,所以
src/typecheck.ts 断言已发布的 SubagentRuntime 满足插件的接口。
这个校验已经抓出两个真实错误:一处 readonly ContentBlock[](SDK 要求可变数组),
一处 outputSchema 类型过宽导致不可赋值。
发布
本包就是一个普通 npm 包,只是多了个 dsh.bundle.patch 字段,所以发布流程是常规的:
npm run check # typecheck + build + 测试
npm pack --dry-run # 确认真实发布内容(58 个文件,无测试、无 node_modules)
npm publish # publishConfig.access 已经是 "public"
首次发布前,把 package.json 里的占位仓库地址换成你自己的——GitHub 的"发布到 npm"
指引会读它,而已发布 manifest 里留着一串 REPLACE-WITH-YOUR-USERNAME 比不写仓库字段更糟。
然后把仓库的 GitHub 主题设为包含 dsh-plugin,即上文社区收录里描述的索引。
主题名和同名的 npm keyword 共同保证从两侧都能被发现。
有两个打包性质是承重的,而且很容易丢:
lib/必须在files里。 忘了它的 manifest 会发布一个"声明的入口文件不存在"的包。lib/必须被提交,而且是最新的。 它已被纳入版本控制(见"安装"一节),所以一次忘了 重新构建的推送,会把旧版插件发给所有从 git 安装的人。npm 那侧由prepublishOnly兜住; git 那侧得靠你自己:npm run build && git add lib && git commit。
第二点就是把构建产物纳入版本控制的真实代价,值得明说,而不是等发完版才发现。
开发
npm install
npm run typecheck # 对着真 SDK 类型跑 tsc --noEmit
npm run build # tsc -p tsconfig.build.json -> lib/
npm test # node --test,每文件一个进程
npm run check # 上面三个一起
npm run test:sandbox 在单进程里跑同一批用例。它存在有两个原因:受限沙箱会拒绝
node --test 需要的 spawn;以及已发布 SDK 的依赖闭包不完整——
@deepseek-ai/dsh-tools 会走到 @deepseek-ai/cordis,后者又走到隔离链接器没有
物化出来的包。那个入口为 @deepseek-ai/* 作用域加了一层很窄的解析兜底;在完整的
安装里它完全用不上。
已知边界
- child 路径需要 subagent provider,退化路径需要模型路由。 有 subagent provider 时
child 继承会话模型,整理完全自动;没有的话退化路径需要
consolidationRouteProvider+consolidationRouteModel,或会话已记录的request/header路由;两者都没有时整理会 被跳过而不是猜一个模型。 - 整理在进程内跑且不排队。 一个会话同时只跑一次;运行中崩溃只丢那一次改动, 不会丢已经提交的库。
- 并发写入是拒绝而不是合并。 如果另一个进程在本进程加载之后提交了同一层(比如
两个 harness 窗口开在同一个项目上),写入会被拒绝并返回
MEMORY_STORE_CONFLICT, 同时丢弃过期的缓存,而不是悄悄覆盖另一方的条目。不 bumprevision的手改文件在refresh()之前仍然不可见。 - 闸门是文本启发式。 它只看用户与助手的文本,判断不了一轮是否重要,只能判断 它是否听起来留下了可沉淀的东西。
- 不做近似去重。 同一个事实的两种不同措辞算两条,靠整理器去合并。
maxEntries是每层上限,不是全局上限,所以两层都开时最多可存两倍的条目数。- child 只能写记忆,所以它得出的其他结论会被丢弃。 这是有意为之,但也意味着"需要 先读某个文件才能判断"的整理不在范围内。
许可证
MIT
链接
同类插件
vectorize-io/hindsight#coding-agents★ 57722
Hindsight:会学习的 Agent 长期记忆系统,自动召回/保存、知识页、深度反思与按仓库隔离的记忆银行。
volcengine/OpenViking#examples/dsh-memory-plugin★ 41317
面向 DeepSeek Harness 的 OpenViking 记忆与上下文插件:pre-step 自动召回与画像注入、会话捕获、`viking://` URI 防护,以及对接 OpenViking 服务端的 recall/write 记忆工具。
MemTensor/MemOS#memos-local-plugin★ 12224
DeepSeek Harness 本地记忆插件:分层 L1/L2/L3 记忆,自动沉淀与手动 memos_* 检索工具并存,内置记忆浏览器。
agentscope-ai/ReMe#dsh★ 3735
将 DeepSeek Harness 接入 ReMe 本地优先、自进化的个人知识库:自动把已完成的主 Agent 对话沉淀为用户掌控的 Markdown 记忆,通过 reme_search 结合 BM25、可选向量检索和 wikilink 展开搜索对话与资料,并按日整理长期记忆。
zilliztech/memsearch#MemSearch★ 2815
供 DSH 与其他编程 Agent 共享的 Markdown 记忆,支持自动捕获、步骤前上下文注入、搜索召回,以及通过审阅面板实现 memory-to-skill 自进化。
vshulcz/deja-vu#extensions/dsh★ 1352
读取本机上其他四十二个编程智能体已经写下的会话文件(Claude Code、Codex、Cursor、VS Code Copilot Chat、opencode、OpenClaw、Hermes、Kimi、Cline、Zed 等),包括安装之前的历史:提供 deja_recall、deja_session、deja_blame、deja_fix、deja_how、deja_remember 六个工具与 /deja 命令,并可选地把召回结果加入运行时上下文。本地 BM25 索引,不用大模型,不用向量嵌入,不联网(dsh plugin --profile web add dsh-deja)。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。