前言¶
用 DeepSeek Harness(DSH)做跨会话的项目开发,会碰到两类遗忘。
一类在会话之间:每次新开对话,agent 不记得你上周定过的方案,文档要重读一遍,同一个坑会再踩一次。另一类更隐蔽,发生在一次会话内部:上下文变长后宿主会压实(compaction),把原始消息替换成摘要。上下文便宜了,但摘要是有损的,实际发生过的数值、取舍背后的推理,被丢掉就回不来了。README 里给过一个实测:588 k token 的历史,压实后的摘要只有约 2 k token。
iv3rins/dsh-memory 想解决的是这两件事,办法是把记忆放进仓库,而不是放进上下文窗口。插件名在 GitHub 仓库上标为 dsh-project-memory,分类是「记忆」。
这是什么¶
一个面向 DSH 的项目级持久化知识与开发状态记忆插件,维护者是 iv3rins,包名 @dsh-external/dsh-project-memory,MIT 协议,要求 Node 20+。
它是一个宿主侧的 cordis 插件:注册一批 agent 工具、一段 system prompt 小节、几条斜杠命令,外加一个浏览器端侧边栏面板,面板通过两条回环地址路由读取宿主数据,不给宿主增加额外工作。不修改 DSH 本身。
三套存储系统的分工¶
README 把存储拆成三套,各自的写入方和存活范围都不同,这一步是理解整个插件的前提。
| 系统 | 存什么 | 谁写 | 能熬过什么 |
|---|---|---|---|
| A — 文档 | 仓库里已有的 Markdown | 你自己在 git 里写 | 一切 |
| B — 状态 | 任务、进度、阻塞项、决策、下一步、git 事实、交接笔记 | state_update / state_handoff 工具调用 |
重启、压实 |
| C — 原子(memory atoms) | 简短的持久陈述 | 压实观察器 + memory_remember |
重启、压实 |
设计上刻意做了切分。三种内容生命周期不同,塞进一个 blob 会同时损害三者。
系统 A 里 Markdown 是真相源,插件索引、搜索、按 ## 小节读取,但从不改写权威文档;需要提改动时写到 docs/proposals/。系统 B 只由工具调用持久化,插件明确不抓取对话去推断进度,从不调用 state_update 的 agent 拿不到任何状态记忆——这是一次可见的失败,而不是某个安静出错的推断。系统 C 是上面「压实即丢失」问题的答案。
稀疏召回:原子与门控¶
系统 C 的写与读是两件事。
每次压实发生时,插件把摘要里值得留下的内容提取成短原子,追加到 .dsh-memory/memory/atoms.jsonl。下一次请求到来时,把请求与原子库做匹配,只选出少数几条能放进预算的,而不是把整个库倒进上下文。原子 id 是「类型 + 归一化文本」的哈希,同一句话遇到两次是强化同一条记录,不是追加副本。
README 给了这条链路的四个性质,值得逐条看,因为它们决定了这套东西的失效方式:
- 稀疏而非倾倒。几百条里选几条,并且有硬 token 上限,成本由配置决定,而不是由项目规模决定。
- 记录而非抓取。系统 B 靠工具调用写入,不靠解析对话。
- 规则优先于相关性。带硬约束的原子在任何打分之前就被选中,路由器无法移除它。只在你碰巧提到时才生效的规则不是规则。
- 选择过程不写库。路由器子代理只能返回交给它的那些 id,自己编出来的 id 会被丢弃。记忆的变更始终由确定性代码完成,幻觉不会写坏存储。
原子还有两个互相独立的属性:重要性(weight,1–5)只影响排序,常驻资格(standing)决定能否入选。常驻资格由类型 constraint 决定,与权重无关。README 坦白过一个旧判据 STANDING_MIN_WEIGHT 是真实缺陷:它会静默丢掉所有以权重 2 存下的约束,在真实库上意味着 66 条硬规则里有 32 条不合格。常驻原子仍然受条数与 token 上限约束,是预算分成,不是豁免。
打分是确定性的:词面重叠、类型优先级、强化次数、新近度。词面匹配不够时可以让子代理充当路由器,三档:
routerTier |
行为 | 成本 |
|---|---|---|
off |
只用确定性门控 | 免费 |
smart(默认) |
先走确定性门控,只有没选到相关内容或匹配很弱时才升级给路由器 | 近乎免费,routerProvider 与 routerModel 都设置之前不花调用 |
always |
每一步都问路由子代理 | 每步一次调用,建议配便宜模型 |
路由器还有个额外的策略维度,三选一:task 偏向这一步即将要做的事,compliance 保留管辖当前所涉文件的规则,orient 在「想弄懂」而非「想动手」的请求里偏向子系统如何运作。
上下文侧还有一条注入路径。会话第一步注入一段基线(项目身份、git 事实、当前任务、不变量、下一步、阻塞项、决策、文档地图),适配预算项 l0Budget / l1Budget,只在身份变化时重发,且发送前会扫一遍会话表面避免重复。每一步可能额外收到一个 recall 块。基线以插件自有、包在 nonce 围栏里的用户消息形式到达,项目文本被当作数据:角色前缀被中和,”ignore previous instructions” 这类措辞被剥除,不可见字符与双向控制字符被移除,代码围栏被闭合。
一个被清楚标注的实测¶
README 有一组对比数据,来源是 CodeMemo 基准(每个项目 40 题),模型与裁判都是 deepseek-v4.1-flash,温度 0。三种上下文配置看同一道题:A 只给压实摘要(约 1.3k / 2.0k token),B 只给最后 6k token 原始转录,C 给摘要加召回的原子(约 0.4k token)。80 题汇总,正确率分别是 16.3%、15.0%、25.0%。
README 自己对这组数据的态度很明确,照录三点:
- 历史几乎能装下时(17k token)摘要与原始尾部一样好,两者都是 30.0%,此时压实不是瓶颈。历史装不下时(588k token)三种方案都崩塌,记忆崩塌得最慢,但三者的绝对值都很低。
- 置信区间包含零。C − A 是 +8.8 pp,区间 −0.7 … +18.2,与「没有效果」和「效果约 18 个点」都兼容。两个项目与 partial credit 三个方向一致,这是扩大样本的理由,不是引用这个数字作为定论的理由。
- 召回改善的是部分正确的答案(32.5% → 41.9%),上下文更接近充足,但不等于充足。
这组数据测的是「预算内的可检索性」,不是路由质量(routerTier: 'off'),也不是压实永久销毁了多少文本。完整方法、逐题数据和构建过程中发现的缺陷在仓库内的 docs/design/benchmark-compaction-loss.md。
安装与启用¶
README 给出的安装方式是把仓库放到本地后,以 link: 协议注册进 profile:宿主自己会加上依赖和 bundle 条目,因为清单里声明了 dsh.bundle,reconcileProfilePlugins() 会把任何新装且带该声明的依赖提升进去。
$dsh = "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
& $dsh plugin --profile desktop add -w "link:D:\path\to\dsh-memory"
D:\path\to\dsh-memory 换成你自己的本地路径。随后重启宿主。用 project_inspect 验证:能报出项目根目录与已索引文档数,说明插件已经生效。
README 也提示了另外一条手改 profile 的路(dependencies 加一条 + dsh.profile.bundles 加一条):更慢,而且容易只做对一半,只加依赖不加 bundle 条目的结果是装上了但永远不会加载。pnpm 会警告 link: 依赖不从目标的 node_modules 解析 peer,这里无害:插件没有运行时依赖,唯一的 peer @deepseek-ai/dsh-llm 由宿主提供。
bundle 补丁 cordis.patch.yml 只插入一行,默认即开箱可用;要改默认值时在 config 上写:
- insert:
- id: project-memory
name: '@dsh-external/dsh-project-memory'
config:
l0Budget: 300
l1Budget: 1200
registerCommands: true
routerTier: smart
宿主侧有完整的验证脚本,npm run setup 从已安装的桌面版负载里提取宿主包并链接,是 test:host 的一次性前置:
npm run setup # 从已安装的 app.asar 里提取宿主包并链接
npm test # 14 个单元测试,针对纯 core/ 层(不需要 DSH)
npm run test:host # 14 项宿主级检查,对着真实 harness
tests/compat.mjs 把真实清单喂给宿主自己的 evaluatePluginCompatibility,engines.dsh 被写成 >=0.2.0-rc.0 <0.3.0,真正被测过的版本是 0.2.1-rc.2。README 解释这个窄范围是有意的:宿主小版本升级就可能改动工具 schema 子集或者注入消息的形状,而那样一来插件会在挂载时静默失败,而不是在安装时大声报错。对着更新的宿主重跑测试套件之后再放宽这个范围。
常用工具与命令¶
一共 15 个工具。任何陌生会话里,第一个该调的是 memory_status——记忆初始化了没、当前修订号、相对 git 是否陈旧、有多少原子,也可以重建索引。
| 工具 | 回答什么问题 |
|---|---|
memory_bootstrap |
一次调用给出新会话所需的全部必要内容,且在 token 预算内 |
memory_status |
记忆状态与修订号,可重建索引 |
memory_search |
哪些文档谈到了这件事,带状态/类型/路径过滤,有排序 |
memory_read |
读一篇文档或其中某个 ## 小节,有长度上限,被框为数据 |
memory_recall |
已存原子里哪些与这个问题相关,排序、有预算、带路由与置信度 |
memory_remember |
存入持久事实、约束与决策 |
memory_forget |
按 id 退役原子,写的是墓碑,再次提取无法复活 |
state_get / state_update / state_handoff |
读状态 / 记录变更(compare-and-set)/ 写会话末交接笔记 |
project_init / project_inspect |
创建 .dsh-memory/ / 只读诊断根目录、git 事实、索引状态 |
docs_check / docs_sync / architecture_check |
文档 front-matter 检查 / 更新文档或提改动建议(拒绝受保护路径)/ 按 arch.json 做确定性 import 规则检查 |
一条典型的长任务收尾流程:state_update 记录进度与决策,memory_remember 存下绕过坑的规则,state_handoff 写交接笔记,下一个会话开屏先跑 memory_status。
斜杠命令:/memory-status、/memory-context、/memory-init、/memory-handoff、/memory-recall、/memory-route、/memory-forget、/memory-sync、/architecture-check。其中 /memory-route 是试运行,显示当前档位会走哪条路由以及为什么,不花任何模型调用。
存储布局¶
全部位于项目根下的 .dsh-memory/。提交它或忽略它都自洽:提交给团队共享状态,忽略则给每个开发者一份私有的。
.dsh-memory/
project.json 稳定的 projectId + 首次见到的 git 事实
state/
current.json 状态文档(schema 2,带修订计数)
journal.jsonl 已接受写入的追加日志
compactions.jsonl 每次观测到的压实一行,含无产出的
.lock 短时写锁
memory/
atoms.jsonl 持久原子,一行一条 JSON 记录
.lock 短时写锁
index.json 派生缓存,可安全删除
arch.json 可选的架构规则
settings/
<项目键>.json 按项目覆盖
compactions.jsonl 值得单独说。插件每观测到一次压实就记一行,包含没有产出原子的那些,每行带一个封闭集合里的 skipReason(empty-summary、no-extractable-statements、store-write-failed 等)。原子库只能体现成功,没有这个文件时,「提取器坏了」和「项目很闲」看起来完全一样。
侧边栏面板¶
插件在左侧边栏增加一个「项目记忆」入口,显示生效与已遗忘的原子数、其中有多少是常驻硬约束、按类型和重要性的分布、压实记录、当前召回预算与上限,以及路由控制(routerTier、策略、提供方与模型)。
保存走宿主的 configEditor,改动生效方式与其他配置改动一致;某个 profile 没有暴露 configEditor 时面板会直说不支持,而不是报告一次并未发生的保存。面板没有任何改动记忆的控件——删除仍然走 memory_forget,因为一个能删原子的浏览器按钮会同时绕过压实观察器的来源信息与墓碑机制。
它的两个端点只接受回环地址。端点会暴露项目记忆并改变路由行为,所以来自其他主机的请求会被拒绝而不是被回应。
它明确不做的事¶
这些边界写在 README 与 LIMITATIONS.md 里,不做过度解释:
- 不修改
AGENTS.md、docs/decisions/、docs/adr/、.git/,docs_sync拒绝这些路径并在结果里说明原因。 - 不 commit、branch、push,只记录 git 事实。
- 不悄悄编造状态,每次变更经过
state_update的 compare-and-set 修订号,并发写入会冲突而不是互相覆盖。 - 搜索与原子打分都是词面的,没有 embeddings、没有同义词、除了小写化不做词干化。换一种措辞问,文档会漏。
- 召回的原子不是权威文档,是带出处的一句话,可能过时,可能把摘要里的内容简化过。真正要紧的东西应该写进文档,让
memory_read能返回整个小节。 memory_forget是墓碑不是删除,记录留在atoms.jsonl里当证据。- 路由器输出不被信任:只采纳提交给它的 id,自己编的 id 计入 rejected 并丢弃,路由器从不写库。
- 提取器「保守」得有限:确定性解析器会把多数散文当成可能的事实,把裸标题或代码围栏也存成原子。
另外 LIMITATIONS.md 列出了未端到端验证的部分:GUI 会话跑得不深(没有真实压实、恢复、切分支、长程多会话),路由子代理从未对真实模型跑过(smart 档在路由器失败时退化为确定性选择,失效模式是漏一次改进,不是坏掉一步),面板从未被人看过渲染效果。
适用场景与注意¶
适合的场景比较明确:单个项目跨越几十次会话、上下文经常被压实、需要在不同分支之间共享一份决策记录,或者团队想用 .dsh-memory/ 共享状态。搜索是词面匹配这一点意味着,文档写得分散、同义词多的项目收益有限,这类项目的痛点更适合先把文档结构本身理清楚。
使用上注意四点。
- 插件以当前 dsh 进程的权限运行。安装前请阅读源码与
LICENSE(MIT),不需要的功能可以关掉对应配置项,比如registerCommands: false。 engines.dsh范围是>=0.2.0-rc.0 <0.3.0,宿主升级到 0.3.0 系列之前先去仓库确认兼容性,或者自己重跑一次npm run test:host。always档每步一次路由器调用,配便宜模型,并且确认routerProvider与routerModel都就位。- 侧边栏的两条回环端点不要转发到其他主机,它与项目记忆和路由行为直接相关。
结尾¶
这个插件的价值不在于让模型变得更聪明,而是把「读过的文档、做过的事、定过的规则」从一次性上下文里剥出来,落成仓库里可检索的普通文件,每次按需召回几条,成本由预算项钉住。三套系统、稀疏门控、规则优先于相关性这三处设计是它的主要取舍,LIMITATIONS.md 里对未验证部分的说明也足够坦率。想上手前按前一节检查一遍源码与宿主版本是必要的。
仓库地址:
- 目录页:https://www.skillhub.cn/plugins/iv3rins/dsh-memory
- GitHub:https://github.com/iv3rins/dsh-memory