@modusensus/dsh-mneme 0.9.4 → 0.10.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 (126) hide show
  1. package/README.md +57 -10
  2. package/bin/cli.mjs +215 -2
  3. package/docs/CONFIGURATION.md +25 -2
  4. package/docs/DAEMON.md +13 -2
  5. package/docs/DISTILL.md +150 -4
  6. package/docs/SHARE.md +552 -0
  7. package/docs/SLEEP.md +21 -7
  8. package/docs/STORAGE.md +2 -1
  9. package/lib/api-standalone.js +294 -8
  10. package/lib/api.js +217 -3
  11. package/lib/client.js +919 -39
  12. package/lib/config.js +33 -1
  13. package/lib/continuity.js +11 -3
  14. package/lib/document.js +15 -2
  15. package/lib/dream/decisions.js +176 -30
  16. package/lib/dream/sleep.js +530 -94
  17. package/lib/dream.js +174 -21
  18. package/lib/fence.js +24 -0
  19. package/lib/index.js +15 -6
  20. package/lib/inject.js +9 -4
  21. package/lib/lang.js +38 -16
  22. package/lib/memory-source.js +9 -0
  23. package/lib/organize.js +5 -1
  24. package/lib/pipeline-nudge-auto.js +3 -0
  25. package/lib/pipeline-nudge.js +41 -8
  26. package/lib/pipeline-runner.js +15 -0
  27. package/lib/pipeline.js +62 -1
  28. package/lib/pre-inject-gate.js +14 -0
  29. package/lib/profile-extract.js +4 -1
  30. package/lib/provenance-http.js +251 -0
  31. package/lib/serve.js +13 -0
  32. package/lib/service.js +97 -4
  33. package/lib/session-gate.js +91 -0
  34. package/lib/settings.js +13 -1
  35. package/lib/share-http.js +281 -0
  36. package/lib/share-import.js +234 -0
  37. package/lib/share.js +335 -0
  38. package/lib/store.js +797 -28
  39. package/lib/summarize.js +209 -23
  40. package/lib/tools.js +26 -6
  41. package/package.json +4 -2
  42. package/src/api-standalone.js +294 -8
  43. package/src/api.js +217 -3
  44. package/src/config.js +33 -1
  45. package/src/continuity.js +11 -3
  46. package/src/document.js +15 -2
  47. package/src/dream/decisions.js +176 -30
  48. package/src/dream/sleep.js +530 -94
  49. package/src/dream.js +174 -21
  50. package/src/fence.js +24 -0
  51. package/src/index.js +15 -6
  52. package/src/inject.js +9 -4
  53. package/src/lang.js +38 -16
  54. package/src/memory-source.js +9 -0
  55. package/src/organize.js +5 -1
  56. package/src/pipeline-nudge-auto.js +3 -0
  57. package/src/pipeline-nudge.js +41 -8
  58. package/src/pipeline-runner.js +15 -0
  59. package/src/pipeline.js +62 -1
  60. package/src/pre-inject-gate.js +14 -0
  61. package/src/profile-extract.js +4 -1
  62. package/src/provenance-http.js +251 -0
  63. package/src/serve.js +13 -0
  64. package/src/service.js +97 -4
  65. package/src/session-gate.js +91 -0
  66. package/src/settings.js +13 -1
  67. package/src/share-http.js +281 -0
  68. package/src/share-import.js +234 -0
  69. package/src/share.js +335 -0
  70. package/src/store.js +797 -28
  71. package/src/summarize.js +209 -23
  72. package/src/tools.js +26 -6
  73. package/test/api.test.js +523 -6
  74. package/test/audit.test.js +2 -2
  75. package/test/cli-receive.test.js +390 -0
  76. package/test/cli-share.test.js +437 -0
  77. package/test/client-share.test.js +225 -0
  78. package/test/client.test.js +354 -0
  79. package/test/config.test.js +8 -0
  80. package/test/conflict-reviewed.test.js +79 -0
  81. package/test/continuity.test.js +29 -1
  82. package/test/dedupe-candidate-window.test.js +180 -0
  83. package/test/dependency-overrides.test.js +78 -0
  84. package/test/differentiate-feedback.test.js +248 -0
  85. package/test/dream-candidate-hybrid.test.js +2 -2
  86. package/test/dream-decision-evidence.test.js +314 -0
  87. package/test/dream-fence.test.js +207 -0
  88. package/test/dream.test.js +87 -5
  89. package/test/helpers/dream-mock.js +6 -2
  90. package/test/inject-pools.test.js +2 -1
  91. package/test/llm-message-source.test.js +1 -1
  92. package/test/mint-source.test.js +84 -1
  93. package/test/pipeline-nudge-auto.test.js +26 -2
  94. package/test/pipeline-nudge.test.js +87 -0
  95. package/test/pipeline-r1r2.test.js +129 -0
  96. package/test/pipeline-runner.test.js +26 -0
  97. package/test/policy-epoch.test.js +2 -2
  98. package/test/prompt-stance.test.js +128 -0
  99. package/test/provenance-http.test.js +367 -0
  100. package/test/readme-claims.test.js +45 -0
  101. package/test/reasoning-effort.test.js +10 -10
  102. package/test/release-docs.test.js +7 -1
  103. package/test/scope-candidates.test.js +7 -4
  104. package/test/scope-state.test.js +130 -1
  105. package/test/security-doc.test.js +43 -4
  106. package/test/serve.test.js +62 -0
  107. package/test/session-gate.test.js +264 -0
  108. package/test/settings.test.js +4 -0
  109. package/test/share-foreign-rows.test.js +144 -0
  110. package/test/share-http.test.js +144 -0
  111. package/test/share-import.test.js +577 -0
  112. package/test/share-imported-guard.test.js +311 -0
  113. package/test/share-imported-identity.test.js +111 -0
  114. package/test/share-pack.test.js +511 -0
  115. package/test/share-receive-http.test.js +756 -0
  116. package/test/share-routes.test.js +337 -0
  117. package/test/share-store.test.js +133 -0
  118. package/test/share-validate.test.js +101 -0
  119. package/test/sleep-action-set.test.js +16 -11
  120. package/test/sleep-catchup.test.js +315 -0
  121. package/test/sleep-failure-backoff.test.js +156 -0
  122. package/test/sleep.test.js +293 -0
  123. package/test/standalone-api.test.js +574 -5
  124. package/test/store.test.js +220 -0
  125. package/test/summarize-parse-diagnostics.test.js +75 -0
  126. package/test/summarize.test.js +187 -2
package/docs/DISTILL.md CHANGED
@@ -111,10 +111,15 @@ distillRecords:
111
111
  **不读这个键**,跟同一条 `--older-than`(默认 7 天)走——两个窗口互不联动,想一次对齐就把
112
112
  `--older-than` 显式传成与 `retentionDays` 相同的值。
113
113
 
114
- 删记忆是另一条路:`store.remove(id)` 在事务内**显式**清掉它的溯源链接。之所以不靠
115
- `ON DELETE CASCADE`,是因为 `src/store.js` 从不启用 `PRAGMA foreign_keys`(全库唯一的
116
- `REFERENCES` 是 `recall_events → recall_runs`,翻这个 pragma 会顺带激活那条既有约束的
117
- 行为,不是本次改动的范围)。声明式约束保留在 schema 里作为意图记录,实际清理是显式的。
114
+ 删记忆是另一条路:`store.remove(id)` 在事务内**显式**清掉它的溯源链接。这条显式清理**不是**
115
+ 「外键没开才要手工补」——`createStore` 走 `new DatabaseSync(path)` 不带选项,而 node:sqlite 的
116
+ `enableForeignKeyConstraints` 默认为真(C1 实测:新库上 `PRAGMA foreign_keys` 返回 1,删父行
117
+ 确实级联清掉本表),所以 `ON DELETE CASCADE` 今天一直是**活的**。显式 DELETE 留着是为了不把
118
+ 清理押在一份没人钉过的驱动默认值上:连接哪天以 `enableForeignKeyConstraints: false` 打开,
119
+ 悬空链接只有那条 DELETE 挡得住(口径见 `src/store.js:278-283` 与 `:1387-1391` 的注释,
120
+ `deleteBySharePackId` 同处置)。本库一共**两条**外键,都是活的——这一条与
121
+ `recall_evals(recall_run_id) → recall_runs(id)`;此前本节写的 `recall_events` 那张表并不存在。
122
+ 声明式约束因此既是意图记录、也是今天实际生效的执行,两边不矛盾。
118
123
 
119
124
  `reclaim` 报告因此从两项变三项:
120
125
 
@@ -160,3 +165,144 @@ cleared: { dream_run_inputs, archived_embeddings, distill_records }
160
165
  - **启动期裁剪**:读装配后的 cfg(面板改了保留期也认),且默认关时不动手;
161
166
  - **白名单与往返**:两个键过 `settings` 校验(含范围外整数被拒),PUT 往返后 effective 读得回;
162
167
  - **轻量模式**:预设关 `enabled`、保留用户显式设的 `retentionDays`、不污染用户对象。
168
+
169
+ ## 10. 读出口:`/memory/provenance` 与 `distillRecords.enabled` 门(#421 写、#509 整改、#524 补方向 2)
170
+
171
+ 两张表自 #399 起只有写入路径:库里有答案、`service.provenanceFor` 能查,外部拿不到——
172
+ 「这条记忆是从哪几句蒸出来的」只能开 sqlite 手写 JOIN。#421 补上读出口,两个入口共用
173
+ 一份实现:
174
+
175
+ - 宿主内面板路由 `GET /api/dsh-mneme/memory/provenance`(`policy: token`);
176
+ - Bearer 数据面 `GET /memory/provenance`(daemon 与宿主外部访问同享)。
177
+
178
+ **两个读方向共用同一条路径**(#524):`?id=<memoryId>` 是「这条记忆派生自哪些窗口」
179
+ (方向 1,`store.provenanceFor`),`?sessionId=<sid>` 是「这个会话蒸出了哪些窗口」
180
+ (方向 2,`store.distillWindowsFor`)。两个主键**互斥**:都缺 `400 missing-id`、都给
181
+ `400 ambiguous-id`。**刻意不做「都给则择一」**——择一意味着调用方可以发一个自己也不
182
+ 知道会被读哪个主键的请求,而两个方向的答复形状不同(`memoryId` / `sessionId` 各认
183
+ 领自己那一侧),那种静默选择只会在出事之后才被发现。
184
+
185
+ 为什么不新开一条路径:多一条路径就多四处要登记的地方(`SECURITY.md` 两张表、包
186
+ README 路由表、`test/security-doc.test.js` 的清单锁),而两个方向读的是**同一批数
187
+ 据**,只是入口不同。`SECURITY.md` 只登记路径、不登记 query 形状,所以方向 2 不需要
188
+ 在那两张表里再加一行。
189
+
190
+ 口径只写在 `src/provenance-http.js` 一处,两个壳都调它——#411 已经为 reclaim / share 各
191
+ 踩过一次「两张皮整段复制必然漂移」,`test/provenance-http.test.js` 的静态锁把「错误码字面
192
+ 量与开关判据只许出现在本模块、两个壳都真的把 `config` 转交进来、读侧判据与写侧
193
+ `src/summarize.js:988` 逐字同源」写成断言。
194
+
195
+ **形状**:窗口在 `{session_id, seq_start, seq_end, created_at, records[]}` 之外按方向分叉——方向 1(`?id=`)每行是一个溯源链接,另带 `distill_run_id / run_type / trigger_source / run_status`(后四个字段见 §11),答复级再带 `mints[]`;方向 2(`?sessionId=`)把同一窗口的多条链接合并成一行,另带 `link_count`。**run 归因只挂在方向 1 上**:方向 2 的一行是一个窗口、通常派生多条记忆,没有单一归因主体,要逐条查仍走方向 1。
196
+ `records` 是区间内的逐条会话原文(按 `seq` 升序、不越界)。没有链接的记忆返回**空
197
+ `windows`**——「不是蒸馏来的」是有效答案,不是错误;未知 id 同样回空 `windows`(读端点
198
+ 不替调用方判断记忆是否存在)。到期裁剪后链接保留、指向空区间,面板据此显示「原文已过期」。
199
+
200
+ `link_count` 只有方向 2 会真的不等于 1(方向 1 的每一行本来就是「一条记忆 → 一个窗
201
+ 口」,用 `?? 1` 兜住,免得答复凭空多一个 null 键)。方向 2 的存储读法是
202
+ `GROUP BY session_id, seq_start, seq_end`——`memory_provenance` 的粒度是「一条记忆 →
203
+ 一个窗口」,一次蒸馏窗口通常产出**多条**记忆,逐链接返回会让「这个会话蒸了几次」这
204
+ 个唯一有意义的计数被重复行淹没。`link_count` 是按窗口看的第二维;**刻意不回
205
+ `memory_id` 列表**——方向 1 已经是「记忆 → 窗口」,把 id 也塞进这一侧会让两个方向不
206
+ 再正交(同一份映射有两条读法就得有两处维护),真要逐条查仍走方向 1。
207
+ `ORDER BY seq_start` 而不是 `created_at`:seq 是事件序号、就是会话内的先后,按它排才
208
+ 是「这个会话按时间蒸了哪几段」,按 `created_at` 会把同一批重跑的行打散。
209
+
210
+ **开关门(#509 评审整改)**:读出口吃 `distillRecords.enabled`——与写侧**同一枚开关、同一
211
+ 个判据表达式**(`config?.distillRecords?.enabled === true`,读侧唯一一份在
212
+ `isDistillRecordsEnabled`)。此前全树只有 `src/index.js:270`(启动期裁剪)与
213
+ `src/summarize.js:988`(写入)行为性读它,读出口一处都不读,于是「把 distill_records 关
214
+ 掉」的实际语义是*不再写、也永久不再删*,却还多一个能把历史原文读出去的口。状态码阶梯:
215
+
216
+ | 状态码 | 含义 | 为什么不是别的 |
217
+ |---|---|---|
218
+ | `401` | 没带 token | 两个壳各自的门,在共用模块之前 |
219
+ | `400 {"error":"missing-id"}` | 两个主键一个都没给 | 语法畸形,与服务端状态无关,所以排在功能开关之前(与 reclaim 的 `invalid-json` 同档) |
220
+ | `400 {"error":"ambiguous-id"}` | 两个主键都给了 | 不静默择一,理由见上;同为语法类,同样排在开关之前 |
221
+ | `400 {"error":"invalid-limit"}` | `limit` 畸形或越界(只收十进制整数字面量,1–100) | **不用 `Number()`**:它把 `""` / `" "` / `0x10` / `1e3` 都化成数,于是 `?limit=` 会静默变成 0 个窗口——一个畸形输入拿到最坏解释。越界也给 400 而不是截到 100:调用方要 1000 个窗口,实际给 100 却不吭声比直接拒更糟 |
222
+ | `409 {"error":"distill-records-disabled"}` | 用户关了 `distillRecords.enabled` | 与 `src/api.js:525` 的 `profileExtractEnabled` 同型:**用户关了 = 409,装配坏了 = 503**,两个答案必须可区分(`src/index.js:589-591`) |
223
+ | `503 {"error":"provenance-unavailable"}` | service 上缺**该方向**要用的方法(headless / 旧装配) | 「没接线」不是「这条记忆没有来源」,两种空必须分开读数。**按方向判**:只缺 session 那侧的方法时方向 1 仍然 200——把一个方向的问题报成整张脸坏了,会让人去查一个没坏的部署 |
224
+ | `200` | `{memoryId\|sessionId, windows, truncated}` | 空 `windows` = 不是蒸馏来的;`truncated` 见下 |
225
+
226
+ **limit 与 truncated(#524 的第 2 条前置)**:按会话读是这张脸里天然最容易放大
227
+ 的方向——一个会话几十个窗口,每个窗口都是整段逐字原文,不限量就是「按会话导出全量转
228
+ 录」,与 `/export`(一次拿全库)同量级,只是换了形状,而它同样受门控。所以 `limit`
229
+ 不是可选的礼貌参数:默认 20、上限 100,两个方向共用同一个读数。20 这个数的取法是本
230
+ 机实测一个蒸馏会话的窗口数在个位数,20 已覆盖正常排查;真要更多就显式往上要,**不
231
+ 许**靠缺省值默默给全量。100 的上界比列表类接口(`MAX_PAGE_LIMIT` 1000 / `MAX_SEARCH_TOP_K`
232
+ 200)小一档,因为每个窗口带的是一整段原文而不是一行元数据。
233
+
234
+ 撞上 limit 时答复带 `truncated: true`。**少了它,被截断的答复与「本来就只有这么多」
235
+ 逐字节相同**,调用方会把半份转录当成全部——那正是这张脸最容易骗人的地方。实现上
236
+ session 侧向 store 要 `limit + 1` 行、多出来那条只当探针不出口;memory 侧
237
+ `provenanceFor` 本来就全量返回,按长度判即可。store 侧只照 limit 取,**不替调用方判
238
+ 断「还有没有」**——那是协议层的事,判据只许有一份。
239
+
240
+ **为什么不是 404**:本端点被明确设计成永远不就「这条记忆是否存在」作断言(缺 id 是 400、
241
+ 未知 id 是 200 + 空 windows),而 404 在本仓已有稳定含义——未知路由与资源不存在
242
+ (`/memories/:id`、`entity-not-found`)。把「功能关了」塞进 404,调用方会在状态码这个
243
+ 被代理 / 监控 / 重试逻辑唯一读取的通道上,把它读成「这条记忆不存在」;那两种误读的后续
244
+ 动作完全不同(一个去开开关,一个去查这条记忆)。409 也不给 200 的形状加说明性键——响应
245
+ 体只多一个 `windows`,开关状态走状态码通道。
246
+
247
+ **生效节奏**:`cfg` 是启动期冻结值(面板改开关与 `panel_mode` 一样在下次启动生效),读写
248
+ 两侧取的是同一份快照,所以两道门永不分歧;该键在 settings 白名单与 `NESTED_FLAG_PATHS`
249
+ 里都登记着,面板会显示「待重启」(#481「面板开着、功能却不注入」的教训)。
250
+
251
+ **daemon 这一面**:`src/serve.js` 把 `feature_flags` 里的 `distillRecords.enabled` 展开进
252
+ cfg(与宿主 `index.js:217-233` 同款展开,只取蒸馏这一组),与宿主共用同一个 memory.db 与
253
+ 同一份 kv——面板开着,daemon 就读得到。残留不对称:daemon 结构上看不到宿主的 bundle
254
+ config,所以「只写在部署侧 bundle 配置里打开、从没在面板点过」的用户,宿主认、daemon 不
255
+ 认(每个请求回 409)。这不是故障,写进 `docs/DAEMON.md` 的那条已知边界。
256
+
257
+ **关开关 ≠ 删除已留存原文**(本轮之后唯一诚实的表述):这道门只封 HTTP 读路径。关掉后
258
+ `src/index.js:270` 跳过启动期裁剪,已留的 `distill_records` 行永久冻在盘上,任何能摸到
259
+ memory.db 的进程仍可读;唯一出路是 `dsh-mneme reclaim`(`src/maintenance.js` 不读这个
260
+ 键,手动可删)或重新开启后的启动裁剪。把「关 = 删掉」写进任何文档都是谎话。后续可做的
261
+ 是让裁剪与写开关解耦(`retentionDays` 有值就裁,无论 `enabled`),那是让开关语义完全诚实
262
+ 的唯一修法,超出本轮范围。
263
+
264
+ ## 11. 运行归因:从「会话区间」到「哪一次蒸馏」(#512)
265
+
266
+ §10 的窗口只回答「这条记忆蒸自哪一段原文」。同一段区间被重蒸馏过几次、哪一轮失败了、
267
+ 失败在哪一步——这些在 §10 的形状里读不出来,因为 `memory_provenance` 的粒度是
268
+ (memory, 会话, 区间),而区间是**输入**的坐标,不是**运行**的身份。
269
+
270
+ 拆成两层,因为它们回答的不是同一个问题:
271
+
272
+ | 层 | 表 | 回答 | 谁能填 |
273
+ |---|---|---|---|
274
+ | 窗口级 | `memory_provenance.distill_run_id` → `distill_runs` | 这条记忆是**哪一轮蒸馏**从这段原文里提出来的 | 只有按窗口写入的 autoSummarize 路径 |
275
+ | 行级 | `memory_mints(memory_id, run_id)` | 这条记忆**出生自哪一轮巩固** | dream / sleep 的 create 动作 |
276
+
277
+ **为什么行级要另开一张表**:dream / sleep 铸造的 pattern 没有会话区间——session 与 seq
278
+ 都不存在。把合成区间塞进 `memory_provenance` 就是编造坐标,而一个错的坐标比没有坐标更坏
279
+ (它看起来完全合法,会让「能追到」变成谎话)。所以那一层只记 run id,不记区间。
280
+
281
+ **为什么行级只填 create**:merge / conflict / update / supersede 动的是**既有行**,
282
+ `receipt_chain` 已按 (run_id, record_id) 记过一遍。再造一份就是第二套账,两边迟早对不上。
283
+ create 是那边唯一的洞:`buildOutcome` 刻意不给 create 派 disposition,`receipt_chain`
284
+ 也就不产行。
285
+
286
+ **为什么不复用 `llm_audit_logs.id` 当 run id**:`llmAudit.retentionDays` 到期裁剪会删掉
287
+ audit 行,归因会跟着悬空;而 `memory_provenance` 刻意活得比原文更久(§10 的「来源已过期」
288
+ 就是这条设计的产物)。所以 run 是**归因主体**,audit 行只是它的**开销读数**,两边靠
289
+ `distill_runs.audit_id` 连——不是靠时间戳猜。
290
+
291
+ **为什么不复用 `dream_runs`**:那张表每一列都是巩固语义(`snapshot_hash` / `input_count` /
292
+ `decisions` / `outcome` / `receipt`),「每行都能回放一次巩固」是它对消费方的契约。硬塞
293
+ `run_type='autoSummarize'` 会让这条契约失效,而一个字段两种含义是这套 schema 里最容易腐烂
294
+ 的那种设计。dream / sleep 的轮次仍只记在 `dream_runs`,不复制进 `distill_runs`——两张表
295
+ 各说一件事,比一张表说两件事好。
296
+
297
+ **历史行不猜**:迁移只加列/加表,存量 `memory_provenance.distill_run_id` 一律 NULL。反推
298
+ 一个「看起来能追」的合成 id 是最坏的一种坏:读数正常,实际是编的。面板遇到 NULL 显示
299
+ 「无运行归因(历史行)」,不是空白。
300
+
301
+ **失败轮也记账**:`distill_runs.status` 取 `ok | degraded | noop | failed | aborted`。
302
+ 模型侧流失败与解析失败都会留一行 `failed`——否则「这个窗口为什么一直没有记忆」在台账上
303
+ 查不到原因。只有 dispose / 会话关闭才是 `aborted`(这一轮被主动打断),两者必须分得开。
304
+
305
+ **读出口**:`GET /api/dsh-mneme/memory/provenance?id=` 的窗口按上表带上
306
+ `distill_run_id / run_type / trigger_source / run_status`(读不到整组 null),答复级另带
307
+ `mints[]`(`{run_id, run_type, trigger_source, created_at}`)。面板详情抽屉有一个
308
+ 「蒸发归因」区块渲染这两层。