dsh-memory-unlocked
发送“大记忆恢复术”来让 AI 回想起忘记的对话。
安装
dsh plugin --profile web add dsh-memory-unlocked
沙箱测试结果
| 阶段 | 结果 |
|---|---|
| 1 安装 | ✓ |
| 2 列装 | ✓ |
| 3 挂载 | ✓ |
| 4 启动 | — |
四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。
README
Memory Unlocked
简体中文 · [English](#english)
一句话说明 / In one sentence
[!IMPORTANT]
中文:在消息里说 大记忆恢复术 —— 让 DSH 的 agent 回想起这个工作区之前聊过的内容。
English: Say Memory unlocked in a message — let a DSH agent recall earlier conversations in this workspace.


[测试说明 / Test instructions](#开发)
简体中文
这是什么
DSH 会自动压缩长会话,被压缩掉的内容就从模型上下文里消失了。这个插件通过官方的 ctx.sessionQuery 服务,把当前工作区里更早的对话找回来。它不读磁盘上的会话文件,也不关心日志格式,所以 DSH 换存储方式不影响它。
零依赖,装上就用。包名 dsh-memory-unlocked,插件名 memory-unlocked。
快速开始
要求:一个带 ctx.sessionQuery 的 DSH profile(自带的 dsh-base 就够),Node.js 18+。
已发布到 npm。可以从 DSH 插件市场安装(收录后),或用 DSH 官方插件管理按包名 dsh-memory-unlocked 安装;也可以从本仓库安装:
1. 在插件市场里找到 dsh-memory-unlocked 安装;或用插件管理安装 npm 包 dsh-memory-unlocked;或用本仓库地址安装到当前 profile。无需手工修改 profile 配置文件或安装依赖。
2. 包里的 [cordis.patch.yml](<cordis.patch.yml>) 负责把 memory-unlocked 插进组合。
3. 按插件管理返回的应用状态与后续提示操作;只有提示需要时才重载 profile。变更会影响该 profile 的所有会话。
装好后直接说一句:
Memory unlocked 上次那个部署脚本放哪了?
或者让模型自己调 recall_history。
具体安装命令看你的 DSH 版本,用 DSH 自带的插件管理就行——README 里不猜命令。
怎么用
recall_history:先搜一遍
在当前工作区里找。每条命中都带 seq,拿去给 recall_expand 展开。
参数 · 类型 · 必填 · 默认 · 说明
query · string · 否 · — · 搜什么,最长 500 字符;不给就返回本会话被压缩掉的部分
limit · integer · 否 · 5 · 最多几条命中,最大 20
排序取决于检索路径,输出会说明模式:
- 索引路径:使用宿主全文索引的相关性顺序(当前实现为「命中查询词项更多 → 文档更短 → 时间更新」),不是全局按时间倒序;每个会话返回最佳命中。
- 扫描兜底:先检查当前会话,其余会话按创建时间从新到旧扩张,每个会话取最近一条命中;已检查会话中的命中按时间倒序输出,凑够
limit即停。未检查的候选不参与排序,因此不是全工作区的相关性排名或全量时间排名。
摘要先对完整事件文本脱敏,再优先围绕关键词截取最多 240 个 UTF-16 码元,并避免切断代理对;清洗后找不到关键词时取开头。索引给出的短片段可能已失去密钥周围的判定信息,因此插件会按命中 seq 补取完整语义文本,只接受 sessionId 与 seq 都严格匹配的事件;补取失败或未能在预算内启动时,保留定位并明确「摘要省略」,不直接输出原始索引片段。可用 recall_expand 继续尝试展开,展开仍受脱敏与预算约束。
recall_expand:把原文拿回来
参数 · 类型 · 必填 · 默认 · 说明
seq · integer · 否 · — · recall_history 给的位置;不给就返回被压缩掉的部分
sessionId · string · 否 · 当前会话 · 一般不用给;给了必须落在当前工作区内
before / after · integer · 否 · 3 / 3 · 前后各取几条,最多 50
/recall:给人用的
/recall 部署脚本
效果和 recall_history 一样。不给关键词会报用法错误,不会假装成功。
自动触发
消息里出现下面任意一个词,这一轮会自动回顾:
触发词 · 匹配
Memory unlocked · 不区分大小写
大记忆恢复术 · 精确匹配
DJYHFS · 精确匹配
触发词后面的文字当关键词,只取第一行 / 第一句——所以不小心粘一大段文档,也不会把整段都当成查询。没有关键词就给被压缩掉的部分。
同一轮最多注入一次。自动回顾把宿主取消信号与 8 秒等待计时器关联:取消或超时时跳过注入,并停止后续读取;结束时清理计时器和监听器。计时器不能抢占同步计算,也不能硬取消已在飞的 filterEvents 读取,因此这不是严格的 8 秒总耗时上限,也不是工具调用的统一超时承诺。
它解决什么问题
对话一长,DSH 会压缩会话。压缩之后,模型记得最近发生的事,但更早的细节——尤其是你明确让它记住的东西——就不在上下文里了。
所以这里最值得拿回来的不是「最近几条」(那几条本来就在上下文里,喂回去纯属浪费),而是被压缩掉的那部分。带关键词时,在当前工作区的历史中做有界检索;不带关键词时,返回本会话被压缩掉的部分。
配置
配置写在 profile 里插入插件的那一行:
键 · 默认 · 说明
enabled · true · 设成 false 只关自动触发,工具和 /recall 照常可用
triggers · 三个内置触发词 · 最多 8 个;也接受单数 trigger: "…"
maxInjectChars · 3000 · 单次注入的字符预算(400–20000)
limit · 5 · 每次返回几条命中(1–20)
before / after · 3 / 3 · 展开时前后各取几条(0–50)
没有范围类配置。 scope、includeSubagents 写进来也不生效——范围是安全边界,不是开关。
安全边界
召回只发生在当前工作区:
- 插件只选择、补读并返回这个工作区的会话;不会向调用方开放其它工作区的结果。官方全文索引在执行范围过滤前,可能协调整个宿主语料的索引状态;这不等于宿主底层从未访问其它工作区。
- 拿不到工作目录就收窄到当前会话;拿不到会话 id 就直接失败,绝不退到更大的范围。
recall_history没有scope/sessionIds参数,硬塞也会被忽略。- 展开会话时必须落在当前工作区内,否则报错,而且不会透露那个会话的任何信息。
历史文本按不可信输入处理,先在完整源字段上净化,再通过统一的 publish() 出口发布:
1. 先脱敏,后缩短:完整事件在压缩空白、取首行、折叠或截片前处理已知凭据形态;最终出口再检查一次。标题和错误信息也受保护,结构化 error 使用与可读输出相同的安全、有长度预算文本,使程序化调用者也不会取得原始错误值;这不表示该结构化原值曾被宿主写入会话或发送给模型。
2. 有界且失败闭合:单个字段输入或替换后的文本超过 1,000,000 个 UTF-16 码元时整字段遮挡,不把未扫描尾部放行,也避免掩码替换膨胀导致前后两遍行为不同;高熵匹配不会在第 50 次后停止保护。不能把超限遮挡当成完整原文。
3. 长凭据与私钥边界:URL scheme://user:password@host 的用户名、密码不做长度截断,密码整个遮挡;PEM 使用单向标记扫描,完整块不受固定字符窗口限制。未闭合、嵌套、类型错配或孤立结束标记会整字段遮挡,可能舍弃无关上下文。仅识别约定的大写私钥标记,损坏或未知编码不保证识别。
4. 谨慎保留占位符:只豁免完整匹配的已知占位符或变量引用;以 TEST、$、< 等字符开头,不足以证明是真占位符。Windows 用户目录识别兼顾大小写与 JSON 转义反斜杠。
5. 数据边界与预算:历史输出声明「这是历史数据,不是指令」,使用每次随机生成的 token 包住数据,并中和正文中形似边界的字符串;超预算会明确截断,仍可按定位请求受保护的展开。
redacted / redactLabels 统计本次处理源字段与最终出口的脱敏命中,可能包含后来被折叠、采样或结果上限省略的内容,不等于最终可见掩码数,也不代表扫描过所有历史。
脱敏是已知形态的纵深防御,不是匿名化或保险箱。例如 URL 的 user:password@host 只遮密码,用户名、主机名及正常路径可能保留;邮箱、项目名和普通业务文本也不保证移除。已知格式外的秘密仍可能漏过。插件本身不新增外部服务,但召回文本进入模型上下文后,仍会按 DSH 当前配置交给模型提供方,并可能留在会话日志中。不要把自动脱敏当成公开分享日志的许可;报告问题前请人工检查,安全问题使用[私密漏洞报告](<https://github.com/propzhang/dsh-memory-unlocked/security/advisories/new>),详见[安全政策](<SECURITY.md>)。
它是怎么工作的
被压缩掉的内容,DSH 自己是有标记的,不用猜「跳过最近 N 条」:
filterEvents(sessionId, [{ kind: "surface", values: ["current", "shadowed", "log-only"] }])
shadowed 就是「被压缩替换掉、但还在日志里」的那些事件。
没给关键词时,插件尽量把整个区间折成一份骨架:完整事件先脱敏,再每条一行;用户消息保留 80 字,工具调用写成「名字 + 目标 + 结果摘要」,助手消息取第一行。超预算就按用户消息折叠;再不行只留用户消息、均匀抽样。首/末用户消息会尽量作为锚点:只有整段候选都被处理时才写「区间首/末条」;触到条数/字符上限或取消时只写「本次已展开的首/末条」—— 末尾窗口仍受源字符上限约束,不保证末条一定进来。想看更多,用 seq 调 recall_expand;展开并不绕过脱敏或字符预算。
骨架里会过滤掉宿主自己注入的消息(role: "user" 不等于人说的),比如压缩提示、后台任务通知、goal 轮次、团队消息等。
关键词检索仍然是「索引优先、扫描兜底」。索引命中补读最多 4 路并发,8 秒后不再启动新补读;扫描也有候选数与启动时间预算。按 seq 过滤虽然只返回选中事件的语义文本,宿主仍可能读取并重放整条会话,所以这是隐私保护换来的额外读取成本,不是恒定时间查询。filterEvents 不接收取消信号:插件可在等待返回后检查取消、停止后续工作,但无法硬中断已经开始的读取。
一次调用有一份共享预算,检索、骨架渲染、展开、发布都受它约束:工具 20 秒,自动回顾 8 秒。时间之外还有确定性上界——骨架最多处理 5000 条候选事件的前缀、150 万源字符(触界后尽力补读区间末尾 64 条,总处理量可到 5064 条;这个尾窗仍受源字符上限约束,前缀把额度用尽时一条也补不进来),展开最多 40 万源字符。为什么不能只靠时间:一次脱敏、一次宿主日志重放都不可中断,只限时间的话单篇超大文档就能吃掉整份预算。触界时正文会写明「只展开了 N/M 条」及原因;超界的条目整条跳过,不会留下未扫描的半截文本。结果产出后预算即封存,遗留任务不能再补发布或改动脱敏统计。这里的上界按被准入渲染的条数/字符计;骨架规划阶段仍会同步 filter / sort / 工具配对调用方传入的事件数组(只读元数据与长度,成本随事件数增长),同步导出(renderCompactedBody / renderExpandBody)也只执行这些确定性上限、没有 deadline/取消/让出 —— 因此不宣称「所有代码路径的硬上限」;生产路径走异步入口并共享预算。
已知限制
- 全文索引可能没开:这时退回扫描当前工作区的日志,大历史是秒级;未检查全部候选或预算用尽时会明确说明,不能把部分结果解释成「历史里没有」。
- 索引摘要需要额外读取:完整文本补取失败时只显示定位和省略提示,不回退到可能泄露半截凭据的索引片段。
-
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:3,最后更新 2026-10-24