Bundle
dsh-memory
Cross-session persistent memory for the DeepSeek Harness: a model-facing memory tool, an always-on index section, and a bundled maintenance skill.
- Source
- guyuefangyuanl
- stars
- 3 stars
- License
- MIT
- Updated
- Updated 19 hours ago
Readme
# dsh-memory DeepSeek Harness 的跨会话持久记忆:**模型自己判断值得记什么、自己写入、后续会话自动召回**的那一层。 这不是 `AGENTS.md` / `CLAUDE.md` —— 那层 dsh 已经由 [`@deepseek-ai/dsh-agent-instructions`](https://github.com/deepseek-ai/deepseek-harness) 做掉了,而且做得更完整。这里补的是它上面那一层:人没有写下来、但模型在干活过程中发现值得留住的事实。 ## 由三个部件组成 **1. `memory` 工具** —— 模型可调用,六个动作: | action | 作用 | |---|---| | `write` | 建一条或整条替换(`name` + `description` + `type` + `content`,可选 `scope`) | | `read` | 按 name 读回全文;`names` 可一次取多条 | | `list` | 列出索引摘要(不含正文),可按 `type` 过滤;附带待写清单 | | `search` | 按内容检索,返回命中片段 | | `edit` | 局部改写:按 `old_string` / `new_string` 锚点替换,或只换 `description` / `type` | | `delete` | 删除一条并重建索引 | **2. 一个常驻的 system-prompt section**(`order: 50`)—— 每次 prompt 组装时把索引注入上下文。 **3. 一个捆绑的 `memory-maintenance` 技能** —— 记忆库需要整体过一遍时用:合并重复、清理过期、修悬空链接。只在 host 挂载了 skills 服务时才注册,没挂载就静默跳过。 ## 关键设计:索引常驻,正文按需 每条记忆在上下文里只占一行 `name — description`,模型看到相关的才用 `read` 拉全文,或用 `search` 直接按内容命中。几十条记忆只花几百 token,而不是把全部正文塞进每一次请求。 索引按 `type` 分组注入,`user` / `feedback` 在前 —— 这两类会改变模型的行为,`project` / `reference` 只是背景。 ## 两层作用域 ``` $DSH_HOME/memory/ ├─ global/ # 跨项目共享 │ ├─ MEMORY.md │ └─ prefers-chinese.md └─ C--Users-x-repo/ # 按 cwd 隔离 ├─ MEMORY.md └─ otc-deploy-target.md ``` 默认(`scope: layered`)两层同时可见,索引里全局条目标 `[global]`。写入默认落项目层,`write` 时传 `scope: "global"` 才落全局层。同名时**项目层遮蔽全局层**。 这样"这个人是谁、希望你怎么工作"可以写一次全项目通用,而"这个仓库在做什么"仍然不会串味。 单条记忆的格式: ```markdown --- name: prefers-chinese description: 用户要求所有回复使用中文 metadata: type: feedback created: 2026-08-21T03:11:07.412Z updated: 2026-08-21T03:11:07.412Z --- 所有回复用中文。 **Why:** 用户母语。 **How to apply:** 包括代码注释。 ``` 四种 `type`:`user`(这个人是谁)· `feedback`(希望你怎么工作,含原因)· `project`(在做什么,写绝对日期)· `reference`(外部资源指针)。正文里用 `[[other-name]]` 互链,**包括还没写的名字**——见下面「链到还没写的记忆是特性」。 `created` 在同名更新时会被保留,`updated` 每次改写刷新。老格式(没有这两个字段)的记忆照常可读,时间戳落回文件 mtime。 ## 性能 索引注入发生在**每一次 prompt 组装**,也就是模型每走一步都会跑一遍。天真实现是每次把目录里所有记忆全文读一遍再解析 —— 120 条记忆时这在本机实测约 **107 ms/次**,是白白加在每一轮上的延迟。 这里用签名缓存:每次只做一次 `readdir` + 每个文件一次 `stat`(不读内容),拼成 `文件名:mtime:size` 的签名。签名没变就直接返回上次的结果,连渲染都跳过;签名变了也只重读真正变过的那几个文件。同样 120 条记忆,**约 1.8 ms/次**。 自己写完的文件会显式失效缓存,不依赖签名 —— 同一毫秒内把一条记忆改成等长的另一份内容时 mtime 和 size 都不变,只靠签名会漏。外部进程(另一个会话、手工编辑)的改动则由签名捕获。 `search`、反向链接、悬空链接检测全部跑在这份缓存上,不额外碰磁盘。 ## 配置 | key | 默认 | 说明 | |---|---|---| | `dshHome` | `$DSH_HOME` 或 `~/.dsh` | 记忆根目录的父级 | | `scope` | `layered` | `layered` 两层都可见;`project` 只按 cwd 隔离的那层;`global` 只共享层 | | `cwd` | `$DSH_CWD` 或 `process.cwd()` | 决定项目 slug | | `indexBudgetBytes` | `16384` | 注入索引的字节上限 | | `maintenanceSkill` | `true` | 设为 `false` 则不注册捆绑技能 | ## 装法 **作为 preset 的一行**(推荐,这样是 agent-plane,每个 preset 自己决定要不要): ```yaml - id: memory name: /absolute/path/to/dsh-memory/index.js ``` preset 里绝对路径按原样解析(mount 会转成 `file:` URL 再 import,空格会被正确编码),相对路径按 preset 目录解析。npm 安装进 profile 之后可换成裸包名 `dsh-memory`。 **作为 profile bundle 的一行**(全局生效,但会落到 host 的全局层): ```powershell dsh plugin --profile <name> add <本包绝对路径> ``` 本包根 `package.json` 声明了 `dsh.bundle.patch`,所以 `dsh plugin add` 会自动把它追加进该 profile 的 `dsh.profile.bundles` 并应用 `cordis.patch.yml` —— **不需要手工编辑 profile 的 package.json**。反过来说,路径指错时只会打一条 warning,不报错,是静默失效,装完记得确认插件真的挂上了。 捆绑技能要生效还需要该 preset 挂了 skills 服务(`skill-filesystem` / `tool-skill` 之类)。没挂就只是少一个技能,`memory` 工具和索引注入照常工作。 ## 权限与副作用 装这个插件之前,它会碰什么、不会碰什么: | | | |---|---| | **磁盘** | 只读写 `$DSH_HOME/memory/` 下自己那两层目录(`global/` 和项目 slug 目录)。不读、不写工作区里的任何文件。 | | **网络** | 无。不发任何请求,也不带任何运行时依赖。 | | **子进程** | 无。 | | **生命周期脚本** | 无。纯 ESM,无构建步骤,`scripts` 里只有 `test`。所以从 git 直接装时不会撞上 pnpm 对 `prepare` 的构建门禁。 | | **host 服务** | `tools`、`systemPrompt` 必需,缺任一在装配时立即报错而不是静默降级;`skills` 可选,只用来注册捆绑技能。 | | **上下文** | 注入一个 `order: 50` 的 system-prompt section,默认上限 16 KiB(`indexBudgetBytes` 可调)。不注入记忆正文。 | **信任边界**:记忆正文是模型自己写进去的,属于不可信文本。注入前会转义 `</system-reminder>`,`description` 里的换行会被压平,所以记忆内容既关不掉插件自己的注入框,也塞不进第二个 frontmatter 头。但记忆**内容本身**仍然会被模型当作背景读到——注入的措辞明确说了那是过去的记录、不是用户当前的指令,不过如果你的部署里有人能往 `$DSH_HOME/memory/` 里写文件,那等于能往每次请求的上下文里写字,按这个前提设权限。 ## 卸载 `dsh plugin` 把参数转发给 profile 目录里的 pnpm,所以卸载就是 remove: ```powershell dsh plugin --profile <name> remove dsh-memory ``` 装在 preset 里的话,删掉那一行即可。 两种方式都**不会删记忆文件**——它们在 `$DSH_HOME/memory/` 下,是你的数据不是插件的。要一并清掉就手工删那个目录;只想清掉某个项目的,删对应的项目 slug 目录,`global/` 留着。 ## 几个刻意的取舍 **`MEMORY.md` 是给人看的镜像,不是注入源。** 每层各有一份,写入/删除后从磁盘上真实存在的文件重建。模型看到的索引是每次组装时重新扫 `.md` 文件本身扫出来的,**不读 `MEMORY.md`** —— 手工编辑它对模型零影响。文件头部自己写了这句话,免得下次有人对着它改半天。 **索引是派生物,不做增量维护。** 手改坏、并发写、进程中途挂掉都不会让它和实际文件长期失配。 **超预算时按 type 优先级裁,不按字母序截尾。** 先保 `user` / `feedback`,同类里保最近更新的,并明确写出「还有 N 条被省略,用 `list` / `search` 够得到」。按名字尾部截断等于让字母表决定模型记得什么。 **记忆文本会被转义。** 记忆内容里的 `</system-reminder>` 字面量会被转义,模型写进记忆的文本关不掉插件自己的注入框。插件自己的框不转义。`description` 里的换行会被压平,塞不进第二个 frontmatter 头。 **局部编辑不经过 frontmatter。** `edit` 的 `old_string` 只在**正文**里匹配,改完之后 frontmatter 由插件重新渲染。所以无论锚点匹配到什么、`new_string` 里塞了什么,都改不动 `name` / `created`,也注入不进第二个文档头。替换按下标切片做,不走 `String.replace`——否则 `new_string` 里的 `$&`、`$1` 会被当成替换模式解释。锚点不唯一时直接拒绝并说明出现了几次,除非显式 `replace_all`。 **链到还没写的记忆是特性,不是错误。** `[[some-name]]` 指向一条尚不存在的记忆,记录的是"这件事值得单独写一条"。所以 `write` **不会**为此报警;这些名字被 `list` 收集成 `unwritten` 待写清单,按被引用次数排序——被最多条记忆惦记的那个,就是记忆库最明显缺的那块。`read` 也会把某条记忆里尚未写的链接单独列出来。 唯一需要修的情况是名字**曾经**存在过:`delete` 会点名谁还在引用它,让你决定这件事是搬走了(那就把引用指过去)还是不成立了(那就留着,它退回成一条待办)。 **近重复只提示,不拦。** `write` 一条和已有记忆高度相似的新条目时会给出候选名,判断权留给模型。 **损坏的文件被跳过,不影响其余。** 没有 frontmatter 或读取失败的 `.md` 不进索引,其他记忆照常工作。整个 section provider 外面还有一层兜底:记忆库坏了也只是不注入,不会让会话起不来。 **注入的措辞明确说了这是背景、可能过时。** `read` 一条超过 14 天的记忆时,结果里会直接标出它有多旧,并提醒先确认它提到的东西还在。 ## 测试 ```bash npm test ``` 56 个用例,两个文件:`test/memory.test.js` 覆盖插件装配、工具的六个动作、分层与遮蔽、待写清单、近重复提示、注入转义、预算裁剪、`output.schema` 一致性,以及 `edit` 的几条边界(锚点不唯一、锚点碰不到 frontmatter、`$&` 是字面量、拒绝路径不落盘);`test/store.test.js` 覆盖缓存的正确性与性能 —— 包括"内容被等长替换且 mtime 复原时确实没读盘"这种直接验证缓存生效的用例。 ## 与 Claude Code auto-memory 的关系 行为目标是对标它:一条事实一个文件、四种 type、`[[name]]` 互链(包括"链到还没写的名字是特性"这个立场)、索引常驻上下文、读取时提示陈旧度、配一个维护技能。实现是独立写的,提示词措辞、分层作用域、签名缓存、`search` / 批量 `read` / `edit` 局部改写 / 待写清单这些都是本包自己的。 有一处它更强而本包补齐得晚:Claude Code 的记忆是普通文件,可以用通用编辑工具改一句话;本包的记忆在 `$DSH_HOME` 下、模型只能走 `memory` 这一个接口,所以 v0.2.1 才补上 `edit`。反过来,本包的索引是从文件派生的,不像手工维护的 `MEMORY.md` 那样会和真实文件漂移。 ## License MIT
Install
dsh plugin --profile web add github:guyuefangyuanl/dsh-memory
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-memory from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.