DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

guyuefangyuanl /

guyuefangyuanl/dsh-memory

Verified

Cross-session persistent memory for the DeepSeek Harness: a model-facing memory tool, an always-on index section, and a bundled maintenance skill.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@a9b00969

dsh-memory

DeepSeek Harness 的跨会话持久记忆:模型自己判断值得记什么、自己写入、后续会话自动召回的那一层。

这不是 AGENTS.md / CLAUDE.md —— 那层 dsh 已经由 @deepseek-ai/dsh-agent-instructions 做掉了,而且做得更完整。这里补的是它上面那一层:人没有写下来、但模型在干活过程中发现值得留住的事实。

由三个部件组成

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" 才落全局层。同名时项目层遮蔽全局层。

这样"这个人是谁、希望你怎么工作"可以写一次全项目通用,而"这个仓库在做什么"仍然不会串味。

单条记忆的格式:

---
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 自己决定要不要):

- id: memory
  name: /absolute/path/to/dsh-memory/index.js

preset 里绝对路径按原样解析(mount 会转成 file: URL 再 import,空格会被正确编码),相对路径按 preset 目录解析。npm 安装进 profile 之后可换成裸包名 dsh-memory。

作为 profile bundle 的一行(全局生效,但会落到 host 的全局层):

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:

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 天的记忆时,结果里会直接标出它有多旧,并提醒先确认它提到的东西还在。

测试

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

—/ 5

No ratings yet

Verified DSH bundle

Commit a9b009699b9c

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout