记忆系统
本地优先的长期记忆,让每次对话都接得上。
JSONL 落盘 · 本机向量检索 · 五个模型工具 · 自动召回 · 硬性规则 · 可视化管理面板
它解决的不是「存一段文本」,而是「让模型在该记的时候记住、该用的时候用上」。 记忆默认只落在本机;没有向量服务也能工作——检索自动退化为词法,不产生任何网络请求。

它解决什么
| 常遇到的问题 | 记忆系统怎么做 |
|---|---|
| 新会话从零开始,换个窗口就问不出上次的结论 | 结论落成记忆;新会话第一步注入摘要,之后每轮按问题召回 |
| 同一个坑反复踩,上次查到的命令又要再查一遍 | 发布 / 部署 / 工具失败等行为会触发定向召回;查到的做法直接复用 |
| 写了规则没人执行,改过的规则旧会话不知道 | kind: rule 进系统提示常驻段;新增或编辑会补一条提醒 |
| 临时状态被当成永久事实 | kind: state 带默认 7 天 TTL 和「使用前验证」前缀;过期后默认隐藏,但不会自动删除 |
| 云端记忆不可控,敏感内容不敢写 | 全部本地 JSONL;写入前过凭据扫描;检索默认不联网 |
它也不是「把聊天记录整段塞给模型」:每条记忆是一句自洽的话,按作用域、类型和重要度管理;
召回有 token 预算与去重;旧结论可以用 supersedes 标注修正,被修正的旧版本默认不再参与
检索 / 摘要 / 召回(历史仍可用 memory_get 和面板查看),而不是悄悄覆盖。
功能
| 能力 | 说明 |
|---|---|
| 六个工具 | memory_save / memory_search / memory_list / memory_get / memory_forget / memory_scopes。memory_search 只回印象(8 位 id / 作用域 / 类型 / 标签 / 短摘要),正文和元数据用 memory_get 的 fields 按需取;同一会话里已出现过的条目只回 id。description 字段在普通限制下必填,审批卡会展示它。memory_scopes 只读目录:给作用域名字 / 用途 / 当前可读可写状态,不返回任何正文 |
| 四种作用域 | 会话 / 项目 / global / 自定义名。默认写进当前会话,读默认合并会话 + 项目 + global;项目作用域可起别名。自定义作用域首次由 Agent 创建时必须带 scopeDescription(一句话用途),可选带 scopeRule 写一条初始规则;元数据存在 <storeDir>/scope-meta/,删光记录也不会丢 |
| 作用域目录与绝对隔离 | memory_scopes 列出范围内 / 范围外的名字与用途;normal / none 下还带初始化记录正文(每个作用域最多 3 条,方便 Agent 直接读到“这个作用域主要放什么”)。绝对限制下范围外作用域仍可发现,但正文(含初始记录)永不可读、不可写、不可申请(没有任何审批放行路径);普通限制下范围外条目可申请,memory_scopes request 走审批后写入本会话授权(默认只读,写权限必须单独申请),子 Agent 沿父会话继承 |
| 会话授权(grants) | Agent 申请、用户批准的例外单独存在 <storeDir>/grants/<session>.json,不并入可见域策略;只读 / 可写独立、可撤销、只在本会话(及子 Agent)生效;absolute 下授权不生效,作用域被删除重建后按创建代次自动失效 |
| 双通道检索 | 词法(IDF + 中文 bigram)+ 向量(本机 llama-server 的 OpenAI 兼容 /v1/embeddings)。向量缺席自动降级,不报错、不阻塞 |
| 自动召回 | 每轮按用户问题召回;新会话第一步注入项目 / global 摘要;发布、部署、工具失败等行为按标签触发定向召回 |
| 印象形态 | 每轮只给每条记忆的短摘要;模型判断相关但需要细节时再 memory_get 取全文,省 token 也不丢信息 |
| 硬性规则 | kind: rule 进系统提示常驻段;行首带 8 位 id,注入的是摘要,需要全文时用 memory_get;旧会话会收到新增 / 编辑提醒;超预算有明确警告,不静默丢弃 |
| 记忆权限 | 输入框旁的「记忆权限」按钮设置本会话可读的作用域 + 限制等级(绝对 / 普通 / 无限制);每个作用域一行就是一条设置:勾选表示本会话可访问,行内**【绑定】/【可读】/【可写】控制工作区默认可见与读写权限——绑定到当前工作区后新会话默认可见,可读 / 可写独立影响工具读写;Agent 通过 memory_scopes request 申请到的范围外授权也用勾选呈现**(名称右边挂一个「AI」气泡,样式与你自己勾选的行区分开),取消勾选只是草稿,点「保存」才真正撤销(0.25.4 起),且按钮数字 / 「已选」都把 AI 授权算进去(0.25.5 起)并实时轮询——浮窗关着,Agent 刚拿到的授权也会让按钮数字自己变(0.25.6 起);Agent 及其子 Agent 都按它检索、注入与 memory_get |
| 修正版编辑 | 面板里编辑一条记忆 = 新建一条修正版并通过 supersedes 指向旧记录,旧记录不原地修改;记录页「是否被修正」筛选默认隐藏旧版本,可切到全部 / 只看已修正 |
| 范围外审批 | 普通限制下,工具命中可见域外的记忆时会在对应会话的输入框上方出现审批卡(样式与系统权限审批一致):展示工具、Agent 说明、查询关键词与范围外记忆摘要,支持逐条勾选 / 全选 / 取消全选(「允许选中的 N 条」或整单「允许一次」)或「拒绝」;点记忆 ID 直接跳到面板「记录」页并展开该条 |
| 上下文去重 | memory_search / memory_list 返回过的条目记进会话上下文状态;同一版本再次命中只回记忆 ID(memory_search 的裸 ID 与带摘要条目之间空一行),并提示用 memory_get 取全文(内容更新后重新给摘要) |
| 状态记忆 | kind: state 默认 7 天 TTL;注入带「状态,使用前验证」;过期后默认不进检索 / 摘要 / 召回,但 memory_get 与面板仍可取 |
| 流程索引 | kind: procedure 的目录进系统提示:按当前工作区 + 触发词(工具名 / 关键词)列 id · 触发词 → 标题,需要时再用 memory_get 取全文 |
| 可视化管理 | 记录浏览 / 搜索 / 新建 / 编辑 / 删除(编辑生成修正版);记录页可主动做语义检索(与 memory_search 同一套排序),并有「是否被修正」筛选;记忆树展示作用域、类型与修正链;设置改完即存;导入导出 |
| 一键向量后端 | 面板里下载 llama.cpp 运行时与内置模型,按 CPU / 内存 / 显卡推荐;换模型自动重建向量 |
| 跨进程安全 | 每个作用域一把写锁,锁内重新读盘再合并;多个 DSH 实例或 MCP server 共用一个 store 不丢数据 |
界面
记录页的语义检索
记录页搜索框旁边有「语义检索」开关。开启后,搜索不再按关键词过滤,而是把这句话交给和
memory_search 同一套的召回排序(向量 + 词法,必要时再重排),结果按相关度返回并标出得分。
- 开关只在向量后端已配置时可用;没装后端时保持禁用并说明原因。
- 语义检索是只读浏览:不增加访问计数,不会把
memory_search后续的排序带偏。 - 结果卡上的「得分」是排序分(向量/词法命中 × 重要度 × 时效 × 访问次数),不是概率, 访问次数多时可能大于 1;结果顺序与这个分数一致。
- 结果说明会如实写出这次是「语义排序」还是「关键词回退」:例如记录还没嵌入向量、或向量服务 没有响应时,不会把词法结果冒充成语义结果。嵌入完成后列表会自动刷新成语义结果。
- 作用域下拉与新建 / 编辑 / 删除仍然照常工作;从审计页点记忆 ID 跳过来时会自动回到普通列表并展开该条。
记忆树
按 作用域 → 类型 → 记录 三层展开,根节点显示总数。supersedes 是关系而不是层级,
所以不改树结构:金色虚线从修正版弯到旧版,悬停或选中任意一端,整条修正链一起点亮。

设置
召回、摘要、印象、策略、状态、流程索引、规则、自动召回逐项开关;每个开关都有可见 label、说明和当前状态。
修改立即写入 settings.json,不需要点保存。

输入框的记忆权限
输入框工具行右侧有「记忆权限」按钮。打开会话时就会读取这个会话的设置,按钮上的数字是当前生效的作用域数量,边框样式表示限制等级(绝对限制 = 橙黄实线 / 普通限制 = 蓝色实线 / 无限制 = 灰色虚线;title 里写明「已设置 / 使用默认 / 继承父会话 + 数量 + 等级」)。点开的浮窗使用毛玻璃背景,按当前会话列出可选作用域(当前会话 / 当前项目 / global / 已有自定义作用域):
- 默认范围:没有显式设置时(新会话)按默认设置生效 —— 当前会话 + 当前项目 + global + 已绑定的作用域 + 普通限制;面板按同一份默认范围预选,不会出现空面板。
- 搜索与批量:顶部可搜索作用域,支持「全选 / 取消全选」;列表最大高度受限,作用域多时在浮窗内滚动。
- 状态按钮(0.23.0 / 0.23.1):作用域列表每行有【绑定】、【可读】|【可写】。默认范围自带的会话 + 项目 + global 显示为已绑定且不可取消;绑定按钮用于把其它作用域额外并入当前工作区的默认范围(没有显式覆盖的新会话可见);可读 / 可写是会话级,独立控制该作用域的读取与写入(关掉可读 → 搜索 / 读取跳过并说明原因;关掉可写 → 保存 / 删除被拒绝并说明原因)。
- 限制等级:
- 绝对限制:可见域外的读取 / 写入 / 删除一律不执行;命中被挡住时会明确告诉 Agent 是「绝对限制」以及怎么调整(不再假装记忆不存在)。
- 普通限制(默认):可见域内正常返回;命中范围外记忆时,工具会暂停并在该会话的输入框上方显示审批卡,展示 Agent 的查询关键词与范围外记忆摘要;读审批可以逐条勾选,只允许其中一条或多条。用户拒绝 / 5 分钟未回答时,工具结果里会明确写出真实原因(「用户拒绝了本次访问」/「审批超时」),模型不会误以为记忆不存在。范围外写入 / 删除同样走审批。
- 无限制:不启用范围白名单,可以读写任意作用域(选择仍会保存,供下次切换)。
- Agent 授权 = 勾选 + 「AI」气泡(0.25.3 – 0.25.6):作用域行就是「勾选该作用域 + 【绑定】/【可读】/【可写】」这一组控件,不再有独立的「Agent 授权」行与浮窗上方的解释文本。Agent 通过
memory_scopes request审批得到的授权直接体现为该作用域的勾选,名称右边挂「AI」气泡(title写明只读 / 可写);AI 授权的行用自己的样式——虚线边框 + 中性底色,勾选框是虚线描边(透明底、蓝勾),不会和你自己勾选的实心蓝底行混淆。取消勾选只是草稿——气泡改标「AI·待撤销」,点「保存」才真正撤销(保存时先POST /grants撤销,再保存可见范围),Agent 下一次 pre-step 收到「记忆权限已更新」。计数:输入框按钮上的数字与弹窗头部「已选」都是「可见范围 ∪ AI 授权」的作用域数,AI 授权算进去。继承自父会话的授权标「AI·继承」,本会话取消勾选会被拒绝并提示到父会话操作;「取消全选」只是把可撤销的授权标记为待撤销。目录里已不可见的作用域仍占一行(标「目录中已不可见」)。列表里已勾选(按保存过的状态)的排在上面、未勾选的排在下面,组内按名称字母排序;保存后才按新状态重排。数字实时:本会话授权按 3 秒轮询,浮窗没打开时 Agent 刚拿到的授权也会让输入框按钮数字自己变(0.25.6 起)。草稿不跨关闭:点浮窗外面 / Esc / 再点按钮关掉浮窗时,未保存的勾选与「AI·待撤销」标记一律丢弃,重开只显示服务端真值(没保存就还原,不记住草稿)。 - 不再按记忆类型收窄:0.19.1 起只按作用域限制;旧配置里的
kinds会被忽略,同一作用域下的事实 / 规则 / 流程等都可读。 - 审批卡(0.21.0 / 0.22.x):注册在会话内的
conversation.input.dock,只在发起审批的那个会话里显示,切到别的会话看不到;视觉与系统自带的权限审批弹窗一致(警告色 strip、卡片、右对齐动作行,同一批主题 token)。按动作分为「记忆读取 / 写入 / 删除审批」。读审批默认全部勾选:可逐条取消、用「全选 / 取消全选」批量操作,点条目任意位置即可切换选中,再点「允许选中的 N 条」,工具只放行勾选的 id、其余写入withheld.reason='partial';全选时等价于原来的「允许一次」。strip 右侧可折叠 / 展开详情,折叠后仍保留「拒绝 / 允许」动作。写 / 删审批是单条,隐藏勾选框。0.22.0 起卡片展示工具调用的description(普通限制下工具层强制模型提供),点记忆 ID 会打开记忆面板的「记录」标签页、选中并滚动到该条、自动展开;0.22.2 起查询与说明固定显示在条目列表上方,条目再多也不会被列表挤没(两行锁flex:none,只有列表滚动);0.22.3 起审批卡高度只受 composer 文本区上限约束(与宿主审批卡同口径),矮窗口下列表也有空间。拒绝、超时或无法发起审批都会把原因告诉模型。Enter = 允许、Esc = 拒绝(焦点在卡片内时)。 - 子 Agent 自动继承父会话 / 根会话的设置;在子 Agent 自己的会话里再设置,只影响那一个会话。
- 保存后,下一次 Agent 步骤会通过注入的上下文消息收到「记忆权限已更新」通知(可见范围 + 限制等级 + 本次授权变更 + 当前会话授权);授权变化走同一条注入,不需要重启会话。
- 「恢复默认」清除覆盖,回到默认设置(会话 + 项目 + global + 已绑定的作用域 + 普通限制)。
- 覆盖按会话落盘在
<storeDir>/visibility/<session>.json,浏览器刷新 / 插件重载后仍在;0.18 的旧文件按「绝对限制」兼容读取。 - 浮窗与审批卡的半透明背景、边框、文字颜色全部走宿主主题 token;勾选框选中态、保存按钮、限制等级选中态使用
--dsw-alias-state-business-primary+--dsw-alias-label-primary-foreground,深色模式下不会出现白底白勾 / 白底白字;按钮三档边框用state-warn-primary(橙黄)/state-business-primary(蓝)/ 虚线灰;阴影已收敛为小范围投影,滚动列表高度收紧到min(220px, 32vh)。 - 可见范围 = 可操作范围(0.20.0 起,0.23.0 拆分读写):
memory_search/memory_list/memory_get与memory_save/memory_forget共用同一份可见域;0.23.0 起每个作用域的「可读 / 可写」状态独立生效,失败原因会写进工具回复。面板管理页仍是用户本人的操作,不受会话语义限制。
向量服务
在本机托管 llama-server,下载运行时与模型;内置清单为每个模型配好 dims 与 pooling,
避免维度写错导致向量通道静默失效。按硬件推荐,也支持自定义 GGUF。

安装
一条命令装进 Web profile:
dsh plugin --profile web add @restregular/dsh-memory
也可以手动加进 profile 的依赖(桌面端 ~/.dsh/profiles/desktop,Web 端 ~/.dsh/profiles/web):
pnpm add @restregular/dsh-memory
# 或
npm i @restregular/dsh-memory
装进任意 DSH profile(桌面端 ~/.dsh/profiles/desktop,Web 端 ~/.dsh/profiles/web)。
包自带 cordis.patch.yml,注册唯一一条 entry:
- insert:
- id: dsh-memory
name: '@restregular/dsh-memory'
不写任何配置就能用:检索退化为词法,不产生任何网络请求。需要语义检索时, 在面板「服务 → 向量服务」里一键装本机后端即可。
宿主只允许一个包名、一个解析源,所以本插件是「一个包 + 一条 entry + 面板与服务端同包」。 拆成两个包会触发
resolves from multiple active Loader sources,客户端图组合失败, 桌面端直接起不来。这段设计约束写在下面的折叠块里,不影响使用。
为什么是一个包(给想改结构的人)
宿主的 dsh-client-modules 只在「某条活的 Loader entry 能解析到包名」时才为该包提供
client bundle,并且 reconcilePackage 要求一个包名只有一个解析源。因此稳定形态只有一种:
一个包名、一条 entry、两侧都在。入口 lib/index.js 组合四个半边,每个半边各自
try/catch——任何一半失败都只记一行日志,绝不让 DSH 启动失败。
| 半边 | 作用 |
|---|---|
lib/provider |
提供 memory 服务:JSONL 存储、双通道检索、向量回填、向量后端生命周期与一键安装 |
lib/tools |
注册五个模型工具 |
lib/inject |
agent/pre-step 召回、摘要、策略、规则与行为触发 |
lib/panel |
/memory/api 管理接口(含同源 / 登录围栏) |
lib/client.js |
设置页「记忆系统」区块与左侧栏入口(注册 id == 包名) |
快速开始
1. 打开面板
设置页 →「记忆系统」;也可以在「设置 → 侧栏入口」打开左侧栏的「记忆系统」按钮。 面板分四个标签:服务 / 记录 / 记忆树 / 设置。记录页的搜索框旁有「语义检索」开关, 开启后可以直接用一句话主动搜索(需要先配置向量后端)。
2. 存一条记忆
模型自己会调,也可以直接对 Agent 说「记住:……」。工具入参如下:
memory_save({
"content": "本仓库用 pnpm 11 安装依赖;PATH 上的 pnpm 10 会报 ERR_PNPM_UNEXPECTED_STORE。",
"kind": "procedure", // fact / decision / preference / procedure / note / rule / state
"tags": ["pnpm", "toolchain"],
"triggers": ["pnpm", "install"], // 可选:流程索引按工具名/关键词匹配;缺省用 tags
"importance": 0.8, // 0~1
"expiresAt": 1793836800000, // 可选:过期时间(ms);kind=state 不传时按默认 TTL(7 天),也可传 ttlMs
"path": "D:/repositories/dsh-memory" // 可选:写入 project:<该路径>
})
3. 检索、取全文、列出、忘记
memory_search({ "query": "pnpm 装依赖", "topK": 5, "description": "查装依赖的约定" }) // 只回 id / 作用域 / 类型 / 标签 / 摘要
memory_search({ "query": "旧的发布流程", "includeSuperseded": true, "description": "核对旧发布流程" }) // 默认隐藏被修正/覆盖的旧版本
memory_get({ "ids": ["3f2a9c1d"], "description": "取装依赖那条的全文" }) // 默认:正文 + scope + kind + tags
memory_get({ "ids": ["3f2a9c1d"], "fields": ["summary", "updatedAt"] })
memory_get({ "ids": ["3f2a9c1d"], "fields": ["content"] }) // 只要正文
memory_list({ "limit": 20 })
memory_forget({ "id": "3f2a9c1d", "description": "删除过期的临时结论" }) // 按内容删:memory_forget({ query, scope })
memory_search 不再返回正文:每条只有 8 位 id、作用域、类型、标签和摘要(默认 160 字,
summaryChars 可调 20~2000)。判断相关后用 memory_get 取正文或元数据:
fields省略 =content + scope + kind + tags(与旧版看到的一致);- 只取摘要:
["summary"];只取正文:["content"]; - 元数据可选
importance/createdAt/updatedAt/expiresAt/triggers/supersedes; id始终返回;未知字段会明确报FIELDS_INVALID,不会静默忽略。
memory_search 省略 scope 时合并会话 + 项目 + global(按来源标注、按 id 去重);
scope: "all" 查所有已发现的作用域,写入路径会明确拒绝 all。
上下文去重(0.21.0):同一会话里已经由 memory_search / memory_list(或摘要 / 召回 / memory_get)
放进上下文的条目,再次命中且 updatedAt 未变时,memory_search / memory_list 只回记忆 ID,并附一句
「本次请求部分记忆摘要因上下文去重机制被省略,若要获取完整内容请使用 memory_get」;内容更新(版本变化)
后会重新返回摘要。memory_search 里这些裸 ID 单独成块,与带摘要的条目之间空一行,不会看起来像
上一条的续行。状态与召回/摘要共用同一份会话去重记录(summary_injected / content_injected)。
description(0.22.0):六个工具都新增 description 参数 —— 一句话说明这次调用要做什么。
本会话「记忆权限」为普通限制(默认)时必填,漏了会被 DESCRIPTION_REQUIRED 拒绝;
绝对限制 / 无限制下可省略。它只用于范围外访问的审批卡(展示给用户判断是否放行),不写进记忆。
4. 作用域怎么选
| 写法 | 含义 |
|---|---|
省略 / session |
当前会话(子代理共享父会话;拿不到会话信息时回退当前项目) |
project |
当前工作区(键是 project:<cwd>) |
path + 可选 scope |
指定工作区目录;scope 成为这个项目作用域的显示名(别名) |
global |
所有项目共享 |
handbook、项目笔记 … |
自定义作用域,首次 memory_save 自动创建(Agent 创建时必须带 scopeDescription) |
session:<id> / project:<path> |
显式钉住某个会话 / 工作区 |
all / * |
只在读取时可用:跨所有已发现作用域 |
项目作用域的身份永远是 project:<绝对路径>,别名只影响显示与引用,不迁移数据。
同一路径重复用同一个名字是幂等的;换名字会覆盖原别名;另一个路径抢同一个名字会被拒绝
(SCOPE_NAME_TAKEN)。
新建自定义作用域(0.24.0):Agent 必须说明用途,可选写一条初始规则;两者在同一次
memory_save 里原子写入(元数据先落 pending,记录与规则写完后转 ready,中途失败下次保存会续上):
memory_save({
scope: "handbook",
content: "本作用域用于收集团队约定、流程与踩坑记录。",
kind: "fact",
scopeDescription: "团队约定与流程;只放可复用的结论,不放一次性排错日志。",
scopeRule: "只记录与团队协作 / 发布 / 工具链有关的内容;一次性排错过程不进这里。",
description: "初始化团队约定作用域"
})
用途会出现在 memory_scopes 的目录里:
memory_scopes({ action: "list", description: "看看有哪些作用域、各自干什么" })
绝对限制下目录可见但正文不可读:范围外作用域仍显示名字与用途(方便判断要不要让用户 调整范围),但记录数、正文、申请入口都不会返回。
申请本会话权限(0.24.0 / D3):普通限制下,Agent 可以对范围外作用域发申请; 默认只申请只读,写权限必须显式列出:
memory_scopes({
action: "request",
scopes: ["handbook"],
permissions: ["read"],
reason: "需要团队约定与发布流程",
description: "申请读取 handbook"
})
审批卡默认只勾只读;用户勾上写权限才会授予写。批准后写入
<storeDir>/grants/<session>.json(本会话 + 子 Agent 继承),不会改可见域策略;
absolute 下所有申请都返回 not-requestable,不会弹卡。一次性访问仍走 memory_*
自己的隐式审批,不需要先申请。
5. 导入 / 导出
导出是纯文本 JSON:作用域、类型、内容、标签、重要度、时间戳,不含向量。 导入时逐条走保存路径,按当前模型重新嵌入,并按「作用域 + 内容」去重;一次最多 5000 条。
注入链路
| 层 | 触发 | 作用 |
|---|---|---|
recall |
每轮 agent/pre-step |
按本轮用户问题检索,按 topK / token 预算注入;默认只给每条 120 字摘要 + memory_get 指引 |
digest |
会话第一步 | 注入项目 + global 概览:按重要度 × 新鲜度挑选;kind=rule 不进摘要(已常驻系统提示);正文超预算时附可检索的标签索引 |
policy |
系统提示常驻 | 什么时候查记忆、查到怎么复用、查不到时低风险推断 / 高风险先问用户 |
rules |
系统提示常驻 | kind: rule 的硬性规则摘要;每行 id · 正文,需要全文时 memory_get;限量与预算;mandatory / pinned 排在最前 |
procedures |
系统提示常驻 | kind: procedure 的目录:当前工作区 + 触发词命中的流程,每行 id · 触发词 → 标题;正文按需 memory_get |
turnEnd |
每轮结束(默认关) | 下一轮开头提醒「有可复用结论先落盘」 |
| 行为召回 | 可见文本 / 任务 / 命令 / 工具失败 | 命中标签后,在下一个 agent/pre-step 定向检索;工具失败优先用该工具自己的 description |
行为召回只检索 global + 当前会话 + 当前工作区,不把自定义作用域或 fork 父会话带进来。
匹配源包括可见 assistant 文本、todo、command/run、goal/change、带 description 的 tool/call,以及工具失败描述;没有 description 的工具调用不触发,memory_* 工具一律不触发(memory_search 本身在取记忆,再召回一次只会重复注入)。不读取隐藏思维链。
规则会跟进旧会话
系统提示段每步都会重渲染,但那只是提示词文本变化,不会产生对话消息;常驻段还有 token 预算,
新规则可能被更重要的旧规则挤掉。rules.delta(默认开)会在每轮第一步比较「该会话上一轮开始时
的规则快照」与当前规则:新增、以及编辑过(同一 id 的行变了)的规则都补一条提醒,没变化不重复;
宿主重启用历史里的规则行做种子,不会整批重提醒。关掉它只停提醒,常驻段与检索照旧。
规则段注入的是摘要:每行是 - `<8位id>` <正文前240字>。段头会明确说明这一点,模型需要完整规则或细节时用 memory_get 按 ID 取;因此规则再长也不会把整段正文常驻在系统提示里。
流程索引(procedure)
流程通常很长,全部常驻会挤爆系统提示;而注入发生在 agent/pre-step,模型决定调用工具之前拿不到工具名。
所以系统提示里只放一个目录:当前工作区的 procedure 总是可见;其他作用域的流程必须命中
triggers(缺省用 tags),匹配源是本轮用户消息、本会话最近调用过的工具名和当前项目路径。
每行是 id · 触发词 → 标题,模型判断相关后用 memory_get 取全文。条数与 token 预算可在设置里调;
没有命中项时不注入。
去重与修正
- 召回按 sessionId + 记忆 id / 版本 去重;只给过标签的记录,任务命中后可补正文;内容更新会重新评估。
- 工具结果也参与去重(0.21.0):
memory_search返回过的摘要记为summary_injected,memory_list返回过的正文记为content_injected。同一会话再次命中同版本条目时只回记忆 ID 并提示用memory_get取全文;版本变化(updatedAt)后重新返回。标签索引(hint_injected)只给 id + tags,不算摘要已出现;memory_get始终显式返回正文,不做去重。 memory_get取到正文后,这条记忆会记成「正文已注入」;同一会话后续的普通召回 / 摘要 / 自动召回都不会再把同一段正文塞一遍。只取summary/tags/ 元数据不算正文,仍可被召回。memory_save保存成功同样算「正文已在上下文里」:写入语句(content)就是模型自己产出的 tool/call 参数,因此这条记录会立刻记成「正文已注入」,本会话后续的召回 / 摘要不会把刚保存的记忆原样再注入一次(去重命中既有记录时也一样)。保存失败不标记;记录之后被更新(updatedAt变化)仍会重新评估。审计里会留一条content_pinned / saved_in_context的 lifecycle 事件,说明后面的「去重跳过」从哪来。- 去重状态持久化:状态写到会话工作区的插件目录
<cwd>/.dsh/dsh-memory/sessions/<session>.jsonl,插件重载 / 宿主重启后自动恢复;只有首次接触该会话时读一次盘。- 只存记录 id、状态、版本(
updatedAt)和时间戳,不存正文,也不存查询。 - JSONL 增量日志:默认 250ms 合并写一行,写延迟不随状态大小增长;行数 / 字节超过阈值时 compact 成一行快照(临时文件 + rename 原子替换)。
- 每个工作区默认保留 30 天、最多 1000 个会话文件;目录不可写或文件损坏时自动退化为内存去重,不阻断注入。
- 插件配置
inject.sessionState.{enabled,dir,debounceMs,maxAgeDays,maxFiles}可覆盖默认值;默认开启。 - 工作区里的
.dsh/已加入本仓库.gitignore;其他项目按需自行忽略。
- 只存记录 id、状态、版本(
- 被
supersedes指向的旧记录不再进入memory_search、memory_list、摘要、规则段和自动召回;memory_get(id)与面板仍可查历史。需要历史检索时memory_search({ includeSuperseded: true })。 - 摘要排除
kind=rule;规则段实际进入系统提示的 id 会写进会话注入状态,recall 不再重复注入同一条规则。 - 同一轮最多发生一次 signal 驱动召回;同 tag / 同 query 的第二个信号只记一条
signal_dropped审计。 kind=state过期后默认从检索 / 摘要 / 召回 / 列表隐藏,但数据不删除;memory_get与面板仍可取。- compaction 后重置摘要与注入去重状态。
检索
- 词法通道:IDF 加权 + 中文 bigram;记录标签也并入可检索文本,所以中文正文 + 英文标签也能被标签查询命中。
- 向量通道:调用
llama-server的 OpenAI 兼容/v1/embeddings;可用面板一键安装,也可接自己的端点。 - 可选 LLM 重排:
retrieval.llm.enabled,默认关。 - 自动降级:端点没配、进程没起、向量身份不匹配时,只走词法,不报错、不阻塞。
- 面板主动语义检索:记录页的「语义检索」开关走
GET /memory/api/search,用同一套召回排序 (只读、不写访问计数),并在结果说明里标明向量通道是否真的参与;旧 provider 会退回关键词过滤并在界面注明。
向量边车 .vec 的首行是身份头 #<model>\t<dims>\t<pooling>。任一对不上就整份丢弃并重新嵌入;
重建期间检索暂时只用词法。老边车只有 model + dims 两段,按兼容接受,下一次写向量时自动升级。
换模型 = 向量作废重建,面板会在切换前告诉你涉及多少条记忆。
minScore 默认 0.5 是按 BGE 的 CLS 噪声底定的;换模型后如果召回太少就调低、太杂就调高。
向量后端与硬件
- 内置 8 个模型:BGE-M3(Q4_K_M / Q8_0)、Qwen3-Embedding 0.6B / 4B / 8B、Nomic Embed v1.5、BGE 中文 small / base / large。
- 维度与 pooling 自动配对:每个模型自带
dims与pooling,选它就自动写入。维度写错会让向量通道静默失效,这是最容易踩的坑。 - 按本机推荐:探测 CPU 核数 / 内存 / 磁盘剩余 / 显卡与显存;质量优先,能整块放进显存就用 GPU;CPU 上重罚 4B / 8B;显存 < 4GB 的核显不算可用显卡。
- 运行时风味:
cpu/vulkan/cuda/metal/auto。Vulkan 自包含;CUDA 会一并下载 cudart 伴随包;所选风味找不到时退回 CPU 并在面板说明。 - 自定义 GGUF:填 URL + 维度即可装清单之外的模型;需要前缀的模型(Nomic 的
search_query:/search_document:)自动带上。 - 一次只保留一份:已装运行时 + 模型时不能再装第二个;换模型先去「维护 → 卸载向量服务」,否则「现在跑的是哪个」说不清。
retrieval:
vector:
preset: 'qwen3-embedding-0.6b' # 清单 id
variant: 'q8_0' # 量化档
flavor: 'auto' # auto / cpu / vulkan / cuda / metal
# customModel: { url: 'https://…/model.gguf', dims: 1024, pooling: 'auto' }
spawn.enabled: true 时,应用启动会后台拉起向量服务、退出时自动关闭;带 PID 文件的遗留实例会被接管,
没有 PID 文件的外部实例不接管。
配置
- id: dsh-memory
name: '@restregular/dsh-memory'
config:
storeDir: '~/.dsh/memory-store' # 建议绝对路径;默认 .dsh-memory
scopes:
global: true
project: true
session: true
custom: { enabled: true, max: 200 } # 只统计自定义作用域
retrieval:
topK: 5
recallN: 30
vector:
enabled: true
endpoint: 'http://127.0.0.1:8078/v1/embeddings'
model: bge-m3
dims: 1024
timeoutMs: 300 # 交互检索预算,要紧
writeTimeoutMs: 10000 # 保存时嵌整条记录
backfillTimeoutMs: 30000
backfillMaxChars: 8000
spawn:
enabled: true
exe: '/path/to/llama-server.exe'
model: '/path/to/bge-m3-Q4_K_M.gguf'
cwd: '/path/to/llamacpp' # 目录里要有 llama-server-impl.dll
logFile: '/path/to/llama-server.log'
llm: { enabled: false }
limits: { maxRecordsPerScope: 2000, maxRecordChars: 4000 }
secrets: { enabled: true, mode: refuse } # refuse | redact
tools: { defaultScope: session } # session / project / global
inject:
enabled: true # 整个注入半边总开关
extraScopes: ['handbook'] # 可选:额外参与普通召回的 scope
recall: { enabled: true, topK: 5, tokenBudget: 500 }
impression: { enabled: true, chars: 120 }
digest: { enabled: true, topK: 8, tokenBudget: 800, hintBudget: 160, hintMaxItems: 6 }
policy: { enabled: true }
rules: { enabled: true, maxItems: 10, tokenBudget: 300, delta: true }
turnEnd: { enabled: false }
panel: { route: '/memory/api' }
面板改过的值写入 <storeDir>/settings.json,优先级高于这里的配置。
注入分层各自独立开关;inject.enabled: false 全关,但工具检索不受影响。
只有写了 endpoint、配了 spawn、或面板里装了本地后端,插件才会碰网络;否则一个请求都不发。
数据与文件布局
| 路径 | 内容 |
|---|---|
<storeDir>/global.jsonl |
global 作用域,一行一条 |
<storeDir>/projects/<sha1(scope)>.jsonl |
其它作用域(会话 / 项目 / 自定义);文件名是 scope 键的 SHA-1,路径分隔符无法穿越 |
<storeDir>/projects/<sha1(scope)>.vec |
向量边车;首行 #<model>\t<dims>\t<pooling>,后面每行 id\t<base64 向量> |
<storeDir>/settings.json |
面板设置(覆盖配置文件里的默认值) |
<storeDir>/scopes.json |
项目作用域别名(显示名 → project:<路径>) |
<storeDir>/scope-meta/<sha1(scope)>.json |
作用域用途 / 来源 / 创建者 / 创建代次 / 初始化状态;每个作用域一个文件,删光记录也不删元数据 |
<storeDir>/grants/<session>.json |
会话级作用域授权(Agent 申请、用户批准);只读 / 可写分开、可撤销、带作用域创建代次 |
<storeDir>/approvals.jsonl |
审批与授权生命周期审计(requested / approved / denied / timeout / granted / revoked);只记 scope 与权限,不记记忆正文 |
<storeDir>/visibility/<session>.json |
会话「记忆权限」的可见范围覆盖(作用域 + 限制等级);没有文件 = 使用默认三路集合 |
<storeDir>/*.lock |
跨进程写锁;陈旧锁(持有者已死或 10s 未动)会被接管 |
<installRoot>/llamacpp/ |
llama-server 运行时、依赖 dll、llama-server.log |
<installRoot>/models/ |
下载的 GGUF 模型 |
<installRoot>/selected-model.json |
面板里选过哪一档 |
storeDir 默认 .dsh-memory(相对启动目录),建议显式配一个绝对路径。文件首次写入才创建;
作用域被删空后,它的 .jsonl 与 .vec 会在下一次落盘时一起移除(global.jsonl 例外:只清内容、留文件)。
作用域元数据(scope-meta/)独立于记录:删光某个作用域的最后一条记忆,不会删除它的用途说明。
scope 键有三种形态:global、project:<cwd>、session:<会话 id>,其余是自定义名
(小写折叠、内部空白折成一个空格、最长 64 字符,禁止 :、路径分隔符、控制字符、..
与保留名 global/project/session/all)。没有任何数据迁移:旧的 project:<cwd> 文件继续原样读写。
installRoot 默认 ~/.dsh/memory-vector;显式配了 storeDir 时取它的父目录。
多进程共用
写入按 scope 走锁文件,每次「锁内重新读盘 + 合并本次增量」,不再用内存快照整份覆盖磁盘; 读路径每秒 stat 一次,别的进程刚写的记录不用重启就能看到。实测 4 个进程 × 20 条并发写同一个 scope 零丢失。 已知取舍:同内容去重仍是进程内判断,两个进程同时写完全相同的内容理论上会落两条。
安全
/memory/api的每条路由都先过refuseRequest:宿主connection.requestRejection可用时以它为准 (Host / Origin 围栏 + 浏览器登录),围栏抛错一律 403(失败关闭);旧宿主没有该服务时退化为 「只认 loopback 权威 + 同源」。原因是注册在裸webServer上的路由不在宿主/api围栏之后, 而这里的接口能读、改、删、清空全部记忆。- 记忆内容写入前过凭据扫描(
secrets.mode: refuse | redact),默认拒绝。 - 统一授权判定(0.24.0):
discover / read / write / delete都走 provider 的同一个纯函数authorize();工具层只决定“要不要发起审批”,不再各自解释绝对 / 普通 / 无限制。absolute下不存在任何审批放行路径;显式关掉「可读 / 可写」的作用域也不通过审批放行。 - 清空记忆是两步确认 + 接口要求显式
confirm: "delete"。 - 数据默认只在本机:JSONL 不加密,向量端点默认 loopback;请自行保护 store 目录,导出文件也是明文。
诊断与审计
面板顶部有独立的「审计」标签页,不需要手工请求 HTTP。它按事件流展示每次注入的评估、过滤、预算、投递与失败原因,并支持按会话 / 通道 / 结果 / 时间筛选、分页、详情抽屉和导出当前筛选。
审计严格区分三件事:
- 评估(evaluation):候选、选中、省略、预算、检索/加载结果;
- 投递(delivery):最终消息是否通过本地保护并被放进上下文;
- 使用/遵守:Agent 后续是否引用或执行了记忆。当前系统无法可靠证明,面板不会声称已验证。
因此 selected.contentIds 只表示“评估选中”,不等于“已投递”。宿主没有向 middleware 回执最终采纳结果时,delivery.status 记为 unknown(面板显示“已准备(投递未确认)”),不会乐观写成 appended。
API
GET /memory/api/audit
?session=<id>&trace=<id>&operation=<id>&memory=<id>
&channel=digest|recall|rules|rules_delta|policy|nudge|signal|lifecycle
&trigger=<trigger>&outcome=success|failed|skipped|partial|unknown
&problem=1&since=<ms|ISO>&until=<ms|ISO>&limit=1..200&cursor=<seq>
GET /memory/api/audit/stats
GET /memory/api/audit/export?format=json|jsonl&<same filters>
GET /memory/api/anchor?session=<id>&turn=<n>&step=<n>&callId=<optional>
GET /memory/api/visibility?session=<id>
POST /memory/api/visibility { session: "<id>", scopes: ["session:<id>", "project:<cwd>", "global"] | null }
响应为 { ok: true, value: { entries, total, nextCursor?, stats, query } }。旧调用 GET /audit?session=<id> 仍兼容;不带 session 时查询全部会话,query.scope 会标记为 all。
/visibility 返回 { sessionId, override, effective, inherited, scopes }:override 是该会话自己的覆盖(null = 使用默认集合),effective 是当前实际生效的读取范围,inherited 表示是否继承自父会话,scopes 是可勾选的作用域目录(含 label 与记录数)。保存 scopes: null 即恢复默认;空数组会被拒绝(至少保留一个范围)。
和会话轨迹互相跳转
DSH 自带的 Trajectory 面板是一个纯投影:它只声明 conversation.trajectory.images 一个子槽,没有行/列扩展点,选中状态也只存在组件内部。因此插件不能直接往轨迹表格里插审计行。当前采用的是一条不碰轨迹 React 树的组合:
- 轨迹 → 记忆审计:Trajectory 的选中行带有
data-trajectory-row-key。注意它在 DOM 上经过encodeURIComponent(工具行实际是tool%00call%00<callId>),插件会先解码再取 callId。有审计的调用行左侧会显示产品年轮线条图标;选中行右侧另有「记忆 N」徽标,点击按op:<callId>打开审计。两者都会按轨迹滚动容器(扣除 sticky 表头与底部浮动 composer)裁剪,行滚出可视区后被遮住时不会残留。 - 轨迹详情页签:点击轨迹里的任意记录后,右侧详情面板的页签行会多出一个「记忆审计」页签(用
shell.overlay贴着详情面板,不改轨迹的 React 树)。它展示该记录 callId 对应的审计事件,底部按钮直接打开本会话的记忆审计面板;没有 callId 的记录明确提示无法精确过滤。 - 记忆审计 → 轨迹:审计详情里的「在轨迹中定位」把条目上的
operationId(记忆工具调用、触发召回的工具调用)交给 Trajectory 的openView('trajectory', callId);主面板会先打开会话,再切换轨迹并选中该记录,右侧详情面板自动打开。没有operationId的按轮/步事件先请求/anchor,由 Host 从会话事件窗口里找该 turn/step 的第一个tool/call;找不到就只切到轨迹页,不伪造定位。 - 会话内的「记忆」视图:与 Chat / 轨迹同一个 view ring,按当前会话展示审计;它直接使用宿主传给每个 view 的
openView,因此跳转是官方支持路径。它声明data-conversation-composer-overlay让壳层压住外层滚动,审计列表在自己的滚动容器里滚动,外层界面不需要跟着滚。主面板不在 view ring 内,通过conversation.session注册的 per-session store 调同一个 action;拿不到时退化为打开会话/轨迹页。 - 记忆工具调用(
memory_save/search/list/get/forget)本身会额外留一条stage: tool_call的审计锚点:即使自动召回关闭,也能从这条记录精确跳回轨迹里的那次调用。
/anchor 只返回 { callId, tool?, turn?, step?, reason },不返回工具参数、消息正文或记忆内容;找不到会话/工具时 callId 为 null 并给出稳定 reason。
审计数据默认只在内存:容量 2000 条、每会话 200 条,超出时优先保留 digest / rules / delivery / error 等关键通道;stats 暴露 entries / capacity / dropped / writeErrors / persistence。重启后记录丢失,面板会明确提示。
写入前所有自由文本都经过统一 sanitizer:先限制扫描长度(4000 字符),再做凭据模式脱敏,最后截到 200 字;只保存 ID、计数、枚举原因、字符数和短 hash,不保存记忆正文、原始工具参数、异常堆栈。模式脱敏是保守匹配,不承诺覆盖所有秘密形状。
卸载 / 迁移
pnpm remove @restregular/dsh-memory # 再从 profile 的 patch 里删掉那条 entry
卸载插件不会移除已配置的向量服务与记忆文件——它们不在 node_modules 里,而是在
<storeDir> 与 <installRoot>。要清掉,先打开面板「服务 → 维护」:
- 卸载向量服务:删运行时与模型(两步确认,显示释放空间);记忆数据保留。
- 清空记忆数据:删全部 JSONL 与向量边车(两步确认,不可恢复)。
插件被卸掉后面板就打不开了,所以这件事只能在卸载之前做。迁移时直接复制这两个目录即可; 只想带走记忆内容,用「设置 → 数据 → 导出」。
开发
npm test # node --experimental-strip-types --test source/*/test/*.test.ts
npm run build # source/ → packages/dsh-memory(含产物契约检查)
npm run verify # 构建到 .verify/,不落 packages/
构建用 Node 内置类型剥离,相对 .ts 说明符改写为 .js,每个产出文件 node --check。
产物契约检查覆盖会静默失败的几处:patch 恰好一行、exports 放行 ./client 与 locale、
__ModuleLoader__.load({ id }) 等于包名、四个半边的相对 import 都在、icon.svg 体积在宿主上限内。
发布:
node tools/publish-npm-packages.mjs --scope @restregular --version x.y.z。 npm token 不经过命令行,默认从 DevCred 读取,由devcred inject写进临时.npmrc,发布后连目录删除。 npm 的暂存发布会短暂出现版本不可下载、latest未更新;先轮询确认,不要重复发布或 bump 版本。
常见问题
向量服务起不来
看 <installRoot>/llamacpp/llama-server.log。常见原因:端口被占、cwd 里缺 llama-server-impl.dll、
模型与运行时风味不匹配(比如 GPU 风味但没驱动)。
检索很差 / 全是词法命中
先看面板「当前模型」那行的维度是否与模型一致,再看 .vec 身份头。维度或 pooling 写错不会报错,
只会让向量通道悄悄失效。minScore 可能需要按模型微调。
想换模型 先「维护 → 卸载向量服务」再装;换完现有记忆会重新嵌入。
面板里看不到「记忆系统」
包名与 Loader entry 必须一一对应;确认 profile 的 patch 里只有一条 @restregular/dsh-memory,
以及依赖真的装上了。
插件列表里还是包名,没有「记忆系统」
先确认版本 ≥ 0.11.0 且包里有 locale/ 目录;然后重启一次 DSH 桌面端。宿主的 Node 进程会缓存
package.json 的 exports 映射,原地更新不会让这份缓存失效;重启后恢复,之后的版本更新不再需要。
记忆没有自动召回
确认 inject.recall / autoRecall 已开、当前作用域在召回范围内(标签触发的自动召回只含 global + 当前会话 +
当前工作区,普通每轮召回按 inject.extraScopes 配置),并检查标签是否真的命中。打开面板顶部的「审计」标签页,
按会话/通道/问题筛选即可看到触发原因、候选、过滤、省略、预算和投递状态;也可以直接请求
GET /memory/api/audit?session=<id>。
作用域列表里出现两个同名路径 / 用 path 存的记忆工作区里看不到
0.11.0 及以前,D:/repo 与 D:\repo 会被当成两个项目作用域(写的人用正斜杠、会话读的是宿主给的反斜杠)。
0.11.1 起路径写法统一,首次启动会把历史分叉合并到规范键(记录、别名都跟着走,旧文件删除)。
数据怎么备份
复制整个 <storeDir>;或用面板导出 JSON。导入会按当前模型重新嵌入,不携带旧向量。
更新日志
- 0.22.3 —— 审批卡高度与宿主审批卡同口径(去掉额外的
46vh上限),矮窗口下详情行固定后条目列表仍有滚动空间。 - 0.22.2 —— 修复审批卡条目不显示 Agent 查询 / 说明:两行原来带
overflow:auto,在 flex 列里会被长列表压成 0 高;现在锁flex:none,只让列表滚动。 - 0.22.1 —— 审批卡可折叠(strip 上「折叠 / 展开」,折叠后动作行仍可用),读审批条目整行点击即可选中。
- 0.22.0 —— 审批卡支持「全选 / 取消全选」,展示工具调用的
description,点记忆 ID 跳到面板「记录」页并展开;memory_*新增description(普通限制下必填);memory_search的去重裸 ID 与带摘要条目之间空一行。 - 0.21.0 —— 范围外审批卡改为会话内展示(对齐系统权限审批的样式与行为),读审批支持逐条勾选允许;
memory_search/memory_list引入会话上下文去重,已出现过的条目只回记忆 ID 并提示用memory_get(内容更新后重新给摘要)。 - 0.12.7 —— 审计详情可完整滚到底;ID 浮窗改为按气泡位置固定显示,延迟 hover 显示、可移入浮窗,点击气泡或浮窗都直接跳转。
- 0.12.6 —— 审计详情分类展示选中/新增/标签/省略/去重 ID;记忆与会话 ID 支持 hover 浮窗和点击跳转;详情独立滚动可到底。
- 0.12.5 —— 审计表头移入滚动容器并 sticky,与行严格对齐;重分配列宽,行内预算改紧凑条。
- 0.12.4 —— 审计页默认每 0.5 秒自动刷新(可关/可调间隔);移除顶部标题与气泡,整页固定在窗口内,仅列表滚动;新增表头并优化列宽;复制/导出提示就地显示。
- 0.12.3 —— 审计页筛选改为两行紧凑布局与自绘时间范围/开关;事件列表放入独立滚动容器。
- 0.12.2 —— 修复
CordisMemoryService未转发审计方法,导致审计页显示“provider 未提供注入审计接口”;升级后需重启 DSH。 - 0.12.1 —— 审计 v2 收尾:详情无障碍/唯一 key、JSON 导出、摘要渲染失败记录、部分注入 outcome。
- 0.12.0 —— 注入审计 v2:评估/投递分离、失败与跳过可解释、统一脱敏、有界存储与容量统计、分页筛选 API、独立「审计」标签页与 JSON/JSONL 导出。
- 0.11.1 —— 修复项目作用域因路径写法不同而分叉:
D:/repo与D:\repo统一成同一个键,历史分叉首次启动自动合并。 - 0.11.0 —— 插件显示名改为「记忆系统」,重写 README 并更新截图;无 API / 配置 / 数据格式变更。
- 0.10.1 —— 深色模式开关跟随宿主 token,修复白点融进白轨道。
- 0.10.0 —— 多进程安全写入、向量身份加入 pooling、读路径能看到其它进程的新写入。
完整记录见 CHANGELOG.md。
License
English: Memory System — local-first long-term memory for DeepSeek Harness. One npm package, one loader entry: JSONL storage, local vector recall through llama-server, five model tools, automatic recall, hard rules, and a visual management panel. Memory stays on your machine.