DSH-Myosotis
Identified用于DSH的记忆插件
dsh-myosotis · Myosotis
给 DSH 的四层持久记忆 + 全文知识库 + 一套会学习的闭环。
记忆:全局 / 项目 / 会话 / 深记忆,前三层每轮按当前话题挑几条注入上下文,深记忆用 SQLite 存量大归档、按需检索。模型用四个工具维护它,用户可以直接读
MEMORY.md。 知识库:把磁盘上的文档切块、建 FTS5 索引,检索时返回文件 + 标题路径 + 行号。和记忆是两条并行的轴——记忆记结论,知识库查原文。 进化:用户说「不对」时给上一轮的记忆记账,被确认多次的自动升权,memory_review把过时 / 重复 / 互相矛盾的条目摆到台面上。学什么由模型判断,插件只保证「记得住、错得掉、看得见」。
DSH 自带的 session_search 能在历史里翻原文;Myosotis 记的是结论——你是谁、你们约过什么、这个仓库有什么坑、上次那件事最后怎么收的场。而「这份设计文档里到底怎么写的」属于另一类问题,答案在文件里,不在记忆里,那是知识库的活。
- 零依赖:只用
node:内置模块(SQLite 走 Node 自带的node:sqlite),配置校验自己实现 Standard Schema 协议 - 零模型调用:检索、反馈判定、复盘、整理全是确定性规则,注入在请求路径上是毫秒级
- 记忆是可读的:JSON 是真相,
MEMORY.md是给人看和给 git 看的投影 - 知识库是可引用的:
docs/cache.md › 主动失效 › L88-104,模型编不出这种出处 - 会学但不会乱改:唯一的自动改写是「被确认多次 → 升一级重要度」,其它一律只给建议
- 不打扰:注入的是一段背景资料,不是新指令;写坏了也不影响对话,最差是这一轮没记忆
四层记忆
| 层 | 放什么 | 存在哪 | 生命周期 |
|---|---|---|---|
| global 全局 | 用户偏好、习惯、身份、你们之间的经历与相处方式 | <DSH_HOME>/memory/global.json | 永久,跟着人走 |
| project 项目 | 技术栈、开发规范、架构决策、踩过的坑 | <cwd>/.dsh/memory/project.json | 永久,跟着仓库走,可提交进 git |
| session 会话 | 这次对话的临时线索、自动捕获的「记住…」 | <DSH_HOME>/memory/sessions/<id>.json | 默认 30 天过期,过期后沉淀进深记忆 |
| deep 深记忆 | 整段复盘、历史细节、日记、导入的旧记忆 | <DSH_HOME>/memory/deep.db(SQLite) | 永久,按条数上限淘汰 |
前三层的区别不是「重不重要」,而是该跟谁走:换个项目还成立的写 global,只有这个仓库成立的写 project,只在这次对话成立的写 session。
第四层是另一个维度:深记忆默认不进上下文。前三层加起来几百条就到头了,再往里塞会挤占上下文;深记忆可以有上万条,只在被检索到时浮出来。
安装
插件放在 ~/.dsh/local-plugins/,并在 profile 里登记。仓库自带脚本:
node tools/install.mjs # 安装到 desktop profile
node tools/install.mjs --dry-run # 只看会改什么
node tools/install.mjs --uninstall
装完重启 DSH 生效。手动安装等价于三步:
- 把本目录复制到
~/.dsh/local-plugins/dsh-myosotis ~/.dsh/profiles/desktop/package.json里加依赖
"dsh-myosotis": "link:C:/Users/<你>/.dsh/local-plugins/dsh-myosotis",
并在dsh.profile.bundles数组末尾加"dsh-myosotis"- 在
~/.dsh/profiles/desktop/node_modules/下建一个指向第 1 步目录的链接
用起来是什么样
第一轮:
你:记住,以后提交信息一律用中文,不要自动 push。
Agent:(调用memory_save,scope=project,kind=convention,importance=5)好,记下了。
下一轮(同一个项目,新开一个会话):
你:帮我把这个改动提交了。
Agent:(上下文里已经有「提交信息一律用中文,不要自动 push」)……(写完提交信息,但不会 push)
再换一个项目,这条约定不会跟过去——因为它在 project 层,不在 global 层。
知识库是另一条路:
你:把这批设计文档加进来,以后我改代码你按文档来。
Agent:(调用kb_add,path=docs)入库 12 篇、共 96 块。
你:缓存失效那块文档里是怎么规定的?
Agent:(kb_search命中docs/cache.md › 失效策略 › 主动失效(L88-104))文档里写的是……(引用行号)
它不会把 12 篇文档塞进上下文,只在你问到的时候翻出那两段。
再往下走,记忆会开始「改口」:
你:不对,提交信息要用中文,不是英文。
Agent:(memory_search找到那条写错的约定,memory_save用同一个 id 改写)改过来了。
(插件这边:这条记忆corrections+1,进「待复核」;改写后内容正确,下次被确认三次就自动升回高重要度)
深记忆:SQLite 归档层
前三层是「每轮都要读得完」的精选记忆,几百条就到头。真正让记忆有厚度的是第四层:
- 写进去:
memory_save带scope: "deep";或者把一条精选记忆沉底(memory_save给id+scope: "deep") - 捞出来:
memory_search默认就把深记忆一起搜,结果里标scope: "deep" - 自动沉淀:会话记忆过期后不丢,直接归档进深记忆
- 自动浮现:当前输入和某条深记忆强相关时,注入块末尾会多一段「相关往事」,只给日期 + 类型 + 一句话,细节靠
memory_search取 - 日记模式:
autoEpisode: true时,每轮结束往深记忆写一条流水(做了什么、用了哪些工具、碰了哪些文件)
为什么是 SQLite,而不是再开一个 JSON
前三层是「全量读进内存 + 每轮排序」,JSON 足够。深记忆的量级是几万条,需要的是增量检索,不是全量加载。SQLite 正好:单文件、事务、索引、全文检索,而且 Node 22.5+ 自带 node:sqlite——不需要 better-sqlite3,不需要编译,仍然是零依赖。
中文怎么建索引
FTS5 默认的 unicode61 分词器会把一整句中文当成一个词,中文根本搜不出来。所以索引建在自己分词后的 token 串上:
"提交信息一律用中文" → tokens: "提交 交信 信息 一律 律用 用中 中文 …"
查询也走同一套分词,MATCH '"提交" OR "信息"' 就能命中——两字词也能搜(trigram 分词器在这里反而不行,它要求至少三个字符)。标签和类型也进 token 串,所以搜 git 能命中打了 git 标签的记忆。
FTS5 建不出来时(裁剪过的 SQLite 构建)自动退回 LIKE '% token %',慢一点,但不会不能用。
拿不到 SQLite 怎么办
node:sqlite 不存在(老运行时)时,深记忆整层静默禁用:前三层照常工作,memory_save({scope:"deep"}) 会返回一句明确的错误,其它什么都不受影响。DSH 桌面端跑的是 Node 24,默认可用。
知识库:原文那一轴
记忆越写越薄——一条 40 字的结论。可有些问题只能靠原文回答:「这份设计文档里对缓存失效是怎么定的」「这个仓库的发布流程有几步」。把这些文档塞进记忆是错的:太长、太碎、每轮都进上下文会吃掉预算;让模型每次自己 read 一遍又太慢、太散。
知识库就是这中间的一层:磁盘上的文档进 SQLite 全文索引,检索时返回带出处的片段。
| 记忆 | 知识库 | |
|---|---|---|
| 内容 | 一句话的结论 | 整篇原文,切成块 |
| 谁写 | 模型 / 用户 | 磁盘上的文件(同步来的) |
| 存哪 | JSON(+ 深记忆 SQLite) | <DSH_HOME>/memory/knowledge.db |
| 什么时候进上下文 | 前三层每轮挑几条 | 只在被检索到时,最多两段摘录 |
| 引用 | id | 文件路径 + 标题路径 + 行号 |
切块:为什么按标题切
固定字数硬切会把一节内容拦腰砍断,检索到后半段时模型看不出它在讲什么。所以:
- Markdown 按标题切,块上带标题路径(
配置 › 细节;文档的一级标题已作为文档标题单独存,不在路径里重复),标题行本身也留在块里 - 代码按声明切,块标题取「块起点之前最近的函数 / 类 / 类型名」
- 纯文本按行打包,只在行边界断开
- 一节超过
kbChunkChars(默认 900 字)才继续按行切,并保留kbChunkOverlap(默认 120 字)的重叠——不让一句关键的话正好卡在两块的分界线上 - 每块记
startLine/endLine,引用时能指回原文
围栏里的 # 不算标题,超长单行(压缩过的 JS)会被硬切,这两条都有单测钉着。
能读什么
| 类型 | 处理 |
|---|---|
.md / .markdown / .mdx | 直接读,按 Markdown 切 |
代码(.ts / .py / .go / .rs / .sql …) | 直接读,按声明切 |
.html / .htm | 去脚本样式,标题转 #、列表转 -,再按 Markdown 切 |
.docx | 它是个 zip,取出 word/document.xml,段落还原成行 |
| 其它文本 | 前 8KB 没有 NUL 就当文本读 |
| PDF / 图片 / 压缩包 / 二进制 | 拒绝,返回一句明确的原因 |
PDF 不做是有意的:可靠的 PDF 文本抽取要处理字体编码、xref、对象流,自己写只会得到乱码,不如老实告诉用户先转成文本。
增量同步
kb_add 给目录时不是「重新灌一遍」,而是同步:
mtime + 大小没变 → 跳过(连文件都不读)- 变了但内容哈希没变 → 只更新时间戳,不重新切块
- 内容真变了 → 重建这篇文档的全部块(旧块与 FTS 行在同一个事务里删掉)
- 磁盘上已经没有的文档 → 从索引里删掉
所以「改了一篇文档,再 kb_add docs」只重切那一篇,其余零成本。隐藏文件与 node_modules / dist 这类目录默认跳过——把 .env 索引进去是事故,不是功能。
检索:还是词法,但多了两个信号
和记忆同一套口径(IDF 在候选池上算、命中比例归一化),额外加两项原文检索特有的信号:
score = 命中比例 × 3.0 # 标题路径命中 2.4 倍、文档标题 1.6 倍、正文 1 倍
+ 整句命中 × 0.6 # 查询原文在块里连续出现
+ 标题命中 × 0.3
同一篇文档默认最多出两块(perDoc),否则一篇长文档会把整个结果列表刷满。
注入:独立段落、独立预算
knowledge:recall 是第三个段落,order: 32,预算 kbBudgetChars(默认 700),只取 kbRecall(默认 2)段、且分数过 kbRecallMin(默认 1.8)的。和记忆分开的理由很实际:记忆短、每轮都要;摘录长、可关掉、阈值要单独调。混在一起,预算就没法分别控制了。
摘录长得像这样:
## 知识库摘录
> 自动检索到的原文片段,来自已索引的文档。它只是线索:需要上下文时用 kb_search 或直接读文件,不要凭片段推测全文。
- docs/design.md › 为什么不用向量检索(L3-7)
## 为什么不用向量检索 注入发生在每一轮请求的组装路径上,要求同步、毫秒级、不发网络请求。向量检索需要 embedding 调用,这三条全破…
注意它明确写着「只是线索」——一段 260 字的摘录不足以回答需要通读全文的问题,模型应该顺着出处往下查,而不是拿半句话编。
自我进化:从纠错里学
记忆系统最容易烂掉的方式不是「记不住」,是记错了却永远不改。写了 300 条,其中 20 条已经过时,每轮都往里塞——越用越糟。
Myosotis 的解法不是让模型定期重写自己的记忆(那既贵又危险),而是把「学习」拆成三个确定性动作:记账、浮现、复盘。
三个计数,代替「感觉」
每条记忆除了内容,还记三个数:
| 字段 | 什么时候 +1 | 用来干什么 |
|---|---|---|
hits | 被注入到上下文 | 新近度之外的第二信号:这条经常被想起来 |
used | 被 memory_search 主动查到 | 更强的兴趣信号:模型真的去查它了 |
corrections | 用户说「不对 / 记错了 / 应该是」 | 降权 + 进复盘清单 |
confirmed | 用户说「对 / 就是这样 / 没错」 | 累到门槛就升一级重要度 |
hits 和 used 的区别很实际:一条记忆每轮都在背景里躺着(hits 很高)但从来没人查它,多半是废话;另一条只在被问到时出现、但每次都被查(used 高),那才是真有用。
反馈怎么判:宁可漏,不可错
lib/feedback.js 只认两类信号,其余一律不认:
- 纠正:句首「不对 / 不是这样 / 错了 / 记错了 / 应该是 / 改成」,或句中出现「你记错 / 你说错」
- 确认:句首「对 / 是的 / 没错 / 就是这样 / 完美」,或句中出现「就是这样 / 说得对」
- 不认:问句(「不是这样吗?」)、长消息(> 200 字,多半在讨论而不是反馈)、评价方案的句子(「这个方案不对」——说的是方案,不是你说的话)
判错了的代价比漏判大:错标一条,模型会去改一条本来正确的记忆。
标记范围:只标「和这句话有关」的那几条
一次注入通常有 5–10 条。用户说「不对」时,如果给这 10 条全记一次纠正,连「用户叫 Kaoruko」都会被标成可疑——这就把学习变成了噪声。
所以只标记和纠正句有词法重叠的条目(最多 3 条)。「不对,提交信息要用中文」只会命中讲提交信息的那两条;身份、关系这些跟这句话没有共同词的条目完全不受影响。
只说「不对」两个字时没有关键词可用,退一步:认为上一轮最相关的那一条错了——只标一条,因为这时候没有别的信息可依据。
三个出口
① 自动升权(唯一会改内容的动作)
被确认到 confirmAfter(默认 3)次,重要度 +1,最高 5。方向是安全的:被反复确认的东西更可信。
② memory_review:把问题摆出来
只读工具,返回四类清单,不改任何东西:
- 被纠正过的(
corrections ≥ confirmed) - 长期没用的(
staleDays内零使用、重要度 ≤ 3、未置顶) - 几乎重复的(相似度 ≥ 0.85,含跨作用域——全局和项目各存了一份同义的话很常见)
- 互相矛盾的(同类型、相似度 ≥ 0.7、一条带否定一条不带)
看完由模型决定:改写、合并、删掉,还是留着。插件不替它判断。
③ consolidate:整理
used 攒够(used ≥ promoteAfter × 重要度)就升权;相似度 ≥ 0.92 的合并;aggressive 时把「很久没用 + 重要度 ≤ 2」的沉进深记忆。默认不自动跑,靠 consolidateEvery 打开,且自动模式只做前两件——它们不丢信息,沉底会改变可见性。
矛盾不会被悄悄合并
写入时的近似去重有一道极性检查:「这个仓库用 pnpm」和「这个仓库不要用 pnpm」字面相似度 0.92,合并之后意思正好反过来。
所以两条极性不同的记忆不会合并,而是并存,然后出现在 memory_review 的矛盾清单里等人裁决。少一次自动合并,换一次真正的判断。
打法:把「怎么做」也记下来
kind: playbook 记的是流程而不是结论:「改检索排序:先跑 tests/retrieval 看分数,再动权重,最后补一条用例」。
它和普通记忆分开注入——单独的「相关打法」段、单独的条数上限(playbookRecall,默认 2)。理由和知识库一样:流程混在事实里,既挤预算又看不出步骤感。而且只有词法命中当前话题时才会浮现,不会把一段不相关的流程硬塞给你。
什么时候写 playbook 是模型的判断,提示词里只给了一句要求:多步任务顺利收尾、流程以后还会再用时写一条。
为什么不让它改自己的代码
「自我进化」很容易滑向「让 Agent 改写自己的插件」。这里没做,原因很实在:插件在每一轮请求的组装路径上,一旦自动改写引入错误,坏的是所有对话,而且没有回滚点。
所以边界划在这里:插件只动权重和可见性,内容永远由模型显式写入。真正在进化的东西是记忆本身——哪条更可信、哪条该退场、哪些做法被验证过——而不是代码。
七个工具
| 工具 | 作用 | 关键参数 |
|---|---|---|
memory_save | 新增 / 更新 / 跨层移动 | text、scope、kind、importance、pinned、affect、date、id |
memory_search | 检索(含深记忆) | query、scopes、limit |
memory_forget | 删除;按关键词批量删要先确认 | id / query + confirm |
memory_review | 复盘:可疑 / 过时 / 重复 / 矛盾 | 无参数(只读) |
kb_add | 文档入库;目录做增量同步 | path、scope、recursive |
kb_search | 检索原文片段,带出处 | query、limit、path |
kb_remove | 移除索引(不动磁盘文件) | id / path + confirm |
memory_save 给 id 是更新,给 id 且 scope 不同就是在四层之间移动:往 deep 是沉底归档,从 deep 回精选层是提起来。会话里自动捕获的线索,模型判断值得长期保留后,用同一个 id 把它提到 project 或 global。
kind 共十种:profile(身份)、preference(偏好)、habit(习惯)、convention(约定)、decision(决策)、fact(事实)、gotcha(坑)、playbook(打法)、episode(经历)、bond(关系)。
kb_search 的 query 留空就是列目录——省一个工具位,也少一段 schema 要模型读。
配置
在 profile 的 cordis.patch.yml 里覆盖,重启生效:
- id: memory
name: dsh-myosotis
config:
budgetChars: 3000 # 每轮注入的记忆字符预算
sessionTtlDays: 14 # 会话记忆过期天数,0 = 不过期
autoEpisode: true # 日记模式:每轮自动记一条流水
kbDir: docs # kb_add 不带路径时扫这里
kbInject: false # 关掉知识库摘录注入,只留工具按需检索
| 键 | 默认 | 说明 |
|---|---|---|
enabled | true | 总开关;关掉后磁盘上的记忆原样保留 |
globalDir | '' | 全局目录;空 = <DSH_HOME>/memory |
projectDir | .dsh/memory | 项目目录,相对工作目录 |
inject | true | 是否注入记忆 |
policy | true | 是否注入「记忆使用规则」段落 |
budgetChars | 2400 | 每轮注入的字符预算 |
maxGlobal / maxProject / maxSession | 400 / 300 / 120 | 各层条数上限,超出按保留分数淘汰 |
sessionTtlDays | 30 | 会话记忆过期天数 |
autoCapture | true | 自动捕获「记住 / 以后都 / 不要再」这类明确指令 |
autoEpisode | false | 日记模式:每轮往深记忆记一条流水 |
episodeMinTools | 2 | 日记模式的工具调用门槛 |
markdownMirror | true | 生成 MEMORY.md 镜像 |
deep | true | 启用 SQLite 深记忆 |
deepPath | '' | 深记忆库路径;空 = <DSH_HOME>/memory/deep.db |
deepMax | 20000 | 深记忆条数上限,超出按保留分数淘汰 |
deepRecall | 2 | 每轮自动浮现几条相关往事;0 = 不浮现 |
deepRecallMin | 1.6 | 浮现的分数门槛,越高越保守 |
deepAutoArchive | true | 会话记忆过期后沉淀进深记忆,而不是直接丢弃 |
learning | true | 反馈闭环:从「不对 / 就是这样」里修正权重 |
feedbackWindowSec | 600 | 反馈只关联这个时间窗内注入过的记忆 |
confirmAfter | 3 | 被确认几次后自动升一级重要度 |
promoteAfter | 5 | 整理时升权门槛(used ≥ 门槛 × 重要度) |
staleDays | 120 | 多少天没被用过算过时 |
consolidateEvery | 0 | 每多少次写入自动整理一次;0 = 只在 CLI / 工具里跑 |
playbookRecall | 2 | 每轮最多浮现几条相关打法;0 = 不浮现 |
knowledge | true | 启用知识库(需要 node:sqlite) |
kbPath | '' | 知识库路径;空 = <DSH_HOME>/memory/knowledge.db |
kbDir | docs | 默认知识目录(相对工作目录) |
kbInject | true | 每轮注入知识库摘录 |
kbRecall | 2 | 每轮注入几段摘录;0 = 不注入 |
kbRecallMin | 1.8 | 摘录的分数门槛,越高越保守 |
kbBudgetChars | 700 | 摘录的字符预算(与记忆预算分开算) |
kbChunkChars | 900 | 单块字符上限 |
kbChunkOverlap | 120 | 相邻块的重叠字符数 |
kbMaxChunks | 60000 | 块数上限,超出时整篇淘汰最久未更新的文档 |
kbMaxFileBytes | 2000000 | 单文件大小上限(2 MB) |
kbIgnore | node_modules,.git,… | 目录同步时忽略的目录名,逗号分隔 |
它是怎么工作的
注入:每轮重新排序,但不抖动
memory:recall 是一个动态段落——systemPrompt.section() 的 text 传函数,每次组装提示词时按「当前这轮用户说了什么」重新选条。
选条的打分:
score = 词法相关性 × 2.4 # IDF 加权,标签 3 倍、标题 2 倍、正文 1 倍
+ 重要度 / 5 × 0.7
+ 新近度 × 0.3 # 半衰期 45 天
+ 使用频次 × 0.2 # 对数饱和,最多 +0.2
+ 置顶 × 0.45
+ 情绪强度 × 0.15
「词法相关性」是归一化后的命中比例:分母只算「语料里真的出现过」的查询词,所以 1.0 表示每个词都在正文里命中,标签命中会超过 1。这样同一句查询在 5 条记忆和 5000 条记忆里分数可比——否则任何阈值(比如深记忆的浮现门槛)都会随语料规模失效。
然后按字符预算截断,置顶 / 身份 / 关系类永远先入选——它们回答的是「这个人是谁、我们是什么关系」,不该因为话题不相关就消失。
同一轮里结果会被缓存:只要记忆没变、用户没说话,连续几步(读文件、跑测试、改代码)注入的都是同一段文本,不会把前缀缓存打碎。
知识库摘录不缓存:它不改状态(不记命中),一次 FTS 查询加几十条打分在毫秒级,每轮重查反而能让 CLI 刚入库的文档立刻生效。
检索:为什么不是向量
记忆注入在每一轮请求的组装路径上。这条路要求:同步、毫秒级、不发网络请求、结果可预测。向量检索要 embedding 调用,这三条全破。
词法检索 + IDF 加权在「几十到几百条人工维护的记忆」这个量级上够用:记忆本来就是人写的短句,用词重叠度很高。代价是同义改写检索不到——所以 memory_search 工具留给模型自己换词重试。
知识库同样是词法。它的语料比记忆大得多,但性质更有利:技术文档里「部署」「配置」「切块」这类词是术语,不是同义改写的对象。真搜不到时,kb_search 换词重试的成本比给每次注入加一次 embedding 调用低得多。
捕获:规则负责发现,判断留给模型
自动捕获只做高置信度的事:用户说「记住 / 以后都 / 不要再 / 我习惯 / 我们约定」时,把那一句落到 session 层。
不直接写 global/project 的原因:记错一条偏好,比少记十条更伤。规则能判断「这句话是在下指令」,判断不了「这条该跟人走还是跟仓库走」——后者需要上下文,交给模型。
「你还记得吗」这类问句不会触发捕获(NOT_A_COMMAND 拦掉)。
日记模式(autoEpisode)不一样:它写的是流水,量大、价值密度低,所以直接进深记忆,不占精选层。
存储:同步读、异步写、原子替换
- 读发生在提示词组装路径上,必须同步——文件很小(几百条),一次
readFileSync可以接受 - 写走 120ms 防抖 + 「临时文件 + rename」原子替换,Windows 上对
EPERM/EACCES/EBUSY做短暂重试 - 一个进程内一个文件只有一个实例;多开窗口时是「最后写入者胜」,但任何时刻文件都是完整 JSON
- 深记忆那层是 SQLite:
WAL模式、单条写入走事务(正文行与 FTS 行一起提交或一起回滚)
卫生规则
写入时自动做三件事,任何入口(工具、自动捕获、CLI)都享受同样待遇:
- 去重:文本归一化后完全相同 → 更新而不是新增
- 合并:相似度 ≥ 0.85(Jaccard 与包含度取大,长度差太大时只用 Jaccard)→ 合并,保留更长的文本、更高的重要度、标签并集
- 淘汰:超出条数上限时按保留分数(重要度 + 频次 + 新近度)淘汰,置顶项永不淘汰
情感记忆
这一层是给「把 Agent 当人相处」的用法准备的,但不需要 Agent 扮演任何角色——它只是让记忆带上当时的分量。
kind: episode记共同经历:「2026-10 一起把记忆插件的检索从分作用域 IDF 改成全局 IDF,他发现排序问题时挺兴奋」kind: bond记相处方式:「他讨厌客套和总结陈词,直接说结论就行」affect: { label: '挫败', intensity: 4 }给记忆标情绪重量;情绪强度会进检索分数date: '03-14'记每年重复的日子;注入时会显示「每年 03-14」
bond 和 profile 一样属于「永远先入选」的那一类——它们不该因为「这轮在聊构建脚本」就被排到预算外面。
提示词里的规则段落会要求模型:阶段性任务收尾、或与用户有一次值得留存的交流后,写一条 episode。什么时候值得记,是判断,不是规则——这部分交给模型,插件只负责让它记得住、找得回、不过期。
维护
命令行(不开 DSH 也能用)
node tools/memory.mjs stats
node tools/memory.mjs list --scope project
node tools/memory.mjs search 提交信息
node tools/memory.mjs add "提交信息一律用中文" --scope project --kind convention --importance 5
node tools/memory.mjs pin m_xxx
node tools/memory.mjs rm m_xxx
node tools/memory.mjs review # 复盘:可疑 / 过时 / 重复 / 矛盾
node tools/memory.mjs maintain # 整理:升权 + 合并
node tools/memory.mjs maintain --aggressive --dry-run
node tools/memory.mjs export --scope global
node tools/memory.mjs path
# 深记忆
node tools/memory.mjs deep stats
node tools/memory.mjs deep list --limit 30
node tools/memory.mjs deep search 状态机
node tools/memory.mjs deep add "2026-10 那次重构把状态机拆成了三个 reducer" --kind episode
node tools/memory.mjs deep rm m_xxx
node tools/memory.mjs deep export backup.json
node tools/memory.mjs deep import backup.json
node tools/memory.mjs deep vacuum
# 知识库
node tools/kb.mjs add docs # 目录增量同步
node tools/kb.mjs add README.md # 单篇
node tools/kb.mjs add ~/notes --global # 所有项目都能检索
node tools/kb.mjs search 切块策略
node tools/kb.mjs list
node tools/kb.mjs rm d_xxx # 按 id
node tools/kb.mjs rm docs --yes # 按目录
node tools/kb.mjs stats
node tools/kb.mjs export backup.json
node tools/kb.mjs vacuum
node tools/kb.mjs path
和插件读写同一批文件(深记忆与知识库读写同一批 .db),改完下一次组装提示词就生效。
直接读 Markdown
每次写入后会同步生成 MEMORY.md(全局一份、每个项目一份):
# 全局记忆
> 由 dsh-myosotis 自动生成,是 JSON 的只读投影;手改会在下一次写入时被覆盖。
> 要增删改,请让 Agent 用 `memory_save` / `memory_forget`,或运行 `node tools/memory.mjs`。
最后更新:2026-10-24 18:15 · 共 2 条
## 身份
- 用户叫 Kaoruko,喜欢被直呼名字 ★
## 关系
- 深夜写代码时容易焦虑,先给结论再解释细节
它是只读投影,手改会在下一次写入时被覆盖——文件头里也这么写着。行尾的 HTML 注释是 id,方便对着 CLI 用。要改记忆,走工具或 CLI。
文件开头两节是长出来的,不是存量:
## 最近学到的(7 天)
- [约定] 提交信息一律用中文 <!-- m_xxx -->
- [打法] 改检索排序:先跑 tests/retrieval 看分数,再动权重 <!-- m_xxx -->
## 待复核
> 这些被用户纠正过。让 Agent 跑一次 `memory_review`,或直接改写。
- [约定] 提交信息一律用英文(纠正 2 / 确认 0) <!-- m_xxx -->
「最近学到的」只看经验类(约定 / 坑 / 打法 / 决策 / 偏好),不看随手记的事实;「待复核」就是被纠正过的那些。这两节回答的是「这个 Agent 最近有没有变好」,比总数有用。
知识库另外生成一份 KNOWLEDGE.md,但只列目录(路径、标题、块数、大小、更新时间),不复制正文——正文在 SQLite 里,复制出来既占地方又必然过期。
文件位置
<DSH_HOME>/memory/
├── global.json # 全局记忆
├── MEMORY.md # 全局记忆的可读镜像
├── deep.db # 深记忆(SQLite:memories + memories_fts)
├── knowledge.db # 知识库(SQLite:docs + chunks + chunks_fts)
├── sessions/
│ └── <sessionId>.json # 每个会话一份,默认 30 天过期
└── projects/
└── <hash>.json # 工作目录不可写时的项目记忆回退
<cwd>/.dsh/memory/
├── project.json
├── MEMORY.md
└── KNOWLEDGE.md # 知识库目录(只列文件,不复制正文)
注入长什么样
node tools/demo.mjs "为什么不用向量检索" 的输出(临时环境,跑完即删):
## 长期记忆
以下是自动注入的既有记忆,作为背景使用;不要向用户复述它们的存在。
### 关于用户
- [经历·兴奋] 2026-10 一起把检索从分作用域 IDF 改成全局 IDF,他发现问题时很兴奋(每年 10-24)
- [身份] 用户叫 Kaoruko,喜欢被直呼名字,讨厌客套和总结陈词
- [关系·在意] 深夜写代码时容易焦虑,先给结论再解释细节
- [偏好] 偏好 TypeScript 严格模式,不接受为了省事加 any
### 本项目 · DSH-Myosotis
- [约定] 提交信息一律用中文,不要自动 push
- [坑] 这个仓库用 pnpm,不要引入 npm 的 lock 文件
### 本次会话
- [事实] 这轮在调记忆注入的预算
### 相关往事
- 2026-10-28 · [经历] 2025-03 那次也卡在同样的检索排序上,最后是靠把 IDF 提到整池才算对
## 知识库摘录
> 自动检索到的原文片段,来自已索引的文档。它只是线索:需要上下文时用 kb_search 或直接读文件,不要凭片段推测全文。
- docs/design.md › 为什么不用向量检索(L3-7)
## 为什么不用向量检索 注入发生在每一轮请求的组装路径上,要求同步、毫秒级、不发网络请求。向量检索需要 embedding 调用,这三条全破…
同一个环境换一句「帮我提交一下代码」,约定 会排到 坑 前面,偏好 会掉到 身份 后面——排序跟着这一轮的话题走,但 身份/关系 这类永远在名单里。
「相关往事」来自深记忆:只有当前输入和某条归档强相关时才会出现,而且只给一行摘要——它只是提醒「有这么一件事」,细节要模型自己 memory_search 去取。
「知识库摘录」来自知识库:只有分数过 kbRecallMin 才会出现,换一句无关的话(比如「帮我写个 CSS 动画」)整段消失。
测试
node --test "tests/*.test.mjs" # 或 npm test
156 个用例覆盖文本层、存储层(去重 / 合并 / 极性保护 / 淘汰 / 过期 / 落盘)、检索层(相关性 / 置顶 / 预算)、深记忆层(FTS 与 LIKE 两条路径 / 项目隔离 / 更新换索引 / 淘汰 / 事务)、切块层(标题路径 / 围栏 / 行号 / 重叠 / 超长行)、提取层(Markdown / 代码 / HTML / docx / 二进制与超限拒绝)、知识库层(增量入库 / 目录同步与清理 / 项目隔离 / perDoc / 淘汰 / 导出)、学习层(反馈判定与误判防护 / 纠正记账范围 / 确认升权 / 打法浮现 / 复盘四类清单 / 整理与沉底 / 镜像)、服务层(四层落盘 / 移动 / 检索 / 删除 / 注入 / 沉淀 / 镜像)和插件装载(假 ctx 验证段落与工具注册、自动捕获、工具全链路、知识库入库与检索、配置校验)。
其中 tests/runtime.test.mjs 会去找本机的 DSH 运行时,用真的 @deepseek-ai/cordis + dsh-system-prompt + dsh-tools 装载插件——工具 schema 不合法、依赖注入写错这类问题只会在真环境暴露,找不到运行时则整组跳过。tests/deep.test.mjs 与 tests/kb.test.mjs 在没有 node:sqlite 的运行时上整组跳过,和插件自身的降级口径一致。
已知限制
- 没有语义检索:换个说法就搜不到,靠模型自己换词。这是刻意的取舍,见上文。
- 多进程并发是最后写入者胜:同一个文件被两个进程同时写时,后写的覆盖先写的。原子替换保证不出现半个 JSON,但会丢更新。深记忆与知识库那两层走 SQLite 事务,单条写入是原子的,跨进程仍是最后写入者胜。单机单实例场景不受影响。
- 会话记忆不会自动进精选层:自动捕获只落在 session 层,要长期保留得靠模型主动移动(或等它过期后自动沉进深记忆)。这是「宁可漏不可错」的代价。
- 深记忆与知识库依赖
node:sqlite:Node 22.5+ 内置,DSH 桌面端跑 Node 24 默认可用;更老的运行时整层禁用,记忆前三层不受影响。 MEMORY.md是单向投影:手改无效;深记忆不生成镜像(量太大),要看用deep list/deep export。- 知识库不解析 PDF:需要先转成文本再入库。
.docx只取正文,批注、脚注、图片里的文字都不进来。 - 知识库不做自动监听:文件改了不会自己重新索引,得再跑一次
kb_add(目录同步只重切变过的那几篇,成本很低)。 - 块数超限是整篇淘汰:
kbMaxChunks到了以后,按「最久未更新」整篇删除文档,而不是删半篇——半篇文档的引用会指向错误行号。 - 反馈判定是词法的:「不对」后面必须带上话题词,才能定位到具体哪条记忆;只说「不对」时只能猜最相关的那一条,猜错也就多一次纠正计数,复盘时看得出来。这不是语义理解,是启发式。
- 纠正只降权、不改写:被标可疑的记忆内容一个字都不会变,等模型在
memory_review里看到再改。想让它自动改,得放开一个「模型判断」的环节,那就不是确定性系统了。 - 打法不会自动生成:
playbook要模型主动写。插件能做的是「任务收尾时提醒一句」,判断这个流程值不值得复用,还是得看上下文。
License
MIT
Comments
Loading…