Bundle
dsh-persist
Persistent memory for DeepSeek Harness: per-conversation notes, selective injection, project memory, semantic Vault search (bge-m3, free optional key) and automatic retrieval — agents stop forgetting.
- Source
- tluoluo
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 10 days ago
Readme
# dsh-persist
[English](README.en.md) | **简体中文**
> 给 DeepSeek Harness 的 agent 装上"不会失忆"的长期记忆。
> Persistent memory for DeepSeek Harness agents.
## 它解决什么问题
默认情况下 DSH 的 agent **换对话就失忆**——上下文一关,什么都不记得。dsh-persist 把记忆做成
文件系统上的分层结构,每个对话**按需注入**:
| 层 | 存储 | 对标 | 怎么被读取 |
|---|---|---|---|
| 用户画像 + 长期记忆 | `USER.md` / `MEMORY.md` | 长期记忆(LTM) | 每对话可选注入 |
| 关键记忆 | `memory.json` | 速查卡片 | 每对话可选注入;工具读写 |
| 对话记忆 | `sessions/<id>/memory.md` | 工作记忆 | 只注入本对话 |
| 项目记忆 | `projects/<key>/memory.md` | PARA 的 Project 层 | 按项目勾选注入 |
| Vault 语义记忆 | `vault.md` + `vault.db` | Zettelkasten 卡片盒 | 按需语义召回(工具或「自动检索」开关,不静态注入) |
## 特性
- **对话独有记忆**——每个对话一本"不会丢的笔记本",其他对话看不到、不注入
- **选择性注入**——勾选什么才注入什么;新对话默认零注入,不占上下文
- **项目记忆互通**——同一工作目录共享项目经验;命名项目可把同目录的多项目分开
- **Vault 语义检索**——配一个免费 key 就能"按意思"找记忆;不配自动降级为关键词搜索
- **自动召回(三态模式)**——记忆 tab 里选择自动检索模式:`关闭`(默认)/ `智能`(纯本地规则过滤闲聊,提到历史/项目/主题才查,零外发)/ `LLM 判断`(调用模型判断是否需要检索,更准但会把消息发给模型提供商,超时/失败自动降级为智能)
- **记忆 tab**——对话顶部可视化编辑 + 注入配置 + 实时预览(所见即所得);tab 右上角的「打开记忆管理页」在新标签打开完整管理页(全部会话/项目/Vault/全局文件)
- **全部纯文本**——`~/.dsh-memory/` 下每个文件人类可读可改,随时备份、迁移、导出
## 快速上手
```sh
dsh plugin --profile web add dsh-persist # 1. 安装
# 2. 重启 DSH(dsh --profile web)
# 3. 打开对话 → 顶部「记忆」tab → 勾选要注入的块
```
想让 agent 记住什么,直接对它说"记住这个",它会自动写入本对话记忆。
要开语义搜索:在 [硅基流动免费申请 key](https://siliconflow.cn/models?q=bge-m3),设置环境变量
`SILICONFLOW_API_KEY=...` 后重启即可(不配也能用,自动降级为关键词搜索)。
## 怎么装?
> 曾用名 `dsh-memory`(npm 名已被占用,故改名 `dsh-persist`)。存储路径 `~/.dsh-memory/`、路由 `/dsh-memory/` 不变,旧数据无需迁移。
**环境要求**:Node.js **>= 22.6**(推荐 24+;测试用 `--experimental-strip-types` 自 22.6 起可用)。Vault 层用内置 `node:sqlite`:23.4+ 默认可用,22.6–23.3 需 `--experimental-sqlite` 标志;更低版本插件照常加载,仅 Vault 工具自动降级禁用(记忆/注入/UI 不受影响)。宿主为 DeepSeek Harness(需提供 `tools` / `systemPrompt` / `webServer` / `sessions` / `agents` 服务与 `conversation.view` UI 槽位)。
`dsh-persist` 是一个标准 DSH **组合包(bundle)**:声明了 `dsh.bundle`,通过 `dsh plugin` 装进任意 profile。`@deepseek-ai/*` 是 **optional peer 依赖,由 DSH 宿主在运行时提供**,npm 不会、也不需要安装它们。
从 **npm** 安装(自带构建好的 `lib/`,无需构建):
```sh
# 装进你的 web profile(首选)
dsh plugin --profile web add dsh-persist
# 或装进别的 profile
dsh plugin --profile demo add dsh-persist
```
安装后重启(`dsh --profile web`),插件即生效。
从 **GitHub** 安装(会拉源码并跑 `prepare` 构建,见下文):
```sh
dsh plugin --profile web add github:tluoluo/dsh-persist
```
> 从 git 安装时 pnpm 在首次 `add` 后可能需要你授权运行 `prepare` 构建脚本:把 `dsh` 打印的包键复制进 profile 的 `pnpm-workspace.yaml` 的 `allowBuilds`,再重跑 `add`(详见 DSH 官方文档 [publish.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/publish.md))。
可选环境变量(**全部非必需**,装完就能用):
- `SILICONFLOW_API_KEY=...` → **可选**,开启"语义搜索"。bge-m3 会把记忆转成向量、按意思找(比纯关键词更懂你)。**不配也能用**:Vault 记忆自动降级成关键词搜索,功能不受影响。[在硅基流动免费拿 key](https://siliconflow.cn/models?q=bge-m3),然后设这个环境变量即可。它是本插件唯一可选的"外挂大脑",用来让搜索更聪明,但不是必需。
- `DSH_MEMORY_INJECT=0` → 关闭自动注入(默认开启)
- `DSH_MEMORY_ALLOW_REMOTE=1` → 允许非本机(非 loopback)访问记忆 API(默认一律 403;仅当 DSH web server 绑定 0.0.0.0 时需要,请自行评估隐私风险)
- `DSH_MEMORY_AUTO_VAULT=0` → **强制关闭**所有对话的自动语义检索;`=1` → 强制开启(不设置则按每个对话记忆 tab 里的「自动检索」模式,默认关。注意:`=1` 且未显式设置 GATE 时一律按智能门控,会话 tab 里选的 LLM 档不生效)
- `DSH_MEMORY_AUTO_VAULT_GATE=heuristic|llm|off` → 全局覆盖每个对话的自动检索模式(`heuristic` 智能门控:纯本地规则过滤闲聊,零成本、零外发、零延迟;`llm` 调用模型判断(模型见 `DSH_MEMORY_JUDGE_MODEL`),任何失败自动降级为 heuristic;`off` 关闭门控——每回合都检索)。不设置则按各对话记忆 tab 的选择——注意 tab 的「关闭」= 不检索,与这里的 `off` 含义不同:`off` 只在显式设置该变量时生效。词表是启发式的,存在已知边界(如无触发词的短消息不查、长闲聊可能漏网),属设计取舍
- `DSH_MEMORY_JUDGE_MODEL=provider/model` → LLM 判断模式用的模型(默认 `deepseek-official/deepseek-chat`,便宜快速;格式 `provider/model`)
- `DSH_MEMORY_JUDGE_TIMEOUT_MS=5000` → LLM 判断超时(默认 5000ms,超时降级为智能模式)
- `DSH_MEMORY_AUTO_VAULT_NAMESPACES=user,dsh-persist` → 限制自动检索只查这些 namespace(默认查全部 namespace,含项目归档)
> **隐私提示**:开启语义搜索(`SILICONFLOW_API_KEY`)后,每条用户消息和记忆内容都会发送给硅基流动(SiliconFlow)做向量化;自动检索同样如此。**LLM 判断模式**还会把当前消息发送给 `DSH_MEMORY_JUDGE_MODEL` 指定的模型提供商做"是否需要检索"的判断。介意请勿配置 key,或设 `DSH_MEMORY_AUTO_VAULT=0` 关闭自动检索(手动 `vault search` 仍可用)。
**一句话:装完 `dsh plugin add` 重启就有记忆功能;想要更聪明的语义搜索,再去硅基流动拿个免费 key 配上。**
### 装完怎么用(新手三步)
1. **重启 DSH**(`dsh --profile web`,别用还在跑的旧进程),插件即生效。
2. 打开 Web 界面,进入任一对话,会话顶部(轨迹 tab 右边)会出现一个 **「记忆」tab**——这里就是你本对话的记忆和注入开关。
3. 想让 agent 记住什么,就在对话里直接说,agent 会自动通过 `memory` 工具写入本对话记忆;或你在记忆 tab 里手动编辑。默认**不注入任何记忆**(不占上下文),你在记忆 tab 勾选后才把对应记忆每轮放进上下文。
> 可选:想用语义搜索,先在硅基流动拿到免费 key,设置环境变量 `SILICONFLOW_API_KEY=...` 再重启,Vault 层就从关键词搜索升级为语义搜索(README 不替你存 key,请放在 DSH 宿主能读到的环境里)。
**安全提示**:`/dsh-memory/api/*` 只允许 loopback 访问(非本机请求返回 403,除非显式设置 `DSH_MEMORY_ALLOW_REMOTE=1`)。记忆内容包含个人身份信息,请勿在共享网络中开放。
## 开发构建
**使用者不需要构建**——发布包自带 `lib/`,`dsh plugin add` 直接装。
**外部开发者**在自己环境 clone 后,`npm install` 会自动运行 `prepare`(tsdown 纯转译,不依赖 `@deepseek-ai` 类型即可产出 `lib/`),因此能自包含地构建出可用的产物:
```bash
npm install # 自动跑 prepare → lib/index.js + lib/client.js
npm run prepare # 显式重建自包含产物(host 用 tsdown.host.config.ts,client 用 tsdown.config.ts)
npm test # smoke 测试(node --experimental-strip-types src/smoke.ts)
```
`prepare` 只做**转译**(不 type-check):它把源码里对 `@deepseek-ai/*` 的 `import type` 全部擦除,产物运行时只保留对宿主提供的两个 import(`@deepseek-ai/dsh-tools.defineTool` 与 `@deepseek-ai/dsh-llm.createUserMessage`)——所以无宿主类型也能构建,产物由 DSH 宿主持有并加载。
**维护者**(在 DSH 宿主树内、junction 到宿主依赖以获得 `@deepseek-ai` 类型的场景)可跑全量构建,额外产出 `.d.ts` 并做完整类型检查:
```bash
npm run build # typecheck + typecheck:client + build:host + bundle + dts
```
> 语义检索的端到端测试在 `src/smoke-semantic.ts`(需要真实 `SILICONFLOW_API_KEY`),**不包含在 `npm test` 里**——需要时手动运行:
> - PowerShell:`$env:SILICONFLOW_API_KEY=...; node --experimental-strip-types src/smoke-semantic.ts`
> - bash:`SILICONFLOW_API_KEY=... node --experimental-strip-types src/smoke-semantic.ts`
> 注意:`lib/` 已 gitignore;运行中的 DSH 需重启才加载新 host 代码(client bundle 刷新页面即可)。
## 怎么用?
### 工具动作(model 调用)
`memory` 工具的 `scope` 参数决定写/读到哪:
- `scope=conversation`(**默认**)→ 本对话记忆:`add` 追加一条笔记,`list`/`search` 读全文
- `scope=project` → 当前工作目录的项目记忆(同目录对话互通)
- `scope=global` → 全局 keyed 记忆:`add`/`get`/`search`/`delete`(按 key)
| 动作 | 参数 | 作用 |
|---|---|---|
| `add` | `scope`, `content`(global 还需 `key`) | 存一条记忆 |
| `get` / `search` / `delete` | `scope`, `key` | global 的 keyed 操作 |
| `list` | `scope` | 读对话/项目记忆全文 |
| `profile` | `profileKind`, `profileOp`, `content` | 读写全局用户画像(USER.md/MEMORY.md) |
| `vault` | `vaultOp`, `content/query`, `namespace` | 语义存取:`add` 存、`search` 查、`list` 列、`export` 导出、`import` 导回(带 namespace 隔离) |
### 记忆文件(全部人类可读可改)
| 文件 | 内容 | 怎么改 |
|---|---|---|
| `~/.dsh-memory/sessions/<id>/memory.md` | 本对话记忆 | 记忆 tab 里编辑,或 `memory add` |
| `~/.dsh-memory/sessions/<id>/inject.json` | 本对话注入配置 | 记忆 tab 里勾选 |
| 所有历史会话 | 各对话记忆 + 注入配置 | `/dsh-memory/` 管理页「会话记忆」tab:浏览 / 编辑 / 删除(记忆 tab 只能编辑当前对话) |
| `~/.dsh-memory/projects/<key>/memory.md` | 项目经验 | `memory add`(scope=project),或直接编辑 |
| `~/.dsh-memory/USER.md` / `MEMORY.md` | 全局池 | `/dsh-memory/` 页面或直接编辑 |
| `~/.dsh-memory/memory.json` | 全局 keyed | 直接编辑 JSON |
| `~/.dsh-memory/vault.md` | Vault 语义记忆 | 工具增删(`memory(vaultOp="add"/"delete")`)**自动同步**此文件;手改文件后 `memory(vaultOp="import")`(或管理页「Vault 同步」)写回数据库 |
### 例子
```
对话 A(工作目录 /work/projA):
- 记忆 tab 勾选:用户画像 + 本对话记忆 + 项目记忆(projA)
- agent 每轮自动注入这三块;对话 B 看不到对话 A 的记忆
agent: 用户说他喜欢用空格缩进
agent: memory(action="add", content="用户偏好空格缩进") # 写入对话 A 的记忆
agent: 用户问"你记得我喜欢怎么缩进吗"(同一对话)
agent: memory(action="list") → 读回对话 A 的记忆
agent: 记录项目经验
agent: memory(action="add", scope="project", content="构建脚本在 build.ps1")
# 写入 /work/projA 的项目记忆,同目录其他对话也能勾选注入
agent: 全局 keyed 记忆(跨对话共享)
agent: memory(action="add", scope="global", key="user-name", content="小明")
```
## 技术设计(对应 DSH 课程)
| 部分 | 实现 | 课程模块 |
|---|---|---|
| 存储层 | `MemoryStore` / `ProfileStore` / `SessionMemoryStore`,文件 + 原子写入 | 模块 ⑤ 方案 A |
| Vault 层 | `VaultStore` + `embedder`(SQLite + bge-m3) | 模块 ⑤ 方案 C |
| 工具层 | `ctx.tools.register(defineTool({...}))`,scope 三态 | 模块 ③④ |
| 选择性注入 | `ctx.systemPrompt.context()` 按会话读 inject.json 渲染(子 agent 沿 parentSession 继承所属对话) | 模块 ⑥ |
| 多 agent 隔离 | namespace + 对话/项目两级隔离 | 模块 ⑦ |
| Client UI | `conversation.view` 槽位 id `memory` order 20(轨迹右边),tsdown 自包含 bundle | 模块 ⑧ |
| 混合检索 | 有向量走语义、无向量走关键词,合并排序 | 模块 ⑤ |
| 插件结构 | `name` / `inject` / `apply` 三件套 | 模块 ② |
> 注意:改代码后需要**重启 DSH** 才会加载新 host 代码(`lib/` 更新了,但运行中的进程持有旧模块;client bundle 刷新页面即可)。
## 故障排查
| 现象 | 处理 |
|---|---|
| 记忆文件损坏(memory.json / inject.json 无法解析) | 插件会自动把损坏文件备份为同目录下 `*.corrupt-<时间戳>` 并重置为空,控制台会打印备份路径——从备份找回内容即可 |
| 记忆 API 返回 403 | loopback 守卫生效(默认只允许本机)。确认 DSH 绑定 127.0.0.1;确实需要远程时设置 `DSH_MEMORY_ALLOW_REMOTE=1`(自行评估隐私风险) |
| 想完全关闭自动注入 | `DSH_MEMORY_INJECT=0` 后重启 DSH |
| 改 host 代码不生效 | DSH 进程持有旧模块,需要重启 DSH(client bundle 刷新页面即可) |
| 注入内容过长 | 静态记忆块(USER/MEMORY/对话/项目)有 100 行截断,超出的部分不会注入——请在 `/dsh-memory/` 页面精简对应文件。Vault 无静态注入:只经「自动检索」topK=3 或工具按需召回,不受行数限制 |
| 对话/项目记忆文件越来越大 | 注入有 100 行截断,但磁盘上的 memory.md 会持续增长——建议定期(如每个里程碑)用 `memory` 工具或直接编辑精简,过时条目移入 Vault 归档 |
| 手改 vault.md 后内容丢失 | 工具增删(`memory(vaultOp="add"/"delete")`)会**全量重写** vault.md(自动同步镜像)。手改请在无工具操作的间隙进行,改完立即 `memory(vaultOp="import")` 或管理页「Vault 同步」写回数据库 |
## 贡献
- 代码结构:`src/` 顶层 = 宿主逻辑 + 公共纯函数,`src/client/` = 浏览器侧;两套产物独立构建(host 用 `tsdown.host.config.ts`,client 用 `tsdown.config.ts`),类型声明(`.d.ts`)由 tsc 生成
- 开发流程:改代码 → `npm run build`(typecheck + host + client + d.ts 全量)→ `npm test` 全绿;只改 client 时可单独 `npm run bundle` 快速迭代
- 测试永远用临时目录(`src/smoke.ts` 已如此),绝不直接读写 `~/.dsh-memory/` 真数据
- 提交前跑 `npm pack --dry-run` 确认发布内容(`prepack` 钩子会自动构建)
## 路线图
- [x] v1 基础:`memory` 工具 + 文件存储
- [x] Builtin 层:用户画像 + 自动注入(`context()`)
- [x] Vault 层:向量语义检索(bge-m3,含关键词降级)
- [x] 对话层:每对话独有记忆 + 注入配置 + 项目(cwd)记忆
- [x] Client UI:记忆 tab(轨迹右边)+ 编辑/注入配置面板
- [x] 真正的自动向量注入:`agent/pre-step` 按当前消息检索 Vault topK=3 注入(记忆 tab 勾选「自动检索」,默认关;`DSH_MEMORY_AUTO_VAULT=0/1` 可全局强制)
## License
MIT
Install
dsh plugin --profile web add github:tluoluo/dsh-persist
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-persist from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.