@local/dsh-memory
记忆系统:设置页「记忆管理」+ 给模型一行中立状态
같은 패키지 이름의 다른 저장소
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:mounta1n-18/dsh-memorydsh-memory
一个 DeepSeek Harness 插件:把「跨对话的记忆」做成一摞本地 Markdown + 一张索引表,再配上一套管理界面。 零依赖、无构建,纯手写 JavaScript(Host 半 + Client 半)。
模型平时的记忆成本只有系统提示词里的那一行大纲;只有压缩流水线(每 100 轮或点一次沙漏)才会调模型。
它做什么
- 一行大纲进系统提示词:按分类分组的索引(
【教训】/【资料】/【说明书】/【项目记忆】/【其它】),教训永远排第一;关键词写的是「以后什么场景该想起它」,不是摘要。 - 四个记忆工具:
memory_read/memory_write/memory_update/memory_delete,锁定项一律拒写;项目记忆只走人工通道。 - 教训三段式:写教训时强制填「场景 / 错误链条 / 绕开与修复」,并自动在大纲末尾追加一句「动手前先过一遍【教训】」。
- 设置页「记忆管理」:搜索、分类折叠、新建/删除、待审批改稿的 diff 审阅、压缩状态。
- 记忆日志页:会话顶部多一个「记忆」页签,仿轨迹列表 —— 操作分色标签、行内结果摘要、右侧详情(来源/类型/关键词/状态/时间/完整正文),来源可一跳跳回轨迹里那次工具调用。
- 回忆统计:一轮 = 用户按一次回车;窗口内从没被回忆过、且复核日超期的条目会被标成待删除候选。
快速安装
只能走 DSH 的 plugin_manager(官方唯一支持的安装路径):
plugin_manager { "action": "install_bundle", "target": "" }
- Host 改动(
index.js)必须重启 DSH 生效; - Client 改动(
client.js)刷新页面即可; - 卸载:
plugin_manager { "action": "remove_bundle", "target": "@local/dsh-memory" }。
已知边界
- 依赖宿主的私有槽位(
settings.section、conversation.view、commandUi.register等),DSH 大版本升级后需要复核一次。 - 设置页导航图标的字形由外壳按 section id 写死,本插件借用了
archived-sessions这个 id 来拿到归档图标(否则会落回齿轮)。 - 记忆日志页是独立的
conversation.view页签:外壳的轨迹行类型是固定枚举,第三方插件不能往里插新行类型。 - 数据全在本机
~/.dsh/memory/,不上传、不埋点;只有压缩流水线会把记忆正文发给当前配置的模型。
数据面
~/.dsh/memory/
MEMORY.md:7 列索引表(文件|关键词|启用|类型|何时读|复核|受保护),唯一索引。*.md:一份记忆一个文件,YAML 头写name/description/type/reviewed,下面是正文。.tidy/status.json:上次整理运行的记录,只有lastRun / result(ok|failed) / detail。.tidy/proposed/*.md:待审批的改稿,只存正文;存在即代表有待审批方案。.tidy/turns.json:全局用户轮计数{count};.tidy/recall.json:回忆流水[{turn,file,ts}]。.tidy/compress.json:压缩流水线状态{startedAt,finishedAt,stage,reason,files,detail}。
status.json 不存 state / count。 待审批份数的唯一事实源是 proposed/ 目录里有没有文件,由 snapshot() 现算;写进 status.json 就会在批准/忽略之后与事实不一致。读 status.json 时剥 BOM(PowerShell 5.1 写文件必带 BOM,JSON.parse 会直接抛错)。
回忆率与「待删除」也是现算的:不进索引表,索引只存人写的事实。一轮 = 用户按一次回车(session/event 的 user/message 且 source.kind === 'user'),工具调用、子代理、goal 自动续轮都不计数。窗口 = 最近 min(100, 总轮数) 个用户轮;分子 = 这些轮里读过它的轮数(一轮读多次算一次);pendingDelete = 窗口内一次没回忆过 且 复核日距今 ≥ 14 天 且 启用 且 未锁定。2026-10-29 修掉一处错位:早先 recordRecall 把读记成 turnCount + 1(下一轮),当轮读的记忆要等用户再发一条才进窗口;现在记在当前轮,读完立刻计入。
批准只改正文:proposed/ 里只有正文,approveFile() 沿用原文件的 YAML 头(所以 description 不会被改稿冲掉),刷新复核日,删掉方案。
Host 半边(index.js)
系统提示词两段
memory:outline(order 9890):按分类分组的多行索引,取自MEMORY.md,停用条目不出现。第一行说明格式与用法(「名称=关键词」、关键词=「以后什么场景该想起它」、正文用memory_read取),其后每行一组:【教训】…、【资料】…、【说明书】…、【项目记忆】…;组名已表明分类,关键词里省掉重复的前缀。教训组永远排第一。 单条关键词超过 42 字时在/边界裁剪并加…(预算扣掉分类前缀);不做整行截断,任何一条记忆都不会从大纲里消失(旧的 240 字整行截断实测丢过huashu-motion-install)。 只要存在启用的教训,末尾追加一句前置检查:「动手前先过一遍【教训】:这次任务命中哪个场景,就先 memory_read 读完再开始,不要等报了错才回来找。」memory:tidy-status(order 9900):正常时零输出,异常才一行「待审批 N 份 / 上次失败:原因」。
关键词口径(2026-10-29 定稿):关键词不是摘要,是「以后什么情况下能想起它」的触发词。
- 资料 → 这份资料是什么内容类型(
资料/身份/联系方式、资料/笔记本规格),不要只写人名; - 教训 → 犯错之前正在做的场景/任务(
教训/插件往会话注入消息、教训/截断长文本、教训/卸载重装DSH),不许写 400 / 422 / 报错 这类事后症状名——那是修的时候才想得起来的抓手; - 说明书 → 对应的工具/插件名(
说明书/btw插件、说明书/context面板)。
教训正文三段式(创建时强制):① 场景(什么任务、什么动作会踩到,读者一看就知道「我现在就在这个场景里」);② 错误链条(第一步做了什么、哪一步开始不对、为什么没察觉);③ 绕开与修复(下次怎么走、已经部署了什么防护)。
四个模型工具:memory_read(名称 / 关键词 / 描述命中,只返回正文)、memory_write、memory_update、memory_delete。三个写操作对锁定的记忆一律拒绝。
首关键词硬约束:memory_write / memory_update 的第一个关键词必须是 资料 / 教训 / 说明书 / 其它 之一;项目记忆 只能由人工通道产生(设置页新建,或大脑按钮开出的 5 分钟窗口,只放行一次)。
两道闸门,都走同一个 memoryKind() 判定(realpath + path.relative,大小写 / .. / 符号链接都归一到同一身份):
| 路径身份 | read | grep / glob |
|---|---|---|
body 记忆正文 | 拒绝,改用 memory_read | 拒绝 |
root 仓库根 | 放行(read 目录本来就会失败) | 拒绝 |
index(MEMORY.md) | 放行 | 放行 |
internal(.tidy/**) | 放行 | 放行 |
tools/post-execute 对写进 body 的文件做 2000 字软提示:只提示,写入已生效;措辞刻意放轻,说重了模型会为缩字数丢信息。
十四条路由:GET /dsh-memory/status(兼 ?client=1 心跳)、GET /dsh-memory/calls(按会话列记忆调用)与 POST keyword / open / approve-file / reject-file / toggle / protect / create / delete / search / compress / remember / dockping。
记忆调用流水:tools/post-execute 里记下每次 memory_* 调用(会话 id、轮、读/写/改/删、目标、关键词、字数,最多 400 条)到 .tidy/calls.json,供对话界面的「记忆」标签页显示;不进上下文、不写会话历史。
面板只在自身操作后更新,所以另有一条兜底:回到窗口(window.focus)自动重拉快照,工具栏也有「刷新」按钮;索引行在、文件不在的卡片标「文件已丢失」,垃圾桶改叫「确认清理」。
create:人工通道,文件与索引行一起写;索引补不上就把刚建的文件撤回,不留孤儿。delete:锁定的拒绝;删文件 + 删索引行 + 顺带清掉指向它的未审方案。文件已经不在磁盘上的死行也能删(回包ghost: true),否则卡片会永远钉在面板上(2026-10-28 实测:报「文件不存在」,再也清不掉)。search:关键词、「何时读」、frontmatter 描述、正文都算命中;正文命中回原文附近 ±2 行(最多 3 处)。compress:沙漏按钮,立刻跑一次压缩判定(不阻塞,前端轮询status.compress.running)。
/remember 是客户端 action(ctx.commandUi.register,大脑图标进斜杠列表),不产生 command 事件、不写会话历史:点一下只向 POST /dsh-memory/remember 发一个 sessionId:宿主优先借 DSH 的 llm 服务,把 session.deriveMessages() 出来的上下文原样当前缀、末尾追加一句隐藏指令(只存在于这一次请求里),并把 memory_write 的工具声明一起递过去——模型「往里填空」拿到的是协议层的 tool-call-delta 结构化参数,天然合法 JSON,不再出现「模型没有返回 JSON」。llm 服务不可用时退回插件自己发 HTTP + 只带转录 + 同一条工具声明(tool_choice 点名)。回 {file, chars, turns}。状态条挂在 conversation.input.right(输入框控件行):「记忆中」→「记忆完毕,正文长 N」,下一次人类发言(turns 变大)后自动消失。
不要用 agent.followup() 往会话里塞提示词:它收的是完整 UserMessage,少 id / role 会把这条消息写进会话日志,DeepSeek 侧序列化出没有 role 的 messages[i],整个会话从此每轮 422。
压缩流水线(每 100 轮 / 沙漏)
规则(2026-10-27 定稿):
| 窗口内回忆次数 | 距今 。设置页图标借用外壳映射里唯一空闲的 archived-sessions` id。
界面只放状态、数字、输入框、按钮,不加说明性备注。
工具(tools/)
selfcheck.mjs:mock 一个 cordis ctx 把apply()整条路径跑起来(临时DSH_HOME,不碰真实记忆库),覆盖装载面、大纲、闸门、status、分类约束、回忆统计、新建/删除、搜索、压缩、/remember、调用流水共 173 项。改完先跑它,再考虑重启:node tools/selfcheck.mjs。tidy.mjs+run.cmd:旧的「整理」worker,已不再挂计划任务(压缩不是定时任务)。保留作手动兜底:node tools/tidy.mjs [--dry-run|--if-due],只写proposed/与status.json。
未完成
- 压缩只按「回忆次数」分档;窗口不足 100 轮(手动沙漏提前跑)时用的是次数而不是百分比,语义上等价于按满窗折算。
- 卡片聚类顺序写死为
资料 / 教训 / 说明书 / 项目记忆 / 其它 / 未分类,没有做成可配置。