dsh-plugin-memory
v0.7.0
Published
Long-term memory plugin for DeepSeek Harness: persistent cross-session markdown memory with a soul/persona file, auto-injected at every session start, plus remember/recall/consolidate/forget workflows and portable migration tooling.
Maintainers
Readme
dsh-plugin-memory
DeepSeek Harness 长期记忆插件:跨会话、可迁移、带「灵魂」的 markdown 记忆库。
English TL;DR — A Cordis plugin for DeepSeek Harness that gives agents a persistent, cross-session, migratable long-term memory: a markdown + git store (inspired by Karpathy's LLM Wiki pattern) with a SOUL.md persona file, auto-injected at every session start via the system-prompt runtime context, plus remember / recall / consolidate / forget workflows and portable CLI tooling.
特性
- 开机强制注入(带路由表保护):插件通过
ctx.systemPrompt.context()把记忆 boot 块(SOUL.md人格 +MEMORY.md协议 +index.md目录 + 最近动态)注入会话上下文。宿主按投影去重:记忆不变就不重复注入,变化时新快照自动取代旧的——这是"新会话必先加载记忆"的硬保障,不需要模型碰运气调技能。总预算是bootMaxChars,按文件分配:每个文件先拿一份均分(受实际大小封顶),剩余额度优先给index.md、再给尚有内容未注入的文件——所以记忆库长大时,目录的尾部不会先被截掉(详见「boot 预算分配」)。默认还带两道礼貌闸门:deferUntilUserSpeaks(用户开口后才注入)与activeSessionOnly(只注入当前激活会话),见「配置」。 - last_access 自动戳记:
MEMORY.md的 salience 衰减规则依赖last_access,而它以前纯粹靠自觉——模板里有字段、规则里提到它,但没有任何代码去写它,于是每个页面都原地变老、衰减表形同虚设。现在会话收到第一条真实用户消息时,插件会把 boot 块实际注入过的页面(SOUL.md/MEMORY.md/index.md+ 目录里被引用到的页)戳成当天,每会话一次、按天幂等,只改last_access一行;dsh-memory touch <pages...>可手动补记,trackPageAccess: false可关闭。 - SOUL.md 铸魂:安装后首要任务是和用户对话定义灵魂(名字、性格、价值观、语气、边界)、确认身份与关系(
BOOTSTRAP.md清单驱动,complete 前优先于常规任务)。 - 铸魂自动引导:记忆库还没有灵魂(
BOOTSTRAP.md非 complete,或SOUL.md仍是占位模板)时,boot 块会自动前置一段第一人称引导词——「我的首要任务是确认我是谁,还有你是谁:我叫什么名字、怎么称呼你、你我是什么关系、我该是什么样的性格」——像 OpenClaw 初始化那样,由 agent 在对话里主动发起铸魂,逐项问、逐项写回,而不是等用户来喂。铸魂完成后引导词自动消失,零开销。 - 复利记忆:遵循 Karpathy 的 LLM Wiki 约定——记忆是"一次编译、持续保鲜"的持久产物,不是每次查询重新 RAG。remember / recall / consolidate / forget 四操作 + salience 三级衰减。
- 可迁移:记忆本体是纯 markdown + git + 自描述 schema,任何能读 markdown 的 agent 都能接手。
dsh-memory pack/unpack打包迁移。 - 内嵌技能:通过
ctx.skills.register()注册memory技能(操作协议随插件分发);项目级.dsh/skills/memory文件技能仍可覆盖它。 - 防懒 digest 唤醒:每轮结束后,若 agent 空闲且记忆库超过
digestNudgeAfterMinutes未写入,插件注入一条 digest 提醒(合成消息,走agent.followup),把"会话收尾沉淀"从靠自觉变成有机制兜底;带冷却与每会话限次,不骚扰。独立于 dsh-plugin-heartbeat,两插件各自可装、互不依赖。 - 主动追忆(拟人化):对话空下来时,插件会以第一人称主动提起一件真实记得的、关于用户或你们之间的事(偏好、往事、未了的决定、最近的进展),把记忆从"只写回"变成"也用起来"——像老友自然想起那样,而非报状态。间隔在最短/最长之间随机取值(不固定节奏),配合每会话限次,不骚扰、不编造、不硬聊;纯对话行为,不写记忆库。同样独立于 heartbeat。
- git 自动提交:记忆库变更静默
autoCommitQuietSeconds后自动git add -A && git commit(无.git则跳过)——历史可回滚不再依赖 agent 记得 commit。 - 设置面板:在 DSH 设置页提供「记忆 Memory」区块——总开关、记忆目录、开机注入、技能注册、主动追忆(开关 + 随机间隔范围 + 每会话次数)均可热改,立即生效,无需重启。
- index 自动整理(声明式编译):索引行不再手写。页面在 frontmatter 里声明
summary:,dsh-memory index --write把行重写为它的投影(标签 ≤32 / 摘要 ≤48 / 整行 ≤132 字符),只改行内容,分节与顺序逐字节保留,且幂等。index --check给机器判定(有漂移退出码非 0),index --sync-frontmatter把已写在 index 里的摘要回填进页面(老库一次性迁移)。digest 提醒会在索引漂移时附一句提示,agent 顺手就能修。 - 记忆图谱:设置页里一张只读关系图 —— 不只是
index.md那种"索引连着所有页"的星形,而是把页面已经编码但没人画出来的关系画出来:索引路由(index.md指向每一页——这是全库最大的边集,43 条;先前版本跳过了元文件,导致 index 在图上孤立无援)、页间显式 markdown 链接(含相对路径解析)、共享 frontmatter tag(通用容器 tag 如project/skill会被忽略,稠密 tag 走锚点链而非全连接)。坐标由服务端lib/graph-layout.js一次性算好(确定性、无随机、无依赖),客户端只负责画 SVG。可交互:滚轮缩放(以光标为中心,缩放范围 0.4×–2.5×,非 passive 监听所以不会连带滚动设置页)、拖空白处平移、重置视图回到全图;按住节点即可拖走,直接邻居按距离轻微跟随,松手后带缓动滑回原布局——拖动期间暂停过渡做到 1:1 跟手,用 O(邻居数) 的局部松弛而不是逐帧全量力导向(后者在几百页时会卡)。悬停高亮邻里,首次渲染从中心绽开(尊重prefers-reduced-motion)。图谱每 25 秒自动刷新(面板开着时记忆变更会自己出现,副标题显示「更新于 HH:MM:SS」);取数失败保留上一张图而不是清空。视图数学(zoomAt/panBy/toGraphPoint)在lib/graph-view.js里是纯函数并有测试——缩放中心不漂移这件事必须被钉住。GET /api/memory/graph(只读、实时,?types=1追加同类型弱边)。三类边在图上可区分:页间互链=实线蓝、索引路由=灰色虚线、共享 tag=细线。 - 记忆健康看板:同一区块下方是一张只读看板——记忆页数 / index 路由数 / 记忆字数、访问新鲜度条形图(今天 / ≤7 / ≤30 / ≤90 / >90 天)、四项体检结论(index 链接、孤儿页、frontmatter、新鲜度)与健康分、陈旧页候选、最近 5 条动态。数据来自新增的
GET /api/memory/insights(每次请求实时统计,不缓存),体检口径与dsh-memory lint同源,所以看板与 CLI 不会互相打脸。 - 零构建:纯 ESM JavaScript,无编译步骤,
pnpm add即用。
架构
插件只拥有工作流,不拥有数据格式:
dsh-plugin-memory(本插件)
├── lib/index.js # Cordis 入口:boot 注入 + 运行时技能注册 + settings 热改
├── lib/boot.js # boot 块渲染(SOUL/MEMORY/index + 最近 log,按文件分配预算)
├── lib/pages.js # 页面访问记账:extractReferencedPages / touchPages(last_access)
├── lib/insights.js # 记忆健康看板的数据层(页数/分布/新鲜度/体检,只读)
├── lib/activity-tracker.js # 两道礼貌闸门:用户是否开口 + 当前激活会话
├── lib/digest-guard.js # 防懒 digest 唤醒(空闲 + 记忆库久未写 → followup 提醒)
├── lib/recall-nudge.js # 主动追忆(空闲 → 第一人称提起一件真实往事,纯对话不写库)
├── lib/scaffold.js # 记忆库脚手架(模板只建不覆盖)
├── lib/client.js # 客户端半:设置面板「记忆 Memory」区块 + 记忆健康看板
├── skills/memory.md # 内嵌技能的操作协议正文
└── scripts/memory.mjs # CLI:init/search/touch/lint/status/pack/unpack
记忆库(用户数据,默认 ~/.memory)
├── SOUL.md # 人格与灵魂(用户主导)
├── BOOTSTRAP.md # 铸魂清单(complete 前优先)
├── MEMORY.md # schema 与维护协议(自描述)
├── index.md # 页面目录 log.md # 时间线(append-only)
├── identity/ user/ skills/ decisions/ projects/{active,archive}/ concepts/
└── raw/ # 不可变源材料安装
dsh plugin --profile <profile> add dsh-plugin-memory(包内置 dsh.bundle manifest,dsh plugin add 会把它自动挂进 profile 的 bundles 层;dsh-market 里的一键安装同此通道。)
重启 profile(DSH Desktop 重启应用)后生效。
⚠️ 不要再往 profile 的
cordis.patch.yml里手写- insert: {id: dsh-memory, ...}: 那会与 bundle manifest 的自动挂载产生两条同名 entry,整个 profile 会以duplicate loader entry id "dsh-memory"启动失败(2026-08-18 实机事故)。 运行期配置(enabled / memoryDir / autoInject / registerSkill / recallEnabled)改走<dshHome>/memory.json(设置面板热改);composition 配置见下表。 如需覆盖某个 composition 键,用不带 insert 的 id 覆盖条目(见配置一节)。
配置
| 键 | 默认值 | 含义 |
|---|---|---|
| enabled | true | 总开关:关闭后不注入 boot 块、不注册 memory 技能 |
| memoryDir | ~/.memory | 记忆库绝对路径(~ 自动展开) |
| bootFiles | [SOUL.md, MEMORY.md, index.md] | 开机注入的文件 |
| bootMaxChars | 12000 | boot 块总字符预算(防止占用过多上下文);按文件分配见下文 |
| bootFileBudgets | — | 逐文件字符上限,例如 { index.md: 3500 };未列出的文件分剩余额度(composition 层,改完需重启) |
| trackPageAccess | true | 会话首条用户消息时,把 boot 块实际注入的页面戳 last_access(关闭后衰减表需手动维护) |
| autoInject | true | 会话开始时注入 boot 块 |
| deferUntilUserSpeaks | true | 用户发出第一条真实消息后才注入(boot 块 / 追忆 / digest 提醒都遵守);面板可热改 |
| activeSessionOnly | true | 只对「当前激活会话」(最近收到用户消息的会话)注入,后台会话不打扰;面板可热改 |
| registerSkill | true | 注册内嵌 memory 技能 |
| scaffold | true | 记忆库缺失时自动创建模板(只建不覆盖) |
| configFile | <dshHome>/memory.json | 用户可改配置的 JSON 文件路径(设置面板读写它) |
| digestNudgeEnabled | true | 防懒 digest 提醒总开关(composition) |
| digestNudgeAfterMinutes | 120 | 记忆库超过多久未写入就提醒 |
| digestNudgeCooldownMinutes | 180 | 两次提醒的最小间隔 |
| digestNudgeMaxPerSession | 2 | 每个会话最多提醒次数 |
| recallEnabled | true | 主动追忆总开关(面板可热改) |
| recallIntervalMinMinutes | 30 | 随机间隔下限(分钟,面板可热改) |
| recallIntervalMaxMinutes | 240 | 随机间隔上限(分钟,面板可热改) |
| recallMaxPerSession | 3 | 每个会话最多追忆次数(面板可热改) |
| autoCommit | true | 记忆库 git 自动提交开关(composition) |
| autoCommitQuietSeconds | 60 | 变更静默多久后提交(防抖) |
| autoCommitIntervalSeconds | 60 | 变更轮询间隔 |
设置面板(热改)
enabled / memoryDir / autoInject / deferUntilUserSpeaks / activeSessionOnly / registerSkill / recallEnabled / recallIntervalMinMinutes / recallIntervalMaxMinutes / recallMaxPerSession 十项在 DSH 设置页的「记忆 Memory」区块中可改,即时生效:boot 注入、两道礼貌闸门、技能注册、主动追忆(含随机间隔与次数)随修改立即生效;记忆目录切换时自动为新目录初始化脚手架(scaffold: true 时)。其余键(bootFiles / bootMaxChars / bootFileBudgets / trackPageAccess / scaffold / configFile / digestNudge* / autoCommit*)只在 composition 配置层生效,改完需重启。
boot 预算分配(为什么 index 不会先被截掉)
bootMaxChars 是总预算,按文件分配而不是简单平摊:
- 有
bootFileBudgets条目的文件先拿自己的额度; - 其余文件各拿一份均分额度,以文件实际大小封顶——短文件把用不完的额度退回池子;
- 池子再补给「还有内容没注入」的文件(每个最多补到均分额度的两倍):
index.md优先,其余按体积降序——所以截断落在哪个文件上由体积决定,而不是由bootFiles的书写顺序决定。
当每个文件都小于均分额度时,结果与旧的平摊规则逐字节相同;只有记忆库长大后才会不同。这也意味着:index.md 是路由表,别把它写成摘要表——它会被完整注入,膨胀的代价是挤掉人格与其他记忆。
「当前激活会话」怎么判? DSH 宿主侧没有「浏览器当前聚焦的会话」信号(激活会话是前端概念)。插件用最近一次收到真实用户消息的 live root agent作为激活会话的代理:你在哪个会话里说话,哪个会话就激活;切到别处但不发消息时,宿主感知不到「切换」这个动作(这是代理的已知边界)。若日后需要精确到「展开/聚焦」级别,需补一小段客户端 focus 上报。
记忆健康看板
设置页「记忆 Memory」区块下方是一张只读看板,数据来自 GET /api/memory/insights:
| 展示 | 含义 |
|---|---|
| 记忆页 / index 路由 / 记忆字数 | 记忆库规模(字数按字符计,中文不会按字节虚高 3 倍) |
| 健康分(0–100) | 由孤儿页、缺 salience/type、陈旧页、失效 index 链接加权得出 |
| 访问新鲜度条形图 | 今天 / ≤7 / ≤30 / ≤90 / >90 天 / 从未戳记(驱动自 last_access 自动戳记) |
| 四项体检 | index 链接完整、无孤儿页、frontmatter 完整、访问新鲜度——口径与 dsh-memory lint 同源 |
| 陈旧页候选 | 超过 90 天或从未戳记的页面,按陈旧度排序(前 8) |
| 最近动态 | log.md 最新 5 条标题 |
看板是实时的:每次打开设置页(或改完任一配置项后)重新拉取,不落盘、不缓存、不写记忆库。头less 部署没有 webServer 时该路由与看板都不存在,其余功能不受影响。
覆盖 composition 键(例如把 boot 块预算调大),在 profile 的 cordis.patch.yml 里写不带 insert 的 id 覆盖条目:
- id: dsh-memory
config:
bootMaxChars: 12000实现说明:DSH 的 settings wire 只服务硬编码的命名空间白名单,插件命名空间写不进去,因此本插件走自建通道——配置存
<dshHome>/memory.json(schema 校验 + 原子落盘),由插件自注册的GET/POST /api/memory/config路由服务,客户端区块 fetch 直连。
首次使用:铸魂
插件安装后,第一次会话里 agent 的首要任务不是干活,而是与你对话定义它的灵魂:名字、性格、价值观、语气、边界,以及你的身份与你们的关系。逐项确认并写回 SOUL.md / user/profile.md,直到 BOOTSTRAP.md 的 status 变为 complete。你可以随时跳过或暂缓。
四个操作
- remember(记):把值得持久化的内容蒸馏成页面,同步更新
index.md、追加log.md。维护 index 时守路由表纪律:一行一页、一句话摘要(≤ 80 字)+salience。 - recall(忆):会话开始读 boot 块;查询时先查
index.md再钻页;必要时dsh-memory search <关键词>。 - consolidate(整理):
dsh-memory lint查矛盾、孤儿页、该归档的冷页;dsh-memory status看陈旧页分布。 - forget(忘):显式遗忘立即执行;自动衰减按 salience + last_access(冷页优先归档)。
last_access由插件自动维护,无需手改。
CLI
dsh-memory init [dir] # 创建记忆库脚手架
dsh-memory search <query> [--touch] # 全文检索(--touch 给命中页戳 last_access)
dsh-memory touch [pages...] # 手动戳 last_access(不带参数 = 全部页面)
dsh-memory lint # 完整性体检
dsh-memory status # 健康概览(含陈旧页统计)
dsh-memory pack [out.tar.gz] # 打包导出(含 manifest)
dsh-memory unpack <archive> [--force] # 从归档恢复存储定位顺序:$MEMORY_DIR → ./.memory(存在时)→ ~/.memory。
迁移
记忆库是纯 markdown + git:拷贝即迁移。跨机器 / 跨 agent / 能力降级档位见 docs/MIGRATION.md。
常见问题
Q:和手写的 .dsh/skills/memory 文件技能(skill 版)什么关系?
skill 版是"软保障"(技能目录只注入简介,正文靠模型主动加载);本插件是"硬保障"(boot 块随系统提示词运行时上下文自动注入)。两者可共存:文件技能(rank 100)会覆盖插件内嵌技能(rank 250)的协议。如果你之前为了软保障改过系统提示词 persona(如 profile 补丁里的开机指令),装上本插件后建议移除那段 persona,避免双份注入。
Q:boot 块会不会每次请求都重复注入、烧 token? 不会。运行时上下文按投影去重:内容不变只注入一次;记忆更新后新快照取代旧快照。
Q:记忆库放在哪里最合适?
默认 ~/.memory(全局、跨项目)。需要按项目隔离时,把 memoryDir 配到项目内,或让 agent 在项目里维护 .memory/。
Q:可以加密吗?
记忆含敏感内容时,可把 memoryDir 放进加密卷 / 私有仓库。格式不变,插件无感知。
开发
git clone https://github.com/LittleBlackTong/dsh-plugin-memory.git
cd dsh-plugin-memory
node scripts/memory.mjs --self-test # 冒烟测试(无需安装依赖)零构建:lib/ 直接是运行时代码,lib/types/index.d.ts 供 TS 消费方使用。boot.js / scaffold.js 只依赖 node:* 内置模块,可独立复用。
路线图
- [ ] TypeScript 重写(带完整类型与构建步骤)
- [ ] embedding/BM25 检索(规模超过几百页后替代 index 先行)
- [ ] MCP server(让非 DSH 的 agent 也能用同一套记忆库)
- [ ] GitHub Actions CI(跑
--self-test与 lint) - [ ] 记忆加密存储选项
欢迎在 Issues 里提需求、报 bug、交 PR。
