@modusensus/dsh-mneme 0.7.31 → 0.7.32

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 (86) hide show
  1. package/README.md +24 -8
  2. package/lib/api.js +79 -6
  3. package/lib/client.js +217 -30
  4. package/lib/commands.js +6 -2
  5. package/lib/config.js +80 -14
  6. package/lib/dream/decisions.js +147 -14
  7. package/lib/dream/sleep.js +84 -34
  8. package/lib/dream.js +250 -60
  9. package/lib/embedding.js +22 -2
  10. package/lib/index.js +68 -37
  11. package/lib/inject.js +16 -9
  12. package/lib/lang.js +156 -0
  13. package/lib/local-embedder.js +23 -9
  14. package/lib/mirror.js +17 -14
  15. package/lib/reranker.js +23 -8
  16. package/lib/runtime/adopt.js +176 -0
  17. package/lib/runtime/closure.js +172 -0
  18. package/lib/runtime/download.js +247 -0
  19. package/lib/runtime/layout.js +235 -0
  20. package/lib/runtime/loader.js +249 -0
  21. package/lib/runtime/provision.js +237 -0
  22. package/lib/runtime/tarball.js +130 -0
  23. package/lib/runtime/verify.js +191 -0
  24. package/lib/service.js +102 -8
  25. package/lib/settings.js +36 -2
  26. package/lib/store.js +26 -8
  27. package/lib/summarize.js +108 -35
  28. package/lib/tools.js +119 -0
  29. package/package.json +7 -2
  30. package/runtime-manifest.json +596 -0
  31. package/scripts/build-runtime-manifest.mjs +217 -0
  32. package/scripts/mneme-runtime.mjs +277 -0
  33. package/src/api.js +79 -6
  34. package/src/commands.js +6 -2
  35. package/src/config.js +80 -14
  36. package/src/dream/decisions.js +147 -14
  37. package/src/dream/sleep.js +84 -34
  38. package/src/dream.js +250 -60
  39. package/src/embedding.js +22 -2
  40. package/src/index.js +68 -37
  41. package/src/inject.js +16 -9
  42. package/src/lang.js +156 -0
  43. package/src/local-embedder.js +23 -9
  44. package/src/mirror.js +17 -14
  45. package/src/reranker.js +23 -8
  46. package/src/runtime/adopt.js +176 -0
  47. package/src/runtime/closure.js +172 -0
  48. package/src/runtime/download.js +247 -0
  49. package/src/runtime/layout.js +235 -0
  50. package/src/runtime/loader.js +249 -0
  51. package/src/runtime/provision.js +237 -0
  52. package/src/runtime/tarball.js +130 -0
  53. package/src/runtime/verify.js +191 -0
  54. package/src/service.js +102 -8
  55. package/src/settings.js +36 -2
  56. package/src/store.js +26 -8
  57. package/src/summarize.js +108 -35
  58. package/src/tools.js +119 -0
  59. package/test/api.test.js +89 -9
  60. package/test/client.test.js +94 -10
  61. package/test/config.test.js +17 -1
  62. package/test/dream-candidate-hybrid.test.js +281 -0
  63. package/test/dream.test.js +172 -0
  64. package/test/embedding-ready.test.js +196 -0
  65. package/test/helpers/runtime-source.js +60 -0
  66. package/test/helpers/tar-builder.js +80 -0
  67. package/test/inject.test.js +37 -1
  68. package/test/lang.test.js +87 -0
  69. package/test/quality-filter.test.js +68 -0
  70. package/test/reasoning-effort.test.js +106 -15
  71. package/test/reindex-backfill.test.js +62 -0
  72. package/test/runtime-adopt.test.js +206 -0
  73. package/test/runtime-cli.test.js +89 -0
  74. package/test/runtime-closure.test.js +193 -0
  75. package/test/runtime-download.test.js +302 -0
  76. package/test/runtime-layout.test.js +163 -0
  77. package/test/runtime-loader.test.js +237 -0
  78. package/test/runtime-manifest.test.js +158 -0
  79. package/test/runtime-provision.test.js +101 -0
  80. package/test/runtime-tarball.test.js +82 -0
  81. package/test/runtime-verify.test.js +214 -0
  82. package/test/sleep-action-set.test.js +422 -0
  83. package/test/store.test.js +14 -0
  84. package/test/summarize.test.js +224 -0
  85. package/test/tools.test.js +30 -5
  86. package/test/webServer-optional.test.js +34 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
6
6
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
7
7
  [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
8
- [![tests](https://img.shields.io/badge/tests-745%20passed-success)](https://github.com/modusensus/dsh-mneme)
8
+ [![tests](https://img.shields.io/badge/tests-916%20passed-success)](https://github.com/modusensus/dsh-mneme)
9
9
  [![CI](https://img.shields.io/github/actions/workflow/status/modusensus/dsh-mneme/ci.yml)](https://github.com/modusensus/dsh-mneme/actions)
10
10
  [![node](https://img.shields.io/badge/node-24%2B-blue)](https://nodejs.org)
11
11
  [![npm downloads](https://img.shields.io/npm/dm/@modusensus/dsh-mneme?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
@@ -90,14 +90,14 @@ dsh web
90
90
  | 中等(1 万-5 万字) | `65536` |
91
91
  | 大型(5 万字以上) | `131072`(上限) |
92
92
 
93
- > 若使用**思考型模型**(如 deepseek-v4-flash / DeepSeek-R1 类),模型可能把全部预算花在 reasoning 上导致正文为空(日志出现 `no json array in llm output`)。处理顺序:① 把 `dreamReasoningEffort` 设为 `low` 显式压低思考(v0.7.26+ 若模型不支持该档位,`resolveDreamEffort` 会自动换用模型支持的默认/首个档位或省略字段,拒绝原因会记入 llm_audit);② 重试走模型默认思考行为后正文仍为空的,调大 `dreamMaxTokens`(reasoning 与正文共享该预算)或配置 `dreamProvider`/`dreamModel` 指向非思考模型。sleep 侧对应 `sleepReasoningEffort`。
93
+ > 若使用**思考型模型**(如 deepseek-v4-flash / DeepSeek-R1 类),模型可能把全部预算花在 reasoning 上导致正文为空(日志出现 `no json array in llm output`)。#135 修复后:未配置 `dreamReasoningEffort` 时会**自动取模型支持的最低档**发流(不再省略字段让模型自带默认档——v4-flash 系默认 high——顶上烧光预算),默认配置即生效;显式 `none` = 不发送字段用服务商默认,`off`/`low`/`medium`/`high` 原样传递,模型不支持的档位自动换用(拒绝原因记入 llm_audit)。正文仍为空时再调大 `dreamMaxTokens`(默认已抬至 131072)或配置 `dreamProvider`/`dreamModel` 指向非思考模型。sleep 侧对应 `sleepReasoningEffort`。
94
94
 
95
95
  **巩固模型分类声明**(settings panel「巩固模型」= `dreamProvider`/`dreamModel`,睡眠侧对应 `sleepProvider`/`sleepModel`):
96
96
 
97
97
  | 模型类别 | 例子 | 说明 |
98
98
  |---------|------|------|
99
99
  | **非思考模型(推荐)** | glm-5-2 类等 | 无 reasoning 声明;即使配了 effort 被 harness 拒绝,fallback 去掉字段重试即成功。空体风险最低 |
100
- | **思考模型(需实测)** | deepseek-v4-flash-ga 等 v4-flash-ga 系 | 默认开推理,可能烧光 token 预算返回空体;部分型号(如 v4-flash-ga)在 harness 侧被声明为**不接受任何 reasoning effort**(去掉 effort 时 harness 的 `defaultEffort` 会顶上来再次拒绝)。v0.7.26+ 已根治:`resolveDreamEffort` 发流前探测模型支持的档位,不支持的配置档位自动换用模型默认/首个支持档位,声明无 reasoning 能力的型号则省略字段。选用时建议配 `dreamReasoningEffort` 实测,不行就换非思考模型 |
100
+ | **思考模型(需实测)** | deepseek-v4-flash-ga 等 v4-flash-ga 系 | 默认开推理,可能烧光 token 预算返回空体;部分型号(如 v4-flash-ga)在 harness 侧被声明为**不接受任何 reasoning effort**(去掉 effort 时 harness 的 `defaultEffort` 会顶上来再次拒绝)。v0.7.26+ 已根治:`resolveDreamEffort` 发流前探测模型支持的档位,不支持的配置档位自动换用模型默认/首个支持档位,声明无 reasoning 能力的型号则省略字段。选用时建议配 `dreamReasoningEffort` 实测(未配置时自动取最低档),不行就换非思考模型 |
101
101
 
102
102
  ### Sleep Mode 系统级睡眠 💤(v0.4.0,opt-in)
103
103
 
@@ -153,9 +153,19 @@ v0.2 起新增**完全离线的语义记忆引擎**(本地模型 + 精排 +
153
153
  - **Rerank 精排**:`Xenova/bge-reranker-base` 对召回候选交叉编码精排,提升 Top-K 准确率
154
154
  - **autoDream 语义增强**:对记忆向量聚类(`clusterMemories`),自动发现主题相近 / 疑似矛盾的记忆,巩固更精准
155
155
  - **搜索流水线**:混合召回(关键词 + 向量)→ Rerank → Top-K
156
+ - **自管运行时**:本地推理的依赖闭包(`@huggingface/transformers` + `onnxruntime-node` + `sharp`,从宿主收编时本机实测 49 个包 / 约 393MB)可收编到 `~/.dsh/mneme/runtime/`,与 profile 的依赖图解耦——由于 profile 是所有插件共用的依赖图,这份闭包留在此前的位置会让「安装任何插件」都替它重走一遍整条依赖链;收编优先硬链接,同盘时几乎不额外占盘。`@huggingface/transformers` 已从 `dependencies` 降为**可选 peer**,因此安装插件不再携带它;正在使用本地嵌入的用户请先按文档收编,否则升级后本地嵌入不可用(读写信道不受影响)
157
+ - **取回运行时的三条来源**:① 收编本机已有(零网络、同盘硬链接)→ ② 本地 `.tgz` 目录(`runtimeTarballDir`,某个包网络下不到时用)→ ③ npm registry(`runtimeMirror` 可换镜像),按随包发布的 `runtime-manifest.json` 逐个取并**先校验 sha512 再落盘**(win32-x64 实测约 33 个包 / 2142 个文件 / 数百 MB;清单平台无关,一份覆盖 win32/darwin/linux × x64/arm64;剔除 `onnxruntime-web`——Node 构建从不 import 它)。面板「向量索引」卡片在不就绪时会给出这段代价说明并提供一个按钮;不想开面板也可以让 agent 用 `memory_runtime` 工具(`status` / `provision` / `verify`)代做
156
158
 
157
159
  配置只需在 `cordis.patch.yml` 里设置 `embedProvider`(默认 `openai`,保持 v0.1 行为;改为 `local` 即离线)。升级无需迁移数据。
158
160
 
161
+ ```bash
162
+ node scripts/mneme-runtime.mjs status # 运行时在哪、来源、结构是否完整(不加载模型)
163
+ node scripts/mneme-runtime.mjs adopt --from ~/.dsh/profiles/<profile>/node_modules
164
+ node scripts/mneme-runtime.mjs verify # 真加载运行时并跑一次推理
165
+ ```
166
+
167
+ > 三个命令的退出码为 `0` 健康 / `1` 不健康 / `2` 用法错误,便于脚本 gate;`--json` 输出机器可读结果。运行时不可用时不会崩——检索降级为关键词/BM25,读写不受影响。详见 [本地模型部署指南 §2.5](docs/LOCAL_MODEL.md)。
168
+
159
169
  ### 实体结构化记忆(Entity Gene)🧬
160
170
 
161
171
  v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带时间轴的属性**、**实体间关系**,让搜索从"字面关键词"升级为"按实体/属性精确召回"。
@@ -176,9 +186,9 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
176
186
 
177
187
  - `score ≥ 60`:正常存储
178
188
  - `30 ≤ score < 60`:`quality_score` 落库,注入排序改为按 `importance × quality/100` 降权(degraded)
179
- - `score < 30`:归档并标记 `low_quality`——仍可显式搜索召回,只是**永不自动注入**
189
+ - `score < 30`:归档并标记 `low_quality`——仍可显式搜索召回,只是**永不自动注入**。**例外(#135)**:`importance ≥ memoryQualityFilter.exemptImportance`(默认 4)的记忆只降权不归档——评分与信号标签照常落库,归档决定不再静默越过用户标注的重要性
180
190
 
181
- `memoryQualityFilter.enabled` 可整体关闭,`archiveThreshold` / `degradeThreshold` / `minContentLength` 可调。
191
+ `memoryQualityFilter.enabled` 可整体关闭,`archiveThreshold` / `degradeThreshold` / `minContentLength` / `exemptImportance`(1=全部豁免,5=仅最重要豁免)可调。此外更新记忆(`memory_update` 等)不会抹掉系统信号标签(`low_quality` / `duplicate` / `meta` / `self_referential` / `repetitive` / `short_content`)——它们是「为什么被降权/归档」的审计线索,与用户自传标签并集保留(#135)。
182
192
 
183
193
  ### LLM 消耗审计 📊(v0.4.6,默认开)
184
194
 
@@ -215,7 +225,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
215
225
 
216
226
  | 版本 | 亮点 |
217
227
  |------|------|
218
- | **v0.7.31** | peerDependencies 宿主版本声明修复(#121):原 `^0.1.0-rc.6` 按 node-semver 预发布元组规则不匹配 `0.1.5-rc.1` 等中间预发布版本(当前 dsh 用户安装会 ERESOLVE),也违反 awesome-dsh-plugin 的 peer-range 预发布分支规范;改显式三段式预发布分支 `>=0.1.0-rc.6 <0.2.0 \|\| >=0.1.5-rc.0 <0.3.0 \|\| >=0.2.0-rc.0 <0.3.0`——覆盖全部已发布 0.1.x(含 `0.1.5-rc.1`)、0.2 预发布留显式分支、0.3+ 待验证后放开;745 测试全绿 |
228
+ | **v0.7.32** | 冲突动作集扩展 + 运行时自管化 + 记忆/提示语言 + 候选集 hybrid 档 + 一批修复:`sleepActionSet` 新增 `full` 档(supersede/differentiate/update/merge/conflict/keep 六分支,默认 `conflict` 零行为变化)——supersede 赢家正文干净、输家归档附「已被取代」注记,differentiate 双留+差异注记;sleep 校验接入 `dreamSkipInvalid` 宽容策略(默认 true,一票否决变宽容,严格可关);运行时三档取件(收编/本地 .tgz/registry)+ transformers 转可选 peer(#131);`memory.language`(zh/en)提示/注入/镜像语言可选(#124);summarize 节流 + 产出上限 + 同会话去重(#127);inject 正文读取修复(#129)、#135 五连修(前缀解析/effort 最低档/importance 豁免/ready 语义/覆盖度降级告警)、boot 回填指纹短路修复(#128)、degraded 轮跳过明细落库(#104);916 测试全绿、总覆盖 92.7%(runtime 96%、embedding 100%) |
219
229
  | **v0.7.30** | 状态页向量卡修复(issue #118):改读开放的 `/semantic`——不再吃 `/vector-config` 的 401「加载失败」,ollama/local 模式不再恒显「未启用」;新增「初始化中(embedder 不可达,正在重试)」与「已索引 N / M 条」回填进度显示;legacy OpenAI 兼容 embedder「Object · 0D」显示修复(embedder 显式 `name: "OpenAI"` + 维度回退 `index.dimension`);「已索引 N / M」分子分母同口径(`embeddedCount` 剔除归档/遗忘,与 `count()` 默认过滤对齐);embedder init 失败有界重试(共 5 次,不再一次性永久降级,unload 清理定时器)+ `OllamaEmbedder.ready` 生命周期位 + `/semantic` 新增 `ready` 字段;745 测试全绿 |
220
230
  | **v0.7.29** | 实体抽取路由契约修复 + 面板控件 + 帮助与反馈:`streamEntityText` 抽取为可测试导出的 `createEntityStreamAdapter`——显式 `provider`/`model` 优先、缺省兜底读默认路由选择;实体抽取思考强度 `entityExtractionReasoning` 被模型拒绝时自动去 effort 重试(与 autoDream/sleep 同一降级策略);设置面板新增实体抽取 `provider`/`model`/思考强度三控件;面板底部新增「帮助与反馈」卡片(GitHub issue 预填 + mailto `work@modusensus.space` + 浏览已知问题)+ 配套 `GET /api/dsh-mneme/info`;744 测试全绿 |
221
231
  | **v0.7.28** | 连通性测试三连修:`POST /api/dsh-mneme/test-model` 最小调用 `maxTokens` 16→1024——思考模型的推理过程足以耗尽 16 token 预算致 `reply` 恒为空(「真的答了 ok 而非只报通」对思考模型不成立;按实际用量计费,手动按钮无放大成本);连通性测试状态按巩固/睡眠路由各持一份——此前共享单份,点任一「测试连通性」按钮两组同显「测试中/结果」且互相禁用;面板挂载草稿补 `sleepProvider`/`sleepModel`——后端一直保存正常,但重挂载(切页签/退出重进)后下拉恒显「跟随默认路由」,看起来像设置被重置,补键后正常回填(历史已存值无需重填);733 测试全绿 |
@@ -319,6 +329,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
319
329
  | **v0.7.29** | ✅ 完成 | 实体抽取路由契约修复 + 面板控件 + 反馈入口 | 实体抽取 LLM 路由契约修复(#108/#109):`createEntityStreamAdapter` 显式 provider/model 优先 + 兜底默认路由 + effort 拒绝自动去重试;设置面板新增实体抽取 provider/model/思考强度三控件;「帮助与反馈」卡片(GitHub issue 预填 + mailto + 浏览已知问题)+ `GET /api/dsh-mneme/info`;744 测试全绿 |
320
330
  | **v0.7.30** | ✅ 完成 | 状态页向量卡修复 + embedder 有界重试 | 卡片改读 `/semantic`(不再 401「加载失败」、ollama/local 不再恒显「未启用」)+「初始化中 / 已索引 N / M 条」显示(issue #118);legacy OpenAI 兼容 embedder 显式 `name: "OpenAI"` + 维度回退 `index.dimension`(「Object · 0D」修复);`embeddedCount` 对齐 `count()` 默认口径、剔除归档/遗忘(「已索引 269 / 36」修复);embedder init 失败 5 次有界重试不再一次性永久降级 + `OllamaEmbedder.ready` + `/semantic` `ready` 字段;745 测试全绿 |
321
331
  | **v0.7.31** | ✅ 完成 | peerDependencies 宿主版本声明修复 | 原 `^0.1.0-rc.6` 按 node-semver 预发布元组规则不匹配 `0.1.5-rc.1` 等中间预发布版本(当前 dsh 用户安装 ERESOLVE),也违反 awesome-dsh-plugin 的 peer-range 预发布分支规范;改显式三段式预发布分支(覆盖 0.1.0-rc.6 至 0.1.5-rc.1 全部已发布 0.1.x 含预发布 + 0.2 预发布留显式分支 + 0.3+ 待验证后放开);745 测试全绿 |
332
+ | **v0.7.32** | ✅ 完成 | sleep 冲突动作集扩展 + 运行时自管化 + 候选集 hybrid 档 + 一批修复 | `sleepActionSet` 新增 `full` 档(supersede/differentiate/update/merge/conflict/keep 六分支,默认 `conflict` 零行为变化)+ sleep 校验接入 `dreamSkipInvalid` 宽容策略 + 被跳决策带 phase 落库 `dream_runs.skipped`;运行时三档取件 + transformers 转可选 peer(#131);`memory.language` 提示/注入/镜像语言可选(#124);候选集向量驱动 hybrid 档(#125,PR #147);summarize 节流 + 产出上限 + 同会话去重(#127);inject 正文读取修复(#129)+ #135 五连修 + boot 回填指纹短路修复(#128)+ degraded 轮跳过明细落库(#104);runtime 覆盖统计归一 + 补齐(总覆盖 92.7%);916 测试全绿 |
322
333
  | **v0.8.0** | 🚧 计划中(9 月末) | 图谱增强 | 兴趣漂移可视化 + scope 隔离(issue #17)+ 跨 workspace 记忆共享 |
323
334
 
324
335
  > 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
@@ -364,6 +375,7 @@ dsh web
364
375
  name: '@modusensus/dsh-mneme'
365
376
  config:
366
377
  memoryDir: ~/.dsh/memory
378
+ language: zh
367
379
  autoInject: true
368
380
  autoSummarize: true
369
381
  maxInjectedItems: 5
@@ -379,6 +391,7 @@ dsh web
379
391
  | 键 | 默认值 | 说明 |
380
392
  |----|--------|------|
381
393
  | `memoryDir` | `~/.dsh/memory` | 记忆存储目录(SQLite + Markdown) |
394
+ | `language` | `zh` | 记忆语言:生成的记忆条目、注入标题与后台 LLM 提示词所用语言(`zh` / `en`) |
382
395
  | `autoInject` | `true` | 会话启动自动注入记忆 |
383
396
  | `autoSummarize` | `true` | 会话结束自动提炼摘要 |
384
397
  | `summarizeProvider` / `summarizeModel` | 空 | 摘要的 LLM 路由覆盖(空=使用当前会话模型);推荐轻量模型节省主模型 token |
@@ -391,6 +404,9 @@ dsh web
391
404
  | `dreamProvider` / `dreamModel` | 空 | dream 的 LLM 路由覆盖(显式配置优先于 agent 默认模型;留空则回退到 agent 默认模型) |
392
405
  | `dreamMaxTokens` | `32768` | dream LLM 调用最大 token 数(上限 131072;思考型模型的 reasoning 与正文共享该预算,正文为空时优先调大,见下方调优指南) |
393
406
  | `dreamReasoningEffort` | `none` | dream LLM 推理强度透传:`low` / `medium` / `high` / `none`(`none`=不传该字段,沿用模型默认;思考型模型(如 deepseek-v4-flash)想压低思考可设 `low`;v0.7.26+ 模型不支持配置档位时自动换用其支持的默认/首个档位,无 reasoning 能力的型号省略字段) |
407
+ | `dreamCandidateMode` | `window` | dream 候选集构造:`window`(只取最近 `dreamMaxSnapshotSize` 条)/ `hybrid`(在此基础上并入向量翻出的高相似组)。纯时间窗口下,实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口——该合并的一对几乎永远碰不到面。**已知边界**:dream 侧动作集仍是五分支(keep / merge / archive / update / conflict),hybrid 翻出的互补型 / 演进型对在 dream 里没有 `differentiate` / `supersede` 出口;正确出口在 sleep 侧(`sleepActionSet: full`)|
408
+ | `dreamCandidateMax` | `0` | hybrid 的候选总量上限;`0` = 复用 `dreamMaxSnapshotSize`。候选总量与库总量解耦,输入成本不随库增长 |
409
+ | `dreamCandidateMinSim` | `0.85` | hybrid 判「高相似」的阈值(与 sleep 的 normal 档对齐,两个模块共用同一个「高相似」定义) |
394
410
  | `apiToken` | 空 | 可选 API 鉴权 token;设置后写操作与密钥接口要求 `Authorization: Bearer <apiToken>` |
395
411
  | `embedProvider` | `openai` | 语义后端:`openai`(默认,兼容 v0.1)/ `local`(ONNX 离线)/ `ollama` |
396
412
  | `localEmbedModel` | `Xenova/bge-small-zh-v1.5` | 本地 ONNX embedding 模型 |
@@ -422,7 +438,7 @@ dsh web
422
438
  | `entitySearchEnabled` | `true` | `entity:` / `attr:` 前缀搜索开关 |
423
439
  | `trustEpistemicWeighting` | `false` | 记忆可信度加权(v0.4.5,opt-in 默认关):记忆按来源分级 `observation`> `inferred` > `subjective`,开启后检索排序优先高可信记忆、注入对 observation 标注 `[verified]`、dream merge/conflict 偏向高可信一方;关闭时 `epistemic_status` 仅随保存落库、不参与行为 |
424
440
  | `evalPersistTestResults` | `false` | 检索评估落库(v0.4.5,opt-in 默认关):开启后 `evaluateRetrieval` 把 precision/recall/mrr 快照写入 `recall_evals`;默认关时仅返回调用方不落库。生产 `searchMemories` 审计始终走 `recall_runs`,无条件不触碰 `recall_evals` |
425
- | `autoReindexOnBoot` | `true` | 存量记忆缺 embedding 时,向量已配置则启动后延迟后台按批次限速自动回填重建(设为 `false` 仅手动重建) |
441
+ | `autoReindexOnBoot` | `true` | 存量记忆缺 embedding 时,向量已配置则启动后延迟后台按批次限速自动回填重建(设为 `false` 仅手动重建)。只补**活跃**记忆(归档/遗忘行不参与召回,#128);模型指纹一致时照常补缺失行、不再短路(#128) |
426
442
  | `hybridInject` | `true` | 注入语义召回优先(v0.4.6,Bug4):`injectCandidates` 带非空 query 时先走向量索引语义召回候选,规则筛选补足/去重;空 query / 无向量回退旧逻辑 |
427
443
  | `bm25SearchEnabled` | `true` | BM25 稀疏第三路召回(v0.5.0):ASCII 词元 + CJK bigram,IDF 加权,散词/ID/代码片段查询不再依赖子串命中 |
428
444
  | `adaptiveThresholdEnabled` | `true` | 自适应相似度阈值(v0.5.0):按查询形态动态截断(前缀 0.5 / 短查询 0.7 / 长查询 0.6 / 头部分差大放宽 0.5),显式传 `threshold` 走旧行为 |
@@ -438,7 +454,7 @@ dsh web
438
454
  | `selectiveInjectEnabled` | `true` | 选择性注入(v0.5.0):query 向量可用时注入候选按主题相似度重排,替代固定规则序 |
439
455
  | `searchSemanticDedup` | `false` | 搜索时语义去重(v0.5.0,激进选项默认关):embedding 余弦 ≥0.95 近重复行在 Rerank 前丢弃 |
440
456
  | `searchSemanticDedupThreshold` | `0.95` | 语义去重相似度阈值(v0.5.0,默认 0.95,范围 0.5-1.0):`searchSemanticDedup=true` 时生效,调整可防小模型误折叠 |
441
- | `memoryQualityFilter` | `{enabled:true, archiveThreshold:30, degradeThreshold:60, minContentLength:10}` | 记忆质量过滤(v0.4.6,默认开):写库前启发式打分 0-100,元记忆词汇/自指/过短/重复/近似重复扣分;≥60 正常存储,30-60 降权(注入排序按 importance×quality/100),<30 归档标记 `low_quality`(显式搜索仍可召回,永不自动注入) |
457
+ | `memoryQualityFilter` | `{enabled:true, archiveThreshold:30, degradeThreshold:60, minContentLength:10, exemptImportance:4}` | 记忆质量过滤(v0.4.6,默认开):写库前启发式打分 0-100,元记忆词汇/自指/过短/重复/近似重复扣分;≥60 正常存储,30-60 降权(注入排序按 importance×quality/100),<30 归档标记 `low_quality`(显式搜索仍可召回,永不自动注入;`exemptImportance` 豁免:importance ≥ 该值只降权不归档,#135) |
442
458
  | `llmAudit` | `{enabled:true, retentionDays:90}` | LLM 消耗审计(v0.4.6,默认开):每次后台 LLM 调用(autoDream/autoSummarize)写 `llm_audit_logs`(tokens/duration/status/source);失败记 error 不阻塞;只读 API `/api/dsh-mneme/semantic/llm-audit` + `/llm-audit/stats` |
443
459
 
444
460
  > 🔐 **API 安全**:DSH 无内置鉴权且默认仅监听 `127.0.0.1`。插件 API 默认开放(便于 Web 面板即装即用)。如需防护(如局域网暴露),在配置中设置 `apiToken`:写操作(画像/规则/命令)与密钥端点(`vector-config`、`vector-reindex`)需携带 `Authorization: Bearer <token>`(前端设置面板可填入同一 token),只读的 `list` / `search` / `semantic` 保持开放。`/api/dsh-mneme/vector-config` 返回的 `apiKey` 已掩码(`sk-***…`),存储仍保留明文供调用;前端回传空或掩码值表示"不改 key"。
package/lib/api.js CHANGED
@@ -3,8 +3,11 @@ import { readFileSync } from "node:fs";
3
3
  import { timingSafeEqual, randomBytes } from "node:crypto";
4
4
  import { FEATURE_FLAG_SPEC } from "./settings.js";
5
5
  import { TYPE_FILE, renderMirrorText, parseHumanEdits } from "./mirror.js";
6
+ import { langOf } from "./lang.js";
6
7
  import { computeHeat } from "./heat.js";
7
8
  import { describeStreamFailure, resolveRoute } from "./dream.js";
9
+ import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
10
+ import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
8
11
 
9
12
  // headers:少数端点(/export 附件下载)需要追加 Content-Disposition 等响应头。
10
13
  function sendJson(res, status, payload, headers = {}) {
@@ -113,6 +116,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
113
116
  const flagKeys = [
114
117
  ...FEATURE_FLAG_SPEC.booleans,
115
118
  ...Object.keys(FEATURE_FLAG_SPEC.ints),
119
+ ...Object.keys(FEATURE_FLAG_SPEC.numbers),
116
120
  ...FEATURE_FLAG_SPEC.strings,
117
121
  ...FEATURE_FLAG_SPEC.urls,
118
122
  ...Object.keys(FEATURE_FLAG_SPEC.enums)
@@ -604,18 +608,87 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
604
608
  handler(req, res) {
605
609
  try {
606
610
  const stats = semantic?.vectorIndex?.getStats?.() ?? null;
611
+ // #118: expose readiness so the status card can tell "initializing /
612
+ // unreachable" from "disabled". An embedder carrying no `ready` prop at
613
+ // all (test doubles, third-party adapters) is assumed usable.
614
+ // issue #135: 但「可用」和「配了」是两件事 —— legacy OpenAI embedder
615
+ // 此前恒报 ready,于是 baseUrl/apiKey/model 全空时状态卡依然是绿的
616
+ // ("绿色假阳性")。下面把两者分开,并附上索引覆盖度。
617
+ const ready = embedder ? ("ready" in embedder ? embedder.ready === true : true) : null;
618
+ // local/ollama 的配置来自 settings 兜底,没有「未配置」态:它们的
619
+ // ready=false 表示「还在初始化」,不是「没配」。
620
+ const configured = embedder ? (typeof embedder.configured === "boolean" ? embedder.configured : true) : null;
621
+ const embedded = Number(stats?.embeddedCount ?? 0);
622
+ const total = Number(stats?.totalCount ?? 0);
607
623
  sendJson(res, 200, {
608
- // #118: expose readiness so the status card can tell "initializing /
609
- // unreachable" from "disabled". Legacy OpenAI embedder has no `ready`
610
- // prop and is immediately usable, so treat that as ready.
611
- ready: embedder ? ("ready" in embedder ? embedder.ready === true : true) : null,
624
+ ready,
625
+ configured,
626
+ // 机器可读的原因;null = 一切正常。面板按它选文案,不用猜布尔组合。
627
+ reason: !embedder ? "no-embedder"
628
+ : configured === false ? "not-configured"
629
+ : ready !== true ? "initializing"
630
+ : null,
631
+ // 空库时 ratio 必须是 null 而非 0 —— 0 会被读成「一条都没嵌上」。
632
+ coverage: { embedded, total, ratio: total > 0 ? embedded / total : null },
633
+ // 配了、库里有东西可嵌、却一条都没嵌上 = 真的降级(本机此前长期
634
+ // 处于 ready:true + embeddedCount:0,面板却显示正常)。
635
+ degraded: configured === true && total > 0 && embedded === 0,
612
636
  // Explicit display name first: the legacy OpenAI-compatible embedder
613
637
  // is an object literal, so constructor.name would be "Object".
614
638
  embedProvider: embedder ? (embedder.name ?? embedder.constructor?.name ?? "unknown") : null,
615
639
  modelHash: embedder?.modelHash ?? null,
616
640
  dimension: embedder?.dimension ?? null,
617
641
  reranker: semantic?.reranker ? "ready" : null,
618
- index: stats
642
+ index: stats,
643
+ // issue #131 / PR-A:自管运行时的状态。无论当前 provider 是不是 local
644
+ // 都报 —— 面板需要区分「已收编但没启用」和「要用本地却没收编」。
645
+ // 这里只做结构检查(不跑推理),失败降级成状态对象,不会让本端点 500。
646
+ // 必须经 publicRuntimeStatus 投影:本端点是免鉴权的,绝对路径与原始错误
647
+ // 文本不能出去(CWE-200)。完整诊断走本机 CLI 的 status。
648
+ localRuntime: publicRuntimeStatus(describeLocalRuntime({ runtimeDir: config?.runtimeDir ?? "" }))
649
+ });
650
+ } catch {
651
+ sendJson(res, 500, { error: "internal" });
652
+ }
653
+ }
654
+ });
655
+
656
+ // --- 一键取回自管运行时(issue #131 / PR-C:三档来源)-------------------------
657
+ // 写路径(requireAuth):它会往 ~/.dsh/mneme/runtime/ 落一份依赖闭包。
658
+ //
659
+ // 第一档源固定取本 profile 的 node_modules(由插件自身路径推出),不接受客户端传源目录 ——
660
+ // 「从任意目录往运行时目录里搬东西」没有必要成为对外能力。
661
+ //
662
+ // 一律回 200:请求本身合法,成不成看 body 里的 ok/status/reason。让 HTTP 码只表达
663
+ // 「有没有鉴权/路径对不对」,结论交给 ok —— 面板要能显示「收编了但闭包缺件」这种
664
+ // 中间状态,用一个 4xx 概括会把信息压掉。
665
+ register({
666
+ kind: "exact",
667
+ path: "/api/dsh-mneme/runtime/provision",
668
+ handler(req, res) {
669
+ try {
670
+ if (req.method !== "POST") {
671
+ sendJson(res, 405, { error: "method-not-allowed" });
672
+ return;
673
+ }
674
+ if (!requireAuth(req, res, apiToken)) return;
675
+ return readBody(req).then(async (text) => {
676
+ const body = parseBody(text);
677
+ const runtimeDir = config?.runtimeDir ?? "";
678
+ // 三档来源依次尝试(收编本机 node_modules → 本地 .tgz 目录 → registry)。
679
+ // 所以这个端点会**联网**:它可能拉一份几十到上百 MB 的闭包。
680
+ const result = await provisionRuntime({
681
+ hostModulesDir: hostModulesDir(import.meta.url),
682
+ runtimeDir,
683
+ localTarballDir: config?.runtimeTarballDir ?? "",
684
+ mirror: config?.runtimeMirror ?? "",
685
+ overwrite: body?.overwrite === true
686
+ });
687
+ // 顺手带回最新状态,面板不必再打一次 /semantic(且它走的是免鉴权投影)。
688
+ sendJson(res, 200, {
689
+ ...result,
690
+ localRuntime: publicRuntimeStatus(describeLocalRuntime({ runtimeDir }))
691
+ });
619
692
  });
620
693
  } catch {
621
694
  sendJson(res, 500, { error: "internal" });
@@ -853,7 +926,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
853
926
  }
854
927
  const sections = [];
855
928
  for (const type of Object.keys(TYPE_FILE)) {
856
- if (byType[type]?.length) sections.push(renderMirrorText(type, byType[type]));
929
+ if (byType[type]?.length) sections.push(renderMirrorText(type, byType[type], langOf(config)));
857
930
  }
858
931
  sendAttachment(res, 200, "text/markdown; charset=utf-8", `dsh-mneme-export-${stamp}.md`, sections.join("\n"));
859
932
  } catch {
package/lib/client.js CHANGED
@@ -427,7 +427,24 @@ window.__ModuleLoader__.load({
427
427
  "memory.status.vectorOff": "未启用",
428
428
  "memory.status.vectorInit": "初始化中",
429
429
  "memory.status.vectorInitHint": "embedder 不可达,正在重试",
430
+ "memory.status.vectorRuntimeMissing": "缺少本地推理运行时",
431
+ "memory.status.vectorRuntimeHint": "本地推理运行时未就绪({status})",
432
+ "memory.status.vectorRuntimeCost": "本地向量化要额外一份本地推理运行时(transformers + onnxruntime 闭包,解包后数百 MB)。下面的按钮会先尝试收编本机已有的那份(同盘则硬链接,不占额外空间),没有再按固定清单从 npm 取回(逐个校验 sha512)。也可以直接让 agent 处理,或在终端里自己跑:node scripts/mneme-runtime.mjs status / adopt [--from <node_modules>] / verify(详见 docs/LOCAL_MODEL.md §2.5)。",
433
+ "memory.runtime.title": "本地推理运行时",
434
+ "memory.runtime.available": "已就绪",
435
+ "memory.runtime.missing": "未就绪 —— 只有用本地嵌入(embedProvider: local)时才需要它",
436
+ "memory.runtime.restart": "本地嵌入要重启 DSH 才会生效",
437
+ "memory.status.vectorRuntimeReady": "运行时已就绪",
438
+ "memory.status.vectorRuntimeReadyHint": "本地嵌入尚未初始化(进程里的 embedder 在运行时缺失时已经失败过一次):若一直停在这里,重启 DSH 即可。",
439
+ "memory.status.vectorRuntimeFetch": "取回本地运行时",
440
+ "memory.status.vectorRuntimeFetchBusy": "正在取回…(先试收编,必要时下载,数千个文件,请稍候)",
441
+ "memory.status.vectorRuntimeFetchAdopted": "已收编本机已有的运行时:{n} 个包 / {m} 个文件({mode})。**请重启 DSH 使本地嵌入生效。**",
442
+ "memory.status.vectorRuntimeFetchDownloaded": "下载完成:{n} 个包 / {m} 个文件。**请重启 DSH 使本地嵌入生效**(取回不会让当前进程里的 embedder 复活)。",
443
+ "memory.status.vectorRuntimeFetchFailed": "取回失败:{reason}",
430
444
  "memory.status.vectorIndexed": "已索引 {n} / {m} 条",
445
+ "memory.status.vectorUnconfigured": "未配置",
446
+ "memory.status.vectorUnconfiguredHint": "未填 embedding 端点/模型或未启用,语义召回不可用",
447
+ "memory.status.vectorDegradedHint": "已索引 0 / {m} 条,语义召回实际不可用",
431
448
  "memory.status.llm": "LLM 消耗",
432
449
  "memory.status.llmCalls": "近 7 天 · {n} 次调用",
433
450
  "memory.status.error": "加载失败"
@@ -732,7 +749,24 @@ window.__ModuleLoader__.load({
732
749
  "memory.status.vectorOff": "Disabled",
733
750
  "memory.status.vectorInit": "Initializing",
734
751
  "memory.status.vectorInitHint": "embedder unreachable, retrying",
752
+ "memory.status.vectorRuntimeMissing": "Local inference runtime missing",
753
+ "memory.status.vectorRuntimeHint": "Local inference runtime not ready ({status})",
754
+ "memory.status.vectorRuntimeCost": "Local vectorization needs an extra local inference runtime (the transformers + onnxruntime closure, hundreds of MB unpacked). The button below first tries to adopt an existing copy on this machine (hardlinked when on the same volume, so no extra disk), and otherwise fetches it from npm against a pinned manifest (every tarball checked against its sha512). You can also just ask the agent, or run it yourself: node scripts/mneme-runtime.mjs status / adopt [--from <node_modules>] / verify (see docs/LOCAL_MODEL.md §2.5).",
755
+ "memory.runtime.title": "Local inference runtime",
756
+ "memory.runtime.available": "ready",
757
+ "memory.runtime.missing": "not ready — only needed when you use local embedding (embedProvider: local)",
758
+ "memory.runtime.restart": "Local embedding takes effect after restarting DSH",
759
+ "memory.status.vectorRuntimeReady": "Runtime ready",
760
+ "memory.status.vectorRuntimeReadyHint": "Local embedder is not initialized yet (the in-process embedder already failed while the runtime was missing): restart DSH if this persists.",
761
+ "memory.status.vectorRuntimeFetch": "Fetch local runtime",
762
+ "memory.status.vectorRuntimeFetchBusy": "Fetching… (tries adopt first, may download — thousands of files)",
763
+ "memory.status.vectorRuntimeFetchAdopted": "Adopted the runtime already on this machine: {n} packages / {m} files ({mode}). **Restart DSH to activate local embedding.**",
764
+ "memory.status.vectorRuntimeFetchDownloaded": "Download complete: {n} packages / {m} files. **Restart DSH to activate local embedding** (fetching does not revive the embedder already running in this process).",
765
+ "memory.status.vectorRuntimeFetchFailed": "Fetch failed: {reason}",
735
766
  "memory.status.vectorIndexed": "Indexed {n} / {m} items",
767
+ "memory.status.vectorUnconfigured": "Not configured",
768
+ "memory.status.vectorUnconfiguredHint": "No embedding endpoint/model configured — semantic recall is off",
769
+ "memory.status.vectorDegradedHint": "Indexed 0 / {m} items — semantic recall is effectively unavailable",
736
770
  "memory.status.llm": "LLM Usage",
737
771
  "memory.status.llmCalls": "Last 7 days · {n} calls",
738
772
  "memory.status.error": "Failed to load"
@@ -957,10 +991,9 @@ window.__ModuleLoader__.load({
957
991
  ".mneme-heat--warm{color:var(--dsw-alias-label-secondary)}",
958
992
  ".mneme-heat--cold{color:var(--dsw-alias-label-tertiary)}",
959
993
  ".mneme-heat--cold .mneme-heatpct{display:none}",
960
- // width:100% 与「新会话」同宽(同一弹性父容器);文案对齐交给复制来的
961
- // 原生类,不再用 flex:1/text-align:left 覆盖成左对齐。
962
- ".mneme-topentry{display:contents}",
963
- ".mneme-topentry-native{width:100%;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
994
+ // 纵向 flex 容器按原生按钮的宽度与外边距分配空间,保留展开和收起态的对齐。
995
+ ".mneme-topentry{display:flex;flex-direction:column}",
996
+ ".mneme-topentry-native{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
964
997
  ".mneme-topentry-native:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
965
998
  ".mneme-topentry-native .mneme-topentry-label{white-space:nowrap}",
966
999
  // --- 功能开关:一行一开关,Claude 式安静排版 ---
@@ -987,6 +1020,8 @@ window.__ModuleLoader__.load({
987
1020
  ".mneme-featsub{margin:2px 0 8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;display:flex;flex-direction:column;gap:6px}",
988
1021
  ".mneme-featsub .mneme-featnum{padding:6px 0;border-bottom:none}",
989
1022
  ".mneme-featsubhint{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}",
1023
+ // 运行时那一块的说明文字:比常规 hint 再小一档,它是背景信息而不是要读的正文。
1024
+ ".mneme-runtimehint{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
990
1025
  // --- 交互式记忆库:工具栏 / 卡片网格 / 详情抽屉 / 更多菜单 ---
991
1026
  // sheet 边距放宽:与窗口边缘保持呼吸距离,居中不顶满。
992
1027
  // 视图切换(卡片/时间线)分段控件挂在子页栏右侧。
@@ -1891,6 +1926,7 @@ window.__ModuleLoader__.load({
1891
1926
  savedTick && h("span", { className: "mneme-saved", style: { marginLeft: 8, fontWeight: 400 } }, t("memory.features.restartHint"))
1892
1927
  ),
1893
1928
  h("div", { className: "mneme-set-desc" }, t("memory.features.desc")),
1929
+ h(RuntimeProvisionBlock, { t }),
1894
1930
  state === null && !error
1895
1931
  ? h("div", { className: "mneme-set-hint" }, "…")
1896
1932
  : h(react.Fragment, null,
@@ -2586,8 +2622,89 @@ window.__ModuleLoader__.load({
2586
2622
  }
2587
2623
 
2588
2624
  // 向量索引 — whether semantic recall is switched on.
2625
+ // 取回本地推理运行时的共享块:**面板状态卡片与设置页共用同一份实现**。
2626
+ //
2627
+ // 为什么设置页也要有:状态卡片是「看到了顺手点」,而设置页是用户主动去找的地方。
2628
+ // 两处都给出代价说明与可复制的命令行,并且在**成功后明确要求重启** —— 取回运行时不会让
2629
+ // 当前进程里那个已经失败的 embedder 复活,重启才生效;不说清这一点,用户会以为功能坏了。
2630
+ function RuntimeProvisionBlock({ t, onChanged }) {
2631
+ const [runtime, setRuntime] = useState(null);
2632
+ const [ready, setReady] = useState(null);
2633
+ const [busy, setBusy] = useState(false);
2634
+ const [msg, setMsg] = useState("");
2635
+ const [tick, setTick] = useState(0);
2636
+
2637
+ useEffect(() => {
2638
+ let cancelled = false;
2639
+ apiFetch("/api/dsh-mneme/semantic")
2640
+ .then((res) => (res.ok ? res.json() : null))
2641
+ .then((j) => {
2642
+ if (cancelled || !j) return;
2643
+ setRuntime(j.localRuntime ?? null);
2644
+ setReady(j.ready ?? null);
2645
+ })
2646
+ .catch(() => {});
2647
+ return () => { cancelled = true; };
2648
+ }, [tick]);
2649
+
2650
+ const available = runtime?.status === "available";
2651
+ // 运行时就绪但 embedder 没起来 = 刚取回到、还没重启。这时唯一有意义的动作就是重启。
2652
+ const needsRestart = available && ready !== true;
2653
+
2654
+ const provision = async () => {
2655
+ setBusy(true);
2656
+ setMsg("");
2657
+ try {
2658
+ const res = await apiFetch("/api/dsh-mneme/runtime/provision", {
2659
+ method: "POST",
2660
+ headers: { "Content-Type": "application/json" },
2661
+ body: JSON.stringify({ overwrite: runtime?.status === "broken" })
2662
+ });
2663
+ if (!res.ok) throw new Error("HTTP " + res.status);
2664
+ const j = await res.json();
2665
+ if (j?.ok) {
2666
+ const key = j.strategy === "download"
2667
+ ? "memory.status.vectorRuntimeFetchDownloaded"
2668
+ : "memory.status.vectorRuntimeFetchAdopted";
2669
+ setMsg(t(key).replace("{n}", j.packages ?? 0).replace("{m}", j.files ?? 0).replace("{mode}", j.materialize ?? "?"));
2670
+ } else {
2671
+ setMsg(t("memory.status.vectorRuntimeFetchFailed").replace("{reason}", j?.reason ?? j?.status ?? "?"));
2672
+ }
2673
+ setTick((n) => n + 1);
2674
+ if (onChanged) onChanged();
2675
+ } catch (error) {
2676
+ setMsg(t("memory.status.vectorRuntimeFetchFailed").replace("{reason}", String((error && error.message) || error)));
2677
+ } finally {
2678
+ setBusy(false);
2679
+ }
2680
+ };
2681
+
2682
+ // 配好之后就把整块收起来:设置页只在「需要用户知道或行动」时占地方,配好了就是纯噪音。
2683
+ // (msg 例外:刚点完按钮要看结果,那一刻 ready 还没变,本来也不会命中这条。)
2684
+ if (available && ready === true && msg === "") return null;
2685
+
2686
+ return h(react.Fragment, null,
2687
+ h("div", { className: "mneme-runtimehint" },
2688
+ t("memory.runtime.title") + ":" + (available ? t("memory.runtime.available") : t("memory.runtime.missing"))),
2689
+ // 未就绪时才给代价说明与按钮;已就绪只留上面那行状态(以及在需要重启时下面那行提醒)。
2690
+ available ? null : h(react.Fragment, null,
2691
+ h("div", { className: "mneme-runtimehint" }, t("memory.status.vectorRuntimeCost")),
2692
+ h("button", {
2693
+ type: "button",
2694
+ className: "mneme-footbtn",
2695
+ disabled: busy,
2696
+ onClick: provision
2697
+ }, busy ? t("memory.status.vectorRuntimeFetchBusy") : t("memory.status.vectorRuntimeFetch"))
2698
+ ),
2699
+ msg ? h("div", { className: "mneme-runtimehint" }, msg) : null,
2700
+ needsRestart ? h("div", { className: "mneme-saved" }, t("memory.runtime.restart")) : null
2701
+ );
2702
+ }
2589
2703
  function VectorStatusCard({ t }) {
2590
- const [state, setState] = useState({ loading: true, error: false, provider: null, ready: null, dimension: 0, embedded: 0, total: 0 });
2704
+ const [state, setState] = useState({ loading: true, error: false, provider: null, ready: null, configured: null, degraded: false, dimension: 0, embedded: 0, total: 0, localRuntime: null });
2705
+ // 收编是写操作、耗时数秒,所以要有忙碌态与就地结果;reload 只是用来让上面的
2706
+ // 加载 useEffect 重跑一次,从而把最新状态拉回来。
2707
+ const [reload, setReload] = useState(0);
2591
2708
  useEffect(() => {
2592
2709
  let cancelled = false;
2593
2710
  // #118: /vector-config is secret-bearing (401 without a stored token →
@@ -2603,34 +2720,77 @@ window.__ModuleLoader__.load({
2603
2720
  error: false,
2604
2721
  provider: j?.embedProvider || null,
2605
2722
  ready: j?.ready ?? null,
2723
+ // issue #135: missing on older servers → null/false,卡片退化成
2724
+ // 旧行为(只按 ready 判断),不会误报「未配置」。
2725
+ configured: typeof j?.configured === "boolean" ? j.configured : null,
2726
+ degraded: j?.degraded === true,
2606
2727
  // Legacy OpenAI embedder exposes dimension only after its first
2607
2728
  // successful embed — fall back to the index stats' dimension.
2608
2729
  dimension: Number(j?.dimension ?? j?.index?.dimension ?? 0),
2609
2730
  embedded: Number(j?.index?.embeddedCount ?? 0),
2610
- total: Number(j?.index?.totalCount ?? 0)
2731
+ total: Number(j?.index?.totalCount ?? 0),
2732
+ // issue #131: 自管运行时状态。与 provider 无关地拿来,由下面决定是否要展示。
2733
+ localRuntime: j?.localRuntime ?? null
2611
2734
  });
2612
2735
  })
2613
- .catch(() => { if (!cancelled) setState({ loading: false, error: true, provider: null, ready: null, dimension: 0, embedded: 0 }); });
2736
+ .catch(() => { if (!cancelled) setState({ loading: false, error: true, provider: null, ready: null, dimension: 0, embedded: 0, localRuntime: null }); });
2614
2737
  return () => { cancelled = true; };
2615
- }, []);
2738
+ }, [reload]);
2616
2739
  const off = !state.provider;
2617
- const pending = !off && state.ready !== true;
2740
+ // issue #135: 先把「没配」和「配了但在初始化」分开。此前两者都落到
2741
+ // ready !== true,于是未配置的 legacy embedder(ready 恒 true)反而
2742
+ // 一路显示成正常状态。
2743
+ const unconfigured = !off && state.configured === false;
2744
+ const pending = !off && !unconfigured && state.ready !== true;
2745
+ // 配好了、库里有东西、却一条都没嵌上 —— 这才是真正的降级。
2746
+ const degraded = !off && !unconfigured && !pending && state.degraded === true;
2747
+ // 本地 provider 缺运行时:绝不能显示「初始化中」——它永远不会初始化,只会一直重试,
2748
+ // 而那正是那份运行时(数百 MB)缺位的真实原因。这里必须说真话并给出下一步。
2749
+ const localBlocked = /^Local/.test(state.provider || "") && state.localRuntime?.status !== "available" && state.localRuntime != null;
2750
+ // 运行时已就绪但 embedder 还没起来 —— 这就是「刚取回运行时、但进程里的 embedder 早已失败」的过渡态。
2751
+ // 这时候继续说「embedder 不可达,正在重试」会让人以为永远好不了;要说的是可操作的那句:重启 DSH。
2752
+ const runtimeReadyPending = pending && state.localRuntime?.status === "available";
2618
2753
  const num = off
2619
2754
  ? t("memory.status.vectorOff")
2620
- : pending
2621
- ? t("memory.status.vectorInit")
2622
- : `${state.provider.replace(/Embedder$/, "")} · ${state.dimension}D`;
2623
- const cap = pending
2624
- ? t("memory.status.vectorInitHint")
2625
- : off ? "" : t("memory.status.vectorIndexed").replace("{n}", state.embedded).replace("{m}", state.total);
2626
- return h(StatusCard, {
2627
- t,
2628
- title: t("memory.status.vector"),
2629
- loading: state.loading,
2630
- error: state.error,
2631
- num,
2632
- cap
2633
- });
2755
+ : localBlocked
2756
+ ? t("memory.status.vectorRuntimeMissing")
2757
+ : unconfigured
2758
+ ? t("memory.status.vectorUnconfigured")
2759
+ : runtimeReadyPending
2760
+ ? t("memory.status.vectorRuntimeReady")
2761
+ : pending
2762
+ ? t("memory.status.vectorInit")
2763
+ : `${state.provider.replace(/Embedder$/, "")} · ${state.dimension}D`;
2764
+ const cap = localBlocked
2765
+ ? t("memory.status.vectorRuntimeHint").replace("{status}", state.localRuntime.status)
2766
+ : unconfigured
2767
+ ? t("memory.status.vectorUnconfiguredHint")
2768
+ : runtimeReadyPending
2769
+ ? t("memory.status.vectorRuntimeReadyHint")
2770
+ : pending
2771
+ ? t("memory.status.vectorInitHint")
2772
+ : off ? ""
2773
+ : degraded
2774
+ ? t("memory.status.vectorDegradedHint").replace("{m}", state.total)
2775
+ : t("memory.status.vectorIndexed").replace("{n}", state.embedded).replace("{m}", state.total);
2776
+
2777
+
2778
+ // 取回运行时由共享组件负责(设置页用的是同一份实现,见 RuntimeProvisionBlock):
2779
+ // 代价说明、三档来源、结果文案与「要重启」都在那里 —— 同一件事不该有两份代码。
2780
+ return h(react.Fragment, null,
2781
+ h(StatusCard, {
2782
+ t,
2783
+ title: t("memory.status.vector"),
2784
+ loading: state.loading,
2785
+ error: state.error,
2786
+ num,
2787
+ cap
2788
+ }),
2789
+ !state.loading && !state.error && localBlocked
2790
+ ? h("div", { className: "mneme-statuscap", style: { marginTop: "6px" } },
2791
+ h(RuntimeProvisionBlock, { t, onChanged: () => setReload((n) => n + 1) }))
2792
+ : null
2793
+ );
2634
2794
  }
2635
2795
 
2636
2796
  // LLM 消耗 — calls + tokens over the trailing 7 days.
@@ -3742,8 +3902,10 @@ window.__ModuleLoader__.load({
3742
3902
 
3743
3903
  // --- Sidebar entry: 工作区上方的记忆按钮 ---
3744
3904
  // 注册仍在 sidebar.footer.action(list 插槽)——它既是 React 树的挂载
3745
- // 锚点,也是 portal 失效时的原位回退。真实按钮 portal 插到 regionArea
3746
- // (工作区列表容器)之前,即「新会话」按钮下方、工作区上方。
3905
+ // 锚点,也是 portal 失效时的原位回退。真实按钮 portal 插到「新会话」行
3906
+ // 之后、插件入口组的最前:任务看板/技能中心等生态插件也把入口插在新会话
3907
+ // 行后(它们的后插入者在上),若我们固定在 regionArea 之前就会被打成
3908
+ // 入口组末尾的孤立位(issue #130),所以观察器发现错位就搬回锚点位。
3747
3909
  // 几何对齐方案:读取宿主「新会话」按钮的实时 className 原样套用,再用
3748
3910
  // 我们的修饰类覆盖配色(次级观感)。这样展开态与收起态(rail)都直接
3749
3911
  // 继承宿主自己的盒模型与间距——宿主改版/缩进动画零位移,无需硬编码。
@@ -3770,6 +3932,32 @@ window.__ModuleLoader__.load({
3770
3932
  const findNative = (region) =>
3771
3933
  region.parentElement.querySelector('[class*="newSession"]')
3772
3934
  || region.previousElementSibling;
3935
+ // 占位锚点:新会话行(现役外壳里 newSession 按钮嵌在 logoRow 内,
3936
+ // 旧外壳是根的直接子按钮)。判定与生态插件的 sidebar-entry-core
3937
+ // 一致;锚点不可靠时回退到 regionArea 之前的旧位置。
3938
+ const findAnchor = (region) => {
3939
+ const parent = region.parentElement;
3940
+ const btn = parent.querySelector('[class*="newSession"]');
3941
+ if (!btn) return null;
3942
+ const row = btn.closest('[class*="logoRow"]');
3943
+ if (row && row.parentElement === parent) return row;
3944
+ return btn.parentElement === parent ? btn : null;
3945
+ };
3946
+ // 占位规则:紧跟新会话行(插件入口组的顶部),幂等——已在锚点位就
3947
+ // 不动 DOM。生态插件后插入时会把我们压下去一位,观察器里重排搬回;
3948
+ // 它们只在自身节点被移除时才重插,不会与我们来回争抢。
3949
+ // 注意 created 已紧跟锚点时 target 即 created 自身,必须直接返回:
3950
+ // insertBefore(x, x) 在 Chromium 里不是 no-op,会触发 mutation 造成
3951
+ // 观察器自激风暴、冻死整个 SPA。
3952
+ const place = (region) => {
3953
+ const parent = region.parentElement;
3954
+ if (!parent) return;
3955
+ const anchor = findAnchor(region);
3956
+ const target = anchor ? anchor.nextSibling : region;
3957
+ if (target === created) return;
3958
+ if (created.parentElement === parent && created.nextSibling === target) return;
3959
+ parent.insertBefore(created, target);
3960
+ };
3773
3961
  const attempt = () => {
3774
3962
  const region = document.querySelector('[class*="regionArea"]');
3775
3963
  if (region && region.parentElement) {
@@ -3779,12 +3967,13 @@ window.__ModuleLoader__.load({
3779
3967
  // 否则 portal 按钮会停留在捕获时刻的旧类上(宽度/对齐失配)。
3780
3968
  created = document.createElement("div");
3781
3969
  created.dataset.pluginEntry = "@modusensus/dsh-mneme";
3782
- region.parentElement.insertBefore(created, region);
3970
+ place(region);
3783
3971
  setHost(created);
3784
3972
  setNativeCls(findNative(region)?.className || "");
3785
3973
  mo = new MutationObserver(() => {
3786
3974
  const cur = document.querySelector('[class*="regionArea"]');
3787
3975
  if (!cur || !cur.parentElement) return;
3976
+ place(cur);
3788
3977
  const cls = findNative(cur)?.className || "";
3789
3978
  setNativeCls((prev) => (prev === cls ? prev : cls));
3790
3979
  });
@@ -3807,10 +3996,8 @@ window.__ModuleLoader__.load({
3807
3996
  };
3808
3997
  }, []);
3809
3998
  if (!host) return fallback;
3810
- // wrapper 用 display:contents 隐身:我们的按钮成为侧边栏弹性布局的
3811
- // 直接子元素,与「新会话」按钮同级同距;展开/收起只切换图标与文案。
3812
3999
  return reactDom.createPortal(
3813
- h("div", { className: "mneme-topentry", style: { display: "contents" } },
4000
+ h("div", { className: "mneme-topentry" },
3814
4001
  h("button", {
3815
4002
  type: "button",
3816
4003
  className: `${nativeCls} mneme-topentry-native`.trim(),
@@ -3901,4 +4088,4 @@ window.__ModuleLoader__.load({
3901
4088
  exports.inject = inject;
3902
4089
  return module.exports;
3903
4090
  }
3904
- });
4091
+ });