@modusensus/dsh-mneme 0.7.24 → 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 +17 -5
- package/README.md +17 -5
- package/lib/api-standalone.js +43 -3
- package/lib/client.js +139 -6
- package/lib/config.js +19 -6
- package/lib/dream/sleep.js +3 -3
- package/lib/dream.js +56 -2
- package/lib/settings.js +4 -0
- package/lib/tools.js +76 -3
- package/package.json +3 -5
- package/src/api-standalone.js +43 -3
- package/src/config.js +19 -6
- package/src/dream/sleep.js +3 -3
- package/src/dream.js +56 -2
- package/src/settings.js +4 -0
- package/src/tools.js +76 -3
- package/test/api.test.js +8 -6
- package/test/client.test.js +73 -2
- package/test/reasoning-effort.test.js +163 -0
- package/test/tools.test.js +54 -3
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/lib/tools.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
2
|
|
|
3
3
|
const TEXT_OUTPUT = (text) => [{ type: "text", text }];
|
|
4
|
+
// Per-registry tool-name registry: guards against duplicate registration on
|
|
5
|
+
// live patch reload (DSH Desktop `patchReload: "live"`) where a plugin may be
|
|
6
|
+
// re-applied on the same tools registry without an intervening unregister.
|
|
7
|
+
const REGISTERED_TOOLS = new WeakMap();
|
|
4
8
|
|
|
5
9
|
// Wire shape emitted by service.toApiList: shared by memory_search and
|
|
6
10
|
// memory_list so their output schemas always declare every key the runtime
|
|
@@ -22,6 +26,12 @@ const MEMORY_ITEM_SCHEMA = {
|
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
export function createTools(ctx, service, config, embedder) {
|
|
29
|
+
const toolsRegistry = ctx.tools;
|
|
30
|
+
let registeredTools = REGISTERED_TOOLS.get(toolsRegistry);
|
|
31
|
+
if (!registeredTools) {
|
|
32
|
+
registeredTools = new Set();
|
|
33
|
+
REGISTERED_TOOLS.set(toolsRegistry, registeredTools);
|
|
34
|
+
}
|
|
25
35
|
const tools = [
|
|
26
36
|
defineTool({
|
|
27
37
|
name: "memory_save",
|
|
@@ -82,7 +92,18 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
94
|
},
|
|
85
|
-
render: (_args, value) =>
|
|
95
|
+
render: (_args, value) => {
|
|
96
|
+
const items = value.items ?? [];
|
|
97
|
+
if (items.length === 0) return TEXT_OUTPUT("No memory entries found.");
|
|
98
|
+
const body = items
|
|
99
|
+
.map((m, i) => {
|
|
100
|
+
const preview = (m.content ?? "").replace(/\s+/g, " ").trim();
|
|
101
|
+
const cut = preview.length > 200 ? `${preview.slice(0, 200)}…` : preview;
|
|
102
|
+
return `[${i + 1}] ${m.title}\n ID: ${m.id} | type: ${m.type} | importance: ${m.importance} | updated: ${m.updated_at}\n ${cut}`;
|
|
103
|
+
})
|
|
104
|
+
.join("\n\n");
|
|
105
|
+
return TEXT_OUTPUT(`Found ${items.length} memory entr${items.length === 1 ? "y" : "ies"}:\n\n${body}`);
|
|
106
|
+
}
|
|
86
107
|
},
|
|
87
108
|
async execute(args) {
|
|
88
109
|
const limit = args.limit ?? 20;
|
|
@@ -117,7 +138,14 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
117
138
|
total: { type: "integer", required: true }
|
|
118
139
|
}
|
|
119
140
|
},
|
|
120
|
-
render: (_args, value) =>
|
|
141
|
+
render: (_args, value) => {
|
|
142
|
+
const items = value.items ?? [];
|
|
143
|
+
if (items.length === 0) return TEXT_OUTPUT(`0 memory entries (of ${value.total}).`);
|
|
144
|
+
const body = items
|
|
145
|
+
.map((m, i) => `[${i + 1}] ${m.title} (type=${m.type}, importance=${m.importance})\n ID: ${m.id} | updated: ${m.updated_at}`)
|
|
146
|
+
.join("\n\n");
|
|
147
|
+
return TEXT_OUTPUT(`${items.length} memory entries (of ${value.total}):\n\n${body}`);
|
|
148
|
+
}
|
|
121
149
|
},
|
|
122
150
|
async execute(args) {
|
|
123
151
|
const includeArchived = args.include_archived === true;
|
|
@@ -131,6 +159,46 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
131
159
|
}
|
|
132
160
|
}),
|
|
133
161
|
|
|
162
|
+
defineTool({
|
|
163
|
+
name: "memory_get",
|
|
164
|
+
description: "Fetch one memory entry by ID and return its full content as text. Use after memory_list to read a specific entry.",
|
|
165
|
+
parameters: {
|
|
166
|
+
id: { type: "string", required: true, description: "Memory id" }
|
|
167
|
+
},
|
|
168
|
+
output: {
|
|
169
|
+
schema: {
|
|
170
|
+
type: "object",
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
properties: {
|
|
173
|
+
memory: {
|
|
174
|
+
type: "object",
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
properties: {
|
|
177
|
+
id: { type: "string", required: true },
|
|
178
|
+
title: { type: "string", required: true },
|
|
179
|
+
type: { type: "string", required: true },
|
|
180
|
+
importance: { type: "integer", required: true },
|
|
181
|
+
tags: { type: "array", items: { type: "string" } },
|
|
182
|
+
content: { type: "string", required: true },
|
|
183
|
+
source: { type: "string" },
|
|
184
|
+
created_at: { type: "string" },
|
|
185
|
+
updated_at: { type: "string" }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
render: (_args, value) => {
|
|
191
|
+
const m = value.memory;
|
|
192
|
+
return TEXT_OUTPUT(`${m.title}\nID: ${m.id} | type: ${m.type} | importance: ${m.importance}\n\n${m.content}`);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
async execute(args) {
|
|
196
|
+
const memory = service.getById(args.id);
|
|
197
|
+
if (memory === undefined) throw new Error("memory not found");
|
|
198
|
+
return { memory: service.toApiList([memory])[0] };
|
|
199
|
+
}
|
|
200
|
+
}),
|
|
201
|
+
|
|
134
202
|
defineTool({
|
|
135
203
|
name: "memory_update",
|
|
136
204
|
description: "Modify an existing memory entry (title, content, type, tags, importance).",
|
|
@@ -267,7 +335,12 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
267
335
|
];
|
|
268
336
|
|
|
269
337
|
for (const tool of tools) {
|
|
270
|
-
|
|
338
|
+
if (registeredTools.has(tool.name)) {
|
|
339
|
+
ctx.logger?.warn?.(`[dsh-mneme] tool "${tool.name}" already registered, skipping duplicate`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
registeredTools.add(tool.name);
|
|
343
|
+
ctx.tools.register(tool);
|
|
271
344
|
}
|
|
272
345
|
|
|
273
346
|
return tools;
|
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",
|
|
@@ -37,9 +37,7 @@
|
|
|
37
37
|
"client": {
|
|
38
38
|
"inject": [
|
|
39
39
|
"slots",
|
|
40
|
-
"locale"
|
|
41
|
-
"layout",
|
|
42
|
-
"connection"
|
|
40
|
+
"locale"
|
|
43
41
|
],
|
|
44
42
|
"platform": "web"
|
|
45
43
|
},
|
|
@@ -54,7 +52,7 @@
|
|
|
54
52
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
55
53
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
56
54
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
57
|
-
"dsh-better-sidebar": "
|
|
55
|
+
"dsh-better-sidebar": "^0.18.0"
|
|
58
56
|
},
|
|
59
57
|
"peerDependenciesMeta": {
|
|
60
58
|
"dsh-better-sidebar": {
|
package/src/api-standalone.js
CHANGED
|
@@ -16,6 +16,46 @@ const DEFAULT_HOST = "127.0.0.1";
|
|
|
16
16
|
// Hardcoded release version (package.json is bumped at publish time and may
|
|
17
17
|
// lag the code that ships in between).
|
|
18
18
|
const VERSION = "0.7.12";
|
|
19
|
+
const MAX_PORT_ATTEMPTS = 20;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Listen with automatic EADDRINUSE recovery. Tries the configured port, then
|
|
23
|
+
* the next MAX_PORT_ATTEMPTS-1 ports, and finally falls back to port 0 so the
|
|
24
|
+
* OS assigns a free port. Multiple DSH profiles/instances sharing the default
|
|
25
|
+
* port no longer leave the standalone API permanently unavailable.
|
|
26
|
+
*/
|
|
27
|
+
function listenWithRetry(server, startPort, host, logger) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let attempt = 0;
|
|
30
|
+
const tryListen = (port) => {
|
|
31
|
+
const onListening = () => {
|
|
32
|
+
server.off("error", onError);
|
|
33
|
+
const address = server.address();
|
|
34
|
+
resolve(address && typeof address === "object" ? address.port : port);
|
|
35
|
+
};
|
|
36
|
+
const onError = (error) => {
|
|
37
|
+
server.off("listening", onListening);
|
|
38
|
+
if (error?.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS - 1) {
|
|
39
|
+
attempt++;
|
|
40
|
+
const next = startPort + attempt;
|
|
41
|
+
logger?.warn?.(`[dsh-mneme] standalone API port ${port} in use, retrying ${next}`);
|
|
42
|
+
tryListen(next);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (error?.code === "EADDRINUSE") {
|
|
46
|
+
logger?.warn?.(`[dsh-mneme] standalone API port ${port} still in use, falling back to OS-assigned port`);
|
|
47
|
+
tryListen(0);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
reject(error);
|
|
51
|
+
};
|
|
52
|
+
server.once("listening", onListening);
|
|
53
|
+
server.once("error", onError);
|
|
54
|
+
server.listen(port, host);
|
|
55
|
+
};
|
|
56
|
+
tryListen(startPort);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
19
59
|
|
|
20
60
|
function sendJson(res, status, payload) {
|
|
21
61
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
@@ -243,10 +283,10 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
|
|
|
243
283
|
logger?.warn?.(`[dsh-mneme] standalone API error: ${String(error)}`);
|
|
244
284
|
});
|
|
245
285
|
const ready = new Promise((resolve, reject) => {
|
|
246
|
-
|
|
247
|
-
server.
|
|
286
|
+
// listening handled by listenWithRetry
|
|
287
|
+
listenWithRetry(server, boundPort, boundHost, logger).then(resolve, reject);
|
|
248
288
|
});
|
|
249
|
-
server.listen
|
|
289
|
+
// server.listen is called inside listenWithRetry
|
|
250
290
|
ready.then(() => {
|
|
251
291
|
const address = server.address();
|
|
252
292
|
if (address && typeof address === "object") {
|
package/src/config.js
CHANGED
|
@@ -52,20 +52,33 @@ export const Config = z.object({
|
|
|
52
52
|
// 起算,失败/degraded 的 run 也占用间隔;间隔内的触发请求静默跳过,下一次
|
|
53
53
|
// 写入事件会重新评估。
|
|
54
54
|
dreamMinIntervalMinutes: z.natural().min(0).max(10080).default(0),
|
|
55
|
-
dreamProvider
|
|
56
|
-
|
|
55
|
+
// 巩固模型路由(settings panel「巩固模型」/ dreamProvider+dreamModel):
|
|
56
|
+
// dream 的记忆沉淀专用 LLM 路由,显式配置优先于 agent 默认模型(config-first,
|
|
57
|
+
// Issue #25)。模型分类声明:
|
|
58
|
+
// - 非思考模型(推荐,如 glm-5-2 类):无 reasoning 声明,effort 请求被 harness
|
|
59
|
+
// 拒绝后 withEffortFallback 去掉字段重试即成功;空体/no json array 风险最低。
|
|
60
|
+
// - 思考模型(如 deepseek-v4-flash-ga 等 v4-flash-ga 系):默认开推理,可能烧光
|
|
61
|
+
// token 预算返回空体;且部分(如 v4-flash-ga)在 harness 侧被声明为不接受任何
|
|
62
|
+
// reasoning effort —— 即使去掉 effort 重试,harness 的 defaultEffort 也会顶上来
|
|
63
|
+
// 再次拒绝(UNSUPPORTED_REASONING_EFFORT),插件 fallback 无法绕开。
|
|
64
|
+
// 选用时建议配 dreamReasoningEffort 并实测;不行就换非思考模型。
|
|
65
|
+
dreamProvider: z.string().description("记忆巩固专用模型的服务商(settings「巩固模型」)。巩固反复失败时,优先改用官方非思考模型的服务商(如 deepseek / glm)。"),
|
|
66
|
+
dreamModel: z.string().description("记忆巩固专用模型。建议选非思考模型(如 deepseek-chat、glm-5-2 类):思考模型可能烧光 token 预算返回空体,导致巩固失败(UNSUPPORTED_REASONING_EFFORT)。"),
|
|
57
67
|
dreamMaxTokens: z.natural().min(256).max(131072).default(32768),
|
|
58
68
|
// Pass-through reasoning effort for dream's LLM calls. 'none' (default)
|
|
59
69
|
// omits the field so the provider's own default applies; low/medium/high
|
|
60
70
|
// are forwarded verbatim. Useful to cap reasoning spend on thinking-type
|
|
61
71
|
// models that would otherwise drain the whole token budget and return an
|
|
62
72
|
// empty body ("no json array in llm output").
|
|
73
|
+
// Caveat: on some thinking models (e.g. v4-flash-ga) the harness declares NO
|
|
74
|
+
// supported effort, so even the fallback retry (field stripped) is rejected
|
|
75
|
+
// again via its defaultEffort — prefer a non-reasoning dreamProvider/dreamModel.
|
|
63
76
|
dreamReasoningEffort: z.union([
|
|
64
77
|
z.const("low"),
|
|
65
78
|
z.const("medium"),
|
|
66
79
|
z.const("high"),
|
|
67
80
|
z.const("none")
|
|
68
|
-
]).default("none"),
|
|
81
|
+
]).default("none").description("巩固模型的推理档位:'none'(默认)用服务商自带默认;low/medium/high 原样传递。模型不支持的值会自动换用其支持的档位(v0.7.26+)。"),
|
|
69
82
|
// 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
|
|
70
83
|
// 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
|
|
71
84
|
// 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
|
|
@@ -238,8 +251,8 @@ export const Config = z.object({
|
|
|
238
251
|
sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
|
|
239
252
|
// Optional LLM route override for sleep's bulk passes (empty = use dream
|
|
240
253
|
// route / agent default model).
|
|
241
|
-
sleepProvider: z.string().default(""),
|
|
242
|
-
sleepModel: z.string().default(""),
|
|
254
|
+
sleepProvider: z.string().default("").description("sleep 深维护专用模型服务商,留空用巩固模型或当前模型。"),
|
|
255
|
+
sleepModel: z.string().default("").description("sleep 深维护专用模型,留空用巩固模型或当前模型;建议同巩固模型选非思考模型。"),
|
|
243
256
|
// Pass-through reasoning effort for sleep's LLM passes, same semantics as
|
|
244
257
|
// dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
|
|
245
258
|
// are forwarded verbatim.
|
|
@@ -248,7 +261,7 @@ export const Config = z.object({
|
|
|
248
261
|
z.const("medium"),
|
|
249
262
|
z.const("high"),
|
|
250
263
|
z.const("none")
|
|
251
|
-
]).default("none"),
|
|
264
|
+
]).default("none").description("同 dreamReasoningEffort:sleep 各阶段 LLM 的推理档位,'none' 用服务商默认。"),
|
|
252
265
|
|
|
253
266
|
// --- epistemic trust: memory source credibility (v0.4.5) -----------------
|
|
254
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/src/tools.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
2
|
|
|
3
3
|
const TEXT_OUTPUT = (text) => [{ type: "text", text }];
|
|
4
|
+
// Per-registry tool-name registry: guards against duplicate registration on
|
|
5
|
+
// live patch reload (DSH Desktop `patchReload: "live"`) where a plugin may be
|
|
6
|
+
// re-applied on the same tools registry without an intervening unregister.
|
|
7
|
+
const REGISTERED_TOOLS = new WeakMap();
|
|
4
8
|
|
|
5
9
|
// Wire shape emitted by service.toApiList: shared by memory_search and
|
|
6
10
|
// memory_list so their output schemas always declare every key the runtime
|
|
@@ -22,6 +26,12 @@ const MEMORY_ITEM_SCHEMA = {
|
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
export function createTools(ctx, service, config, embedder) {
|
|
29
|
+
const toolsRegistry = ctx.tools;
|
|
30
|
+
let registeredTools = REGISTERED_TOOLS.get(toolsRegistry);
|
|
31
|
+
if (!registeredTools) {
|
|
32
|
+
registeredTools = new Set();
|
|
33
|
+
REGISTERED_TOOLS.set(toolsRegistry, registeredTools);
|
|
34
|
+
}
|
|
25
35
|
const tools = [
|
|
26
36
|
defineTool({
|
|
27
37
|
name: "memory_save",
|
|
@@ -82,7 +92,18 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
94
|
},
|
|
85
|
-
render: (_args, value) =>
|
|
95
|
+
render: (_args, value) => {
|
|
96
|
+
const items = value.items ?? [];
|
|
97
|
+
if (items.length === 0) return TEXT_OUTPUT("No memory entries found.");
|
|
98
|
+
const body = items
|
|
99
|
+
.map((m, i) => {
|
|
100
|
+
const preview = (m.content ?? "").replace(/\s+/g, " ").trim();
|
|
101
|
+
const cut = preview.length > 200 ? `${preview.slice(0, 200)}…` : preview;
|
|
102
|
+
return `[${i + 1}] ${m.title}\n ID: ${m.id} | type: ${m.type} | importance: ${m.importance} | updated: ${m.updated_at}\n ${cut}`;
|
|
103
|
+
})
|
|
104
|
+
.join("\n\n");
|
|
105
|
+
return TEXT_OUTPUT(`Found ${items.length} memory entr${items.length === 1 ? "y" : "ies"}:\n\n${body}`);
|
|
106
|
+
}
|
|
86
107
|
},
|
|
87
108
|
async execute(args) {
|
|
88
109
|
const limit = args.limit ?? 20;
|
|
@@ -117,7 +138,14 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
117
138
|
total: { type: "integer", required: true }
|
|
118
139
|
}
|
|
119
140
|
},
|
|
120
|
-
render: (_args, value) =>
|
|
141
|
+
render: (_args, value) => {
|
|
142
|
+
const items = value.items ?? [];
|
|
143
|
+
if (items.length === 0) return TEXT_OUTPUT(`0 memory entries (of ${value.total}).`);
|
|
144
|
+
const body = items
|
|
145
|
+
.map((m, i) => `[${i + 1}] ${m.title} (type=${m.type}, importance=${m.importance})\n ID: ${m.id} | updated: ${m.updated_at}`)
|
|
146
|
+
.join("\n\n");
|
|
147
|
+
return TEXT_OUTPUT(`${items.length} memory entries (of ${value.total}):\n\n${body}`);
|
|
148
|
+
}
|
|
121
149
|
},
|
|
122
150
|
async execute(args) {
|
|
123
151
|
const includeArchived = args.include_archived === true;
|
|
@@ -131,6 +159,46 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
131
159
|
}
|
|
132
160
|
}),
|
|
133
161
|
|
|
162
|
+
defineTool({
|
|
163
|
+
name: "memory_get",
|
|
164
|
+
description: "Fetch one memory entry by ID and return its full content as text. Use after memory_list to read a specific entry.",
|
|
165
|
+
parameters: {
|
|
166
|
+
id: { type: "string", required: true, description: "Memory id" }
|
|
167
|
+
},
|
|
168
|
+
output: {
|
|
169
|
+
schema: {
|
|
170
|
+
type: "object",
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
properties: {
|
|
173
|
+
memory: {
|
|
174
|
+
type: "object",
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
properties: {
|
|
177
|
+
id: { type: "string", required: true },
|
|
178
|
+
title: { type: "string", required: true },
|
|
179
|
+
type: { type: "string", required: true },
|
|
180
|
+
importance: { type: "integer", required: true },
|
|
181
|
+
tags: { type: "array", items: { type: "string" } },
|
|
182
|
+
content: { type: "string", required: true },
|
|
183
|
+
source: { type: "string" },
|
|
184
|
+
created_at: { type: "string" },
|
|
185
|
+
updated_at: { type: "string" }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
render: (_args, value) => {
|
|
191
|
+
const m = value.memory;
|
|
192
|
+
return TEXT_OUTPUT(`${m.title}\nID: ${m.id} | type: ${m.type} | importance: ${m.importance}\n\n${m.content}`);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
async execute(args) {
|
|
196
|
+
const memory = service.getById(args.id);
|
|
197
|
+
if (memory === undefined) throw new Error("memory not found");
|
|
198
|
+
return { memory: service.toApiList([memory])[0] };
|
|
199
|
+
}
|
|
200
|
+
}),
|
|
201
|
+
|
|
134
202
|
defineTool({
|
|
135
203
|
name: "memory_update",
|
|
136
204
|
description: "Modify an existing memory entry (title, content, type, tags, importance).",
|
|
@@ -267,7 +335,12 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
267
335
|
];
|
|
268
336
|
|
|
269
337
|
for (const tool of tools) {
|
|
270
|
-
|
|
338
|
+
if (registeredTools.has(tool.name)) {
|
|
339
|
+
ctx.logger?.warn?.(`[dsh-mneme] tool "${tool.name}" already registered, skipping duplicate`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
registeredTools.add(tool.name);
|
|
343
|
+
ctx.tools.register(tool);
|
|
271
344
|
}
|
|
272
345
|
|
|
273
346
|
return tools;
|
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(
|
|
@@ -235,7 +235,7 @@ test("better-sidebar tab mounts via an inner sub-plugin, standalone mode intact"
|
|
|
235
235
|
"the inner apply must still guard the service shape before registering"
|
|
236
236
|
);
|
|
237
237
|
assert.ok(
|
|
238
|
-
/id: "dsh-mneme:memory"/.test(clientSource),
|
|
238
|
+
/(?:const TAB_ID = |id: )"dsh-mneme:memory"/.test(clientSource),
|
|
239
239
|
"the registered tab id must be package-prefixed"
|
|
240
240
|
);
|
|
241
241
|
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
|
+
});
|