@modusensus/dsh-mneme 0.7.25 → 0.7.26
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/README.en.md +11 -6
- package/README.md +11 -6
- package/lib/client.js +132 -4
- package/lib/config.js +6 -6
- package/lib/dream/sleep.js +3 -3
- package/lib/dream.js +56 -2
- package/lib/settings.js +4 -0
- package/package.json +1 -1
- package/src/config.js +6 -6
- package/src/dream/sleep.js +3 -3
- package/src/dream.js +56 -2
- package/src/settings.js +4 -0
- package/test/api.test.js +8 -6
- package/test/client.test.js +72 -1
- package/test/reasoning-effort.test.js +163 -0
package/README.en.md
CHANGED
|
@@ -26,13 +26,14 @@ English | [简体中文](README.md)
|
|
|
26
26
|
- Per-type `committed / failed / pending` receipts; the health endpoint distinguishes `ok / degraded / unknown`
|
|
27
27
|
- State write failures are never silent: sync failures are logged and leave debt behind, converging automatically on restart
|
|
28
28
|
|
|
29
|
-
### Model Tools (
|
|
29
|
+
### Model Tools (8)
|
|
30
30
|
|
|
31
31
|
| Tool | Function |
|
|
32
32
|
|------|------|
|
|
33
33
|
| `memory_save` | Save a memory (automatic dedup and merge by title) |
|
|
34
34
|
| `memory_search` | Full-text search (Chinese-substring friendly; vector semantic search can be enabled) |
|
|
35
35
|
| `memory_list` | Paginated listing by type (`include_archived=true` to view archived items) |
|
|
36
|
+
| `memory_get` | Read a single memory's full body by id (v0.7.25; read the full text after a search/list hit) |
|
|
36
37
|
| `memory_update` | Modify an existing memory |
|
|
37
38
|
| `memory_delete` | Delete a memory |
|
|
38
39
|
| `memory_forget` | Suppress injection (down-weighted rather than deleted; recoverable) |
|
|
@@ -66,14 +67,14 @@ The default `32768` reserves headroom for reasoning models, where reasoning alon
|
|
|
66
67
|
| Medium (10k–50k chars) | `65536` |
|
|
67
68
|
| Large (>50k chars) | `131072` (cap) |
|
|
68
69
|
|
|
69
|
-
> With **reasoning models** (e.g. DeepSeek-R1-like), the model may spend the entire budget on reasoning and return an empty body (the log shows `no json array in llm output`). Resolution order: ① set `dreamReasoningEffort` to `low` to suppress reasoning overhead (
|
|
70
|
+
> With **reasoning models** (e.g. DeepSeek-R1-like), the model may spend the entire budget on reasoning and return an empty body (the log shows `no json array in llm output`). Resolution order: ① set `dreamReasoningEffort` to `low` to suppress reasoning overhead (v0.7.26+: if the model doesn't support that tier, `resolveDreamEffort` auto-falls back to the model's default/first supported tier or omits the field — the rejection reason lands in `llm_audit`); ② if the retry still returns an empty body under the model's default reasoning behavior, raise `dreamMaxTokens` (reasoning and body share this budget) or route `dreamProvider`/`dreamModel` to a non-reasoning model. The sleep side has the corresponding `sleepReasoningEffort`.
|
|
70
71
|
|
|
71
72
|
**Consolidation model classification** (settings panel "consolidation model" = `dreamProvider`/`dreamModel`; sleep side: `sleepProvider`/`sleepModel`):
|
|
72
73
|
|
|
73
74
|
| Model kind | Examples | Notes |
|
|
74
75
|
|-----------|----------|-------|
|
|
75
76
|
| **Non-reasoning (recommended)** | glm-5-2-class | No reasoning declaration; even if an effort is configured and the harness rejects it, the fallback strips the field and the retry succeeds. Lowest risk of empty-body runs |
|
|
76
|
-
| **Reasoning (test first)** | deepseek-v4-flash-ga and other v4-flash-ga family | Reasons by default and may burn the whole token budget on an empty body; some SKUs (e.g. v4-flash-ga) are additionally declared by the harness as accepting **no reasoning effort at all**
|
|
77
|
+
| **Reasoning (test first)** | deepseek-v4-flash-ga and other v4-flash-ga family | Reasons by default and may burn the whole token budget on an empty body; some SKUs (e.g. v4-flash-ga) are additionally declared by the harness as accepting **no reasoning effort at all** (the no-effort retry gets rejected through the harness `defaultEffort`, `UNSUPPORTED_REASONING_EFFORT`). Fixed in v0.7.26+: `resolveDreamEffort` probes the model's supported effort tiers before streaming — an unsupported configured tier auto-falls back to the model's default/first supported tier, and the field is omitted for models that declare no reasoning capability. If you use one, set `dreamReasoningEffort` and test; switch to a non-reasoning model otherwise |
|
|
77
78
|
|
|
78
79
|
### Sleep Mode: System-Level Sleep 💤 (v0.4.0, opt-in)
|
|
79
80
|
|
|
@@ -173,6 +174,8 @@ Every **background LLM call** (autoDream consolidation + summary, autoSummarize
|
|
|
173
174
|
|
|
174
175
|
| Version | Highlights |
|
|
175
176
|
|------|------|
|
|
177
|
+
| **v0.7.26** | Consolidation `UNSUPPORTED_REASONING_EFFORT` root-cause fix + dream/sleep model connectivity test: root-caused the **defaultEffort trap** — the harness injects `reasoning.defaultEffort` when effort is omitted, so if that default tier itself is unsupported, retrying without effort never helps (whatever effort is sent gets rejected); fix: new `resolveDreamEffort` proactively queries `ctx.llm.resolveModelInfo()` for the model's supported effort tiers and sends a supported one — an unsupported configured tier auto-falls back to the model's default/first supported tier, and the field is omitted when the model declares no reasoning capability. Settings panel adds selection hints on 6 dream/sleep model fields (guiding toward non-thinking models so they don't burn the token budget on reasoning). New `GET /api/dsh-mneme/llm-providers` (host-side provider/model discovery; keys never touch the plugin side) + `POST /api/dsh-mneme/test-model` (connectivity test; empty body resolves via the consolidation route and returns `modelId`); 732 tests green |
|
|
178
|
+
| **v0.7.25** | Tool-compat hardening + `memory_get`/render content preview + consolidation-model guide: new `memory_get` tool (8th model tool, read a single memory's full body by id) + `memory_search`/`memory_list` render now embeds title/metadata/body preview (the model reads actual memory content even when the host only forwards render text) + consolidation-model selection guide (config comments/README classification, non-thinking vs thinking). Fixed `memory_get` execute nested inside output crashing with `userExecute is not a function` (previous tests only counted tool names, never executed it) + tool de-dup on live patch reload + Standalone API port-collision retry (EADDRINUSE) + client inject declaration alignment + better-sidebar hardening; 718 tests green |
|
|
176
179
|
| **v0.7.24** | Fixed DSH Desktop plugin-tree load crash (v0.7.23 regression): cordis 4's ctx is a Proxy — accessing a property not declared in `inject` throws `cannot get property "webServer" without inject` (not `undefined`), and removing webServer from inject meant cordis no longer waited for the host service, so Desktop crashed on restart; fix: restored webServer to inject (cordis applies the plugin only after the host service is ready) + apply/register guard switched to `ctx.reflect.get` (inject-free read, returns `undefined` when absent, never throws); verified with a real cordis + dsh-host-webserver plugin (API routes 200, unknown path 404, headless silently inactive); 714 tests green |
|
|
177
180
|
| **v0.7.23** | Root-caused "memory consolidation keeps failing": a legal empty decision array `[]` from consolidation is no longer treated as a failure (CONSOLIDATION_PROMPT explicitly allows "no output when nothing needs changing", so a model with a healthy, non-redundant memory legitimately returns `[]` — yet `validateDecisions` hard-rejected it as `decision list must be a non-empty array`, failing the whole run and flooding the audit with failures; **model-agnostic** — ChatGPT/Claude hit the same trap; fix: empty array short-circuits to `ok:true` no-op instead of tripping the implicit-keep coverage check). Plus: empty-body fix part 2 (`dreamMaxTokens` default 8192→32768 so thinking models don't burn the whole budget on reasoning) + skipInvalid splice residue bug (length equality ≠ content equality, skipped decisions leaked into apply/audit); 712 tests green |
|
|
178
181
|
| **v0.7.22** | Restored the v0.6.9 skipInvalid tolerant-validation path (issue #89 regression, lost in the v0.7.11 rewrite): `dreamSkipInvalid` (default true) skips individual invalid decisions, applies the valid subset, and marks the run degraded; `allowCrossTypeMerge` (default false) explicitly relaxes cross-type merging — weak models (e.g. qwen3.8-flash) with jittery schema compliance no longer fail the whole batch and burn LLM calls. Strict mode and the sleep path behave unchanged; global caps/coverage floors still reject the whole run (running over cap = broken model, not minor schema drift). New `dreamMinIntervalMinutes` (0–10080, default 0 = unlimited) minimum autoDream trigger interval — failed/degraded runs also consume the interval (throttling exists to stop back-to-back failing calls); feature_flags whitelist now 34 keys; 696 tests green |
|
|
@@ -230,6 +233,8 @@ Every **background LLM call** (autoDream consolidation + summary, autoSummarize
|
|
|
230
233
|
| v0.7.20 | ✅ Done | Heat restore + phase-two frontend + better-sidebar fix | Heat model fully restored (issue #87, backported from v0.7.10: power-law decay + TYPE_DECAY + sleep heat-combined dual protection + entity heat projection) + acceptance checklist landed (heatEnabled default OFF / feature_flags 31 keys / lightMode linkage / sleep demotion audit / updated_at⊥last_accessed_at contract) + phase-two frontend (/list heat projection, HeatBadge three-tier, order=heat page-local sort) + better-sidebar fix (issue #88: inner dynamic sub-plugin); 685 tests green |
|
|
231
234
|
| v0.7.21 | ✅ Done | effort-fallback stream fix | Catch-based effort fallback was dead code on the stream path (dsh-llm rc.1 turns adapter failures into a terminal error finish chunk instead of a throw) → `streamText` captures the finish-chunk cause (`describeStreamFailure`) + `withEffortFallback` gains a `getStreamError` accessor (retries without effort when rejected) + `runAuditedLlm` supports `spec.streamError` (real cause in audit); 688 tests green |
|
|
232
235
|
| v0.7.22 | ✅ Done | skipInvalid tolerant-validation restore (issue #89) + autoDream throttle | Restored v0.6.9 skipInvalid dual-track structure (lost in the v0.7.11 rewrite): `dreamSkipInvalid` skips individual invalid decisions + applies the valid subset + marks the run degraded; `allowCrossTypeMerge` explicitly relaxes cross-type merging; weak models (qwen3.8-flash) with jittery schema compliance no longer fail the whole batch. New `dreamMinIntervalMinutes` (0–10080, default 0) minimum trigger interval — failed/degraded runs also consume it. Strict mode / sleep path unchanged; feature_flags whitelist 34 keys; 696 tests green |
|
|
236
|
+
| v0.7.25 | ✅ Done | Tool-compat hardening + content preview + consolidation-model guide | New `memory_get` tool (8th model tool, full-body read by id) + `memory_search`/`memory_list` render embeds title/metadata/body preview + consolidation-model selection guide; fixed `memory_get` execute nested inside output (`userExecute is not a function`) + tool de-dup on live patch reload + Standalone API port-collision retry + client inject alignment + better-sidebar hardening; 718 tests green |
|
|
237
|
+
| **v0.7.26** | ✅ Done | Consolidation effort-trap root fix + LLM connectivity test | `UNSUPPORTED_REASONING_EFFORT` root cause (defaultEffort trap: harness injects `reasoning.defaultEffort` when effort is omitted, so an unsupported default can never be retried) → `resolveDreamEffort` proactively queries `ctx.llm.resolveModelInfo()` for the model's supported effort tiers (unsupported configured tier falls back to the model's default/first supported tier; field omitted when the model declares no reasoning capability); settings panel adds selection hints on 6 dream/sleep model fields; new `GET /api/dsh-mneme/llm-providers` + `POST /api/dsh-mneme/test-model` (connectivity test; empty body resolves via the consolidation route; keys never touch the plugin side); 732 tests green |
|
|
233
238
|
| **v0.8.0** | 🚧 Planned (late Sep) | Graph enhancement | Interest-drift visualization + scope isolation (issue #17) + cross-workspace sharing |
|
|
234
239
|
|
|
235
240
|
> All new capabilities ship as **toggleable features** (enabled/disabled via configuration), conservatively on by default and never breaking existing behavior. The `failure_memories` table and the autoDream decision engine have already paved the way for future reflective growth.
|
|
@@ -301,7 +306,7 @@ It works out of the box with the defaults. To adjust, override in `~/.dsh/profil
|
|
|
301
306
|
| `dreamDelayMs` | `2000` | Asynchronous consolidation delay (debounce) |
|
|
302
307
|
| `dreamProvider` / `dreamModel` | empty | Explicit dream LLM route — config wins over the agent's default model (config-first, v0.7.16); left empty, the agent's default model is used |
|
|
303
308
|
| `dreamMaxTokens` | `32768` | Maximum tokens per dream LLM call (cap 131072; reasoning and body share this budget on reasoning models — raise it when the body comes back empty, see the tuning guide below) |
|
|
304
|
-
| `dreamReasoningEffort` | `none` | Reasoning-effort passthrough for the dream LLM: `low` / `medium` / `high` / `none` (`none` = omit the field and use the model default; set `low` when a reasoning model exhausts its budget on reasoning and produces an empty body) |
|
|
309
|
+
| `dreamReasoningEffort` | `none` | Reasoning-effort passthrough for the dream LLM: `low` / `medium` / `high` / `none` (`none` = omit the field and use the model default; set `low` when a reasoning model exhausts its budget on reasoning and produces an empty body; v0.7.26+ auto-falls back to the model's default/first supported tier when the configured tier is unsupported, and omits the field for models without reasoning capability) |
|
|
305
310
|
| `apiToken` | empty | Optional API auth token; once set, write operations and key endpoints require `Authorization: Bearer <apiToken>` |
|
|
306
311
|
| `embedProvider` | `openai` | Semantic backend: `openai` (default, v0.1-compatible) / `local` (ONNX offline) / `ollama` |
|
|
307
312
|
| `localEmbedModel` | `Xenova/bge-small-zh-v1.5` | Local ONNX embedding model |
|
|
@@ -420,7 +425,7 @@ dsh-mneme config show # Show current config (toke
|
|
|
420
425
|
│ 服务层:saveWithDedupe / injectCandidates │
|
|
421
426
|
│ / mergeHumanEdits / onWrite 钩子 │
|
|
422
427
|
├─────────────────────────────────────────────────┤
|
|
423
|
-
│ 模型接口:
|
|
428
|
+
│ 模型接口:8 个工具 + 自动注入 + 会话摘要 │
|
|
424
429
|
├─────────────────────────────────────────────────┤
|
|
425
430
|
│ autoDream:阈值调度 → LLM 决策清单 │
|
|
426
431
|
│ → 校验(fail-safe)→ 应用 → 摘要 │
|
|
@@ -437,7 +442,7 @@ src/
|
|
|
437
442
|
├── mirror.js # Markdown 镜像(渲染/解析,人工优先)
|
|
438
443
|
├── service.js # 领域逻辑(去重合并、注入筛选、写入钩子)
|
|
439
444
|
├── config.js # schemastery 配置 schema
|
|
440
|
-
├── tools.js #
|
|
445
|
+
├── tools.js # 8 个模型工具(defineTool)
|
|
441
446
|
├── inject.js # systemPrompt.context 动态注入
|
|
442
447
|
├── summarize.js # 会话结束 LLM 摘要
|
|
443
448
|
├── dream.js # autoDream 调度 + runDream(LLM 决策 + 摘要)
|
package/README.md
CHANGED
|
@@ -49,13 +49,14 @@ dsh web
|
|
|
49
49
|
- 逐 type 记录 `committed / failed / pending` 回执,健康端点区分 `ok / degraded / unknown`
|
|
50
50
|
- 状态写失败不静默:同步失败落日志并留债务,重启自动收敛
|
|
51
51
|
|
|
52
|
-
### 模型工具(
|
|
52
|
+
### 模型工具(8 个)
|
|
53
53
|
|
|
54
54
|
| 工具 | 功能 |
|
|
55
55
|
|------|------|
|
|
56
56
|
| `memory_save` | 记录一条记忆(自动按标题去重合并) |
|
|
57
57
|
| `memory_search` | 全文搜索(中文子串友好,可启用向量语义搜索) |
|
|
58
58
|
| `memory_list` | 按类型分页列出(`include_archived=true` 可查看已归档) |
|
|
59
|
+
| `memory_get` | 读取单条记忆完整正文(v0.7.25,按 id;memory_search / memory_list 命中后读全文) |
|
|
59
60
|
| `memory_update` | 修改已有记忆 |
|
|
60
61
|
| `memory_delete` | 删除记忆(按记忆 ID 精确删除) |
|
|
61
62
|
| `memory_forget` | 抑制注入(降权不删除,可恢复) |
|
|
@@ -89,14 +90,14 @@ dsh web
|
|
|
89
90
|
| 中等(1 万-5 万字) | `65536` |
|
|
90
91
|
| 大型(5 万字以上) | `131072`(上限) |
|
|
91
92
|
|
|
92
|
-
> 若使用**思考型模型**(如 deepseek-v4-flash / DeepSeek-R1 类),模型可能把全部预算花在 reasoning 上导致正文为空(日志出现 `no json array in llm output`)。处理顺序:① 把 `dreamReasoningEffort` 设为 `low`
|
|
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
94
|
|
|
94
95
|
**巩固模型分类声明**(settings panel「巩固模型」= `dreamProvider`/`dreamModel`,睡眠侧对应 `sleepProvider`/`sleepModel`):
|
|
95
96
|
|
|
96
97
|
| 模型类别 | 例子 | 说明 |
|
|
97
98
|
|---------|------|------|
|
|
98
99
|
| **非思考模型(推荐)** | glm-5-2 类等 | 无 reasoning 声明;即使配了 effort 被 harness 拒绝,fallback 去掉字段重试即成功。空体风险最低 |
|
|
99
|
-
| **思考模型(需实测)** | deepseek-v4-flash-ga 等 v4-flash-ga 系 | 默认开推理,可能烧光 token
|
|
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
101
|
|
|
101
102
|
### Sleep Mode 系统级睡眠 💤(v0.4.0,opt-in)
|
|
102
103
|
|
|
@@ -214,6 +215,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
214
215
|
|
|
215
216
|
| 版本 | 亮点 |
|
|
216
217
|
|------|------|
|
|
218
|
+
| **v0.7.26** | 记忆巩固 `UNSUPPORTED_REASONING_EFFORT` 根治 + 巩固/睡眠模型连通性测试:根治 **defaultEffort 陷阱**——harness 在 effort 省略时注入 `reasoning.defaultEffort`,若该默认档位本身不被模型支持,则去 effort 重试也无济于事(换什么 effort 都会被拒绝);修复:新增 `resolveDreamEffort` 经 `ctx.llm.resolveModelInfo()` 主动探测模型支持的档位再发送——配置档位不被支持时自动换用模型支持的默认/首个档位、模型声明无 reasoning 能力则省略字段;设置面板巩固/睡眠 6 字段补选型提示(引导非思考模型,避免再踩思考模型烧光 token 预算);新增 `GET /api/dsh-mneme/llm-providers`(宿主侧 provider/model 发现,密钥不经插件侧)+ `POST /api/dsh-mneme/test-model`(模型连通性测试,空 body 按巩固路由解析返回 modelId);732 测试全绿 |
|
|
219
|
+
| **v0.7.25** | 工具兼容性加固 + `memory_get`/render 内容预览 + 巩固模型引导:新增 `memory_get` 工具(第 8 个模型工具,按 id 读单条记忆全文)+ `memory_search`/`memory_list` render 嵌入标题/元数据/正文预览(宿主只透传 render 文本时模型也能直接读到记忆内容)+ 巩固模型选型引导(config 注释/README 分类声明,非思考 vs 思考模型差异);修复 `memory_get` execute 嵌套进 output 的崩溃(`userExecute is not a function`,此前测试只数工具名从未执行该工具而掩盖)+ 工具重复注册去重(live patch reload)+ Standalone API 端口冲突重试(EADDRINUSE)+ client inject 声明对齐 + better-sidebar 集成加固;718 测试全绿 |
|
|
217
220
|
| **v0.7.24** | 修复 DSH Desktop 插件树加载崩溃(v0.7.23 回归):cordis 4 的 ctx 是 Proxy,访问未在 inject 声明的 `webServer` 会抛 `cannot get property without inject`(而非返回 undefined),且去掉 inject 后 cordis 不再等待宿主服务 → 桌面端重启即崩;修复:恢复 webServer 到 inject(cordis 等宿主就绪再 apply)+ apply/register 守卫改 `ctx.reflect.get`(免 inject 读取、未提供返回 undefined 不抛错);真实 cordis + dsh-host-webserver 插件实测;714 测试全绿 |
|
|
218
221
|
| **v0.7.23** | 记忆沉淀「反复失败」根治:consolidation 合法空数组 `[]` 不再误判 failed(CONSOLIDATION_PROMPT 允许「无问题无需输出」,模型无冗余时合法返回 `[]`——此前 `validateDecisions` 硬判 non-empty → 整单 failed、审计反复失败,且与模型无关,ChatGPT/Claude 同样踩中;修复:空数组显式短路 `ok:true` no-op)+ 空体修复第二段(`dreamMaxTokens` 默认 8192→32768,思考模型推理烧光预算的根治余量,设置面板可调)+ skipInvalid splice 残留 bug(长度相等≠内容一致,被跳决策残留);712 测试全绿 |
|
|
219
222
|
| **v0.7.22** | 恢复 v0.6.9 的 skipInvalid 宽容校验路径(issue #89 回归,v0.7.11 重写丢失):`dreamSkipInvalid`(默认 true)单条非法决策跳过 + 合法子集应用 + run 记 degraded,`allowCrossTypeMerge`(默认 false)显式放宽跨类型合并——弱模型(如 qwen3.8-flash)决策合规抖动不再整单拒绝白烧 LLM 调用;严格模式/sleep 路径行为不变,全局上限/覆盖率下限仍整单拒绝(刷爆上限=模型坏了,非轻微 schema 漂移);新增 `dreamMinIntervalMinutes`(0-10080,默认 0=不限)autoDream 最小触发间隔,失败/degraded run 也占用间隔(节流防失败调用连发);feature_flags 白名单 34 键;696 测试全绿 |
|
|
@@ -301,6 +304,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
301
304
|
| **v0.7.18** | ✅ 完成 | 生态第一步 + 查询收敛 | better-sidebar 软集成(inject 声明 + optional peer `dsh-better-sidebar` + registerTab 复用四视图,未装安全跳过;窄容器 `@container` 适配)+ `/list?deposited=only` 沉淀视图(receipt_chain ∪ source=dream)+ 记忆库沉淀/已归档筛选 chip + 状态页仪表盘化(统计 + 查看全部跳转预置筛选)+ 抽屉归档记忆「恢复」;667 测试全绿 |
|
|
302
305
|
| **v0.7.20** | ✅ 完成 | heat 回归 + 阶段二前端 + better-sidebar 修复 | heat 热度模型完整找回(issue #87,v0.7.10 移植:幂律衰减 + TYPE_DECAY + sleep 热联合双保护 + 实体热投影)+ 验收清单落地(heatEnabled 默认关 / feature_flags 31 键 / lightMode 联动 / sleep 降级审计暴露 / updated_at⊥last_accessed_at 契约)+ 阶段二前端(/list heat 投影、HeatBadge 三档、order=heat 页内排序)+ better-sidebar 修复(issue #88:内层动态子插件);685 测试全绿 |
|
|
303
306
|
| **v0.7.21** | ✅ 完成 | effort 回退流式修复 | autoDream/sleep 的 catch 式 effort 回退在流式路径是死代码(dsh-llm rc.1 把 adapter 异常转成终态 error finish chunk 不再抛出)→ `streamText` 捕获 finish-chunk 失败原因(`describeStreamFailure` 归一化)+ `withEffortFallback` 增加 `getStreamError` 访问器(effort 被拒去重试)+ `runAuditedLlm` 支持 `spec.streamError`(audit 记真实原因);688 测试全绿 |
|
|
307
|
+
| **v0.7.26** | ✅ 完成 | 巩固 effort 陷阱根治 + LLM 连通性测试 | 记忆巩固 `UNSUPPORTED_REASONING_EFFORT` 根治(defaultEffort 陷阱:harness 省略 effort 时注入 `reasoning.defaultEffort`,默认档位不被模型支持则任何重试无效)→ 新增 `resolveDreamEffort` 经 `ctx.llm.resolveModelInfo()` 探测模型支持的档位(不支持的配置档位自动换用模型支持的默认/首个档位,无 reasoning 能力则省略字段);设置面板巩固/睡眠 6 字段补选型提示(引导非思考模型);新增 `GET /api/dsh-mneme/llm-providers`(宿主侧 provider/model 发现)+ `POST /api/dsh-mneme/test-model`(模型连通性测试,空 body 按巩固路由解析,密钥不经插件侧);732 测试全绿 |
|
|
308
|
+
| **v0.7.25** | ✅ 完成 | 工具兼容性加固 + 内容预览 + 巩固模型引导 | 新增 `memory_get` 工具(第 8 个模型工具,按 id 读单条记忆全文)+ `memory_search`/`memory_list` render 嵌入标题/元数据/正文预览(宿主只透传 render 文本时模型也能读到内容)+ 巩固模型选型引导(config 注释/README 分类声明);修复 memory_get execute 嵌套 output 的崩溃(userExecute is not a function)+ 工具重复注册去重(live patch reload)+ Standalone API 端口冲突重试 + client inject 声明对齐 + better-sidebar 集成加固;718 测试全绿 |
|
|
304
309
|
| **v0.7.24** | ✅ 完成 | 桌面端崩溃紧急修复 | v0.7.23 把 webServer 移出 inject 致 cordis Proxy 抛 `cannot get property "webServer" without inject`(未注入属性直接访问抛错而非 undefined),桌面端重启插件树加载失败;修复:恢复 webServer 到 inject(cordis 等宿主就绪再 apply)+ apply/register 守卫改 `ctx.reflect.get`(免 inject 读取、未提供返回 undefined 不抛错,未来 headless 移出 inject 也安全);真实 cordis + dsh-host-webserver 实测 API 路由 200 / 未知路径 404 / headless 静默不激活;714 测试全绿 |
|
|
305
310
|
| **v0.7.23** | ✅ 完成 | 记忆沉淀「反复失败」根治 + 空体第二段 + skipInvalid splice 修复 | consolidation 合法空数组 `[]` no-op(CONSOLIDATION_PROMPT 允许无问题无需输出;此前 validateDecisions 硬判 non-empty → 整单 failed,模型无关、ChatGPT/Claude 同样踩中;修复:空数组显式短路 ok,不再触发隐式 keep 覆盖率误判);`dreamMaxTokens` 默认 8192→32768(思考模型推理烧光预算根治余量);skipInvalid splice 残留 bug(长度相等≠内容一致,被跳决策残留进 apply);712 测试全绿 |
|
|
306
311
|
| **v0.7.22** | ✅ 完成 | skipInvalid 宽容校验回归(issue #89)+ autoDream 节流 | 恢复 v0.6.9 的 skipInvalid 双轨结构(v0.7.11 重写丢失):`dreamSkipInvalid` 单条非法决策跳过 + 合法子集应用 + run 记 degraded,`allowCrossTypeMerge` 显式放宽跨类型合并;弱模型(qwen3.8-flash)决策合规抖动不再整单拒绝;新增 `dreamMinIntervalMinutes`(0-10080,默认 0=不限)最小触发间隔,失败/degraded run 也占用间隔;严格模式/sleep 路径行为不变;feature_flags 白名单 34 键;696 测试全绿 |
|
|
@@ -375,7 +380,7 @@ dsh web
|
|
|
375
380
|
| `dreamDelayMs` | `2000` | 整理异步延迟(去抖) |
|
|
376
381
|
| `dreamProvider` / `dreamModel` | 空 | dream 的 LLM 路由覆盖(显式配置优先于 agent 默认模型;留空则回退到 agent 默认模型) |
|
|
377
382
|
| `dreamMaxTokens` | `32768` | dream LLM 调用最大 token 数(上限 131072;思考型模型的 reasoning 与正文共享该预算,正文为空时优先调大,见下方调优指南) |
|
|
378
|
-
| `dreamReasoningEffort` | `none` | dream LLM 推理强度透传:`low` / `medium` / `high` / `none`(`none`=不传该字段,沿用模型默认;思考型模型(如 deepseek-v4-flash)想压低思考可设 `low
|
|
383
|
+
| `dreamReasoningEffort` | `none` | dream LLM 推理强度透传:`low` / `medium` / `high` / `none`(`none`=不传该字段,沿用模型默认;思考型模型(如 deepseek-v4-flash)想压低思考可设 `low`;v0.7.26+ 模型不支持配置档位时自动换用其支持的默认/首个档位,无 reasoning 能力的型号省略字段) |
|
|
379
384
|
| `apiToken` | 空 | 可选 API 鉴权 token;设置后写操作与密钥接口要求 `Authorization: Bearer <apiToken>` |
|
|
380
385
|
| `embedProvider` | `openai` | 语义后端:`openai`(默认,兼容 v0.1)/ `local`(ONNX 离线)/ `ollama` |
|
|
381
386
|
| `localEmbedModel` | `Xenova/bge-small-zh-v1.5` | 本地 ONNX embedding 模型 |
|
|
@@ -508,7 +513,7 @@ dsh-mneme config show # 查看当前配置(toke
|
|
|
508
513
|
│ 服务层:saveWithDedupe / injectCandidates │
|
|
509
514
|
│ / mergeHumanEdits / onWrite 钩子 │
|
|
510
515
|
├─────────────────────────────────────────────────┤
|
|
511
|
-
│ 模型接口:
|
|
516
|
+
│ 模型接口:8 个工具 + 自动注入 + 会话摘要 │
|
|
512
517
|
├─────────────────────────────────────────────────┤
|
|
513
518
|
│ autoDream:阈值调度 → LLM 决策清单 │
|
|
514
519
|
│ → 校验(fail-safe)→ 应用 → 摘要 │
|
|
@@ -525,7 +530,7 @@ src/
|
|
|
525
530
|
├── mirror.js # Markdown 镜像(渲染/解析,人工优先)
|
|
526
531
|
├── service.js # 领域逻辑(去重合并、注入筛选、写入钩子)
|
|
527
532
|
├── config.js # schemastery 配置 schema
|
|
528
|
-
├── tools.js #
|
|
533
|
+
├── tools.js # 8 个模型工具(defineTool)
|
|
529
534
|
├── inject.js # systemPrompt.context 动态注入
|
|
530
535
|
├── summarize.js # 会话结束 LLM 摘要
|
|
531
536
|
├── dream.js # autoDream 调度 + runDream(LLM 决策 + 摘要)
|
package/lib/client.js
CHANGED
|
@@ -326,6 +326,15 @@ window.__ModuleLoader__.load({
|
|
|
326
326
|
"memory.features.dreamProvider": "巩固模型 Provider",
|
|
327
327
|
"memory.features.dreamModel": "巩固用模型名",
|
|
328
328
|
"memory.features.dreamModelHint": "留空 = 跟随主对话模型;只影响记忆巩固(autoDream)用的模型",
|
|
329
|
+
"memory.features.sleepProvider": "睡眠 Provider",
|
|
330
|
+
"memory.features.sleepModel": "睡眠模型",
|
|
331
|
+
"memory.features.sleepModelHint": "留空 = 用巩固模型或当前模型;建议选非思考模型",
|
|
332
|
+
"memory.features.routeFollowDefault": "跟随默认路由",
|
|
333
|
+
"memory.features.modelTest": "测试连通性",
|
|
334
|
+
"memory.features.modelTesting": "测试中…",
|
|
335
|
+
"memory.features.modelTestOk": "连通正常",
|
|
336
|
+
"memory.features.modelTestFail": "测试失败",
|
|
337
|
+
"memory.features.modelTestHint": "真实发起一次最小巩固调用,验证 Provider/模型连通与 effort 支持",
|
|
329
338
|
"memory.explorer.viewCards": "卡片",
|
|
330
339
|
"memory.explorer.viewTimeline": "时间线",
|
|
331
340
|
"memory.explorer.viewAria": "视图切换",
|
|
@@ -606,6 +615,15 @@ window.__ModuleLoader__.load({
|
|
|
606
615
|
"memory.features.dreamProvider": "Consolidation provider",
|
|
607
616
|
"memory.features.dreamModel": "Consolidation model",
|
|
608
617
|
"memory.features.dreamModelHint": "Leave empty to follow the main conversation model; only affects autoDream consolidation",
|
|
618
|
+
"memory.features.sleepProvider": "Sleep provider",
|
|
619
|
+
"memory.features.sleepModel": "Sleep model",
|
|
620
|
+
"memory.features.sleepModelHint": "Leave empty to reuse the consolidation model; a non-reasoning model is recommended",
|
|
621
|
+
"memory.features.routeFollowDefault": "Follow default route",
|
|
622
|
+
"memory.features.modelTest": "Test connectivity",
|
|
623
|
+
"memory.features.modelTesting": "Testing…",
|
|
624
|
+
"memory.features.modelTestOk": "Connected",
|
|
625
|
+
"memory.features.modelTestFail": "Test failed",
|
|
626
|
+
"memory.features.modelTestHint": "Fires one minimal consolidation call to verify provider/model connectivity and effort support",
|
|
609
627
|
"memory.explorer.viewCards": "Cards",
|
|
610
628
|
"memory.explorer.viewTimeline": "Timeline",
|
|
611
629
|
"memory.explorer.viewAria": "Switch view",
|
|
@@ -750,6 +768,7 @@ window.__ModuleLoader__.load({
|
|
|
750
768
|
".mneme-chip:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
751
769
|
".mneme-chip.mneme-active{color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)}",
|
|
752
770
|
".mneme-select{box-sizing:border-box;height:30px;padding:0 8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base,transparent);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;outline:none}",
|
|
771
|
+
".mneme-routeselect{width:240px;max-width:60%}",
|
|
753
772
|
".mneme-footbtn{border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-family:inherit;font-size:12px;line-height:16px;padding:3px 8px;border-radius:6px}",
|
|
754
773
|
".mneme-footbtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
|
|
755
774
|
".mneme-hint{color:var(--dsw-alias-label-tertiary);padding:24px 0;text-align:center;font-size:13px}",
|
|
@@ -1601,6 +1620,11 @@ window.__ModuleLoader__.load({
|
|
|
1601
1620
|
const [savedTick, setSavedTick] = useState(false);
|
|
1602
1621
|
const [showAdv, setShowAdv] = useState(false);
|
|
1603
1622
|
const [strs, setStrs] = useState({}); // 字符串输入的本地草稿:key -> string
|
|
1623
|
+
// 巩固/睡眠模型路由下拉的数据源:GET /llm-providers 探测(云端 v0.7.26+
|
|
1624
|
+
// 的插件侧端点)。null = 端点不可用(404/失败)→ 回退纯文本输入,不挡旧后端。
|
|
1625
|
+
const [routes, setRoutes] = useState(null);
|
|
1626
|
+
// 连通性测试结果:{ running: true } | { ok, ms, detail }
|
|
1627
|
+
const [testState, setTestState] = useState(null);
|
|
1604
1628
|
|
|
1605
1629
|
useEffect(() => {
|
|
1606
1630
|
let cancelled = false;
|
|
@@ -1618,6 +1642,16 @@ window.__ModuleLoader__.load({
|
|
|
1618
1642
|
return () => { cancelled = true; };
|
|
1619
1643
|
}, [t]);
|
|
1620
1644
|
|
|
1645
|
+
// 插件侧枚举端点探测:不可用时静默保持 routes=null(文本框回退)。
|
|
1646
|
+
useEffect(() => {
|
|
1647
|
+
let cancelled = false;
|
|
1648
|
+
apiFetch("/api/dsh-mneme/llm-providers")
|
|
1649
|
+
.then((res) => { if (!res.ok) throw new Error("HTTP " + res.status); return res.json(); })
|
|
1650
|
+
.then((j) => { if (!cancelled) setRoutes(Array.isArray(j && j.providers) ? j.providers : []); })
|
|
1651
|
+
.catch(() => {});
|
|
1652
|
+
return () => { cancelled = true; };
|
|
1653
|
+
}, [t]);
|
|
1654
|
+
|
|
1621
1655
|
const eff = (state && state.effective) || {};
|
|
1622
1656
|
|
|
1623
1657
|
const put = async (patch) => {
|
|
@@ -1659,6 +1693,91 @@ window.__ModuleLoader__.load({
|
|
|
1659
1693
|
onToggle: () => put({ [k]: !eff[k] })
|
|
1660
1694
|
});
|
|
1661
1695
|
|
|
1696
|
+
// 模型枚举项的统一形状:string 或 {id, name?}(/llm-providers 契约)。
|
|
1697
|
+
const modelLabel = (m) => (typeof m === "string" ? m : String((m && (m.name || m.id)) ?? ""));
|
|
1698
|
+
const modelValue = (m) => (typeof m === "string" ? m : String((m && m.id) ?? ""));
|
|
1699
|
+
|
|
1700
|
+
// 连通性测试:POST /test-model,空 provider/model = 按巩固路由解析
|
|
1701
|
+
// (agent 默认)。durationMs 优先后端值,缺失时客户端兜底计时;成功行
|
|
1702
|
+
// 附模型的实际回复(reply,后端截 100 字符)——「真的答了 ok」而非只报通。
|
|
1703
|
+
const runModelTest = async (provider, model) => {
|
|
1704
|
+
setTestState({ running: true });
|
|
1705
|
+
const started = Date.now();
|
|
1706
|
+
try {
|
|
1707
|
+
const res = await apiFetch("/api/dsh-mneme/test-model", {
|
|
1708
|
+
method: "POST",
|
|
1709
|
+
headers: { "Content-Type": "application/json" },
|
|
1710
|
+
body: JSON.stringify({ provider, model })
|
|
1711
|
+
});
|
|
1712
|
+
const j = await res.json().catch(() => ({}));
|
|
1713
|
+
const ms = typeof j.durationMs === "number" ? j.durationMs : Date.now() - started;
|
|
1714
|
+
if (!res.ok || j.ok === false) {
|
|
1715
|
+
setTestState({
|
|
1716
|
+
ok: false,
|
|
1717
|
+
ms,
|
|
1718
|
+
detail: [j.modelId, j.error || "HTTP " + res.status].filter(Boolean).join(" · ")
|
|
1719
|
+
});
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
setTestState({
|
|
1723
|
+
ok: true,
|
|
1724
|
+
ms,
|
|
1725
|
+
detail: [j.modelId, j.reply ? "「" + j.reply + "」" : ""].filter(Boolean).join(" · ")
|
|
1726
|
+
});
|
|
1727
|
+
} catch (err) {
|
|
1728
|
+
setTestState({ ok: false, ms: Date.now() - started, detail: String((err && err.message) ?? err) });
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
// 巩固/睡眠路由行:provider/model 级联下拉(数据来自宿主侧已注册的
|
|
1733
|
+
// 适配器,用户不会选到不存在的模型)+ 连通性测试。空值 = 跟随默认
|
|
1734
|
+
// 路由;改动即提交(与 embedProvider 下拉一致)。当前值不在枚举里时
|
|
1735
|
+
// 保留为额外选项,避免静默改值。
|
|
1736
|
+
const routeSelects = (providerKey, modelKey) => {
|
|
1737
|
+
const curP = strs[providerKey] ?? "";
|
|
1738
|
+
const curM = strs[modelKey] ?? "";
|
|
1739
|
+
const entries = Array.isArray(routes) ? routes : [];
|
|
1740
|
+
const entry = entries.find((p) => p && p.provider === curP);
|
|
1741
|
+
const models = (entry && Array.isArray(entry.models)) ? entry.models : [];
|
|
1742
|
+
const mVals = models.map(modelValue);
|
|
1743
|
+
const pList = entries.map((p) => p.provider).concat(
|
|
1744
|
+
curP && !entries.some((p) => p.provider === curP) ? [curP] : []);
|
|
1745
|
+
const putKey = (key) => (e) => {
|
|
1746
|
+
const v = e.target.value;
|
|
1747
|
+
setStrs((c) => ({ ...c, [key]: v }));
|
|
1748
|
+
put({ [key]: v });
|
|
1749
|
+
};
|
|
1750
|
+
return h(react.Fragment, null,
|
|
1751
|
+
h("div", { className: "mneme-featnum" },
|
|
1752
|
+
h("span", { className: "mneme-featnumlabel" }, t(`memory.features.${providerKey}`)),
|
|
1753
|
+
h("select", {
|
|
1754
|
+
className: "mneme-select mneme-routeselect", value: curP, disabled: busy,
|
|
1755
|
+
"aria-label": t(`memory.features.${providerKey}`), onChange: putKey(providerKey)
|
|
1756
|
+
},
|
|
1757
|
+
h("option", { value: "" }, t("memory.features.routeFollowDefault")),
|
|
1758
|
+
pList.map((p) => h("option", { key: p, value: p }, p)))),
|
|
1759
|
+
h("div", { className: "mneme-featnum" },
|
|
1760
|
+
h("span", { className: "mneme-featnumlabel" }, t(`memory.features.${modelKey}`)),
|
|
1761
|
+
h("select", {
|
|
1762
|
+
className: "mneme-select mneme-routeselect", value: curM, disabled: busy,
|
|
1763
|
+
"aria-label": t(`memory.features.${modelKey}`), onChange: putKey(modelKey)
|
|
1764
|
+
},
|
|
1765
|
+
h("option", { value: "" }, t("memory.features.routeFollowDefault")),
|
|
1766
|
+
models.map((m, i) => h("option", { key: modelValue(m) + "|" + i, value: modelValue(m) }, modelLabel(m))),
|
|
1767
|
+
curM && !mVals.includes(curM) ? h("option", { key: "current", value: curM }, curM) : null)),
|
|
1768
|
+
h("div", { className: "mneme-featnum" },
|
|
1769
|
+
h("button", {
|
|
1770
|
+
type: "button", className: "mneme-btn",
|
|
1771
|
+
disabled: busy || (testState && testState.running),
|
|
1772
|
+
onClick: () => runModelTest(curP, curM)
|
|
1773
|
+
}, t((testState && testState.running) ? "memory.features.modelTesting" : "memory.features.modelTest")),
|
|
1774
|
+
testState && !testState.running && h("div", { className: "mneme-featsubhint" },
|
|
1775
|
+
(testState.ok ? "✓ " + t("memory.features.modelTestOk") : "✗ " + t("memory.features.modelTestFail"))
|
|
1776
|
+
+ (typeof testState.ms === "number" ? " · " + (testState.ms / 1000).toFixed(1) + "s" : "")
|
|
1777
|
+
+ (testState.detail ? " · " + testState.detail : "")))
|
|
1778
|
+
);
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1662
1781
|
const strRow = (key) => h("div", { className: "mneme-featnum", key },
|
|
1663
1782
|
h("span", { className: "mneme-featnumlabel" }, t(`memory.features.${key}`)),
|
|
1664
1783
|
h("input", {
|
|
@@ -1689,13 +1808,22 @@ window.__ModuleLoader__.load({
|
|
|
1689
1808
|
eff.embedProvider === "ollama" && strRow("ollamaModel")
|
|
1690
1809
|
);
|
|
1691
1810
|
|
|
1692
|
-
// 巩固模型:autoDream
|
|
1811
|
+
// 巩固模型:autoDream 开着才展开,避免闲置配置占版面。/llm-providers
|
|
1812
|
+
// 可用时用级联下拉 + 连通性测试;旧后端(端点 404)回退纯文本输入。
|
|
1693
1813
|
const dreamSub = eff.autoDream && h("div", { className: "mneme-featsub" },
|
|
1694
|
-
|
|
1695
|
-
|
|
1814
|
+
Array.isArray(routes)
|
|
1815
|
+
? routeSelects("dreamProvider", "dreamModel")
|
|
1816
|
+
: h(react.Fragment, null, strRow("dreamProvider"), strRow("dreamModel")),
|
|
1696
1817
|
h("div", { className: "mneme-featsubhint" }, t("memory.features.dreamModelHint"))
|
|
1697
1818
|
);
|
|
1698
1819
|
|
|
1820
|
+
// 睡眠模型:sleepModeEnabled 开着才展开(sleepProvider/sleepModel 随本版
|
|
1821
|
+
// 进白名单;下拉与测试复用同一 /llm-providers 数据源)。
|
|
1822
|
+
const sleepSub = eff.sleepModeEnabled && Array.isArray(routes) && h("div", { className: "mneme-featsub" },
|
|
1823
|
+
routeSelects("sleepProvider", "sleepModel"),
|
|
1824
|
+
h("div", { className: "mneme-featsubhint" }, t("memory.features.sleepModelHint"))
|
|
1825
|
+
);
|
|
1826
|
+
|
|
1699
1827
|
if (error && !state) return h("section", { className: "mneme-set-card" },
|
|
1700
1828
|
h("div", { className: "mneme-set-title" }, t("memory.features.title")),
|
|
1701
1829
|
h("div", { className: "mneme-set-hint" }, error));
|
|
@@ -1713,7 +1841,7 @@ window.__ModuleLoader__.load({
|
|
|
1713
1841
|
h("div", { className: "mneme-featgroup" }, t(`memory.features.${g.key}`)),
|
|
1714
1842
|
g.items.map(boolRow),
|
|
1715
1843
|
g.key === "group.enhance" && embedSub,
|
|
1716
|
-
g.key === "group.dream" && dreamSub
|
|
1844
|
+
g.key === "group.dream" && h(react.Fragment, null, dreamSub, sleepSub)
|
|
1717
1845
|
)),
|
|
1718
1846
|
h("div", { className: "mneme-featgroup" },
|
|
1719
1847
|
h("button", {
|
package/lib/config.js
CHANGED
|
@@ -62,8 +62,8 @@ export const Config = z.object({
|
|
|
62
62
|
// reasoning effort —— 即使去掉 effort 重试,harness 的 defaultEffort 也会顶上来
|
|
63
63
|
// 再次拒绝(UNSUPPORTED_REASONING_EFFORT),插件 fallback 无法绕开。
|
|
64
64
|
// 选用时建议配 dreamReasoningEffort 并实测;不行就换非思考模型。
|
|
65
|
-
dreamProvider: z.string(),
|
|
66
|
-
dreamModel: z.string(),
|
|
65
|
+
dreamProvider: z.string().description("记忆巩固专用模型的服务商(settings「巩固模型」)。巩固反复失败时,优先改用官方非思考模型的服务商(如 deepseek / glm)。"),
|
|
66
|
+
dreamModel: z.string().description("记忆巩固专用模型。建议选非思考模型(如 deepseek-chat、glm-5-2 类):思考模型可能烧光 token 预算返回空体,导致巩固失败(UNSUPPORTED_REASONING_EFFORT)。"),
|
|
67
67
|
dreamMaxTokens: z.natural().min(256).max(131072).default(32768),
|
|
68
68
|
// Pass-through reasoning effort for dream's LLM calls. 'none' (default)
|
|
69
69
|
// omits the field so the provider's own default applies; low/medium/high
|
|
@@ -78,7 +78,7 @@ export const Config = z.object({
|
|
|
78
78
|
z.const("medium"),
|
|
79
79
|
z.const("high"),
|
|
80
80
|
z.const("none")
|
|
81
|
-
]).default("none"),
|
|
81
|
+
]).default("none").description("巩固模型的推理档位:'none'(默认)用服务商自带默认;low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
|
|
82
82
|
// 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
|
|
83
83
|
// 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
|
|
84
84
|
// 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
|
|
@@ -251,8 +251,8 @@ export const Config = z.object({
|
|
|
251
251
|
sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
|
|
252
252
|
// Optional LLM route override for sleep's bulk passes (empty = use dream
|
|
253
253
|
// route / agent default model).
|
|
254
|
-
sleepProvider: z.string().default(""),
|
|
255
|
-
sleepModel: z.string().default(""),
|
|
254
|
+
sleepProvider: z.string().default("").description("sleep 深维护专用模型服务商,留空用巩固模型或当前模型。"),
|
|
255
|
+
sleepModel: z.string().default("").description("sleep 深维护专用模型,留空用巩固模型或当前模型;建议同巩固模型选非思考模型。"),
|
|
256
256
|
// Pass-through reasoning effort for sleep's LLM passes, same semantics as
|
|
257
257
|
// dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
|
|
258
258
|
// are forwarded verbatim.
|
|
@@ -261,7 +261,7 @@ export const Config = z.object({
|
|
|
261
261
|
z.const("medium"),
|
|
262
262
|
z.const("high"),
|
|
263
263
|
z.const("none")
|
|
264
|
-
]).default("none"),
|
|
264
|
+
]).default("none").description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,'none' 用服务商默认。"),
|
|
265
265
|
|
|
266
266
|
// --- epistemic trust: memory source credibility (v0.4.5) -----------------
|
|
267
267
|
// Distinguish memories by source: observation (measured / witnessed),
|
package/lib/dream/sleep.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { randomUUID, createHash } from "node:crypto";
|
|
18
18
|
import { validateDecisions, applyDecisions } from "./decisions.js";
|
|
19
19
|
import { findPotentialConflicts } from "./clustering.js";
|
|
20
|
-
import { buildReceipt, describeStreamFailure, withEffortFallback } from "../dream.js";
|
|
20
|
+
import { buildReceipt, describeStreamFailure, resolveDreamEffort, withEffortFallback } from "../dream.js";
|
|
21
21
|
import { computeHeat } from "../heat.js";
|
|
22
22
|
|
|
23
23
|
const SUMMARY_MAX = 120;
|
|
@@ -190,7 +190,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
190
190
|
const listText = selected.map((p) =>
|
|
191
191
|
`候选冲突:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(相似度 ${p.similarity.toFixed(2)})`
|
|
192
192
|
).join("\n\n");
|
|
193
|
-
const sleepEffort =
|
|
193
|
+
const sleepEffort = await resolveDreamEffort(ctx, route, config.sleepReasoningEffort, logger);
|
|
194
194
|
let conflictStreamFailure = "";
|
|
195
195
|
const runConflict = (withEffort) => {
|
|
196
196
|
conflictStreamFailure = "";
|
|
@@ -309,7 +309,7 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
309
309
|
.map((m) => `id=${m.id} | type=${m.type} | importance=${m.importance} | updated=${m.updated_at} | title=${m.title} | content=${m.content}`)
|
|
310
310
|
.join("\n");
|
|
311
311
|
const maxPatterns = config.sleepMaxPatternPerRun ?? 3;
|
|
312
|
-
const sleepEffort =
|
|
312
|
+
const sleepEffort = await resolveDreamEffort(ctx, route, config.sleepReasoningEffort, logger);
|
|
313
313
|
let patternStreamFailure = "";
|
|
314
314
|
const runPattern = (withEffort) => {
|
|
315
315
|
patternStreamFailure = "";
|
package/lib/dream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
export { validateDecisions, applyDecisions, withEffortFallback, describeStreamFailure };
|
|
4
|
+
export { validateDecisions, applyDecisions, withEffortFallback, describeStreamFailure, resolveDreamEffort };
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
@@ -373,6 +373,60 @@ async function withEffortFallback(ctx, effort, attempt, fallback, getStreamError
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Resolve the reasoning effort to actually send for a dream/sleep route.
|
|
378
|
+
*
|
|
379
|
+
* Reasoning-effort config that the provider does not accept trips the harness's
|
|
380
|
+
* UNSUPPORTED_REASONING_EFFORT, and the defaultEffort trap makes retrying
|
|
381
|
+
* "without the field" useless: the harness substitutes `reasoning.defaultEffort`,
|
|
382
|
+
* which may itself be unsupported (DSH Desktop volcano-engine adapter declares
|
|
383
|
+
* defaultEffort=low that its model rejects). So instead of blind retries, ask
|
|
384
|
+
* the harness for the model's declared capability and pick a value that is
|
|
385
|
+
* actually accepted — or omit the field entirely when the model declares no
|
|
386
|
+
* reasoning capability at all.
|
|
387
|
+
*
|
|
388
|
+
* @returns a supported effort id, or null when no effort should be sent, or the
|
|
389
|
+
* configured value unchanged when the capability query is unavailable.
|
|
390
|
+
*/
|
|
391
|
+
async function resolveDreamEffort(ctx, route, configuredEffort, logger) {
|
|
392
|
+
if (!configuredEffort || configuredEffort === "none") return null;
|
|
393
|
+
// Capability query unavailable (older harness / minimal mocks): forward the
|
|
394
|
+
// configured value as before — absence of the API proves nothing about the
|
|
395
|
+
// model, and withEffortFallback still guards against rejection.
|
|
396
|
+
if (typeof ctx?.llm?.resolveModelInfo !== "function") return configuredEffort;
|
|
397
|
+
try {
|
|
398
|
+
const info = await ctx.llm.resolveModelInfo(route.provider, route.model);
|
|
399
|
+
const reasoning = info?.reasoning;
|
|
400
|
+
if (!reasoning) {
|
|
401
|
+
// Model declares no reasoning capability: the harness rejects ANY
|
|
402
|
+
// explicit effort for such a model, and omitting the field is safe
|
|
403
|
+
// (no reasoning capability → no defaultEffort substitution).
|
|
404
|
+
logger?.warn?.(`dsh-mneme dream: model ${route.provider}:${route.model} declares no reasoning capability; ignoring configured effort "${configuredEffort}"`);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const supported = reasoning.efforts?.map((effort) => effort.id) ?? [];
|
|
408
|
+
if (supported.includes(configuredEffort)) return configuredEffort;
|
|
409
|
+
// Configured effort unsupported → pick defaultEffort if it is supported,
|
|
410
|
+
// else the first declared effort, so the run never trips
|
|
411
|
+
// UNSUPPORTED_REASONING_EFFORT (nor the defaultEffort trap: we always
|
|
412
|
+
// pass an explicit value, so the harness never falls back to a poison
|
|
413
|
+
// default).
|
|
414
|
+
const picked = reasoning.defaultEffort && supported.includes(reasoning.defaultEffort)
|
|
415
|
+
? reasoning.defaultEffort
|
|
416
|
+
: supported[0];
|
|
417
|
+
if (picked) {
|
|
418
|
+
logger?.warn?.(`dsh-mneme dream: model ${route.provider}:${route.model} does not support effort "${configuredEffort}" (supported: ${supported.join(", ")}); using "${picked}"`);
|
|
419
|
+
return picked;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
// Capability query failed — forward the configured value; withEffortFallback
|
|
424
|
+
// still retries on rejection as before.
|
|
425
|
+
logger?.warn?.(`dsh-mneme dream: resolveModelInfo failed (${String(error?.message ?? error)}); forwarding effort as configured`);
|
|
426
|
+
return configuredEffort;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
376
430
|
/**
|
|
377
431
|
* Resolve the LLM route (Issue #25): an explicit plugin config
|
|
378
432
|
* (dreamProvider/dreamModel) is the user's declared override and wins; the
|
|
@@ -665,7 +719,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
665
719
|
// (不带该字段),避免 thinking 模型配置 low/medium 直接整单失败。解析放
|
|
666
720
|
// 在 auditError 检查器里、闭包交回主流程,避免二次解析;解析失败同时如实
|
|
667
721
|
// 记 audit error 并在日志带原始输出前 300 字节,便于定位"推理吞预算返回空体"。
|
|
668
|
-
const effort =
|
|
722
|
+
const effort = await resolveDreamEffort(ctx, route, config.dreamReasoningEffort, logger);
|
|
669
723
|
let decisions = null;
|
|
670
724
|
let streamFailure = "";
|
|
671
725
|
const runConsolidation = (withEffort) => {
|
package/lib/settings.js
CHANGED
|
@@ -82,6 +82,10 @@ const FEATURE_FLAG_INT_RANGES = {
|
|
|
82
82
|
const FEATURE_FLAG_STRINGS = [
|
|
83
83
|
"dreamProvider",
|
|
84
84
|
"dreamModel",
|
|
85
|
+
// 睡眠侧专用路由(sleep.js 的 config-first 第三层):面板下拉随
|
|
86
|
+
// /llm-providers 端点一起提供,留空 = 用巩固模型或当前模型。
|
|
87
|
+
"sleepProvider",
|
|
88
|
+
"sleepModel",
|
|
85
89
|
"localEmbedModel",
|
|
86
90
|
"ollamaModel"
|
|
87
91
|
];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.26",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/config.js
CHANGED
|
@@ -62,8 +62,8 @@ export const Config = z.object({
|
|
|
62
62
|
// reasoning effort —— 即使去掉 effort 重试,harness 的 defaultEffort 也会顶上来
|
|
63
63
|
// 再次拒绝(UNSUPPORTED_REASONING_EFFORT),插件 fallback 无法绕开。
|
|
64
64
|
// 选用时建议配 dreamReasoningEffort 并实测;不行就换非思考模型。
|
|
65
|
-
dreamProvider: z.string(),
|
|
66
|
-
dreamModel: z.string(),
|
|
65
|
+
dreamProvider: z.string().description("记忆巩固专用模型的服务商(settings「巩固模型」)。巩固反复失败时,优先改用官方非思考模型的服务商(如 deepseek / glm)。"),
|
|
66
|
+
dreamModel: z.string().description("记忆巩固专用模型。建议选非思考模型(如 deepseek-chat、glm-5-2 类):思考模型可能烧光 token 预算返回空体,导致巩固失败(UNSUPPORTED_REASONING_EFFORT)。"),
|
|
67
67
|
dreamMaxTokens: z.natural().min(256).max(131072).default(32768),
|
|
68
68
|
// Pass-through reasoning effort for dream's LLM calls. 'none' (default)
|
|
69
69
|
// omits the field so the provider's own default applies; low/medium/high
|
|
@@ -78,7 +78,7 @@ export const Config = z.object({
|
|
|
78
78
|
z.const("medium"),
|
|
79
79
|
z.const("high"),
|
|
80
80
|
z.const("none")
|
|
81
|
-
]).default("none"),
|
|
81
|
+
]).default("none").description("巩固模型的推理档位:'none'(默认)用服务商自带默认;low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
|
|
82
82
|
// 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
|
|
83
83
|
// 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
|
|
84
84
|
// 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
|
|
@@ -251,8 +251,8 @@ export const Config = z.object({
|
|
|
251
251
|
sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
|
|
252
252
|
// Optional LLM route override for sleep's bulk passes (empty = use dream
|
|
253
253
|
// route / agent default model).
|
|
254
|
-
sleepProvider: z.string().default(""),
|
|
255
|
-
sleepModel: z.string().default(""),
|
|
254
|
+
sleepProvider: z.string().default("").description("sleep 深维护专用模型服务商,留空用巩固模型或当前模型。"),
|
|
255
|
+
sleepModel: z.string().default("").description("sleep 深维护专用模型,留空用巩固模型或当前模型;建议同巩固模型选非思考模型。"),
|
|
256
256
|
// Pass-through reasoning effort for sleep's LLM passes, same semantics as
|
|
257
257
|
// dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
|
|
258
258
|
// are forwarded verbatim.
|
|
@@ -261,7 +261,7 @@ export const Config = z.object({
|
|
|
261
261
|
z.const("medium"),
|
|
262
262
|
z.const("high"),
|
|
263
263
|
z.const("none")
|
|
264
|
-
]).default("none"),
|
|
264
|
+
]).default("none").description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,'none' 用服务商默认。"),
|
|
265
265
|
|
|
266
266
|
// --- epistemic trust: memory source credibility (v0.4.5) -----------------
|
|
267
267
|
// Distinguish memories by source: observation (measured / witnessed),
|
package/src/dream/sleep.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { randomUUID, createHash } from "node:crypto";
|
|
18
18
|
import { validateDecisions, applyDecisions } from "./decisions.js";
|
|
19
19
|
import { findPotentialConflicts } from "./clustering.js";
|
|
20
|
-
import { buildReceipt, describeStreamFailure, withEffortFallback } from "../dream.js";
|
|
20
|
+
import { buildReceipt, describeStreamFailure, resolveDreamEffort, withEffortFallback } from "../dream.js";
|
|
21
21
|
import { computeHeat } from "../heat.js";
|
|
22
22
|
|
|
23
23
|
const SUMMARY_MAX = 120;
|
|
@@ -190,7 +190,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
190
190
|
const listText = selected.map((p) =>
|
|
191
191
|
`候选冲突:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(相似度 ${p.similarity.toFixed(2)})`
|
|
192
192
|
).join("\n\n");
|
|
193
|
-
const sleepEffort =
|
|
193
|
+
const sleepEffort = await resolveDreamEffort(ctx, route, config.sleepReasoningEffort, logger);
|
|
194
194
|
let conflictStreamFailure = "";
|
|
195
195
|
const runConflict = (withEffort) => {
|
|
196
196
|
conflictStreamFailure = "";
|
|
@@ -309,7 +309,7 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
309
309
|
.map((m) => `id=${m.id} | type=${m.type} | importance=${m.importance} | updated=${m.updated_at} | title=${m.title} | content=${m.content}`)
|
|
310
310
|
.join("\n");
|
|
311
311
|
const maxPatterns = config.sleepMaxPatternPerRun ?? 3;
|
|
312
|
-
const sleepEffort =
|
|
312
|
+
const sleepEffort = await resolveDreamEffort(ctx, route, config.sleepReasoningEffort, logger);
|
|
313
313
|
let patternStreamFailure = "";
|
|
314
314
|
const runPattern = (withEffort) => {
|
|
315
315
|
patternStreamFailure = "";
|
package/src/dream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
export { validateDecisions, applyDecisions, withEffortFallback, describeStreamFailure };
|
|
4
|
+
export { validateDecisions, applyDecisions, withEffortFallback, describeStreamFailure, resolveDreamEffort };
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
@@ -373,6 +373,60 @@ async function withEffortFallback(ctx, effort, attempt, fallback, getStreamError
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Resolve the reasoning effort to actually send for a dream/sleep route.
|
|
378
|
+
*
|
|
379
|
+
* Reasoning-effort config that the provider does not accept trips the harness's
|
|
380
|
+
* UNSUPPORTED_REASONING_EFFORT, and the defaultEffort trap makes retrying
|
|
381
|
+
* "without the field" useless: the harness substitutes `reasoning.defaultEffort`,
|
|
382
|
+
* which may itself be unsupported (DSH Desktop volcano-engine adapter declares
|
|
383
|
+
* defaultEffort=low that its model rejects). So instead of blind retries, ask
|
|
384
|
+
* the harness for the model's declared capability and pick a value that is
|
|
385
|
+
* actually accepted — or omit the field entirely when the model declares no
|
|
386
|
+
* reasoning capability at all.
|
|
387
|
+
*
|
|
388
|
+
* @returns a supported effort id, or null when no effort should be sent, or the
|
|
389
|
+
* configured value unchanged when the capability query is unavailable.
|
|
390
|
+
*/
|
|
391
|
+
async function resolveDreamEffort(ctx, route, configuredEffort, logger) {
|
|
392
|
+
if (!configuredEffort || configuredEffort === "none") return null;
|
|
393
|
+
// Capability query unavailable (older harness / minimal mocks): forward the
|
|
394
|
+
// configured value as before — absence of the API proves nothing about the
|
|
395
|
+
// model, and withEffortFallback still guards against rejection.
|
|
396
|
+
if (typeof ctx?.llm?.resolveModelInfo !== "function") return configuredEffort;
|
|
397
|
+
try {
|
|
398
|
+
const info = await ctx.llm.resolveModelInfo(route.provider, route.model);
|
|
399
|
+
const reasoning = info?.reasoning;
|
|
400
|
+
if (!reasoning) {
|
|
401
|
+
// Model declares no reasoning capability: the harness rejects ANY
|
|
402
|
+
// explicit effort for such a model, and omitting the field is safe
|
|
403
|
+
// (no reasoning capability → no defaultEffort substitution).
|
|
404
|
+
logger?.warn?.(`dsh-mneme dream: model ${route.provider}:${route.model} declares no reasoning capability; ignoring configured effort "${configuredEffort}"`);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const supported = reasoning.efforts?.map((effort) => effort.id) ?? [];
|
|
408
|
+
if (supported.includes(configuredEffort)) return configuredEffort;
|
|
409
|
+
// Configured effort unsupported → pick defaultEffort if it is supported,
|
|
410
|
+
// else the first declared effort, so the run never trips
|
|
411
|
+
// UNSUPPORTED_REASONING_EFFORT (nor the defaultEffort trap: we always
|
|
412
|
+
// pass an explicit value, so the harness never falls back to a poison
|
|
413
|
+
// default).
|
|
414
|
+
const picked = reasoning.defaultEffort && supported.includes(reasoning.defaultEffort)
|
|
415
|
+
? reasoning.defaultEffort
|
|
416
|
+
: supported[0];
|
|
417
|
+
if (picked) {
|
|
418
|
+
logger?.warn?.(`dsh-mneme dream: model ${route.provider}:${route.model} does not support effort "${configuredEffort}" (supported: ${supported.join(", ")}); using "${picked}"`);
|
|
419
|
+
return picked;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
// Capability query failed — forward the configured value; withEffortFallback
|
|
424
|
+
// still retries on rejection as before.
|
|
425
|
+
logger?.warn?.(`dsh-mneme dream: resolveModelInfo failed (${String(error?.message ?? error)}); forwarding effort as configured`);
|
|
426
|
+
return configuredEffort;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
376
430
|
/**
|
|
377
431
|
* Resolve the LLM route (Issue #25): an explicit plugin config
|
|
378
432
|
* (dreamProvider/dreamModel) is the user's declared override and wins; the
|
|
@@ -665,7 +719,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
665
719
|
// (不带该字段),避免 thinking 模型配置 low/medium 直接整单失败。解析放
|
|
666
720
|
// 在 auditError 检查器里、闭包交回主流程,避免二次解析;解析失败同时如实
|
|
667
721
|
// 记 audit error 并在日志带原始输出前 300 字节,便于定位"推理吞预算返回空体"。
|
|
668
|
-
const effort =
|
|
722
|
+
const effort = await resolveDreamEffort(ctx, route, config.dreamReasoningEffort, logger);
|
|
669
723
|
let decisions = null;
|
|
670
724
|
let streamFailure = "";
|
|
671
725
|
const runConsolidation = (withEffort) => {
|
package/src/settings.js
CHANGED
|
@@ -82,6 +82,10 @@ const FEATURE_FLAG_INT_RANGES = {
|
|
|
82
82
|
const FEATURE_FLAG_STRINGS = [
|
|
83
83
|
"dreamProvider",
|
|
84
84
|
"dreamModel",
|
|
85
|
+
// 睡眠侧专用路由(sleep.js 的 config-first 第三层):面板下拉随
|
|
86
|
+
// /llm-providers 端点一起提供,留空 = 用巩固模型或当前模型。
|
|
87
|
+
"sleepProvider",
|
|
88
|
+
"sleepModel",
|
|
85
89
|
"localEmbedModel",
|
|
86
90
|
"ollamaModel"
|
|
87
91
|
];
|
package/test/api.test.js
CHANGED
|
@@ -527,16 +527,18 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d
|
|
|
527
527
|
assert.equal(res.statusCode, 200);
|
|
528
528
|
const data = JSON.parse(res.body);
|
|
529
529
|
assert.deepEqual(data.overrides, {});
|
|
530
|
-
// effective 覆盖全部
|
|
531
|
-
// dreamSkipInvalid/allowCrossTypeMerge/dreamMinIntervalMinutes
|
|
532
|
-
//
|
|
533
|
-
// dreamProvider/dreamModel 无 schema
|
|
534
|
-
//
|
|
535
|
-
assert.equal(Object.keys(data.effective).length,
|
|
530
|
+
// effective 覆盖全部 37 个白名单键(含 v0.7.20 heatEnabled、Issue #89 新增
|
|
531
|
+
// dreamSkipInvalid/allowCrossTypeMerge/dreamMinIntervalMinutes、面板可调的
|
|
532
|
+
// dreamMaxTokens 与本轮睡眠路由 sleepProvider/sleepModel),未覆盖时取
|
|
533
|
+
// bundle 配置的解析默认值;dreamProvider/dreamModel 无 schema 默认值
|
|
534
|
+
// (Config({}) 解析为 undefined),不编造给前端 → 37 - 2 = 35
|
|
535
|
+
assert.equal(Object.keys(data.effective).length, 35);
|
|
536
536
|
assert.equal(data.effective.dreamSkipInvalid, true);
|
|
537
537
|
assert.equal(data.effective.allowCrossTypeMerge, false);
|
|
538
538
|
assert.equal(data.effective.dreamMinIntervalMinutes, 0);
|
|
539
539
|
assert.equal(data.effective.dreamMaxTokens, 32768);
|
|
540
|
+
assert.equal(data.effective.sleepProvider, "");
|
|
541
|
+
assert.equal(data.effective.sleepModel, "");
|
|
540
542
|
assert.equal(data.effective.autoInject, true);
|
|
541
543
|
assert.equal(data.effective.codingRetrospect, false);
|
|
542
544
|
assert.equal(data.effective.distillMaxChars, 24000);
|
package/test/client.test.js
CHANGED
|
@@ -147,7 +147,7 @@ test("sidebar entry portals above the workspaces region with footer fallback", (
|
|
|
147
147
|
"the portal entry persists across collapse (no footer jump); footer fallback only covers portal failure"
|
|
148
148
|
);
|
|
149
149
|
assert.ok(
|
|
150
|
-
clientSource.includes('className: `${nativeCls} mneme-topentry-native`'
|
|
150
|
+
clientSource.includes('className: `${nativeCls} mneme-topentry-native`'),
|
|
151
151
|
"the entry must reuse the host New-Session button class for native geometry alignment"
|
|
152
152
|
);
|
|
153
153
|
assert.ok(
|
|
@@ -466,3 +466,74 @@ test("heat badges render from the /list projection and self-hide when off", () =
|
|
|
466
466
|
"toggling heat sort must land on the cards view (sort does not apply to the month tree)"
|
|
467
467
|
);
|
|
468
468
|
});
|
|
469
|
+
|
|
470
|
+
// 巩固/睡眠模型路由 UI:下拉数据来自宿主侧已注册适配器(/llm-providers,
|
|
471
|
+
// 云端插件侧端点),「测试连通性」走 POST /test-model 真实最小调用。旧后端
|
|
472
|
+
// 端点 404 时必须回退纯文本输入——前端自门控,不挡旧版本。
|
|
473
|
+
test("consolidation/sleep model routing: provider dropdowns from /llm-providers, connectivity test via /test-model, graceful fallback", () => {
|
|
474
|
+
// 1. 端点探测与降级
|
|
475
|
+
assert.ok(
|
|
476
|
+
clientSource.includes('apiFetch("/api/dsh-mneme/llm-providers")'),
|
|
477
|
+
"the features card must probe GET /llm-providers for the route dropdowns"
|
|
478
|
+
);
|
|
479
|
+
assert.ok(
|
|
480
|
+
/setRoutes\(Array\.isArray\(j && j\.providers\) \? j\.providers : \[\]\)/.test(clientSource),
|
|
481
|
+
"the probe must only accept a {providers: []} shape"
|
|
482
|
+
);
|
|
483
|
+
assert.ok(
|
|
484
|
+
/Array\.isArray\(routes\)\s*\?\s*routeSelects\("dreamProvider", "dreamModel"\)/.test(clientSource),
|
|
485
|
+
"dream routing must upgrade to dropdowns only when the probe succeeded"
|
|
486
|
+
);
|
|
487
|
+
assert.ok(
|
|
488
|
+
/:\s*h\(react\.Fragment, null, strRow\("dreamProvider"\), strRow\("dreamModel"\)\)/.test(clientSource),
|
|
489
|
+
"when /llm-providers is unavailable the plain text inputs must remain (old-backend fallback)"
|
|
490
|
+
);
|
|
491
|
+
// 2. 睡眠侧行:跟随 sleepModeEnabled 门控,与巩固共用数据源
|
|
492
|
+
assert.ok(
|
|
493
|
+
/const sleepSub = eff\.sleepModeEnabled && Array\.isArray\(routes\) && h\("div", \{ className: "mneme-featsub" \},\s*\n\s*routeSelects\("sleepProvider", "sleepModel"\)/.test(clientSource),
|
|
494
|
+
"the sleep route row must gate on sleepModeEnabled and share the providers source"
|
|
495
|
+
);
|
|
496
|
+
// 3. 连通性测试:真实最小调用 + 结果展示(成功/失败 + 耗时 + 报错原因)
|
|
497
|
+
assert.ok(
|
|
498
|
+
clientSource.includes('apiFetch("/api/dsh-mneme/test-model", {'),
|
|
499
|
+
"the connectivity test must POST /test-model"
|
|
500
|
+
);
|
|
501
|
+
assert.ok(
|
|
502
|
+
/body: JSON\.stringify\(\{ provider, model \}\)/.test(clientSource),
|
|
503
|
+
"the test payload must carry the selected provider/model (empty = follow default route)"
|
|
504
|
+
);
|
|
505
|
+
assert.ok(
|
|
506
|
+
/typeof j\.durationMs === "number" \? j\.durationMs : Date\.now\(\) - started/.test(clientSource),
|
|
507
|
+
"the result duration must prefer the backend's durationMs and fall back to client timing"
|
|
508
|
+
);
|
|
509
|
+
assert.ok(
|
|
510
|
+
/detail: \[j\.modelId, j\.reply \? "「" \+ j\.reply \+ "」" : ""\]\.filter\(Boolean\)\.join\(" · "\)/.test(clientSource),
|
|
511
|
+
"the success line must surface the model's actual reply (backend caps it at 100 chars)"
|
|
512
|
+
);
|
|
513
|
+
assert.ok(
|
|
514
|
+
/detail: \[j\.modelId, j\.error \|\| "HTTP " \+ res\.status\]\.filter\(Boolean\)\.join\(" · "\)/.test(clientSource),
|
|
515
|
+
"the failure line must carry the tested modelId plus the backend error (502/400 bodies)"
|
|
516
|
+
);
|
|
517
|
+
assert.ok(
|
|
518
|
+
clientSource.includes("memory.features.modelTestOk") && clientSource.includes("memory.features.modelTestFail"),
|
|
519
|
+
"the result line must render success/failure with the localized labels"
|
|
520
|
+
);
|
|
521
|
+
// 4. 下拉改动即提交(与 embedProvider 一致),当前值不在枚举时保留为额外选项
|
|
522
|
+
assert.ok(
|
|
523
|
+
/setStrs\(\(c\) => \(\{ \.\.\.c, \[key\]: v \}\)\);\s*\n\s*put\(\{ \[key\]: v \}\)/.test(clientSource),
|
|
524
|
+
"select changes must commit through the features PUT like the embed provider select"
|
|
525
|
+
);
|
|
526
|
+
assert.ok(
|
|
527
|
+
/curM && !mVals\.includes\(curM\) \? h\("option", \{ key: "current", value: curM \}, curM\) : null/.test(clientSource),
|
|
528
|
+
"a configured value missing from the provider's model list must survive as an extra option"
|
|
529
|
+
);
|
|
530
|
+
// 5. 双语 i18n 与样式
|
|
531
|
+
for (const key of ["routeFollowDefault", "modelTest", "modelTesting", "modelTestOk", "modelTestFail", "sleepModelHint"]) {
|
|
532
|
+
const occurrences = clientSource.split(`"memory.features.${key}"`).length - 1;
|
|
533
|
+
assert.ok(occurrences >= 2, `i18n key memory.features.${key} must exist in both zh and en (got ${occurrences})`);
|
|
534
|
+
}
|
|
535
|
+
assert.ok(
|
|
536
|
+
clientSource.includes(".mneme-routeselect{width:240px;max-width:60%}"),
|
|
537
|
+
"the route selects must share the string-input width budget"
|
|
538
|
+
);
|
|
539
|
+
});
|
|
@@ -413,3 +413,166 @@ test("sleep passes the stream failure accessor so a stream-level effort rejectio
|
|
|
413
413
|
assert.equal("reasoningEffort" in captured[1], false, "conflict retry omits the rejected effort field");
|
|
414
414
|
store.close();
|
|
415
415
|
});
|
|
416
|
+
|
|
417
|
+
// ------------------------------------------------------------------ defaultEffort trap
|
|
418
|
+
// DSH Desktop's volcano-engine adapter declares reasoning.defaultEffort="low"
|
|
419
|
+
// for a model that rejects "low", so omitting the field is NOT a safe retry —
|
|
420
|
+
// the harness substitutes the poison default and fails again. resolveDreamEffort
|
|
421
|
+
// queries resolveModelInfo up front and forwards a value that is actually in
|
|
422
|
+
// the model's declared efforts, so the first attempt already carries a
|
|
423
|
+
// supported effort and never trips UNSUPPORTED_REASONING_EFFORT.
|
|
424
|
+
|
|
425
|
+
test("defaultEffort trap: configured 'low' remapped to the first supported effort when default is poison", async () => {
|
|
426
|
+
const store = createStore(":memory:");
|
|
427
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
428
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
429
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
430
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
431
|
+
const captured = [];
|
|
432
|
+
const infoCalls = [];
|
|
433
|
+
const ctx = dreamCtx({
|
|
434
|
+
captured,
|
|
435
|
+
onConsolidation: () => JSON.stringify([
|
|
436
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
437
|
+
])
|
|
438
|
+
});
|
|
439
|
+
// The adapter's capability report: defaultEffort "low" is NOT in efforts
|
|
440
|
+
// (the model rejects it) — exactly the volcano-engine/deepseek-v4-flash trap.
|
|
441
|
+
ctx.llm.resolveModelInfo = async (provider, model) => {
|
|
442
|
+
infoCalls.push([provider, model]);
|
|
443
|
+
return {
|
|
444
|
+
provider,
|
|
445
|
+
model,
|
|
446
|
+
reasoning: {
|
|
447
|
+
efforts: [{ id: "medium" }, { id: "high" }],
|
|
448
|
+
defaultEffort: "low"
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
};
|
|
452
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
453
|
+
assert.equal(result.ok, true, "run succeeds without ever tripping the poison default");
|
|
454
|
+
assert.ok(result.applied > 0, "consolidation lands changes");
|
|
455
|
+
assert.deepEqual(infoCalls[0], ["mock", "mock-model"], "capability queried for the exact dream route");
|
|
456
|
+
assert.equal(captured[0].reasoningEffort, "medium", "poison 'low' remapped to the first supported effort");
|
|
457
|
+
assert.equal(captured[1].reasoningEffort, "medium", "summary pass uses the same resolved effort");
|
|
458
|
+
store.close();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("defaultEffort trap: model with no reasoning capability omits the field entirely", async () => {
|
|
462
|
+
const store = createStore(":memory:");
|
|
463
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
464
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
465
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
466
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
467
|
+
const captured = [];
|
|
468
|
+
const ctx = dreamCtx({
|
|
469
|
+
captured,
|
|
470
|
+
onConsolidation: () => JSON.stringify([
|
|
471
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
472
|
+
])
|
|
473
|
+
});
|
|
474
|
+
// Non-thinking model (e.g. deepseek-v4-flash): adapter reports no reasoning
|
|
475
|
+
// capability, so ANY explicit effort would be rejected — the helper must
|
|
476
|
+
// drop it, which is the harness's safe "no reasoning" path.
|
|
477
|
+
ctx.llm.resolveModelInfo = async () => ({ provider: "mock", model: "mock-model", reasoning: undefined });
|
|
478
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
479
|
+
assert.equal(result.ok, true);
|
|
480
|
+
for (const options of captured) {
|
|
481
|
+
assert.equal("reasoningEffort" in options, false, "no reasoning capability -> effort omitted, never rejected");
|
|
482
|
+
}
|
|
483
|
+
store.close();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("defaultEffort trap: configured effort supported is forwarded verbatim", async () => {
|
|
487
|
+
const store = createStore(":memory:");
|
|
488
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
489
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
490
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
491
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
492
|
+
const captured = [];
|
|
493
|
+
const ctx = dreamCtx({
|
|
494
|
+
captured,
|
|
495
|
+
onConsolidation: () => JSON.stringify([
|
|
496
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
497
|
+
])
|
|
498
|
+
});
|
|
499
|
+
ctx.llm.resolveModelInfo = async () => ({
|
|
500
|
+
provider: "mock",
|
|
501
|
+
model: "mock-model",
|
|
502
|
+
reasoning: { efforts: [{ id: "high" }, { id: "low" }], defaultEffort: "low" }
|
|
503
|
+
});
|
|
504
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
505
|
+
assert.equal(result.ok, true);
|
|
506
|
+
for (const options of captured) {
|
|
507
|
+
assert.equal(options.reasoningEffort, "high", "supported configured value untouched");
|
|
508
|
+
}
|
|
509
|
+
store.close();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("defaultEffort trap: capability query failure falls back to configured effort (retry still guards)", async () => {
|
|
513
|
+
const store = createStore(":memory:");
|
|
514
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
515
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
516
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
517
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
518
|
+
const calls = [];
|
|
519
|
+
const warnings = [];
|
|
520
|
+
const ctx = {
|
|
521
|
+
logger: { warn: (m) => warnings.push(String(m)) },
|
|
522
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
523
|
+
llm: {
|
|
524
|
+
async *stream(options) {
|
|
525
|
+
calls.push(options);
|
|
526
|
+
if (options.reasoningEffort) {
|
|
527
|
+
throw new Error("UNSUPPORTED_REASONING_EFFORT: mock does not support reasoning effort \"high\"");
|
|
528
|
+
}
|
|
529
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
530
|
+
if (userText.startsWith("id=")) {
|
|
531
|
+
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
532
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
533
|
+
]) };
|
|
534
|
+
} else {
|
|
535
|
+
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
536
|
+
}
|
|
537
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
538
|
+
},
|
|
539
|
+
// Adapter knows nothing about the model — helper must not crash, and the
|
|
540
|
+
// configured effort flows through so withEffortFallback still retries.
|
|
541
|
+
resolveModelInfo: async () => { throw new Error("adapter not reachable"); }
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "high" });
|
|
545
|
+
assert.equal(result.ok, true, "run succeeds via the no-effort retry");
|
|
546
|
+
assert.equal(calls[0].reasoningEffort, "high", "configured effort forwarded when capability query fails");
|
|
547
|
+
assert.equal("reasoningEffort" in calls[1], false, "rejected effort retried without the field");
|
|
548
|
+
assert.ok(warnings.some((w) => w.includes("resolveModelInfo failed")), "capability-query failure is logged");
|
|
549
|
+
store.close();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("defaultEffort trap: sleep conflict pass remaps a poison effort too", async () => {
|
|
553
|
+
const { store, service, vectorIndex } = sleepSetup();
|
|
554
|
+
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
555
|
+
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
556
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
557
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
558
|
+
const captured = [];
|
|
559
|
+
const ctx = sleepCtx(
|
|
560
|
+
(userText) => userText.startsWith("候选冲突")
|
|
561
|
+
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
562
|
+
: "[]",
|
|
563
|
+
{ provider: "mock", model: "sleep-model" },
|
|
564
|
+
captured
|
|
565
|
+
);
|
|
566
|
+
ctx.llm.resolveModelInfo = async (provider, model) => ({
|
|
567
|
+
provider,
|
|
568
|
+
model,
|
|
569
|
+
reasoning: { efforts: [{ id: "medium" }, { id: "high" }], defaultEffort: "low" }
|
|
570
|
+
});
|
|
571
|
+
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "low" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
572
|
+
assert.equal(result.status, "ok");
|
|
573
|
+
assert.ok(captured.length >= 2, "conflict + pattern passes both hit the LLM");
|
|
574
|
+
for (const options of captured) {
|
|
575
|
+
assert.equal(options.reasoningEffort, "medium", "poison 'low' remapped on sleep passes too");
|
|
576
|
+
}
|
|
577
|
+
store.close();
|
|
578
|
+
});
|