@modusensus/dsh-mneme 0.8.14 → 0.8.16
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.
- package/LICENSE +21 -21
- package/README.md +660 -654
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +691 -689
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +275 -0
- package/docs/CONTINUITY.md +124 -0
- package/docs/DAEMON.md +46 -0
- package/docs/DISTILL.md +162 -0
- package/docs/ENTITIES.md +245 -0
- package/docs/LOCAL_MODEL.md +222 -0
- package/docs/SEMANTIC.md +256 -0
- package/docs/SLEEP.md +164 -0
- package/docs/STORAGE.md +98 -0
- package/lib/api-standalone.js +656 -673
- package/lib/api.js +1605 -1433
- package/lib/bootstrap.js +211 -211
- package/lib/client.js +6354 -5296
- package/lib/commands.js +95 -95
- package/lib/config.js +947 -843
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +220 -220
- package/lib/document-index.js +94 -94
- package/lib/document.js +303 -303
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +694 -674
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +819 -819
- package/lib/dream.js +1503 -1501
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +252 -252
- package/lib/graph/anchoring.js +179 -179
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +108 -46
- package/lib/index.js +552 -503
- package/lib/inject.js +428 -450
- package/lib/lang.js +248 -229
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +162 -142
- package/lib/mirror.js +330 -330
- package/lib/organize.js +450 -450
- package/lib/peak-hours.js +117 -117
- package/lib/placement.js +13 -0
- package/lib/pre-inject-gate.js +340 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +190 -175
- package/lib/reclaim-http.js +150 -0
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -204
- package/lib/safe-slice.js +18 -0
- package/lib/scope.js +146 -146
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +229 -229
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +215 -215
- package/lib/service.js +2688 -2605
- package/lib/settings.js +528 -511
- package/lib/store.js +3196 -3007
- package/lib/summarize.js +1039 -856
- package/lib/tools.js +743 -737
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/lib/write-admission.js +402 -402
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +118 -118
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +36 -36
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +44 -44
- package/src/api-standalone.js +656 -673
- package/src/api.js +1605 -1433
- package/src/bootstrap.js +211 -211
- package/src/commands.js +95 -95
- package/src/config.js +947 -843
- package/src/content-hash.js +54 -54
- package/src/continuity.js +220 -220
- package/src/document-index.js +94 -94
- package/src/document.js +303 -303
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +694 -674
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +819 -819
- package/src/dream.js +1503 -1501
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +252 -252
- package/src/graph/anchoring.js +179 -179
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +108 -46
- package/src/index.js +552 -503
- package/src/inject.js +428 -450
- package/src/lang.js +248 -229
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +162 -142
- package/src/mirror.js +330 -330
- package/src/organize.js +450 -450
- package/src/peak-hours.js +117 -117
- package/src/placement.js +13 -0
- package/src/pre-inject-gate.js +340 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +190 -175
- package/src/reclaim-http.js +150 -0
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -204
- package/src/safe-slice.js +18 -0
- package/src/scope.js +146 -146
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +229 -229
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +215 -215
- package/src/service.js +2688 -2605
- package/src/settings.js +528 -511
- package/src/store.js +3196 -3007
- package/src/summarize.js +1039 -856
- package/src/tools.js +743 -737
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/src/write-admission.js +402 -402
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-routes-gaps.test.js +899 -218
- package/test/api.test.js +1725 -1690
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -220
- package/test/changelog-prep.test.js +60 -60
- package/test/client.test.js +1582 -1169
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +333 -333
- package/test/contrast.test.js +314 -0
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +671 -671
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-merge-guardrail.test.js +219 -0
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1385 -1383
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +627 -547
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +175 -175
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +308 -308
- package/test/hot-memory.test.js +211 -145
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +106 -106
- package/test/inject-pools.test.js +194 -194
- package/test/inject-recall.test.js +144 -105
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +443 -420
- package/test/lang.test.js +87 -87
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +696 -598
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +501 -391
- package/test/mcp-stdio.test.js +385 -385
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize.test.js +418 -418
- package/test/peer-blockers.test.js +193 -193
- package/test/placement.test.js +36 -0
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +444 -0
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +274 -189
- package/test/receipt-chain.test.js +451 -451
- package/test/reclaim-http.test.js +209 -0
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -279
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +207 -207
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +196 -196
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +410 -405
- package/test/settings.test.js +269 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -126
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +527 -527
- package/test/standalone-api.test.js +751 -751
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +504 -396
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -0
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +1639 -1465
- package/test/supersede-state-tracking.test.js +394 -0
- package/test/test-count-sync.test.js +60 -60
- package/test/tools-dto-schema-matrix.test.js +325 -325
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +428 -374
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/write-admission.test.js +668 -668
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# dsh-mneme 压缩边缘抢救(issue #249 N3)
|
|
2
|
+
|
|
3
|
+
- **日期**:2026-09-24
|
|
4
|
+
- **状态**:✅ 已实现(opt-in,默认关)
|
|
5
|
+
- **范围**:上下文即将被宿主压缩前,抢救「正在做什么」,让它活过这次压缩
|
|
6
|
+
- **相关文档**:[存储与回收](STORAGE.md)(同批的 #275 第一批) · [语义增强](SEMANTIC.md)
|
|
7
|
+
|
|
8
|
+
## 1. 问题
|
|
9
|
+
|
|
10
|
+
宿主的压缩摘要器只看对话里的内容:压缩时它按固定字段模板重建对话(Current Work /
|
|
11
|
+
Next Step / Critical Context 等),**系统提示段里的东西不进这次重建**。所以任何"只放在
|
|
12
|
+
提示段里的工作状态",一过压缩就等于没写过。
|
|
13
|
+
|
|
14
|
+
## 2. 形态:必须双落点
|
|
15
|
+
|
|
16
|
+
| 落点 | 去处 | 为什么不能省 |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| ① 落提案 | `continuity_proposals` 表(`src/store.js`) | 脱离对话独立存活;注入失败/被跳过也不丢工作状态 |
|
|
19
|
+
| ② 追加消息 | 序列末尾一条 `user/message`(`source: {kind: "plugin", plugin: "dsh-mneme"}`) | 这是唯一能进宿主摘要器重建范围的落点 |
|
|
20
|
+
|
|
21
|
+
落库先于注入。落点顺序由宿主契约保证:压缩插件在自己那一步先压缩、再 `return next()`,
|
|
22
|
+
所以我们在 `agent/pre-step` 拿到 `next()` 结果时压缩事件已落库;而我们返回的
|
|
23
|
+
`decision.messages` 由宿主在本步末尾以 `surfaceOp: "append"` 追加,晚于压缩的
|
|
24
|
+
`surfaceOp: "replace"`——追加物天然在压缩之后、靠近序列末尾。
|
|
25
|
+
|
|
26
|
+
注入文本是**结构化快照**(统一前缀 + 三行固定字段,整条 ≤900 字符),不是 §6.3 那种
|
|
27
|
+
≤160B 的单行提醒——那条上限约束的是"一句话提醒",不约束固定字段的快照;字段内部的换行
|
|
28
|
+
仍会被压平成空格,保证它仍是一条消息。**判重按当前表面**(`session.surface.nodes`),
|
|
29
|
+
不按全量日志:被一次压缩折叠出表面的旧快照还在 append-only 日志里,照日志判重会让这次
|
|
30
|
+
该补的注入静默跳过。
|
|
31
|
+
|
|
32
|
+
## 3. 触发
|
|
33
|
+
|
|
34
|
+
**不自定阈值**,就是订阅宿主真的压缩:`compaction/start` / `compaction/summary` /
|
|
35
|
+
`compaction/end`(都在宿主的事件白名单里)。理由:「压缩边缘」由宿主定义,我们再校准
|
|
36
|
+
一份阈值参数只会与之漂移,还多一份没人维护的旋钮。
|
|
37
|
+
|
|
38
|
+
DSH 没有 in-memory 消息改写钩子,所以本机制一律是「注入 + 序号引用寻址」,**不改写
|
|
39
|
+
历史消息本体**。
|
|
40
|
+
|
|
41
|
+
## 4. 字段与抽取口径
|
|
42
|
+
|
|
43
|
+
固定三字段(不用自由散文),全程不调模型、纯确定性抽取(`src/continuity.js`):
|
|
44
|
+
|
|
45
|
+
| 字段 | 来源 | 截断 |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `current_work` | 最近一条**真实** `user/message`(插件注入与子代理上报不算用户指令) | 头部 200 字符 |
|
|
48
|
+
| `next_step` | 最近一条 `assistant/message` | **尾部** 200 字符(下一步活在末尾那句里) |
|
|
49
|
+
| `open_questions` | 恒 `null` | —— |
|
|
50
|
+
|
|
51
|
+
「哪些问题还没解决」判不出就是判不出,硬猜会给出似是而非的字段;空字段在注入文本里
|
|
52
|
+
如实标 `(none)`。这个字段留给转正通道与人工补。
|
|
53
|
+
|
|
54
|
+
字段名以 #249 §6.2 为准(`current_work` / `next_step` / `open_questions`)。§4.4 里的
|
|
55
|
+
Current Work / Next Step / Critical Context 是**宿主压缩模板**的字段名,不是这张表的列名
|
|
56
|
+
(规格里两处用词不同,实现取 §6.2)。
|
|
57
|
+
|
|
58
|
+
## 5. 存储与生命周期
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
continuity_proposals(id, session_id, kind, current_work, next_step, open_questions,
|
|
62
|
+
status, edge_seq, created_at, updated_at)
|
|
63
|
+
UNIQUE (session_id, kind) -- 「同一会话同一类只留一条」:再次触发是刷新,不是新增
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- `kind`:`compaction-edge`(一次会话里这一类只留一条)。
|
|
67
|
+
- `status`:`pending`(本批唯一写入态)→ 未来 `promoted` / `discarded`(转正通道与
|
|
68
|
+
#254 的二次确认共用一套,阶段二才打开)。
|
|
69
|
+
- **队列满则弃新**(`MAX_CONTINUITY_PENDING = 200`):达上限丢掉本次触发,而不是淘汰
|
|
70
|
+
旧行——旧行是别的会话还没转正的工作状态,用「更近的边缘」把它挤掉是反的。满队列
|
|
71
|
+
不影响已有行的刷新。
|
|
72
|
+
- 丢弃会**留痕**(`agent/pre-step` 监听器 `logger.warn`):弃新属于"静默失效",不报出来
|
|
73
|
+
的话 §8 的触发率口径会把它算成"没触发"。
|
|
74
|
+
- **本批没有回收路径**:`status` 只写 `pending`,`maintenance`/`reclaim` 不碰这张表,
|
|
75
|
+
所以 200 是上限——满了之后新会话不再落行,直到转正通道打开(阶段二)。判定是进程内的
|
|
76
|
+
(先查计数再写),两个宿主共用同一 `memoryDir` 时可能各自都看到未满,实际越过 200 条。
|
|
77
|
+
- `edge_seq` 记最近一次触发的压缩事件序号,是「实际触发率」的证据。
|
|
78
|
+
- 落库失败只告警、不抛:可选插件的故障不该把宿主的这一步变成失败,而注入那一半不依赖
|
|
79
|
+
库,照做。
|
|
80
|
+
|
|
81
|
+
指标口径(#249 §8 要求触发率/采纳率可统计):实际触发率 = 提案行数;采纳率 =
|
|
82
|
+
`promoted` / 全部。两者都是就地读表,不需要额外打点。
|
|
83
|
+
|
|
84
|
+
## 6. 开关
|
|
85
|
+
|
|
86
|
+
| 层级 | 键 | 默认 |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| 父 | `autoInject` | 开(既有总闸) |
|
|
89
|
+
| 子 | `continuityRescueEnabled` | **关**(引入新注入表面,按 #249 §10 判据独立成键) |
|
|
90
|
+
|
|
91
|
+
父关则子不生效(`injectChildEnabled`);轻量档默认置关(`applyLightModePreset`
|
|
92
|
+
把它算进 `LIGHT_MODE_OFF`),但那只是默认值——装配时按「用户开关 > 轻量预设 >
|
|
93
|
+
bundle 配置」合并,用户在面板里显式勾选仍然赢。面板落点在 `lib/client.js`
|
|
94
|
+
的 `FEATURE_CHILDREN` / `FEATURE_GROUPS`(该文件无 src 对应物),跨文件关系由
|
|
95
|
+
`test/inject-parent-gate.test.js` 逐个断言钉住。
|
|
96
|
+
|
|
97
|
+
## 7. 降级路径(不要求宿主加接口)
|
|
98
|
+
|
|
99
|
+
宿主若不提供可挂钩的压缩前时机,双落点里的 ② 就没有触发者。这时**不算失败**:把规则
|
|
100
|
+
写成持久规则交给 agent 自判(`src/guide.js` 的 `CONTINUITY_TOOL_RULE`,挂在
|
|
101
|
+
`memory_save` 的工具描述尾部——常驻文本、零注入成本,只在子开关开启时出现)。
|
|
102
|
+
|
|
103
|
+
本批**不做宿主能力探测**(没有可靠的探测口,宿主也没有暴露"有没有压缩前时机"这个事实),
|
|
104
|
+
所以子开关打开时这条规则就常驻,与 ② 并存——两者不是互斥的备选,而是同一个开关下的两层:
|
|
105
|
+
② 在宿主真的压缩时自动生效,持久规则在 agent 自己判断"上下文要炸了"时兜底。
|
|
106
|
+
|
|
107
|
+
## 8. 已知坑
|
|
108
|
+
|
|
109
|
+
- **判重只能做到「同一份文本不追加第二次」**。没有改写钩子,就不能删掉表面里已经存在
|
|
110
|
+
的那条旧快照;内容一变就是新的一份(旧的那份随宿主压缩自然消失)。这是形态上限,
|
|
111
|
+
不是实现偷懒。
|
|
112
|
+
- **注入物不得变成记忆**:追加消息用 `source.kind = "plugin"`,`summarize.js` 的
|
|
113
|
+
`collectMessages` 会跳过它——否则注入物会被下一轮蒸馏成记忆,形成自我强化。
|
|
114
|
+
- **`open_questions` 恒 null** 是刻意留白,不是没实现。
|
|
115
|
+
|
|
116
|
+
## 9. 验收锚点
|
|
117
|
+
|
|
118
|
+
`test/continuity.test.js` 逐条锁形态(不是锁文案):抽取口径(头/尾方向、插件消息跳过、
|
|
119
|
+
插件消息是最后一条 `user/message` 时仍被跳过)、渲染(统一前缀、单行、`none`、整条上限)、
|
|
120
|
+
唯一键刷新(`created_at` 不动、幂等 UPSERT)、队列满弃新且不影响刷新、丢弃留痕、
|
|
121
|
+
pre-step 双落点与一次性消费、同文本不重复追加(按当前表面)、被折叠出表面的旧注入要补、
|
|
122
|
+
落库失败不打断宿主的一步、`reject` 放行且不消费边缘、缺 session 不抛、dispose 后不再动作、
|
|
123
|
+
默认关 + 父关不生效 + 轻量档压掉 + 降级规则只在开启时进描述、门控不调模型(静态锁)、
|
|
124
|
+
存量库重开即建表。
|
package/docs/DAEMON.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# dsh-mneme 独立服务(daemon)
|
|
2
|
+
|
|
3
|
+
`dsh-mneme-serve`:在 DSH 宿主之外把 mneme 跑成一个常驻数据面。#363(Mneme Bridge)确立的方向——第三方集成需要长期挂载,而 DSH 不必一直开着;这是官方推荐姿势,lib 直挂的 embedded 模式降级为无网兜底。
|
|
4
|
+
|
|
5
|
+
## 1. 职责边界
|
|
6
|
+
|
|
7
|
+
daemon 是**数据面**,不是第二个宿主:
|
|
8
|
+
|
|
9
|
+
- **有**:存储(SQLite)、检索(关键词 + BM25 + 向量,`/search` 统一召回,与宿主共用 `src/semantic.js` 同一套装配)、注入候选(`/context`,issue #370,与宿主注入管线同一份 `injectCandidates`)、镜像同步与人改合并、`/maintenance/reclaim`、`/bootstrap`、recall_runs 检索/注入回执。
|
|
10
|
+
- **没有(第一期,无 LLM)**:巩固(autoDream)、蒸馏(autoSummarize)、实体抽取、sleep、注入/工具/面板路由。前两者是**结构性缺失**而非开关——daemon 装配里没有 LLM 句柄,巩固只属于 DSH 宿主进程。这就是 daemon 与宿主「单写者」的机械保证(AGENTS.md externalApi/autoDream 单侧纪律的 daemon 版),不依赖用户自觉。
|
|
11
|
+
|
|
12
|
+
与宿主装配(`src/index.js` apply)的关系:`src/serve.js` 只搬数据面那一半,每步注释锚定 index.js 来源行号;刻意不抽公共装配函数(apply 其余环节与宿主 ctx 纠缠,防御段纪律「最后动或不动」)。装配漂移风险由 `test/serve-bin.test.js` 的多进程共存用例兜底(两进程真开同一个库互写互读)。
|
|
13
|
+
|
|
14
|
+
## 2. 对外接口
|
|
15
|
+
|
|
16
|
+
路由面 = `src/api-standalone.js` 全表(health/status/profile/rules/memories 读写/search/context/maintenance/bootstrap),鉴权同源(Bearer + timingSafeEqual,`GET /health` 免鉴权)。daemon 自身零新路由;#370 的 `/context` 落在 api-standalone 工厂里,宿主外部访问与 daemon 自动同享(参数、scope 语义与 recall_runs 注入分账见包 README 路由表)。daemon 侧唯一自有选项是 `strictPort`(见 §4)。
|
|
17
|
+
|
|
18
|
+
CLI:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
dsh-mneme-serve [--memory-dir <dir>] [--port <n>] [--host <addr>] [--embed <provider>]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `memoryDir`:CLI > env `DSH_MNEME_MEMORY_DIR` > `~/.dsh/memory`(与宿主 config.js 同默认,支持前导 `~`)。
|
|
25
|
+
- `--embed`:语义检索提供方。`local`(默认:自管 runtime 与嵌入模型缺失时**自动取件**,download 档,约 200MB;失败降级关键词并打可操作日志)| `ollama` | `openai`(读宿主面板存的 vector-config)| `off`(纯关键词 + BM25)。取件来源可用 env 换道:`DSH_MNEME_RUNTIME_DIR`(自管 runtime 目录)、`DSH_MNEME_RUNTIME_TARBALL_DIR`(离线 .tgz 目录,优先于联网)、`DSH_MNEME_RUNTIME_MIRROR`(registry 镜像前缀)。
|
|
26
|
+
- port/host 解析链与宿主「外部访问」一致:显式参数 > kv `external_api` 持久值 > 默认 8790 / 127.0.0.1。
|
|
27
|
+
- token 与 DSH 面板 / CLI **共用同一份**(kv `external_api`,首次启动自动生成并持久化)——三方零配置互通。
|
|
28
|
+
- 安全:daemon 使用明文 HTTP,不提供原生 TLS。指定非回环 `--host` 时,请勿直接把服务暴露给不可信网络;远程访问请走 TLS 终止代理或 SSH 隧道。
|
|
29
|
+
- stdout 只在就绪时打一行 `dsh-mneme-serve listening on http://host:port (pid N)`(机器可读,脚本/测试解析端口用);日志全走 stderr。
|
|
30
|
+
- SIGINT/SIGTERM 优雅收库后 exit 0;Windows 强杀由 WAL 回放兜底。
|
|
31
|
+
|
|
32
|
+
## 3. 内部文件
|
|
33
|
+
|
|
34
|
+
- `src/serve.js` — `createServeRuntime({memoryDir, port, host, logger, strictPort, embed, embedder, reranker})`:装配链 createStore → createSettings → createMirror → createService(最小 config)→ recoverMirror → 人改镜像合并闭包 → vectorIndex + semantic → recall recorder → createMaintenance → createStandaloneApi(带 `embedder: embedder ?? semantic?.embedder ?? null`,供 `/context` 查询嵌入),每步锚定 index.js 行号。返回 `{api, store, service, settings, maintenance, semantic, tokenExisted, dispose}`;第三方可 import 它自行托管生命周期(bin 只是薄壳),`embedder/reranker` 参数供注入自管嵌入。
|
|
35
|
+
- `src/semantic.js` — embedder/reranker 装配 + boot 自动回填,**纯搬移自 index.js**(PR2),宿主与 daemon 共用同一份;`backfillMissingEmbeddings` 经 index.js barrel 再出口(测试照旧从 index.js import)。
|
|
36
|
+
- `bin/dsh-mneme-serve.mjs` — CLI 壳。独立成 bin 而非 cli.mjs 子命令:CONTRIBUTING 禁止给 cli.mjs 加 import;命名循 dsh-mneme-mcp 先例。
|
|
37
|
+
- `src/api-standalone.js` 的 `strictPort` 选项 — daemon 自己引入的唯一数据面改动,默认关闭(daemon 侧显式传 true)。#370 起该工厂另增 `/context` 路由与可选 `embedder` 注入,宿主外部访问同享。
|
|
38
|
+
|
|
39
|
+
## 4. 已知坑
|
|
40
|
+
|
|
41
|
+
1. **端口互斥,二选一**:daemon 与 DSH 的「外部访问」抢同一个默认端口。daemon 侧 strictPort 报错退出;反方向( daemon 先占 8790,DSH 后开外部访问)宿主侧会**静默顺延**到下一端口(宿主旁路的多实例恢复语义,api-standalone.js listenWithRetry)——面板显示的端口会变,别当 bug 报。
|
|
42
|
+
2. **双进程写并发**:与 DSH 同时运行是设计内场景(WAL + busy_timeout 先序,store.js createStore)。但 `saveWithDedupe` 的 (type,title,scope) 去重是先查后写、库层无 UNIQUE 约束,两进程并发写同一三元组有极小概率产生重复条目——已知限制,勿当强保证宣传(要不要加 UNIQUE 索引属 schema 防御段,单独决策)。
|
|
43
|
+
3. **镜像双写竞态**:daemon 与宿主都会渲镜像 .md;可再生物,失败由 recoverMirror 自愈,极端并发下单文件可能短暂脏,下次同步覆盖。
|
|
44
|
+
4. **版本偏斜**:库迁移是幂等加法式(PRAGMA 检查 + ALTER),旧代码读新 schema 一般无碍,但该组合无人测过——daemon 与插件请同版本升级。
|
|
45
|
+
5. **第一期不吃宿主配置**:daemon 不加载 config schema(schemastery 是宿主 peer 依赖),面板/feature_flags 对它不生效;它只有 CLI 参数 + 上述固定最小 config(`language: zh`、document 子系统关闭;语义键默认值逐键锚定 config.js,见 `src/serve.js` 的 `daemonSemanticCfg`)。`--embed local` 首次启动会自动取件 runtime + 模型(均有日志);取件/嵌入失败统一降级关键词 + BM25,不影响读写。
|
|
46
|
+
6. **验收锚点**:`test/serve.test.js`(in-process 全链路 + token 复用 + recall_runs 回执 + 注入假 embedder 的向量轴锁)、`test/serve-bin.test.js`(真子进程 + 多进程互写互读)、`test/standalone-api.test.js` 的 strictPort 用例(busy → reject,默认路径仍顺延)、`test/reindex-backfill.test.js`(semantic 纯搬移后宿主行为不变)。
|
package/docs/DISTILL.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# dsh-mneme 蒸馏原文留存与记忆级溯源(issue #399)
|
|
2
|
+
|
|
3
|
+
- **日期**:2026-10-12
|
|
4
|
+
- **状态**:✅ 已实现(opt-in,默认关)
|
|
5
|
+
- **范围**:把蒸馏从「替换」降级为「叠加索引」——窗口原文旁路留存 + 记忆回指来源窗口
|
|
6
|
+
- **相关文档**:[存储与回收](STORAGE.md)(#275 第一批,同一条 reclaim 入口) · [配置参考](CONFIGURATION.md) · [压缩边缘抢救](CONTINUITY.md)
|
|
7
|
+
|
|
8
|
+
## 1. 问题
|
|
9
|
+
|
|
10
|
+
蒸馏(`src/summarize.js`)是一条**有损**管道:它把一段会话窗口喂给 LLM,拿回几条结构化
|
|
11
|
+
记忆,然后**消费掉游标**——窗口原文此后在库内没有任何痕迹。于是三件事同时成立:
|
|
12
|
+
|
|
13
|
+
1. 记忆里没写进去的细节,事后**无法回查**(不是「没保存」,是「没地方找」);
|
|
14
|
+
2. 一条记忆**说不清自己出自哪段对话**,人工复核只能靠标题猜;
|
|
15
|
+
3. 蒸馏质量出问题时,**没有基线可比**(原文已经不在手上)。
|
|
16
|
+
|
|
17
|
+
信息论的依据很直白(arXiv 2609.36059 §3 Eq.1):旁路索引不丢信息——
|
|
18
|
+
`I(Y;M,H,X) = I(Y;M,H)`,留存原文 `X` 不改变 `Y` 与已蒸馏产物 `(M,H)` 的互信息,
|
|
19
|
+
但**取回** `X` 的能力被保住了。所以这不是「再存一份」的冗余,而是把单向替换变成
|
|
20
|
+
可回指的叠加索引。
|
|
21
|
+
|
|
22
|
+
## 2. 形态:两张表,只增不改
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
distill_records(session_id TEXT, seq INTEGER, role TEXT, text TEXT,
|
|
26
|
+
window_id TEXT, distilled_at TEXT, PRIMARY KEY(session_id, seq))
|
|
27
|
+
idx_distill_records_window(window_id)
|
|
28
|
+
idx_distill_records_distilled_at(distilled_at)
|
|
29
|
+
|
|
30
|
+
memory_provenance(memory_id TEXT, session_id TEXT, seq_start INTEGER, seq_end INTEGER,
|
|
31
|
+
created_at TEXT)
|
|
32
|
+
UNIQUE idx_memory_provenance_link(memory_id, session_id, seq_start, seq_end)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `CREATE TABLE IF NOT EXISTS`:**无 ALTER、无回填**,存量库启动即建表,旧库零迁移成本。
|
|
36
|
+
- **不动 `memories`**:溯源是旁路,不往记忆表加列。删掉这两张表,插件行为逐字节回到 #399 之前。
|
|
37
|
+
- `memory_id` 用 **TEXT**(issue 草稿里写的是 INTEGER):`memories.id` 在本仓一直是 TEXT
|
|
38
|
+
(`src/store.js` 的 `save()` 铸 UUID 风格字符串),写成 INTEGER 只会让 SQLite 按
|
|
39
|
+
TEXT 亲和存进去、却在类型声明上误导后来人。
|
|
40
|
+
- 主键 `(session_id, seq)` 决定了**一个事件一行**:`seq` 是会话事件序号,一次事件可能携带
|
|
41
|
+
多条公开文本(`agent-message` / `inbox` / `spliced` 可以一次投递多条子会话消息),它们
|
|
42
|
+
按事件合并成一行(`role` 取首条、`text` 以换行连接)。各写各的行会被
|
|
43
|
+
`INSERT OR IGNORE` 静默丢掉后几条——那是**丢数据**,不是去重。
|
|
44
|
+
- 幂等靠 `INSERT OR IGNORE`(记录)+ 唯一索引 `ON CONFLICT DO NOTHING`(链接):
|
|
45
|
+
同一窗口重蒸一次不翻倍,且**保留首次落盘时间**(`distilled_at` 是「原文何时被留下」的
|
|
46
|
+
事实,重跑改写它才是撒谎)。
|
|
47
|
+
|
|
48
|
+
## 3. 写入点:蒸馏时顺手落,不新建常驻管道
|
|
49
|
+
|
|
50
|
+
`summarize()` 触发时已经持有会话事件、已经算出窗口边界(`previousSeq` / `triggerSeq`),
|
|
51
|
+
原文留存就在那里顺带落盘——**不新开监听器、不常驻订阅会话事件**。好处是留存范围与
|
|
52
|
+
蒸馏范围**天然一致**:蒸过的窗口才留,没蒸过的窗口不留(见 §7 已知坑 2)。
|
|
53
|
+
|
|
54
|
+
落盘与记忆写入同事务(`service.transaction`):两者要么一起进,要么一起退。游标消费也在
|
|
55
|
+
同一个事务里——「记忆写了但原文没落」这种半截状态不存在。
|
|
56
|
+
|
|
57
|
+
## 4. 隐私:渲染与落盘共用同一谓词
|
|
58
|
+
|
|
59
|
+
这是本次改动**最要紧**的约束。原文留存一旦与喂给 LLM 的转录各写一份过滤逻辑,两条路径
|
|
60
|
+
就会漂移,而漂移的方向必然是「**存下来的比 LLM 看过的更多**」——最糟的方向:模型没看到的
|
|
61
|
+
私有内容反而落进了持久库。
|
|
62
|
+
|
|
63
|
+
所以两边都从 `publicTranscriptParts(event)`(`src/summarize.js`)取数,同一个函数、同一份
|
|
64
|
+
判定。口径:
|
|
65
|
+
|
|
66
|
+
| 内容 | 进 LLM 转录 | 落 distill_records |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| 真实 `user/message`(`kind` 缺失 / `user` / `agent-message` / `subagent-settled`) | ✅ | ✅ |
|
|
69
|
+
| 助手公开 `text` 块 | ✅ | ✅ |
|
|
70
|
+
| 助手 `reasoning` 块(私有推理链) | ❌ | ❌ |
|
|
71
|
+
| 插件注入机器内容(`kind: "plugin:dsh-mneme"` / skill 正文 / AGENTS.md / 文件变更通知) | ❌ | ❌ |
|
|
72
|
+
| 工具调用入参、工具结果、code 输出 | ✅(截断) | ❌ |
|
|
73
|
+
|
|
74
|
+
最后一行是**刻意的路径差异**,不是漂移:`codingRetrospect` 靠工具输出沉淀踩坑根因,所以
|
|
75
|
+
渲染路径必须带它;但它体积大、含路径与命令、且不属于「对话内容」,落盘只留对话。
|
|
76
|
+
差异方向仍满足「落盘的 ⊆ 渲染的」。
|
|
77
|
+
|
|
78
|
+
## 5. 留存不受喂模型的裁剪约束
|
|
79
|
+
|
|
80
|
+
喂 LLM 的转录受三道裁剪:`distillMaxChars`(默认 24000,超长保**尾部**)、工具入参
|
|
81
|
+
`trim(args, 300)`、工具结果 `trim(out, 500)`。这三刀**不作用于落盘**——原文就是原文,
|
|
82
|
+
截断过的「原文」没有回查价值。
|
|
83
|
+
|
|
84
|
+
代价是库体积:一条超长提示会原样落库。所以留存默认**关**,并且有保留期(§6)。
|
|
85
|
+
`test/summarize.test.js` 有一条锁专门钉「两条路径的差异」:超长用户提示在记录里逐字全文,
|
|
86
|
+
在喂模型的转录里找不到头部标记。
|
|
87
|
+
|
|
88
|
+
## 6. 生命周期与开关
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
distillRecords:
|
|
92
|
+
enabled: false # 默认关(opt-in)
|
|
93
|
+
retentionDays: 90 # 保留期,1-3650
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
| 层级 | 键 | 默认 |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| 父 | `autoSummarize` | 开(既有总闸) |
|
|
99
|
+
| 子 | `distillRecords.enabled` | **关**(引入新的持久化面,独立成键) |
|
|
100
|
+
| 子 | `distillRecords.retentionDays` | `90` |
|
|
101
|
+
|
|
102
|
+
两个键都进 `settings.js` 白名单(面板可改);`enabled` 进 `config.js` 的 `LIGHT_MODE_OFF`
|
|
103
|
+
(轻量模式关掉留存这个成本/隐私面),`retentionDays` **不进**——它是时长而非行为面,用户
|
|
104
|
+
显式设过就照旧。两个键都进 `api.js` 的 `NESTED_FLAG_PATHS`(漏了 effective 会静默缺键,
|
|
105
|
+
#254 踩过)。
|
|
106
|
+
|
|
107
|
+
**到期裁剪只删 `distill_records`**,`memory_provenance` 保留——链接从此指向空区间,面板
|
|
108
|
+
显示「来源已过期」,而不是查无此记忆。**裁剪原文绝不级联删记忆**:原文是索引,记忆是资产。
|
|
109
|
+
两处裁剪,**窗口各自独立**:启动期(`src/index.js`,只在 `enabled === true` 时动手——默认关时
|
|
110
|
+
一张空表不该被每次启动扫一遍)按 `retentionDays` 裁;`dsh-mneme reclaim`(默认 dry-run)
|
|
111
|
+
**不读这个键**,跟同一条 `--older-than`(默认 7 天)走——两个窗口互不联动,想一次对齐就把
|
|
112
|
+
`--older-than` 显式传成与 `retentionDays` 相同的值。
|
|
113
|
+
|
|
114
|
+
删记忆是另一条路:`store.remove(id)` 在事务内**显式**清掉它的溯源链接。之所以不靠
|
|
115
|
+
`ON DELETE CASCADE`,是因为 `src/store.js` 从不启用 `PRAGMA foreign_keys`(全库唯一的
|
|
116
|
+
`REFERENCES` 是 `recall_events → recall_runs`,翻这个 pragma 会顺带激活那条既有约束的
|
|
117
|
+
行为,不是本次改动的范围)。声明式约束保留在 schema 里作为意图记录,实际清理是显式的。
|
|
118
|
+
|
|
119
|
+
`reclaim` 报告因此从两项变三项:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
planned: { dream_run_inputs, archived_embeddings, distill_records }
|
|
123
|
+
column_bytes: { ... }
|
|
124
|
+
cleared: { dream_run_inputs, archived_embeddings, distill_records }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`reclaim` 的入口语义(默认 dry-run、`--apply` 才动手、`--vacuum` 单独指定)**一字未动**。
|
|
128
|
+
|
|
129
|
+
## 7. 已知坑
|
|
130
|
+
|
|
131
|
+
1. **无历史回填**:本批只对**此后**发生的蒸馏生效。升级前已蒸过的窗口原文已经丢了,
|
|
132
|
+
没有补回来的路(也不该假装能——从记忆反推原文是编造)。
|
|
133
|
+
2. **从未蒸馏过的窗口不留记录**:留存挂在蒸馏上,所以四类窗口天然无记录——① 窗口太小
|
|
134
|
+
(`window-too-small` 门,游标已被消费);② 插件当时是关的;③ 该窗口没触发过蒸馏
|
|
135
|
+
(`autoSummarize` 关 / 限流挡住 / 会话结束得太早);④ 蒸过了但那一轮 LLM 调用 abort。
|
|
136
|
+
这些不是 bug,是「留存范围 = 蒸馏范围」的直接推论,但排查时容易误判成丢数据。
|
|
137
|
+
3. **不覆盖工具结果与 code 输出**(§4 末行):想回查某次命令的真实输出,这两张表帮不上忙
|
|
138
|
+
——那是渲染路径的事,且落盘会带来体积与敏感信息两个问题。
|
|
139
|
+
|
|
140
|
+
## 8. 非目标(本批刻意不做)
|
|
141
|
+
|
|
142
|
+
不嵌入、不进 `fuseRecall`、不进注入、不做「哪份更权威」的取代判定、不做 value histories
|
|
143
|
+
查询接口、不动 `distillMaxChars` / `trim(args,300)` / `trim(out,500)` 三刀。这两张表是
|
|
144
|
+
**旁路索引**,不是第二条检索路——把它接进召回是另一个决定(成本与召回质量都要重新测),
|
|
145
|
+
不该跟着留存一起顺手打开。
|
|
146
|
+
|
|
147
|
+
## 9. 验收锚点
|
|
148
|
+
|
|
149
|
+
`test/summarize.test.js` 五条 + `test/maintenance.test.js` / `test/llm-audit.test.js` /
|
|
150
|
+
`test/settings.test.js` / `test/api.test.js` / `test/pre-inject-gate.test.js` 各一条:
|
|
151
|
+
|
|
152
|
+
- **幂等**:删掉持久化游标 + 换新 summarizer 实例重跑同一窗口 → 记录仍一行、链接无重复;
|
|
153
|
+
- **隐私**:reasoning / AGENTS.md / skill 正文 / 文件变更通知绝不落盘,工具输出不落盘而
|
|
154
|
+
渲染路径仍带它;
|
|
155
|
+
- **未截断**:超长提示在记录里逐字全文,喂 LLM 的转录仍受 `distillMaxChars` 约束;
|
|
156
|
+
- **裁剪不毁资产**:到期裁剪后原文为 0、链接仍在、记忆数不变、`provenanceFor` 仍可查;
|
|
157
|
+
- **默认关零写入**:`enabled=false` 时连收集都不做(写入调用计数为 0),两表均 0 行,
|
|
158
|
+
记忆写入照旧;
|
|
159
|
+
- **进 reclaim 报告**:dry-run 报出真实行数与列文本大小且一个字节不改,执行只删窗口外原文;
|
|
160
|
+
- **启动期裁剪**:读装配后的 cfg(面板改了保留期也认),且默认关时不动手;
|
|
161
|
+
- **白名单与往返**:两个键过 `settings` 校验(含范围外整数被拒),PUT 往返后 effective 读得回;
|
|
162
|
+
- **轻量模式**:预设关 `enabled`、保留用户显式设的 `retentionDays`、不污染用户对象。
|
package/docs/ENTITIES.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# 实体结构化记忆(Entity Gene,v0.3.0)
|
|
2
|
+
|
|
3
|
+
> dsh-mneme 的**记忆基因**层:从记忆文本里抽取**命名实体**、**带时间轴的属性**、**实体间关系**,并支持按实体/属性精确召回记忆。与 autoDream 联动——update 记录 supersedes、merge 迁移属性归属。
|
|
4
|
+
|
|
5
|
+
v0.3.0 四个阶段全部落地:
|
|
6
|
+
|
|
7
|
+
| 阶段 | 内容 |
|
|
8
|
+
|------|------|
|
|
9
|
+
| P1 | 三表 Schema(entities / entity_attrs / entity_relations)+ 索引 + store CRUD |
|
|
10
|
+
| P2 | LLM 抽取器(JSON 抽取 + resolveEntity 去重 + saveAttr 时间轴 + saveRelation + fail-safe) |
|
|
11
|
+
| P3 | 前缀搜索(`entity:` / `attr:`),attr 精确 = 1.0 > keyword 提及 = 0.7 |
|
|
12
|
+
| P4 | autoDream 联动(applyUpdate 写 supersedes 自引用;applyMerge 迁移 loser attrs) |
|
|
13
|
+
|
|
14
|
+
存储层(建表 + CRUD)**始终可用**,不依赖任何 LLM;只有 `entityExtractionEnabled=true` 时流水线才会自动抽取。
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 一、三表结构
|
|
19
|
+
|
|
20
|
+
所有建表语句在 `src/store.js` 的 `SCHEMA` 中,`CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`,对旧库幂等。
|
|
21
|
+
|
|
22
|
+
### `entities` — 命名实体
|
|
23
|
+
|
|
24
|
+
```sql
|
|
25
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
name TEXT NOT NULL, -- 规范化名称(全名/主名)
|
|
28
|
+
type TEXT, -- person|project|concept|technology|organization
|
|
29
|
+
first_seen TEXT NOT NULL, -- 首次出现时间
|
|
30
|
+
last_seen TEXT NOT NULL, -- 最近出现时间
|
|
31
|
+
mention_count INTEGER DEFAULT 1, -- 提及次数(同名去重后递增)
|
|
32
|
+
canonical_memory_id TEXT -- 规范来源记忆(保留位)
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `entity_attrs` — 属性(快照式时间轴)
|
|
39
|
+
|
|
40
|
+
```sql
|
|
41
|
+
CREATE TABLE IF NOT EXISTS entity_attrs (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
entity_id TEXT NOT NULL,
|
|
44
|
+
attr_key TEXT NOT NULL,
|
|
45
|
+
attr_value TEXT NOT NULL,
|
|
46
|
+
memory_id TEXT, -- 属性出处记忆
|
|
47
|
+
valid_from TEXT NOT NULL,
|
|
48
|
+
valid_until TEXT, -- NULL = 当前有效
|
|
49
|
+
confidence REAL DEFAULT 1.0,
|
|
50
|
+
source TEXT
|
|
51
|
+
);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_attrs_entity ON entity_attrs(entity_id);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_attrs_key ON entity_attrs(attr_key);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_attrs_valid ON entity_attrs(valid_from, valid_until);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_attrs_memory ON entity_attrs(memory_id);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### `entity_relations` — 关系(追加式)
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
CREATE TABLE IF NOT EXISTS entity_relations (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
from_entity TEXT NOT NULL,
|
|
64
|
+
to_entity TEXT NOT NULL,
|
|
65
|
+
relation_type TEXT NOT NULL, -- uses|depends_on|part_of|related_to|supersedes
|
|
66
|
+
memory_id TEXT,
|
|
67
|
+
created_at TEXT NOT NULL,
|
|
68
|
+
metadata TEXT -- JSON 自由字段
|
|
69
|
+
);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_relations_from ON entity_relations(from_entity);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_relations_to ON entity_relations(to_entity);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_relations_type ON entity_relations(relation_type);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 二、valid_until 时间轴机制
|
|
78
|
+
|
|
79
|
+
属性采用**快照式**写入:同一 `entity_id + attr_key` 只允许**一行当前有效**(`valid_until IS NULL`)。
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
saveAttr(entity=X, key=role, value="前端工程师")
|
|
83
|
+
→ 先把 X/role 的现有有效行 valid_until 置为 now(invalidateOldAttr)
|
|
84
|
+
→ 再插入新行 valid_from=now, valid_until=NULL
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
读取语义:
|
|
88
|
+
|
|
89
|
+
- `getCurrentAttrs(entityId)` — 只返回有效行(`valid_until IS NULL`),即"当前值"
|
|
90
|
+
- `getAttrHistory(entityId)` — 按 valid_from 升序返回全部历史行(含已失效),用于追溯演变
|
|
91
|
+
- `findMemoriesByAttr(key, value)` — 返回携带该属性**当前有效**行的记忆(去重)
|
|
92
|
+
- **value 非空**:精确匹配 `attr_value`
|
|
93
|
+
- **value 为空 / undefined / null**:返回该 `attr_key` 的全部当前有效记忆("`attr:key` 无 `=value`"契约)
|
|
94
|
+
|
|
95
|
+
> ⚠️ 迁移时注意:同一 entity+key 连存两次会**失效前一行**(快照语义)。要保留多条同 key 的当前值,应把它们挂到**不同实体**上。
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 三、Store CRUD
|
|
100
|
+
|
|
101
|
+
`createStore` 暴露以下实体 API(`src/store.js`):
|
|
102
|
+
|
|
103
|
+
| 方法 | 说明 |
|
|
104
|
+
|------|------|
|
|
105
|
+
| `createEntity({ name, type })` | 建实体,`first_seen=last_seen=now`,`mention_count=1` |
|
|
106
|
+
| `findEntityByName(name)` | 按规范化名称查(去重锚点) |
|
|
107
|
+
| `findEntityById(id)` | 按 id 查 |
|
|
108
|
+
| `updateEntity(id, patch)` | 部分更新;默认 `mention_count+1`、刷新 `last_seen`;可用 `patch.mention_count` 显式覆盖 |
|
|
109
|
+
| `saveAttr({ entity_id, attr_key, attr_value, memory_id, confidence, source })` | 写属性(先失效旧行) |
|
|
110
|
+
| `invalidateOldAttr(entityId, attrKey, now)` | 手动失效;返回受影响行数 |
|
|
111
|
+
| `getCurrentAttrs(entityId)` | 当前有效属性列表 |
|
|
112
|
+
| `getAttrHistory(entityId)` | 全部属性历史(最旧在前) |
|
|
113
|
+
| `getAttrsByMemory(memoryId)` | 某记忆关联的全部属性(autoDream update 用) |
|
|
114
|
+
| `findMemoriesByAttr(key, value)` | 按属性召回记忆(value 空 = 该 key 全部当前有效) |
|
|
115
|
+
| `saveRelation({ from_entity, to_entity, relation_type, memory_id, metadata })` | 追加关系 |
|
|
116
|
+
| `getRelations(entityId)` | 某实体任一侧(from/to)的关系 |
|
|
117
|
+
| `migrateAttrsToMemory(fromId, toId, now)` | 把 from 记忆的属性 memory_id 改指 to(keeper 已有同 entity+key 当前行则失效);返回 `{ migrated, invalidated }` |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 四、LLM 抽取器
|
|
122
|
+
|
|
123
|
+
`src/entities/extractor.js` → `extractEntities(memory, { store, config, callLLM })`。
|
|
124
|
+
|
|
125
|
+
### 触发方式
|
|
126
|
+
|
|
127
|
+
`service.saveWithDedupe` 在 **created** 后 fire-and-forget 触发,仅当:
|
|
128
|
+
|
|
129
|
+
1. `config.entityExtractionEnabled === true`
|
|
130
|
+
2. service 已通过 `service.setEntityExtractor(...)` 注入抽取钩子(`index.js` 在插件启动时接好)
|
|
131
|
+
|
|
132
|
+
抽取**异步执行**、失败绝不阻塞记忆写入(fail-safe)。
|
|
133
|
+
|
|
134
|
+
### LLM JSON 契约
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
{
|
|
138
|
+
"entities": [
|
|
139
|
+
{"name": "string", "type": "person|project|concept|technology|organization",
|
|
140
|
+
"attrs": [{"key": "string", "value": "string", "confidence": 0.9}]}
|
|
141
|
+
],
|
|
142
|
+
"relations": [
|
|
143
|
+
{"from": "entityName", "to": "entityName", "type": "uses|depends_on|part_of|related_to"}
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
抽取管线:
|
|
149
|
+
|
|
150
|
+
1. `buildSystemPrompt` — 约束上限(`entityExtractionMaxEntities` / `entityExtractionMaxAttrs`)、禁编造、用规范化名
|
|
151
|
+
2. `extractJsonFromText` — 先直接 `JSON.parse`,失败再抓首个 `{...}` 块
|
|
152
|
+
3. `sanitizeExtractedData` — 非法 type → `concept`;非法 relation type → `related_to`;非法 confidence → 0.9;超限截断;relations 的 from/to 必须引用实体清单
|
|
153
|
+
4. `resolveEntity` — **同名去重**:`findEntityByName` 命中 → `updateEntity(id, {})`(`mention_count+1`),未命中 → 建新实体
|
|
154
|
+
5. 逐条 `saveAttr`(挂 `memory_id` + `source: "llm_extract"`)→ 逐条 `saveRelation`
|
|
155
|
+
6. 单实体/属性/关系失败只 `skipCount++` 并告警,不中断整批
|
|
156
|
+
|
|
157
|
+
### 返回 / fail-safe
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
{ ok: true, entities: [{ name, type, attrs, entity_id }], attrs: [...], relations: [...], skipped: n }
|
|
161
|
+
{ ok: false, error: "..." }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- 缺失 content、LLM 返回空、JSON 解析失败、sanitize 抛错、`callLLM` reject → 一律 `{ ok: false }`,**绝不向上抛异常**
|
|
165
|
+
- `entityExtractionEnabled=false` 时钩子完全不触发,存储层照常可用
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 五、搜索:`entity:` / `attr:` 前缀
|
|
170
|
+
|
|
171
|
+
`service.searchMemories` 在 `config.entitySearchEnabled === true`(默认 true)时识别前缀路由:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
entity:阿尔托 → searchByEntity("阿尔托")
|
|
175
|
+
attr:国籍=芬兰 → searchByAttr("国籍", "芬兰") 精确匹配
|
|
176
|
+
attr:国籍 → searchByAttr("国籍", undefined) 该 key 全部当前有效
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### searchByEntity 排序
|
|
180
|
+
|
|
181
|
+
**合并优先级:attr 精确关联 = 1.0 > 关键词提及 = 0.7**,attr 命中不覆盖、keyword 只补充召回,最终按 `_score` 降序取 topK:
|
|
182
|
+
|
|
183
|
+
| 来源 | 分数 |
|
|
184
|
+
|------|------|
|
|
185
|
+
| `entity_attrs.memory_id` 精确关联(当前有效) | `_score = 1.0`,`_source = "entity_attr"` |
|
|
186
|
+
| 标题/内容/标签含实体名 | `_score = 0.7`,`_source = "keyword"` |
|
|
187
|
+
|
|
188
|
+
`entitySearchEnabled=false` 时前缀不做路由,按普通关键词搜索(`entity:xxx` 字面串极少命中 → 通常返回空)。
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 六、autoDream 联动
|
|
193
|
+
|
|
194
|
+
`applyDecisions` 在 `config.entityExtractionEnabled === true` 时启用实体副作用,**仅记录、绝不阻断主流程**:
|
|
195
|
+
|
|
196
|
+
- **applyUpdate(supersedes)**:事务提交成功后,为该记忆关联的每条实体属性建立**自引用** supersedes 关系,表示"此属性版本已被替代":
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
saveRelation({ from_entity: attr.entity_id, to_entity: attr.entity_id,
|
|
200
|
+
relation_type: "supersedes", memory_id: id,
|
|
201
|
+
metadata: { attr_key, old_value } })
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- **applyMerge(属性迁移)**:将 loser 记忆关联的 `entity_attrs.memory_id` 迁移到 keeper;若 keeper 已有同 `entity+key` 的当前有效属性,则 loser 行被**失效**(keeper 值优先):
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
{ migrated, invalidated } = store.migrateAttrsToMemory(loserId, keeperId, now)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- `entityExtractionEnabled=false` 时两条副作用**完全跳过**(update/merge 本身照常生效)。
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 七、配置项
|
|
215
|
+
|
|
216
|
+
| 键 | 默认值 | 说明 |
|
|
217
|
+
|----|--------|------|
|
|
218
|
+
| `entityExtractionEnabled` | `false` | 实体抽取总开关(默认关;存储层恒可用) |
|
|
219
|
+
| `entityExtractionModel` | `""` | 抽取专用模型(空 = 用 agent 默认模型) |
|
|
220
|
+
| `entityExtractionMaxEntities` | `10` | 每次抽取实体数上限 |
|
|
221
|
+
| `entityExtractionMaxAttrs` | `20` | 每实体属性数上限 |
|
|
222
|
+
| `entitySearchEnabled` | `true` | `entity:` / `attr:` 前缀搜索开关 |
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
# ~/.dsh/profiles/web/cordis.patch.yml
|
|
226
|
+
- id: dsh-mneme
|
|
227
|
+
config:
|
|
228
|
+
entityExtractionEnabled: true
|
|
229
|
+
entityExtractionModel: deepseek-chat # 可选,默认用 agent 默认模型
|
|
230
|
+
entitySearchEnabled: true
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 八、迁移说明
|
|
236
|
+
|
|
237
|
+
- 实体三表在 `createStore` 的 `SCHEMA` 中 `CREATE TABLE IF NOT EXISTS` 创建,**旧库打开自动建表**,幂等(重复打开/重启无副作用,`test/entities.test.js` 有覆盖)。
|
|
238
|
+
- 无需人工迁移数据;现有 `memories` 表不变,实体表是**旁挂**结构。
|
|
239
|
+
- 抽取开启后,新写入的记忆自动补实体;存量记忆可通过触发抽取或后续 backfill 补全(v0.3.0 未内置存量回扫)。
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 测试
|
|
244
|
+
|
|
245
|
+
`test/entities.test.js`(31 条,node:test):Schema 三表 + 索引 + 旧库幂等迁移 / 实体 CRUD / 属性时间轴(saveAttr 失效旧行、getCurrentAttrs / getAttrHistory)/ `findMemoriesByAttr`(精确匹配 + **空 value 契约** + 过期行排除)/ 抽取器(mock LLM 写入、同名去重、垃圾 JSON / LLM 故障 fail-safe、开关触发)/ 前缀搜索(1.0 > 0.7 排序、attr 精确、attr 空值、开关关闭不路由)/ autoDream(supersedes、merge 迁移与失效、开关关闭跳过)/ fail-safe(抽取失败不阻塞写入)。
|