@modusensus/dsh-mneme 0.8.14 → 0.8.15
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 +655 -654
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +689 -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 +263 -0
- package/docs/CONTINUITY.md +124 -0
- package/docs/DAEMON.md +46 -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 +89 -0
- package/lib/api-standalone.js +673 -673
- package/lib/api.js +1437 -1433
- package/lib/bootstrap.js +211 -211
- package/lib/client.js +5444 -5295
- package/lib/commands.js +95 -95
- package/lib/config.js +880 -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 +674 -674
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +819 -819
- package/lib/dream.js +1501 -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 +46 -46
- package/lib/index.js +535 -503
- package/lib/inject.js +464 -450
- package/lib/lang.js +248 -229
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +142 -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 +175 -175
- 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/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 +2663 -2605
- package/lib/settings.js +517 -511
- package/lib/store.js +3007 -3007
- package/lib/summarize.js +891 -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 +673 -673
- package/src/api.js +1437 -1433
- package/src/bootstrap.js +211 -211
- package/src/commands.js +95 -95
- package/src/config.js +880 -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 +674 -674
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +819 -819
- package/src/dream.js +1501 -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 +46 -46
- package/src/index.js +535 -503
- package/src/inject.js +464 -450
- package/src/lang.js +248 -229
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +142 -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 +175 -175
- 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/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 +2663 -2605
- package/src/settings.js +517 -511
- package/src/store.js +3007 -3007
- package/src/summarize.js +891 -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 +218 -218
- package/test/api.test.js +1702 -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 +1230 -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/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-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 +145 -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 +105 -105
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +420 -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 +598 -598
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +391 -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 +434 -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 +189 -189
- package/test/receipt-chain.test.js +451 -451
- 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 +405 -405
- package/test/settings.test.js +261 -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 +396 -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 +1465 -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/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(抽取失败不阻塞写入)。
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# dsh-mneme 本地模型部署指南
|
|
2
|
+
|
|
3
|
+
- **日期**:2026-08-15
|
|
4
|
+
- **范围**:为 dsh-mneme 配置完全离线的本地语义模型(Embedding + Rerank),覆盖 ONNX(transformers.js)与 Ollama 两种路径
|
|
5
|
+
|
|
6
|
+
## 1. 前置要求
|
|
7
|
+
|
|
8
|
+
| 项目 | 要求 | 说明 |
|
|
9
|
+
|------|------|------|
|
|
10
|
+
| Node.js | ≥ 22.5(建议 24+) | 插件依赖 `node:sqlite`(22.5 起可用,24 稳定) |
|
|
11
|
+
| 磁盘空间 | ≥ 300MB(推荐 1GB+) | bge-small-zh-v1.5 量化约 100MB,bge-reranker-base 约 250MB;Ollama 模型另计 |
|
|
12
|
+
| 内存 | ≥ 1GB 可用 | 纯 CPU 推理亦可运行 |
|
|
13
|
+
| GPU(可选) | 显存 ≥ 2GB | 开启 `localEmbedDevice: "gpu"`(onnxruntime 需要额外安装 cuda 后端) |
|
|
14
|
+
| 网络 | 仅首次下载模型需要 | 已下载后可完全离线(默认走 Hugging Face,可换镜像源) |
|
|
15
|
+
|
|
16
|
+
> 依赖解析分三层:① **插件自管运行时目录**(`~/.dsh/mneme/runtime/`,见 §2.5,优先);② 宿主 profile 里已装的 `@huggingface/transformers`;③ 都没有则本地推理不可用,检索自动降级为关键词/BM25(读写在任何情况下都不受影响)。
|
|
17
|
+
>
|
|
18
|
+
> 从本版起 `@huggingface/transformers` 已从 `dependencies` 降为**可选 peer**(`peerDependenciesMeta.optional`),安装插件时不会再带上它和它那套原生闭包;第 ② 层因此只对「自己装过这份依赖」或「从旧版本升级上来、尚未被 prune」的环境有效。**正在使用本地嵌入的用户请在升级前按 §2.5 收编一份**,否则升级后本地嵌入不可用(读写信道不受影响)。
|
|
19
|
+
>
|
|
20
|
+
> `onnxruntime-node` 作为可选原生后端列在 `allowScripts`(首次安装需确认脚本),无需手动额外安装。
|
|
21
|
+
|
|
22
|
+
## 2. 模型安装 / 下载
|
|
23
|
+
|
|
24
|
+
### 2.1 自动下载(默认,推荐)
|
|
25
|
+
|
|
26
|
+
首次使用本地 Embedder 时会**自动从 Hugging Face Hub 下载**对应模型到本地缓存,无需手动操作:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node scripts/benchmark-embed.js --provider local --model Xenova/bge-small-zh-v1.5
|
|
30
|
+
# 首次运行会显示下载进度 → 加载模型 → 输出基准表格
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
默认缓存目录:`~/.dsh/mneme/models`(用户级,重装/升级依赖不丢;`embedModelCacheDir` 留空时启用)。transformers.js 把模型存成 `<cacheDir>/<org>/<name>/`(实测 4.2.0,例如 `~/.dsh/mneme/models/Xenova/bge-small-zh-v1.5/`):
|
|
34
|
+
|
|
35
|
+
- Linux/macOS:`~/.dsh/mneme/models/`
|
|
36
|
+
- Windows:`%USERPROFILE%\.dsh\mneme\models\`
|
|
37
|
+
|
|
38
|
+
> 实测 transformers.js 4.2.0 在离线加载(`allowRemoteModels=false`)时按 `<cacheDir>/<org>/<model>/` 解析模型文件,例如 `<cacheDir>/Xenova/bge-small-zh-v1.5/config.json`。与本段上面描述的 `hub/models--…` 布局不一致时,以实际报错里给出的路径为准;缓存不在默认位置时按 §2.5 用 `--cache-dir` 指过去。
|
|
39
|
+
|
|
40
|
+
### 2.2 手动指定缓存目录
|
|
41
|
+
|
|
42
|
+
内存受限/离线机器可把模型缓存放到指定位置(迁移、共享缓存):
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// 配置示例(插件配置里设置 embedModelCacheDir)
|
|
46
|
+
const embedder = createEmbedderByProvider("local", {
|
|
47
|
+
cacheDir: "/data/hf-models", // 模型缓存目录
|
|
48
|
+
model: "Xenova/bge-small-zh-v1.5"
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2.3 Ollama 路径
|
|
53
|
+
|
|
54
|
+
如果本机已有 Ollama:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ollama pull nomic-embed-text # 或 bge-m3(多语言)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Ollama Embedder 走 `/api/embeddings`,**不下载任何文件到插件缓存**,模型由 Ollama 管理。
|
|
61
|
+
|
|
62
|
+
### 2.4 断点续传
|
|
63
|
+
|
|
64
|
+
下载基于 Hugging Face Hub 缓存协议,**中断后重试自动续传**(按分片断点续传,不重新下载已完成部分)。若反复中断导致缓存损坏,删除对应模型的缓存目录后重跑即可:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
rm -rf ~/.dsh/mneme/models/hub/models--Xenova--bge-small-zh-v1.5
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2.5 自管运行时(收编 / 校验 / 排查)
|
|
71
|
+
|
|
72
|
+
本地推理真正要用的是一整套依赖闭包:`@huggingface/transformers` + `onnxruntime-node` + `sharp`(本机实测从宿主 node_modules 收编时是 **49 个包、3203 个文件、约 393MB**)。插件支持把它收编到自管目录 `~/.dsh/mneme/runtime/`,与宿主 profile 的依赖图解耦;收编**优先使用硬链接**,同盘时几乎不额外占盘。
|
|
73
|
+
|
|
74
|
+
**为什么要独立出来**:这份闭包此前留在插件 `dependencies` 里,而 profile 是所有插件共用的依赖图,于是「装任何插件」都要替它重走一遍整条链,弱网下极慢。摘掉该声明后,运行时由自管目录承接——所以要**先收编、再升级**,否则升级后本地嵌入不可用。
|
|
75
|
+
|
|
76
|
+
**三条取件来源,按可用性依次尝试**(面板按钮、`memory_runtime` 工具、CLI 三者共用同一套编排):
|
|
77
|
+
|
|
78
|
+
| 顺序 | 来源 | 特点 |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| ① | 收编本机 profile 的 `node_modules` | **零网络**;同盘硬链接,几乎不占额外空间 |
|
|
81
|
+
| ② | `runtimeTarballDir` 指向的本地 `.tgz` 目录 | 某个包网络下不到时用(文件名按 npm 约定 `<basename>-<version>.tgz`) |
|
|
82
|
+
| ③ | npm registry(`runtimeMirror` 可换镜像) | 按随包发布的 `runtime-manifest.json` 逐个取,**每个 tarball 先校验 sha512 再落盘** |
|
|
83
|
+
|
|
84
|
+
走 ③ 时体积是 **win32-x64 实测约 33 个包、2142 个文件、数百 MB**(各平台不同,故不写死数值)——比①少,因为清单剔除了 `onnxruntime-web`(127.7MB,占全量三分之一):transformers 的 Node 构建从不 import 它(把一个空的 `onnxruntime-web` 打桩进去,嵌入结果逐字节相同),风险由 `verify` 的真实推理兜底。清单本身**平台无关**(数据源是 `package-lock.json` 里所有平台的条目),下载时按各包的 `os`/`cpu` 过滤,所以 win32 / darwin / linux × x64 / arm64 都被覆盖 —— 不需要谁在某个系统上先跑一次生成脚本。
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 在插件目录下运行(例如 node_modules/@modusensus/dsh-mneme)
|
|
88
|
+
|
|
89
|
+
# 1) 看状态:在哪、来源、结构完不完整(不加载模型,秒回)
|
|
90
|
+
node scripts/mneme-runtime.mjs status
|
|
91
|
+
|
|
92
|
+
# 2) 收编宿主里已有的那份(只读源、只写自管目录,不安装任何东西)
|
|
93
|
+
node scripts/mneme-runtime.mjs adopt --from ~/.dsh/profiles/<profile>/node_modules
|
|
94
|
+
|
|
95
|
+
# 3) 真加载运行时并跑一次推理,确认可用
|
|
96
|
+
node scripts/mneme-runtime.mjs verify --cache-dir ~/.dsh/mneme/models
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- 退出码:`0` 健康 / `1` 不健康或失败 / `2` 用法错误(便于脚本直接 gate,区分「没装」和「装坏了」看输出里的 `status`)。加 `--json` 得到机器可读结果。
|
|
100
|
+
- `verify` 的功能验证**不触网**(`allowRemoteModels=false`):模型必须已在缓存目录里,否则会明确失败——这是有意的,验证不该偷偷触网。缓存不在默认位置时用 `--cache-dir` 指到与 `embedModelCacheDir` 一致的位置。
|
|
101
|
+
- 三条入口(CLI / 面板 / `memory_runtime`)的判据是同一份:缓存目录取 `embedModelCacheDir`(**留空即用户级默认目录**,与真实嵌入同一处解析),探针模型取 `localEmbedModel`(留空即默认小模型)。也就是说验证用的就是真实推理用的那份模型——CLI 不需要额外 `--cache-dir`,面板也不会跑到包内去找(#387 修的就是这个不一致)。
|
|
102
|
+
- 收编来的目录没有可比对的原始产物,完整性如实标 `unverified`,不假装验过;结构检查通过 ≠ 功能可用,两者分开报。
|
|
103
|
+
- `status` 只做结构检查,所以 `functional` 恒为 `unknown`——要看真实推理结果就跑 `verify`。
|
|
104
|
+
- 运行时不可用时插件不会崩:检索降级为关键词/BM25,读写在任何情况下都不受影响;错误信息会带上 `adopt` 的具体命令。
|
|
105
|
+
- 同一份状态也能从 `GET /api/dsh-mneme/semantic` 的 `localRuntime` 字段读到(面板与 CLI 同源)。
|
|
106
|
+
|
|
107
|
+
#### 不用命令行也可以
|
|
108
|
+
|
|
109
|
+
- **面板**:记忆面板的「向量索引」卡片在本地 provider 且运行时不就绪时,会显示**代价说明**(要额外一份运行时、解包后数百 MB)、先试收编、没有再下载,并按实际走的那一档分开报结果。卡片下面还有一个「取回本地运行时」按钮,点一下即完成。
|
|
110
|
+
- **让 agent 代做**:`memory_runtime` 工具提供三个动作——`status`(只读,**返回里带代价说明**,便于 agent 先告知你再动手)、`provision`(走上面三档来源)、`verify`(真跑一次推理)。所以即便你不看面板,也可以直接让 agent 处理。
|
|
111
|
+
- **这条路由与其它写路由走同一道门**:`POST /api/dsh-mneme/runtime/provision` 是唯一会联网拉数百 MB 并写盘的写面,鉴权规则与其他写路由完全一致——配了 `apiToken` 就必须带 token,**没配就等于不校验**(这是插件既有的鉴权模型,不是这条路由特有的)。本机默认部署下它开箱可用;但若这个端口能被别的页面/机器够到,请照第 3.1 节配上 `apiToken`。面板按钮与 `memory_runtime` 都走这道门,不需要额外配置。
|
|
112
|
+
- **平台过滤的取向是「宽松优先」**:清单是平台无关的一份(含所有平台的包),下载时按本机 `os`/`cpu` 过滤;Linux 上如果**确认**了本机是 glibc,还会剔掉另一套 libc 的变体(`sharp` 与 `libvips` 的 musl 包,十几 MB)。拿不到 libc 证据时一律不过滤——多下十几兆,好过赌错方向装出一份跑不起来的运行时。
|
|
113
|
+
- **宿主那份 transformers 会被版本门禁**:第 ② 层(宿主裸 specifier)只接受 `>=4.2.0` 且 `<5` 的版本;宿主装到 v5 时不会被静默使用,而是明确报出「版本越界」并退到第 ③ 层的可操作提示。版本读不到(某些打包形态)时不拦,以免这层对老用户整体失效。
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
# 两个可选配置键(默认留空 = 不改变任何行为)
|
|
117
|
+
runtimeTarballDir: "" # 本地 .tgz 目录:有它就优先于联网
|
|
118
|
+
runtimeMirror: "" # registry 镜像前缀,例如 https://npmmirror.com/mirrors/npm/
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### 升级前必做(否则本地嵌入会断)
|
|
122
|
+
|
|
123
|
+
如果你正在使用本地嵌入(`embedProvider: local`),**在升级到已摘掉 `dependencies` 声明的版本之前**先执行一次 `adopt` + `verify`。顺序不能反:升级时 pnpm 会 prune 掉宿主里那份依赖,那之后就没有可收编的源了,只剩联网下载一条路。
|
|
124
|
+
|
|
125
|
+
#### 自己取一份来收编(相对上面的手动路径)
|
|
126
|
+
|
|
127
|
+
上面的面板按钮 / `provision` 已经能在没有可收编副本时自行下载。但如果你希望**完全掌控取件过程**(例如用自己的镜像、或先在一处统一缓存),仍然可以手工取一份再收编——不必逐个下载包:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Windows(PowerShell / cmd)
|
|
131
|
+
mkdir %TEMP%\mneme-fetch && cd /d %TEMP%\mneme-fetch
|
|
132
|
+
npm i @huggingface/transformers@^4.2.0
|
|
133
|
+
node "<插件目录>\scripts\mneme-runtime.mjs" adopt --from "%TEMP%\mneme-fetch\node_modules"
|
|
134
|
+
|
|
135
|
+
# Linux / macOS
|
|
136
|
+
mkdir -p /tmp/mneme-fetch && cd /tmp/mneme-fetch
|
|
137
|
+
npm i @huggingface/transformers@^4.2.0
|
|
138
|
+
node "<插件目录>/scripts/mneme-runtime.mjs" adopt --from /tmp/mneme-fetch/node_modules
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- **安装脚本必须能执行**:npm 默认会执行生命周期脚本,而 `onnxruntime-node` / `sharp` 正是靠 postinstall 取原生二进制;加 `--ignore-scripts` 会得到一个装不起来的闭包。pnpm 10+ 默认拦截构建脚本,所以这里用 npm 更省事。
|
|
142
|
+
- 收编完那个临时目录可以直接删掉:闭包已经在 `~/.dsh/mneme/runtime/` 里了(硬链接时几乎不额外占盘)。
|
|
143
|
+
- 收编后**仍然建议跑一次 `verify`**——结构完整不等于能推理。
|
|
144
|
+
|
|
145
|
+
## 3. 配置
|
|
146
|
+
|
|
147
|
+
### 3.1 插件配置(`~/.dsh/profiles/web/cordis.patch.yml`)
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
- id: dsh-mneme
|
|
151
|
+
name: '@modusensus/dsh-mneme'
|
|
152
|
+
config:
|
|
153
|
+
# ── Embedding ──────────────────────────────
|
|
154
|
+
embedProvider: local # local | ollama | openai(默认 openai,保持 v0.1 行为)
|
|
155
|
+
localEmbedModel: Xenova/bge-small-zh-v1.5
|
|
156
|
+
localEmbedDimension: 512 # 与模型匹配的向量维度
|
|
157
|
+
localEmbedDevice: cpu # cpu | gpu(仅 local 生效)
|
|
158
|
+
localEmbedBatchSize: 8 # 分批嵌入条数,内存紧张调小
|
|
159
|
+
embedModelCacheDir: "" # 模型缓存目录(空=用户级 ~/.dsh/mneme/models;注意:不做 ~ 展开,要指别处必须写绝对路径)
|
|
160
|
+
ollamaBaseUrl: "http://localhost:11434" # 仅 ollama 生效
|
|
161
|
+
ollamaModel: "nomic-embed-text" # 仅 ollama 生效(openai 端点走面板 vector-config)
|
|
162
|
+
# ── Rerank(Phase 2,可选)─────────────────
|
|
163
|
+
rerankEnabled: false # 开启向量召回后的精排
|
|
164
|
+
rerankModel: Xenova/bge-reranker-base
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 3.2 环境变量(可选)
|
|
168
|
+
|
|
169
|
+
| 变量 | 作用 |
|
|
170
|
+
|------|------|
|
|
171
|
+
| `HF_ENDPOINT` | Hugging Face 镜像源,例如 `https://hf-mirror.com`(国内加速) |
|
|
172
|
+
| `HF_HUB_CACHE` | 覆盖默认缓存目录 |
|
|
173
|
+
|
|
174
|
+
### 3.3 各后端配置对照
|
|
175
|
+
|
|
176
|
+
| 配置 | local (ONNX) | ollama | openai |
|
|
177
|
+
|------|--------------|--------|--------|
|
|
178
|
+
| `localEmbedModel` | HF 模型 id(如 `Xenova/bge-small-zh-v1.5`) | —(用 `ollamaModel`) | —(用面板 vector-config) |
|
|
179
|
+
| `ollamaBaseUrl` | — | `http://localhost:11434`(默认) | — |
|
|
180
|
+
| `ollamaModel` | — | Ollama 模型名(如 `nomic-embed-text`) | — |
|
|
181
|
+
| `localEmbedDevice` | ✅ | — | — |
|
|
182
|
+
| `localEmbedBatchSize` | ✅ | — | — |
|
|
183
|
+
| `embedModelCacheDir` | ✅ | — | — |
|
|
184
|
+
|
|
185
|
+
## 4. 首次使用
|
|
186
|
+
|
|
187
|
+
1. 配置 `embedProvider`(或保持 `openai` 不动)
|
|
188
|
+
2. 保存配置并重启 DSH,或直接跑 `scripts/benchmark-embed.js` 验证
|
|
189
|
+
3. 首次触发本地模型时,transformers.js 自动下载 → `init()` 打印 `local embedder ready: <model> (dim=512, device=cpu)`
|
|
190
|
+
4. 之后所有 `memory_save` 写入自动补向量;已有记忆可用「重建索引」批量补建
|
|
191
|
+
|
|
192
|
+
> 注意:切换 `localEmbedModel` 或 `embedDimension` 后,旧向量与新向量维度不同、无法混算,需**重建索引**(复用 v0.1 的 `reindexMissing` 机制)。
|
|
193
|
+
|
|
194
|
+
## 5. 常见问题
|
|
195
|
+
|
|
196
|
+
### Q1:模型下载失败 / 下载超慢
|
|
197
|
+
|
|
198
|
+
- 国内网络:设置镜像源 `HF_ENDPOINT=https://hf-mirror.com`(或 `export HF_ENDPOINT=https://hf-mirror.com`)
|
|
199
|
+
- 公司代理拦截:检查 `HTTPS_PROXY` / `HTTP_PROXY` 环境变量
|
|
200
|
+
- 磁盘不足:清理缓存或用 `embedModelCacheDir` 换目录
|
|
201
|
+
|
|
202
|
+
### Q2:`Xenova/bge-small-zh-v1.5` 加载报错 / 显存不足(OOM)
|
|
203
|
+
|
|
204
|
+
- 默认 `device: cpu`、`dtype: q8`,内存占用已最小化
|
|
205
|
+
- 尝试 `embedBatchSize` 调小(如 4/2)降低峰值内存
|
|
206
|
+
- 显存不足:`device: gpu` 时改回 `cpu`;或检查 onnxruntime-node 是否安装了 GPU 后端
|
|
207
|
+
|
|
208
|
+
### Q3:中英文混排 / CJK 文本处理
|
|
209
|
+
|
|
210
|
+
- `bge-small-zh-v1.5` 对中文与中英混排均优化良好;纯英文场景可换 Ollama 的 `nomic-embed-text`
|
|
211
|
+
- 单条文本上限 8000 字符(与 v0.1 一致),超长自动截断
|
|
212
|
+
- 检索时 query 与记忆正文走同一 Embedder、同一归一化(mean pooling + L2),余弦分数才可比
|
|
213
|
+
|
|
214
|
+
### Q4:换模型后结果变差 / 向量混乱
|
|
215
|
+
|
|
216
|
+
- 换模型(尤其维度不同)后旧向量不可复用,必须重建索引
|
|
217
|
+
- 若在 Web 面板/API 配置过 v0.1 的 `vector-config`,v0.2 会优先采用插件配置,旧配置仅作降级路径
|
|
218
|
+
|
|
219
|
+
### Q5:本地模型加载很慢(首次/每次启动)
|
|
220
|
+
|
|
221
|
+
- 首次含下载,属正常;已缓存后每次启动仅加载进内存(数百 ms 级)
|
|
222
|
+
- 大模型(如 bge-m3)可考虑换用更小的 `Xenova/bge-small-zh-v1.5`(量化 q8 约 100MB)
|