@kira905/dsh-memory-toolkit 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +130 -1
  2. package/README.en.md +225 -2
  3. package/README.md +208 -2
  4. package/cordis.patch.yml +32 -0
  5. package/lib/deploy-guard.mjs +463 -275
  6. package/lib/guard-http.mjs +59 -62
  7. package/lib/pitfall-form-map.mjs +94 -84
  8. package/lib/pitfall-score.mjs +262 -185
  9. package/lib/session-file-name.mjs +116 -100
  10. package/lib/zstd-frames.mjs +88 -67
  11. package/memory-db-tools/README.md +29 -1
  12. package/memory-db-tools/audit-analyze.mjs +1 -1
  13. package/memory-db-tools/audit-export.mjs +60 -16
  14. package/memory-db-tools/audit-struct-probe.mjs +29 -8
  15. package/memory-db-tools/lib/known-devices.mjs +137 -0
  16. package/memory-engine/README.md +58 -4
  17. package/memory-engine/bench/bench.mjs +1 -1
  18. package/memory-engine/cli/band-sentinel.mjs +164 -0
  19. package/memory-engine/cli/distill-rollback.mjs +112 -44
  20. package/memory-engine/cli/distill-sessions.mjs +237 -0
  21. package/memory-engine/cli/distill.mjs +196 -95
  22. package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
  23. package/memory-engine/cli/doc-index.mjs +47 -38
  24. package/memory-engine/cli/domain-tags.mjs +246 -162
  25. package/memory-engine/cli/layer-migrate.mjs +176 -0
  26. package/memory-engine/cli/layer-move.mjs +189 -0
  27. package/memory-engine/cli/pitfall-hook.mjs +136 -70
  28. package/memory-engine/cli/recall.mjs +243 -110
  29. package/memory-engine/cli/selfcheck.mjs +93 -44
  30. package/memory-engine/cli/serve-start.mjs +221 -140
  31. package/memory-engine/cli/sweep.mjs +83 -44
  32. package/memory-engine/cli/sync.mjs +93 -57
  33. package/memory-engine/config/audit-devices.json +6 -0
  34. package/memory-engine/config/bands.json +7 -0
  35. package/memory-engine/config/layers.json +7 -0
  36. package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
  37. package/memory-engine/dict/lexicon-config.json +11 -0
  38. package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
  39. package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
  40. package/memory-engine/lib/band-sentinel.mjs +417 -0
  41. package/memory-engine/lib/cli-strict.mjs +128 -0
  42. package/memory-engine/lib/cli-util.mjs +147 -125
  43. package/memory-engine/lib/config.mjs +83 -96
  44. package/memory-engine/lib/dirty-ledger.mjs +127 -94
  45. package/memory-engine/lib/distill-rollback.mjs +173 -133
  46. package/memory-engine/lib/distill.mjs +878 -585
  47. package/memory-engine/lib/doc-index-store.mjs +208 -235
  48. package/memory-engine/lib/doc-index.mjs +351 -350
  49. package/memory-engine/lib/doc-scan.mjs +178 -156
  50. package/memory-engine/lib/doc-summarize.mjs +220 -205
  51. package/memory-engine/lib/domain-tags.mjs +358 -277
  52. package/memory-engine/lib/embedder.mjs +175 -194
  53. package/memory-engine/lib/hashes.mjs +22 -7
  54. package/memory-engine/lib/layer-migrate.mjs +330 -0
  55. package/memory-engine/lib/layer-move.mjs +307 -0
  56. package/memory-engine/lib/layers.mjs +191 -0
  57. package/memory-engine/lib/lexicon-recall.mjs +473 -0
  58. package/memory-engine/lib/memory-source.mjs +78 -80
  59. package/memory-engine/lib/pitfall-source.mjs +115 -114
  60. package/memory-engine/lib/policy-source.mjs +279 -261
  61. package/memory-engine/lib/recall-log.mjs +285 -0
  62. package/memory-engine/lib/recall.mjs +565 -240
  63. package/memory-engine/lib/selfcheck.mjs +142 -106
  64. package/memory-engine/lib/session-distill.mjs +603 -0
  65. package/memory-engine/lib/session-source.mjs +421 -0
  66. package/memory-engine/lib/sweep.mjs +107 -130
  67. package/memory-engine/lib/sync.mjs +110 -140
  68. package/memory-engine/lib/tick-hook.mjs +123 -148
  69. package/memory-engine/lib/vectors-store.mjs +213 -188
  70. package/memory-engine/test/12-layering.test.mjs +539 -0
  71. package/memory-engine/test/13-session-distill.test.mjs +616 -0
  72. package/memory-engine/tools/bridge-smoke.mjs +94 -55
  73. package/memory-engine/tools/fetch-model.mjs +167 -87
  74. package/memory-engine/tools/smoke-recall.mjs +94 -38
  75. package/memory-engine/tools/verify-recall-chain.mjs +413 -0
  76. package/package.json +11 -1
package/README.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # dsh-memory-toolkit
2
2
 
3
+ > **本仓装完即用**(0.1.3 起):`dsh plugin --profile <profile> add @kira905/dsh-memory-toolkit` 一条命令,`dsh.bundle` 会让它自动挂进 profile 的组合列表,**不需要再手工写 `cordis.patch.yml`**。包内 3 个 CLI(`dsh-memory-audit` / `dsh-memory-summary` / `dsh-memory-export`)与挂载无关,`npm i -g` 或 `npx` 直接用。
4
+
5
+ ## 装完自检(三条只读验法)
6
+
7
+ `<profile>` 换成你实际用的 profile(默认 `web`),`<数据根>` 换成你的 DSH 数据根(默认 `~/.dsh`)。
8
+
9
+ ```powershell
10
+ # ① 组合树里有没有它(只读、约 0.6 秒、不启服务)
11
+ dsh --profile <profile> --dump-config | Select-String "dsh-memory-toolkit"
12
+ # 期望:bundle 层出现一行 "# == <登记名>"、insert 层出现 "- id: <entry>"(问的都是「它挂上没」);
13
+ # 本仓包名 @kira905/dsh-memory-toolkit —— 带 scope 装的写带 scope 名、老工装 / 手写的可能是裸名,两种都搜一遍
14
+ # ⚠️ 若该包会 insert 一行,还要数一遍"同一个 id 出现了几次",必须 == 1
15
+
16
+ # ② 组合列表里有没有它(只读)
17
+ Get-Content <数据根>/profiles/<profile>/package.json | Select-String "@kira905/dsh-memory-toolkit"
18
+ # 期望:dependencies 与 dsh.profile.bundles 两处都有(CLI 类只需要 dependencies;bundles 里也可能写裸名 dsh-memory-toolkit)
19
+
20
+ # ③ 命令在不在 —— 【只查存在,不执行】
21
+ Test-Path <数据根>/profiles/<profile>/node_modules/.bin/dsh-memory-summary.CMD # 装进 profile 的
22
+ Test-Path "$env:APPDATA\npm\dsh-memory-summary.CMD" # npm i -g 装的
23
+ # 期望:True。两条路径按你实际用的装法挑一条
24
+ ```
25
+
26
+ > 探针只做存在性检查(只看命令文件在不在,不执行任何命令)。
27
+ > 要不要用 `--help` 验,见本仓当前说明与该命令的参数表。
28
+
29
+ ---
30
+
3
31
  **DSH(DeepSeek Harness)生态的记忆面工具集** —— 一个独立向量记忆引擎、一套记忆库只读体检工具链、
4
32
  一个自持 fork 的本地记忆插件,以及一个 persona ↔ 记忆体检小工具。
5
33
 
@@ -12,12 +40,188 @@
12
40
 
13
41
  | 目录 | 是什么 | 形态 |
14
42
  |---|---|---|
15
- | [`memory-engine/`](./memory-engine/README.md) | 记忆引擎(离线层):独立矢量库 · 增量同步 · 兜底 hash 轮转 · 混合召回(向量 + FTS5 + RRF) · 文档索引 · 启动自检 · 本地编码桥 | 纯 Node ESM + `node:sqlite` |
43
+ | [`memory-engine/`](./memory-engine/README.md) | 记忆引擎(离线层):独立矢量库 · 增量同步 · 兜底 hash 轮转 · 混合召回(向量 + FTS5 + 词表 BM25 + RRF + 编码降级兜底 + 旁路留痕) · 文档索引 · 启动自检 · 本地编码桥 | 纯 Node ESM + `node:sqlite` |
16
44
  | [`memory-db-tools/`](./memory-db-tools/README.md) | 记忆库工具集:只读导出 / 六项体检分析 / 结构探针 / 特征词探针 / 与 persona 的重合度 / 看板账本只读探针 / 零改动自证 / 新会话开工简报 | 一组独立 CLI |
17
45
  | [`dsh-memory-local/`](./dsh-memory-local/README.md) | 本地记忆插件:上游 `dsh-memory` 的自持 fork(跨会话持久记忆 + 召回提示段 + 指针注入链),带构建 / 幂等部署 / 一键回滚 / 部署后自检 | DSH 插件 |
18
46
  | [`persona-memory-audit.mjs`](./persona-memory-audit.mjs) | persona ↔ 记忆体检:把「persona 正文体积」与「记忆可见率」两项指标固定下来,超阈值即报红 | 单文件 CLI |
19
47
  | [`lib/`](./lib) | 四件共用的无依赖小件(参数白名单 / HTTP 存活判据 / zstd 帧解析 / 会话文件名 / 坑库打分) | 纯函数模块 |
20
48
 
49
+ ## 同源说明
50
+
51
+ 本仓**部分文件与一套内部生产实现同源**(同一份工程长期演进下来的两处落点)。
52
+
53
+ - **同源程度**:按**行级同源度**口径统计(同名可比件里,仓内文件的非空行有多少能在参照件中找到,多重集扣减),本仓「≥50% 行同占比」= **64%**。
54
+ - **这意味着**:本仓是那套实现的**提炼导出** —— **既不是逐字副本,也不是完全重写的独立实现**。拿单个文件当"独立参考实现"读之前,请先跑下面的判据。
55
+ - **怎么复跑**:同源度扫描器见 `dsh-quality-toolkit` 的 `dsh-sim-scan.mjs`(`--repo <本仓> --prod-root <参照根>`);分档阈值与口径写在它的 `config/sim-scan.json` 里,不藏在代码里。
56
+ - **许可**:本仓许可见仓根 `LICENSE`(有子树例外时以子树 `LICENSE` 为准)。
57
+
58
+ ## 召回链:降级 / 词表第二路 / 命中留痕(0.2.0 新增)
59
+
60
+ 召回有三类老毛病,这一版各给一条**可一键关回**的解法;出厂默认一条都没改。
61
+
62
+ | 毛病 | 现在的做法 | 开关 | 出厂 |
63
+ |---|---|---|---|
64
+ | 编码超时/失败 ⇒ 那一轮**一条记忆都不注入** | 查询先截断到 256 字符;编码带预算(默认 1500 ms),超时 / 抛错 / 空向量一律**降级成固定 Top-K**(按最近改动取,条数默认 8),结果里带 `degraded.mode = "fixed-topk"` 与 `encodeError.name` | `fallbackTopK` / `encodeTimeoutMs` / `--fallback-top-k` / `--encode-timeout-ms` | 降级**常开**(只作用于失败路径,成功路径一字不改) |
65
+ | 纯向量对**精确词**不敏感;中文又走不了 FTS5(`unicode61` 不切中文) | **词表第二路**:jieba 词典最长匹配切词 + **BM25**(tf 饱和 + 文档长度归一 + 语料内 idf × 通用词具体度),与向量、FTS 一起进 RRF(k=60) | `memory-engine/dict/lexicon-config.json` 的 `enabled`、`MEMORY_ENGINE_LEXICON=on/off`、`--lexicon on` | **关**(关掉时结果与改动前**逐字节一致**,见下) |
66
+ | 召回了什么、命中了哪条、准不准,事后无从判断 | **旁路 JSONL**:query / 命中 id / 分数 / 是否降级 / 时刻;按字节轮转 + 天数清理,不无限增长 | 给目录:`--recall-log <目录>` 或 `DSH_MEMORY_RECALL_LOG_DIR`;`DSH_MEMORY_RECALL_LOG=off` 一键关回 | **不写**(库不该在纯查询里偷偷落盘) |
67
+
68
+ **为什么这么设计**(完整版见 [`memory-engine/README.md`](./memory-engine/README.md) 的「设计依据」):
69
+
70
+ - **降级用「最近改动」而不是随便凑数**:拿不到向量时,语义相关已无从计算;此时最可能的"有用"是刚写进来的东西。兜底三档依次是数据源直给的 `recent()` → 元数据按 `updated_at` 降序 → 向量表里 id 最大的几条,**保证非空、顺序确定、仍排除 pinned**。
71
+ - **词表默认关**:它的收益是"精确词多捞回来一截",代价是"两路融合会重排名次"。本仓对外的承诺是默认行为不改 ⇒ 出厂关、由使用方一行配置打开。打开后 `stats.lexiconStatus` 从 `off` 变 `ok`,命中里出现 `sources: ["vector","lexicon"]`。
72
+ - **留痕默认不写**:query 可能是隐私内容,库不该在纯查询里落盘;要判"召回准不准"就显式给目录,且字节/份数/天数三重封口。实际记录里**不存记忆正文**,只存 id 与分数。
73
+ - **为什么不复用生产那份补丁**:生产侧是"按行号锚点重放"的补丁(`patch-*-lexicon.mjs`),搬进独立仓必碎;本仓是按行为重写的模块,靠夹具与旧版对拍证明等价,而不是靠 sha 相同。
74
+
75
+ **词典体积与许可**:`词典.txt`(1,029,582 B)+ `词频.txt`(219,541 B)来自
76
+ [jieba](https://github.com/fxsjy/jieba) 的 `extra_dict/dict.txt.small`(MIT),随包内嵌在
77
+ `memory-engine/dict/`,合计约 **1.25 MB**(`npm pack --dry-run` 实测:整包 tarball **1.1 MB** / 解包 **2.8 MB** / **140** 个文件)。
78
+ **为什么要内嵌**:不内嵌的话 `npm i` 完这一路直接不可用(要么首次运行去联网拉,要么用户自己找词典)。素材目录可用 `MEMORY_ENGINE_DICT_DIR` 指到别处(打包方嫌大可以把它们挪出包,代价是这一路默认不可用)。
79
+
80
+ 许可按**子树优先**:`memory-engine/dict/` 保持上游 **MIT**(全文与版权声明见 [`dict/LICENSE-词典.md`](./memory-engine/dict/LICENSE-词典.md)),
81
+ 与仓级 AGPL-3.0 **不冲突** —— MIT 与 AGPL 兼容,分发时把那份版权声明原样带上即可(与本仓 `dsh-memory-local/` fork 的上游 MIT 同一口径)。
82
+ 词典/词频按字节原样保留(`.gitattributes` 里 `-text`),可与上游 sha256 直接对拍。
83
+
84
+ **怎么复现这些结论**:
85
+
86
+ ```bash
87
+ cd memory-engine
88
+ node tools/verify-recall-chain.mjs # 15 项:截断 / 降级 / 词表 / 留痕 / 多来源 / CLI
89
+ node tools/verify-recall-chain.mjs --baseline <改动前的 lib 目录> # 追加 2 项:开关关掉时与旧版逐字对拍
90
+ ```
91
+
92
+ ## 分层软淘汰 + 号段哨兵(0.2.0 新增)
93
+
94
+ 两个老毛病:记忆只能删、不能降级;多台机共用一个库时,id 号段越界没人喊。这一版各给一件,**出厂全是干跑 / 只读**。
95
+
96
+ | 件 | 干什么 | 命令 | 出厂 |
97
+ |---|---|---|---|
98
+ | `cli/layer-migrate.mjs` | 给记忆表加一列 `layer`,存量行回填「缺省层」 | `--db <库>`(干跑)· `--apply --backup <文件>` · `--restore <备份> [--apply]` | **干跑**:不带 `--apply` 一个字节都不写 |
99
+ | `cli/layer-move.mjs` | 把**一整层**搬到同库的边表 `memories_evicted`(不删),一条命令出去、一条命令回来 | `--layer <层> --apply` · `--back --apply` · `--dump <f>` / `--verify <f>` | **干跑** |
100
+ | `cli/band-sentinel.mjs` | 看「下一条新 id」会不会落到别人的号段里 | `--line` · `--apply --out <文件>` | **只读**:缺省连报告文件都不写 |
101
+
102
+ 层名与号段表都在配置里(`memory-engine/config/layers.json`、`memory-engine/config/bands.json`),
103
+ 代码里没有任何一处写死 `core / recall / archival`,也没有任何一台机器的名字。
104
+
105
+ ### 单机 vs 多机:怎么退化
106
+
107
+ - **单机(缺省)** = 配置里只有一条 band。此时哨兵**只读本机库**的 `sqlite_sequence`,
108
+ 判「下一条新 id」是否落在 `[base, base+width)`。**不 ssh、不猜对端、不假装知道别的机器**。
109
+ 这一步就能抓住最常见的事故形态:合并搬入把 `seq` 顶高 ⇒ 之后每条新记忆都写进别人的段。
110
+ - **多机(可选)** = 配置里有多条 band,且非本机条目带 `remote: { ssh, node, db? }`。只有这时才逐台去读,
111
+ 远端命令 / 别名 / node 路径**全部来自配置**。
112
+ - **远端读不到** ⇒ 结论写「未探测 / 不可达」,**退出码不是 4**(这条是生产件的血泪假红:把"连不上"和"号段坏了"
113
+ 混成同一个码,就会天天告警、把真事故淹掉)。`4` 只留给「本机库读不到」这种真失败。
114
+ 要"有段没探测就算失败",显式加 `--require-all`(那时 rc=5)。
115
+
116
+ 判据沿用生产件的好口径:`seq` 决定的下一条 id 越出本段 ⇒ 红;出现**更高位段**的行 ⇒ 红
117
+ (更高位 id 不可能由这一台合法产生);出现**更低位的别台段**行 ⇒ **只披露不判红**
118
+ (历史搬入 / 合并搬入共用段,删行=删记忆,而且不影响后续新 id)。
119
+
120
+ ### 为什么默认不自动动库
121
+
122
+ 三个命令出厂都是干跑或只读,理由就一条:**它们都直接改记忆库**,而这个仓的定位是"工具集",
123
+ 不能替使用者决定"什么时候动、动哪本"。具体收口:
124
+
125
+ - 迁移 `--apply` **必须**同时给 `--db` 与 `--backup`:不给库路径就不许动(防手滑改到家目录那本),
126
+ 不给备份路径就拒写(动 schema 前先备份是硬闸,不是建议)。
127
+ - 搬迁默认干跑;真搬要先 `--dump` 留一份逐行指纹,搬回来再 `--verify` 对拍。
128
+ - 哨兵是只读件:不写库、不碰 `sqlite_sequence`、不 VACUUM;要接定时/自检就显式 `--apply --out <文件>`,
129
+ 而且**机器可读内容只落文件,不上 stdout**(日志/报告才可被采集,stdout 留给人读)。
130
+
131
+ ### 搬走不等于删
132
+
133
+ `layer-move` 把整层搬进**同一个库文件里的边表** `memories_evicted`,行原样躺着,另带两列:
134
+ `content_sha`(与该行内容绑定的指纹)和 `batch`(哪一批搬的)。搬回时**先核指纹再写回**:
135
+ 边表里只要有一行被改过,整批拒动(不会出现"一半搬回、一半丢了")。
136
+
137
+ - **硬判据**:搬出行数 + 剩下行数 == 搬前总行数(事务内断言,不成立整批回滚)。
138
+ - **代价(选边表而不是另一个库文件)**:搬走的行**仍占这个库文件的体积** —— 软淘汰省的是上下文与召回面,
139
+ 不是磁盘;边表也没有 FTS 索引,搬走的行在引擎召回里看不见,只能用 SQL 或 `--back` 取回。
140
+ 换来的好处是搬进搬出都在一个事务里,守恒能当场断言,也不会留下一个可能被误删的旁路库文件。
141
+ - 会搬空主表时默认拒跑(`--force` 才动):那通常是层名填错了。
142
+
143
+ **怎么复现本节结论**:
144
+
145
+ ```bash
146
+ cd memory-engine
147
+ node test/run.mjs 12-layering # 21 项:干跑零写 / 加列回填 / 幂等 / 还原 / 守恒 / 指纹一致 / 号段红绿对照
148
+ node cli/layer-migrate.mjs --help
149
+ node cli/layer-move.mjs --help
150
+ node cli/band-sentinel.mjs --help
151
+ ```
152
+
153
+ ## 会话日志 → 记忆:提炼的边界(0.2.0 新增)
154
+
155
+ 记忆的**输入面**原先只有一条:库里已经有的记忆(`cli/distill.mjs` 走的就是这条)。这一版补上第二条 ——
156
+ **一份会话日志进来**,产出 ≤N 条**可追溯、可整批撤回**的结论条。两个新件,旧入口一个字节都没动:
157
+
158
+ | 件 | 干什么 | 出厂 |
159
+ |---|---|---|
160
+ | `lib/session-source.mjs` | 会话 JSONL 适配层:只挑「能回指来源的结论句」,每条带**场次 + 行号 + 字节偏移 + 原文锚** | 纯本地、零依赖 |
161
+ | `cli/distill-sessions.mjs` | 新入口:抽句 → 预算闸 → 一次模型调用 → 拍快照 → 写回(`distilled` 前缀 + 批次标签) | **dry-run** |
162
+
163
+ ### 一、不做摘要式写入
164
+
165
+ 抽的是**原句**,不是"这段讲了什么"的概括。所以下面这些一律不进库(每条都计数,跑完一次性告诉你挡了多少):
166
+
167
+ - 寒暄("你好""好的""收到")与疑问句(句末问号);
168
+ - 过程叙述("先…然后…接着…最后…"这种流水账,哪怕句尾偶然出现"决定"两个字);
169
+ - 推测("我觉得/可能/大概/应该是",除非同一句里有"一律/必须"这类规则词撑着);
170
+ - 纯工具输出(工具事件、命令回显)与图片 / 思考段;
171
+ - 超过长度上限(缺省 240 字)的整段转述 —— 宁可漏掉,也不把长文硬塞进记忆库。
172
+
173
+ ### 二、只写能回指来源的结论条
174
+
175
+ 每条结论都挂着它依据的那几句原文:`sessionId` + 行号 + 行首字节偏移 + **原文前 48 字**。
176
+ 场次 id 从输入日志 `{"type":"session", …}` 那一行取:**`id` 或 `sessionId` 两种字段名都认**;
177
+ 两者都没有就传 `--session-id`,再没有就一律标 `unknown`(回指退化成「文件名 + 行号」,CLI 会显式告警)。
178
+ 模型这一层只允许**合成**,且必须报出自己引用了哪几条候选(`refs`);越界引用、空结论、超 120 字、
179
+ 重复的条目会被**直接剔掉**(结果里的 `rejected` 写明理由)。回指不成立 ⇒ 这条宁可不写。
180
+
181
+ 标签形态(与旧提炼条同前缀,撤回器认的就是这个前缀):
182
+
183
+ ```
184
+ distilled src=<批次键> srcids=<行号,...> date=<YYYYMMDD> sess=<场次> run=session:<批次 id>
185
+ ```
186
+
187
+ ### 三、没配每日预算就不跑
188
+
189
+ **不给 `--max-tokens`、台账里也没配 ⇒ 一次模型都不调**,打印「未配置每日预算 ⇒ 本轮不跑(这是故意的)」,
190
+ 退出码 **0**(这不是错误)。它**不继承**内核那 150000 的缺省额度,也绝不"没配 = 无限跑"。
191
+ 给了预算但当日已超、或者"这一次调用估算下来就会超",同样不跑,并把已用 / 预算写出来。
192
+
193
+ 实测(deepseek-flash,自造三轮夹具会话,每轮一次调用):prompt **344–355** + completion **80–98** =
194
+ **424–453 token/轮**(三轮合计 1311;脚本估算按 1018 token/轮,约 2.3 倍保守)。
195
+ 建议 `--max-tokens 20000`:按估算约 19 轮/天,按实测约 45 轮/天。
196
+
197
+ ### 撤回:一批一个键
198
+
199
+ 一轮 = 一个批次 = 一个来源键,所以撤回是一条命令的事(用既有的撤回器,没另写一个):
200
+
201
+ ```bash
202
+ node memory-engine/cli/distill-rollback.mjs --db <memory.db> --key <批次键> --apply
203
+ ```
204
+
205
+ 撤回器真删前会先 `VACUUM INTO` 拍快照,并生成一键恢复脚本;删除语句恒带 `AND tags LIKE 'distilled%'`,
206
+ 所以就算键算错也不会误伤真实记忆。写库前那一份快照则由 `distill-sessions` 自己拍(`--snapshot-dir` 可指定),
207
+ **拍不成就不写库**。
208
+
209
+ ### 压缩日志(`.jsonl.zstd`)与"不静默失败"
210
+
211
+ DSH 的会话日志是 zstd 压缩的(而且**一次追加一帧**,是多帧文件)。`session-source` 会自己走一遍帧结构、
212
+ **逐帧**解压(node 22.15+ / 24 自带 zstd,零依赖);解不开就抛一条人话错误,告诉你「这是压缩日志,请先解压再喂」,
213
+ **不会**只解第一帧就假装读完了,也不会静默返回空结果。
214
+
215
+ **怎么复现本节结论**:
216
+
217
+ ```bash
218
+ cd memory-engine
219
+ node test/run.mjs 13-session-distill # 40 项:抽句回指 / zstd 多帧 / 预算闸三态 / 快照 fail-closed / 整批撤回
220
+ node cli/distill-sessions.mjs --help
221
+ node cli/distill-sessions.mjs --source <会话.jsonl> # 无预算 ⇒ 打印"不跑",rc=0
222
+ node cli/distill-sessions.mjs --source <会话.jsonl> --max-tokens 30000 # dry-run ⇒ 列出将写什么
223
+ ```
224
+
21
225
  ## 二、环境要求
22
226
 
23
227
  - **Node.js ≥ 22**(用到内置 `node:sqlite`,以及 ESM 顶层能力)
@@ -92,7 +296,9 @@ node memory-db-tools/audit-verify-unchanged.mjs --baseline ./_audit/memories.jso
92
296
 
93
297
  > **子树例外**:`dsh-memory-local/` 是上游 `dsh-memory`(MIT)的 fork,
94
298
  > 该子树**保持上游 MIT 许可**(见其 `package.json` 的 `license` 字段与 `FORK-BASE.json`);
95
- > 其余目录(引擎、工具链、共用件)为本项目原创,按 AGPL-3.0 + 商业授权双许可发布。
299
+ > `memory-engine/dict/`(词典与词频)来自 jieba(MIT),同样**保持上游 MIT**(见 `dict/LICENSE-词典.md`);
300
+ > 其余目录(引擎代码、工具链、共用件)为本项目原创,按 AGPL-3.0 + 商业授权双许可发布。
301
+ > 子树许可优先:分发整包时,上述两个 MIT 子树按 MIT 原样附版权声明即可,不适用 AGPL 的传染条款。
96
302
 
97
303
  ## 八、相关仓
98
304
 
@@ -0,0 +1,32 @@
1
+ # Package-root bundle patch for @kira905/dsh-memory-toolkit.
2
+ #
3
+ # Why this file exists at the package root (0.1.3): the fork's own patch lives
4
+ # inside the private sub-package `dsh-memory-local/`, so `dsh plugin add`
5
+ # reported "declares no dsh.bundle" and mounted nothing. The declaration now
6
+ # sits next to `dsh.bundle.patch` in the root package.json.
7
+ #
8
+ # `id` is deliberately NOT `memory`: that id belongs to the official
9
+ # `dsh-memory` bundle. Two entries carrying the same id either override each
10
+ # other silently (loader 1.0.x) or abort boot (older loaders), and
11
+ # `--dump-config` prints both rows with rc=0 either way. One memory slot, one
12
+ # implementation: install this package OR `dsh-memory`, never both.
13
+ #
14
+ # `config` is a deliberate full copy of `dsh-memory-local/cordis.patch.yml`
15
+ # (a patch replaces the targeted row's whole config, so a row cannot inherit
16
+ # keys from another file). `path` intentionally has no code-side default: a
17
+ # durable store of user facts must not land in whatever directory the harness
18
+ # happened to start in. The deployment value lives here, beside the row,
19
+ # resolved through the loader's `dshHomePath` so every profile on this machine
20
+ # shares one memory file.
21
+
22
+ - insert:
23
+ - id: kira905-memory
24
+ name: '@kira905/dsh-memory-toolkit/dsh-memory-local'
25
+ config:
26
+ path: !!js dshHomePath('memory/memory.db')
27
+ promptRecentCount: 10
28
+ promptMaxChars: 2000
29
+ maxTextChars: 2000
30
+ searchLimitDefault: 10
31
+ searchLimitMax: 50
32
+ promptOrder: 50