Repository navigation
Releases: Max-Null/dsh-memory
Release list
v0.19.1
Added
日记进定时备份。 <DSH_HOME>/diary/*.md 与记忆文件同一轮复制,落在 <backupRoot>/diary-archive/:固定目录、只增不删,不参与记忆快照的份数轮转。
保留策略之所以不同于记忆:记忆快照是同一份数据的多个时间点副本,丢一份还有别的份;归档里每一篇都是日记的唯一副本,丢了没有任何地方能重建。所以归档目录名刻意不匹配轮转正则(^\d{8}-\d{6}(?:-\d+)?$),靠名字天然躲开清理,不必给 pruneBackups 开白名单。
- 三档处理:归档里没有 → 复制;逐字节相同 → 跳过;不同 → 以日记为准覆盖(人可能事后改过错字)。
- 归档里有、日记里已没有的篇目一律保留——那正是归档的价值。
manifest.json增diary: { root, copied, kept },记这一轮归档做了什么。- 日记归档先于「记忆文件是否存在」的判断执行:只剩日记可备份时照样归档。
修正
备份能力归包,不归宿主壳。 日记备份曾被写进思灵的 shell/scripts/backup-storages.mjs,那等于「装了那个壳才有日记备份」,而日记是本包产生的数据。改在包内后,换任何宿主编排这条能力都成立。
v0.19.0
Added
日记:第二套记忆。 新增 diary_read / diary_write,正文落在 <DSH_HOME>/diary/YYYY-MM-DD.md。
记忆与日记的差别不在存储,在写的时候记什么:记忆记「从中得出了什么」(准入判据是有用性),日记记「今天发生了什么」(没有准入判据)。memory_save 那套判据对「他说手麻了」这类经历本来就失效——那不是事实,是经历。
- 一天一个文件,按
## HH:MM分段;同日多场对话是追加,不覆盖。 - 日记正文不经过
ctx.storage:那条路是 writer per process and last-write-wins,日记不该被另一个实例的「读—改—写」周期吃掉。 - 不参与检索、不占常驻注入预算;注入里只有一行指针(目录 + 最近日期)。
- 指针在距上次写日记超过 6 小时后附带一句提醒,同一次挂载只出现一次。
Config schema(schemastery)。 本包此前只有 TS 接口 MemoryConfig,没有可被 loader 识别的配置 schema,因此在插件页里是空白。现在导出 Config,并把 diary.enabled 标 .volatile()——表单由 loader 依 schema 自动构成,写进 profile 的 cordis.patch.yml,本包不另设存储文件。
边界
- 关闭日记后两个工具拒绝执行而不是被注销:动态注销要响应式状态,而插件配置改动走 cordis 的 entry 重跑——为一次开关把注册生命周期做成可变的,代价大于收益。拒绝信息里写明「已有日记不会被删」。
- 提醒搭在指针行上而不是
agent/pre-step的消息:后者要引入dsh-llm的createUserMessage与三个 peer 依赖,而效果只是从「system prompt 里一句话」变成「对话里一条消息」。 - 除
diary外的配置字段一律透传(schemastery 字段默认可选),不加.default():它们的默认值依赖运行时($DSH_HOME、平台路径),schema 里硬填一个会顶掉MemoryConfig的?? 默认解析。
v0.18.0
Added
钉入常驻时返回预算占用:memory_save / memory_update 的结果在该记录为常驻时附带 resident: { total, injected, omitted, budget }——钉下去的那一刻就能看到「现在有几条常驻、几条真进场、几条被挤出」。
动机是 #2:出局由下一轮注入的诊断行报出,而看到那行的人通常已经想不起是哪一次钉入造成的。本机现状:37 条常驻、预算 1500、实际进场 11 条、出局 26 条。
边界
- 排序规则不动(仍按
updatedAt):换排序只改变「谁先出局」,不改变「出局多少」——37 条塞不进 11 个位子,是容量与库存不匹配,不是排序偏好。 - 不引入
priority/pinned:它们会把「预算不够」这个事实藏起来(全都显示为已注入),而超出预算的部分并没有更好的去处。 - 未常驻的记录不带该字段(
memory_list也不受影响)——它只在「挤掉别人」这个决定发生的时刻有意义。
v0.17.0
Added
memory_search 增加 limit:默认最多返回 20 条,0 表示不限,负值按 20 处理。返回体形状不变(仍是 { record, score } 数组),"被截断"只能从条数等于 limit 推断——这条约定写进了工具描述。
动机是 Max-Null/dsh-memory#1:中文分词是「单字 + 2-gram」,常见汉字让几乎全部记录拿到正分。本机 279 条库实测宽查询命中 235 条(84%),一次返回体可达数万字符;而 20 名之后分数已趋同(ssid 这类单 token 查询前 50 名几乎同分)。
Changed
- 默认返回变少(0.17.0 之前是全量)。要看全量传
limit: 0;memory_list不受影响。 - 工具描述补两点:条数上限与"条数等于 limit 时可能还有更多"的约定;以及"多词窄查询比单个常用词排序好得多"——截断只降成本,不改召回质量。
边界
- 截断只在工具层,
engine.search()仍是「全部正分命中」——prompt_search依赖它的全量结果给模板排序,写进 engine 会让命中的模板静默沉底。 - 命中记账不变:仍只记 BM25 前 5(宽查询不该把半个库记成热的)。
- 不做 offset 分页:第 50 名之后的分数与末位同量级,翻出来是噪声;看全库走
memory_list。
设计:docs/设计/2026-10-21-memory-search-结果截断-设计方案.md。
v0.15.1 — 补上 0.15.0 漏进包体的自述与 README 核对
这是一个补丁版:0.15.0 发布时,它的 README 与自述核对还没落地。
发生了什么
0.15.0 在 06:05 发布,而 README / 自述的核对在 06:06–06:08 才完成——发布抢在了核对前面。于是已发布的包体里:
- 自述(
memory:self,模型每轮都会读到的那段)说「你有 11 个工具」,而实际注册 12 个 - 自述里那条「自动升上来的常驻在 30 天未命中后撤下」——0.13.0 已改为只计量不生效
- README 有五处在描述两条已被取消的自动规则(命中累计 2 次自动升常驻、30 天未命中自动撤下)
验证方式:从 npm 下载 @max-null/dsh-memory@0.15.0 的 tarball 解包,dist/self.js 里 11 个工具 在、12 个工具 不在。
这一版改了什么
- 自述的工具清单补上
memory_move,数字改为 12 个;那条已停用的自动规则改成如实描述。 - README:删掉流程图里两行已取消的规则,改写两段说明,「超预算按最近使用优先」改为最近更新(0.12.0 就改了的行为,README 一直停在旧写法),工具清单补全到 12 个。
- 工具清单不再手抄:
index.ts新增registeredToolNames——注册时登记(ToolRegistry只提供register/get,没有列举 API,没法反查注册表)。新增用例遍历它,断言每个工具名都出现在自述清单里。
关于那把锁
负向控制实测过三轮才真正锁住:从清单里删掉 memory_move —
- 第一版断言没红:它检查「名字出现在自述全文里」,而
memory_move在后面的括号说明里也有一份。 - 第二版收紧到「工具清单那一段」——仍然没红:正则
[^。]*太贪,跨到了说明段。 - 第三版「取清单段 + 剥掉中文括号」才红在正确位置。
这条也顺带暴露了一个坑:registeredToolNames 是模块级数组,apply 时不清空的话,同进程多次挂载会累积成双份——而断言里的数字是固定的。这个坑只有跑全套才暴露,单独跑那条用例反而是绿的。
门槛
typecheck 零错误,174 测试全绿,构建产物已确认转正(dist/self.js 里 12 个工具 在、11 个工具 不在),check:memory 全绿。
教训
§2(README 与实现核对)应当在 §8(发布)之前完成并进入包体。这次是发布先落地、核对后完成——两者的时间差就是这一版的全部内容。
v0.15.0 — 写入兜底与失败可见化 + 自动降级改为只计量(含 0.13.0 / 0.14.0)
这一条 Release 覆盖 0.13.0 / 0.14.0 / 0.15.0 三版。 中间两版没有单独的 Release:0.13.0 未发布到 npm(版本号直接跳到 0.14.0);0.14.0 发布过,但若给它补 tag,那个 tag 指向的代码状态会含之后的复核修复,与 npm 上的包体不一致——tag 与包体不一致比缺 tag 更糟,所以不补。完整逐版说明见 CHANGELOG。
0.15.0 — 扫描器在装版环境跑不起来
check:memory(0.14.0 新增的存量体检门)用「插件自己的 blockSchema」做判据,但脚本从 dist/engine.js import 它——而那个模块在运行时 import 了 @deepseek-ai/cordis、dsh-storage-domain、dsh-storage-json,三个都是宿主提供的 peer,装版实体里没有:
Cannot find package '@deepseek-ai/cordis' imported from .../dist/engine.js
开发仓有 devDependencies,所以本地一直能跑;这个缺口是发布后在装版实体上实测才发现的。(顺带修正:脚本原来的提示写的是「先跑 npm run build」——问题不在构建,而在 peer 解析。)
修法:blockSchema 与 describeIssues 抽到 src/schema.ts——一个零 peer 依赖的模块(只依赖 zod 这个真 dependencies,以及只用 node 内置模块的 anchors.ts)。判据仍然只有一份。
验证:在真实装版环境(有 zod、没有那三个 peer)里实测——干净存量退出 0;注入坏记录退出 1 且给出 anchor.kind: Invalid option: expected one of "env"|"tool-list"|"self-version"|"path-exists"。
0.14.0 — 写入兜底与失败可见化
起因是一次真实故障:一条记忆写了 anchor.kind: "path-exists",写入当场成功,用户重启内核后该工作区 154 条 project 记忆全部不可见——数据一条没丢,丢的是可读性。故障链条是「写入放行 → 加载才炸 → 失败还被吞」,三层里任何一层立住都不会发生。
治本:ANCHOR_KINDS 成为全仓库唯一一份 anchor kind 集合,其余全部由它派生——存储 zod 此前只有 3 个 kind,正是那次故障的根因。加 path-exists 时改了两处、漏了第三处;三份副本各自维护,漏改是结构性必然。收敛前实测散在 8 处。编译期穷尽检查已验证(注入假 kind → TS2322: not assignable to type 'never')。
门一(写入端校验):新增 putBlock,11 处 table.put 全部收敛。写前跑 blockSchema.safeParse,不合格抛错并带 zod issue。
门二(打开失败可见化):projectTableFor 记录失败原因,成功的打开清掉对应项;memory_sweep 新增 coverage 字段声明数据完整性(included / absent / unreadable)。宁可显式写「未纳入」,也不要让人对着一个看起来正常但偏小的数字做判断。
门三(存量体检):新增 scripts/check-memory-files.mjs 与 npm run check:memory。只读,退出码 0/1/2。判据从 dist/schema.js import 插件自己的 schema——同源是刻意的,另写一套会漂移成「扫描器说没事、打开时炸」。
一处实测发现:存储层封装后的错误给到 domain 名 + 记录 id + 表名,但说不出字段级原因——所以门二与门三互补而非冗余:门二答「有没有坏」,门三答「坏在哪」。
0.13.0 — 自动降级改为只计量(行为变更)
demoteStale 不再撤销常驻注入,改为返回「本会被撤下」的候选清单。
理由是那个阈值(AUTO_DEMOTE_DAYS = 30)没有分布依据——它是 2026-09-15 从面板的折叠阈值 COLD_DAYS 借来的。而两者代价不对等:展示阈值折错了点一下就能展开;行为阈值一撤,0.12.0 取消自动升之后基本不会自己回来。同一个数字,两种不对等的代价。而同批的 N=2(AUTO_INJECT_HITS)已被证伪并删除,T=30 却从未被同样审过。
判据取自第三方记忆插件的写入准入阶段一:「在没有真实分布时,拍任何数字都是误杀面」——所以先只计量,等分布。
- 判据抽成纯函数
isAutoDemoteCandidate,体检与降级共用一份 - 体检报告新增
autoDemoteCandidates(含idleDays),让那个数字变得可观察
顺带
- README 与自述(
memory:self)核对:翻出五处在描述两条已被取消的自动规则,以及自述说「11 个工具」而实际注册 12 个(memory_move自 0.11.0 起就没进过清单,README 更早,只列了 7 个)。工具清单改为注册时登记(ToolRegistry只有register/get,没有列举 API),新增用例遍历它——不手抄名字。 - 测试 166 → 174。
v0.12.2 — 适配 DSH 0.2.0-rc.1
适配 DSH 0.2.0-rc.1:内核 peer 放宽为覆盖两代。
改了什么
- 6 个
@deepseek-ai/dsh-*peer 从^0.1.1-rc.1改为>=0.1.1-rc.1 <0.3.0。
为什么
caret 的上界是「下一个 minor」——^0.1.1-rc.1 展开为 >=0.1.1-rc.1 <0.2.0-0,对 0.2.0-rc.1 判定 false。而 peer 不满足时插件不会被加载,也不出现在 did not activate 列表里:它在进入 fiber 图之前就被跳过,界面不报错,只表现为少一批功能。内核一跨 minor,所有写成 ^0.1.x 的插件会一起静默失效。
判据用 typecheck,不用推理
把 devDependencies 的 6 个内核包升到 0.2.0-rc.1,用新内核的类型定义跑 tsc --noEmit:零错误——说明本插件没有使用新内核的 API,属「宽范围两边都能跑」那一类。故下限保持原值(作者实际验证过的最早版本),只放开上界。devDeps 不进发布包。
验证
- typecheck 零错误(用的就是 0.2.0 的类型定义),测试 166 例全绿。
- 隔离实例(
home-100/ssid-dev)实机加载:三个失败信号did not activate/pending (waiting for service/incompatible全为 0,@max-null/dsh-memory在 client combo URL 内。 - 关键:该实例的
compatibility.json豁免只写了@max-null/dsh-memory@0.12.1,本次挂 0.12.2 匹配不上仍加载成功——证明确实是 peer 本身满足了,而不是靠豁免续命。
兼容性
- 验证过的内核:
0.1.1-rc.2、0.2.0-rc.1。 - 0.1 线与 0.2 线都可装载;比
0.1.1-rc.1更早的内核未验证,不声称支持。
v0.12.1 — 锚点可改写、体检写回
[0.12.1] - 2026-09-23
0.12.0 上线当天实测暴露的两处缺口,都是一行级改动;连带把 anchor 投影给模型。
Fixed
memory_sweep调用后刷新注入里的失效提示:lastSweep原先只在会话启动维护里赋值,于是「本次校验发现 N 条失效」那句整个会话内不再变化,可能一直挂着一条已经被删掉的记录(实测:报的两条早已forget并重建,提示照旧)。改为sweep()自己写回最近一次报告,工具调用因此也能刷新它。只写实例字段——不落盘、不改记忆,「只读」的承诺不变。anchor从来没有投影给模型:memory_save能写锚点,但工具返回的 record 与memory_list里都看不见它——写对没有、当前绑的是什么值,只能猜;memory_sweep只在失效时顺带带出锚点名。在本版之前这只是一处不便,加上「改锚点」之后就成了硬伤:改完看不见结果。现在anchor进RECORD_SCHEMA与recordValue(仅在有锚点时出现)。
Added
memory_update支持改写 / 清除锚点:新增anchor参数——省略保持、给值改写、null清除。给值会当场重新探测并同步stale:新锚点成立就撤掉旧锚点留下的失效标记(不必等下次会话维护),不成立就立刻标失效并撤下常驻,探测不到(未校验)则不动状态。清除锚点时只清「因该锚点而标的失效」——「被取代」「被撤回」两种来源不动,解除约束与撤销作废是两件事。参数用oneOf: [对象, null],与MemoryPatch.anchor?: MemoryAnchor | null逐字对应。- 自述补一句(
self.ts):锚点可以用memory_update改写或清除。此前模型读得到锚点的作用,却没有任何改它的手段,读起来像一条只能写死一次的东西。
内部
- 锚点失效的原因文案抽成
MemoryEngine.anchorStaleReason(verifyAnchorsInner与update共用)。「清除锚点时认得出这条失效是不是它标的」要求两处逐字相同——各写一份就会认错,把撤回、被取代标的失效一起抹掉。 - 测试 161 → 166(含工具层一例:
anchor: null与对象形态都要真过 schema 校验,另加一条反向断言——非法形态必须被拒,否则前两条只测到了引擎、没测到 schema)。scripts/mutate-check.mjs补两条守卫(体检写回、改锚点当场校验),现共 5 条,全部如期变红。
v0.12.0 — 记忆的生命周期:准入 / 在场 / 退场
0.12.0 — 记忆的生命周期
准入 / 在场 / 退场,外加只读体检与时间维度。
行为变更(升级前值得知道)
- 取消自动升常驻:命中次数达标不再自动把记忆钉成常驻,只进候选提示。原判据「被反复检索命中」度量的是它被想起来了,而常驻的语义是它不能被忘——能被检索命中的恰恰是检索已经够用的那些。实测:本机 56 条常驻里 49 条(87.5%)来自自动升,多数是事实与案例记录,而显式钉的那几条全部是规则、委托与判据。
- 注入排序不再看
lastUsedAt:改为updatedAt降序(一行)。lastUsedAt由检索路径上的记账刷新,拿它决定「谁每轮在场」,会让一次memory_search就改变下一轮的注入内容。 memory_update新增supersedes/retract:作废时只标状态、保留内容,检索侧仍可检回并带原因。此前改内容会让旧结论直接消失,不留「被谁取代」的痕迹。- 注入行带日期:
- [memory:xxxxxxxx:ns@2026-09-14@..] …(取updatedAt,本地时区),每条 +11 字符。
新增
path-exists锚点:把记忆绑到一条路径上,文件不存在即自动失效。相对路径以会话工作区为基准;拿不到 cwd 时判「未校验」而不是「不存在」。memory_sweep工具:只读体检——失效记录(带原因)、久未命中的非常驻记录、被反复检索却未常驻的候选、常驻与预算占用。不修改任何状态。memory_list时间过滤:after/before,接受YYYY-MM-DD或7d/30d相对形式。
内部
- 注入诊断行新增两行(都不占预算):本次校验发现的新失效、候选提示。
- 测试 138 → 161;
scripts/mutate-check.mjs三条变异守卫全过(取消自动升 → 7 红、排序 → 1 红、注入中和 → 1 红)。 - 文案同步:
self.ts/GUIDANCE/injected参数描述三处行为控制器。
完整说明:CHANGELOG.md 与 docs/设计/2026-09-23-记忆生命周期-迭代方案.md
已知缺口(留给下一版):锚点写入后无法修改——memory_update 没有 anchor 参数,写错只能删除重建(会换 id)。实测触发:一条设计原则误绑 self-version: 0.11.1,升级后当场被判失效。
v0.11.0 — memory_move 跨层移动 + 脉络引导
Added
memory_move工具(迭代设计 §3.2):把一条记忆移到另一层——global、self(当前工作区),或相对祖先(../../..)。两个方向共用一个操作:向上提到祖先层或global,向下把误放高层的降回它真正属于的工作区(global里那 11 条 DSH/SSiD 生态经验正等着这一步)。记录保持 id,返回值报告移动后源层是否仍看得见它——约束从「禁止方向」换成「让后果可见」,因为移动的本意就是改变谁能看见。- GUIDANCE 补脉络引导(迭代设计 §3.1):记结论时一并记下它是怎么来的——为什么成立、什么场景把它逼出来。没有出处的结论像一把没有来历的尺子:看起来天经地义,而天经地义的东西最容易被机械套用。
内部
- 抽出
locateWithKey():定位记录并带上它所在的层——locateRecord只返回表,而移动必须知道来源层。 MemoryChange增加moved事件(带from/to)。test/memory-move.spec.ts7 例。测试隔离教训:造工作区的用例必须在自己的mkdtemp根下建树,绝不能往tmpdir()写记忆——那是所有 spec 的共同祖先,写一条进去,别的用例的工作区链就会走上去把它读进来,表现为一堆毫不相干的断言凭空多出一条记录。那不是泄漏,是祖先链在正常工作。