@modusensus/dsh-mneme 0.7.31 → 0.7.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/lib/api.js +79 -6
- package/lib/client.js +217 -30
- package/lib/commands.js +6 -2
- package/lib/config.js +80 -14
- package/lib/dream/decisions.js +147 -14
- package/lib/dream/sleep.js +84 -34
- package/lib/dream.js +250 -60
- package/lib/embedding.js +22 -2
- package/lib/index.js +68 -37
- package/lib/inject.js +16 -9
- package/lib/lang.js +156 -0
- package/lib/local-embedder.js +23 -9
- package/lib/mirror.js +17 -14
- package/lib/reranker.js +23 -8
- package/lib/runtime/adopt.js +176 -0
- package/lib/runtime/closure.js +172 -0
- package/lib/runtime/download.js +247 -0
- package/lib/runtime/layout.js +235 -0
- package/lib/runtime/loader.js +249 -0
- package/lib/runtime/provision.js +237 -0
- package/lib/runtime/tarball.js +130 -0
- package/lib/runtime/verify.js +191 -0
- package/lib/service.js +102 -8
- package/lib/settings.js +36 -2
- package/lib/store.js +26 -8
- package/lib/summarize.js +108 -35
- package/lib/tools.js +119 -0
- package/package.json +7 -2
- package/runtime-manifest.json +596 -0
- package/scripts/build-runtime-manifest.mjs +217 -0
- package/scripts/mneme-runtime.mjs +277 -0
- package/src/api.js +79 -6
- package/src/commands.js +6 -2
- package/src/config.js +80 -14
- package/src/dream/decisions.js +147 -14
- package/src/dream/sleep.js +84 -34
- package/src/dream.js +250 -60
- package/src/embedding.js +22 -2
- package/src/index.js +68 -37
- package/src/inject.js +16 -9
- package/src/lang.js +156 -0
- package/src/local-embedder.js +23 -9
- package/src/mirror.js +17 -14
- package/src/reranker.js +23 -8
- package/src/runtime/adopt.js +176 -0
- package/src/runtime/closure.js +172 -0
- package/src/runtime/download.js +247 -0
- package/src/runtime/layout.js +235 -0
- package/src/runtime/loader.js +249 -0
- package/src/runtime/provision.js +237 -0
- package/src/runtime/tarball.js +130 -0
- package/src/runtime/verify.js +191 -0
- package/src/service.js +102 -8
- package/src/settings.js +36 -2
- package/src/store.js +26 -8
- package/src/summarize.js +108 -35
- package/src/tools.js +119 -0
- package/test/api.test.js +89 -9
- package/test/client.test.js +94 -10
- package/test/config.test.js +17 -1
- package/test/dream-candidate-hybrid.test.js +281 -0
- package/test/dream.test.js +172 -0
- package/test/embedding-ready.test.js +196 -0
- package/test/helpers/runtime-source.js +60 -0
- package/test/helpers/tar-builder.js +80 -0
- package/test/inject.test.js +37 -1
- package/test/lang.test.js +87 -0
- package/test/quality-filter.test.js +68 -0
- package/test/reasoning-effort.test.js +106 -15
- package/test/reindex-backfill.test.js +62 -0
- package/test/runtime-adopt.test.js +206 -0
- package/test/runtime-cli.test.js +89 -0
- package/test/runtime-closure.test.js +193 -0
- package/test/runtime-download.test.js +302 -0
- package/test/runtime-layout.test.js +163 -0
- package/test/runtime-loader.test.js +237 -0
- package/test/runtime-manifest.test.js +158 -0
- package/test/runtime-provision.test.js +101 -0
- package/test/runtime-tarball.test.js +82 -0
- package/test/runtime-verify.test.js +214 -0
- package/test/sleep-action-set.test.js +422 -0
- package/test/store.test.js +14 -0
- package/test/summarize.test.js +224 -0
- package/test/tools.test.js +30 -5
- package/test/webServer-optional.test.js +34 -1
package/src/dream/sleep.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STR, langOf } from "../lang.js";
|
|
1
2
|
// System-level sleep (v0.4.0): an idle-triggered, LLM-assisted deep pass over
|
|
2
3
|
// the memory store. Four independent, fail-safe phases:
|
|
3
4
|
// 1. conflict resolution — high-similarity same-type pairs are either parked
|
|
@@ -15,11 +16,16 @@
|
|
|
15
16
|
// abortable via an AbortController signal (user activity) — phases check the
|
|
16
17
|
// signal between batches so a running cycle yields promptly.
|
|
17
18
|
import { randomUUID, createHash } from "node:crypto";
|
|
18
|
-
import { validateDecisions, applyDecisions } from "./decisions.js";
|
|
19
|
-
import { findPotentialConflicts } from "./clustering.js";
|
|
20
|
-
import { buildReceipt, describeStreamFailure, resolveDreamEffort, withEffortFallback } from "../dream.js";
|
|
19
|
+
import { validateDecisions, applyDecisions, ACTIONS } from "./decisions.js";
|
|
20
|
+
import { findPotentialConflicts, cosineSimilarity } from "./clustering.js";
|
|
21
|
+
import { buildReceipt, describeStreamFailure, resolveDreamEffort, withEffortFallback, maintainIndexAfterDream } from "../dream.js";
|
|
21
22
|
import { computeHeat } from "../heat.js";
|
|
22
23
|
|
|
24
|
+
// Issue #126 review(Copilot):默认档(sleepActionSet="conflict")允许的动作 = 全局
|
|
25
|
+
// ACTIONS 去掉 #126 新增的两个。从 ACTIONS 派生而非硬编码,将来再扩动作集时自动跟随。
|
|
26
|
+
// 只换 prompt 挡不住模型自发输出新动作,必须由校验器按档位收口。
|
|
27
|
+
const LEGACY_ACTIONS = [...ACTIONS].filter((a) => a !== "supersede" && a !== "differentiate");
|
|
28
|
+
|
|
23
29
|
const SUMMARY_MAX = 120;
|
|
24
30
|
// Conflict similarity threshold per strictness level (v0.4.0):
|
|
25
31
|
// gentle only high-confidence pairs (0.92) — first-time users
|
|
@@ -27,26 +33,6 @@ const SUMMARY_MAX = 120;
|
|
|
27
33
|
// aggressive low-confidence pairs too (0.75) — bloated stores
|
|
28
34
|
const CONFLICT_THRESHOLDS = { gentle: 0.92, normal: 0.85, aggressive: 0.75 };
|
|
29
35
|
|
|
30
|
-
const CONFLICT_PROMPT = `你是记忆库冲突仲裁助手。下面是检测到的高相似度记忆对,可能内容矛盾或重复。
|
|
31
|
-
对每一对输出一个 decision 对象:
|
|
32
|
-
- 两条确实矛盾/重复 → { "action": "conflict", "winner": <保留的id>, "loser": <归档的id>, "reason": "理由" }
|
|
33
|
-
- 两条只是主题相近、并无矛盾 → { "action": "keep", "ids": [<两个id>] }
|
|
34
|
-
规则:
|
|
35
|
-
- winner 应为信息更完整、更新或更可信的一条
|
|
36
|
-
- 只使用提供的 id,不要编造
|
|
37
|
-
- 每对必须输出一个 decision
|
|
38
|
-
- 只输出 JSON 数组,不要其他文字`;
|
|
39
|
-
|
|
40
|
-
const PATTERN_PROMPT = `你是记忆库模式发现助手。下面是最近的记忆条目(id、类型、标题、内容)。
|
|
41
|
-
请发现跨条目的稳定模式:用户偏好的规律、反复出现的主题、可复用的工作流或项目规律。
|
|
42
|
-
对每个模式输出一个 create decision:
|
|
43
|
-
{ "action": "create", "type": "pattern", "title": "模式一句话标题", "content": "模式详细描述(2-4句)", "importance": 1-5, "evidence": ["支持该模式的记忆id"] }
|
|
44
|
-
规则:
|
|
45
|
-
- 只输出有据可依的模式,宁缺毋滥
|
|
46
|
-
- evidence 必须是列表中真实存在的 id
|
|
47
|
-
- 最多输出 N 个模式
|
|
48
|
-
- 只输出 JSON 数组,不要其他文字`;
|
|
49
|
-
|
|
50
36
|
function parseJsonArray(text) {
|
|
51
37
|
if (typeof text !== "string") return undefined;
|
|
52
38
|
const start = text.indexOf("[");
|
|
@@ -109,6 +95,7 @@ function makeSummary(m) {
|
|
|
109
95
|
* each pair → winner kept / loser archived. Returns a per-run summary.
|
|
110
96
|
*/
|
|
111
97
|
async function phaseConflicts(ctx, service, config, logger, runId, semantic = null, signal = null) {
|
|
98
|
+
const language = langOf(config);
|
|
112
99
|
const embedder = semantic?.embedder;
|
|
113
100
|
const vectorIndex = semantic?.vectorIndex;
|
|
114
101
|
if (!embedder || !vectorIndex || typeof embedder.embed !== "function") {
|
|
@@ -150,7 +137,24 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
150
137
|
const usableMemories = usable.map((i) => memories[i]);
|
|
151
138
|
const usableVectors = usable.map((i) => vectors[i]);
|
|
152
139
|
|
|
140
|
+
// Issue #126:动作集档位既要决定 prompt,也要决定**校验器允许的动作集合**(见下方
|
|
141
|
+
// allowedActions),并且在候选构造阶段就要知道——full 档需要补跨类型对。因此在这里
|
|
142
|
+
// 解析,而不是等到拼 prompt 时。
|
|
143
|
+
const actionSet = config.sleepActionSet ?? "conflict";
|
|
153
144
|
const pairs = findPotentialConflicts(usableMemories, usableVectors, threshold);
|
|
145
|
+
if (actionSet === "full") {
|
|
146
|
+
// Issue #126 review(Copilot):findPotentialConflicts 只看同类型,而 full 档的
|
|
147
|
+
// prompt 明确宣传 differentiate 用于互补型——互补本来就常跨类型(同一事实的项目侧
|
|
148
|
+
// 与偏好侧)。不补这一遍,那条路径永远选不到。merge 的类型守卫不受影响:它拦在
|
|
149
|
+
// validateDecisions 里(allowCrossTypeMerge),不在候选生成。
|
|
150
|
+
for (let i = 0; i < usableMemories.length; i++) {
|
|
151
|
+
for (let j = i + 1; j < usableMemories.length; j++) {
|
|
152
|
+
if (usableMemories[i].type === usableMemories[j].type) continue;
|
|
153
|
+
const sim = cosineSimilarity(usableVectors[i], usableVectors[j]);
|
|
154
|
+
if (sim > threshold) pairs.push({ a: usableMemories[i], b: usableMemories[j], similarity: sim });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
154
158
|
if (pairs.length === 0) return { status: "skipped", reason: "no conflicts found" };
|
|
155
159
|
|
|
156
160
|
// Dedupe: each memory participates in at most one pair, highest similarity
|
|
@@ -170,7 +174,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
170
174
|
let frozen = 0;
|
|
171
175
|
for (const p of selected) {
|
|
172
176
|
try {
|
|
173
|
-
service.saveConflictPending({ run_id: runId, memory_a: p.a.id, memory_b: p.b.id, reason:
|
|
177
|
+
service.saveConflictPending({ run_id: runId, memory_a: p.a.id, memory_b: p.b.id, reason: STR.similarityReason[language](p.similarity.toFixed(2)) });
|
|
174
178
|
frozen++;
|
|
175
179
|
} catch (error) {
|
|
176
180
|
logger?.warn?.(`dsh-mneme sleep: failed to freeze conflict ${p.a.id}/${p.b.id}: ${String(error)}`);
|
|
@@ -188,10 +192,13 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
188
192
|
snapshot.set(p.b.id, p.b);
|
|
189
193
|
}
|
|
190
194
|
const listText = selected.map((p) =>
|
|
191
|
-
|
|
195
|
+
STR.candidateConflicts[language](p)
|
|
192
196
|
).join("\n\n");
|
|
193
197
|
const sleepEffort = await resolveDreamEffort(ctx, route, config.sleepReasoningEffort, logger);
|
|
194
198
|
let conflictStreamFailure = "";
|
|
199
|
+
// actionSet 已在候选构造前解析(跨类型候选需要它),这里只按档位选 prompt:
|
|
200
|
+
// 默认档保持只有 conflict/keep 的窄 prompt,"full" 才开放六分支。
|
|
201
|
+
const conflictPrompt = actionSet === "full" ? STR.prompts.conflictFull[language] : STR.prompts.conflict[language];
|
|
195
202
|
const runConflict = (withEffort) => {
|
|
196
203
|
conflictStreamFailure = "";
|
|
197
204
|
return streamText(ctx, {
|
|
@@ -201,7 +208,7 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
201
208
|
maxTokens: 2048,
|
|
202
209
|
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
203
210
|
messages: [
|
|
204
|
-
{ role: "system", content: [{ type: "text", text:
|
|
211
|
+
{ role: "system", content: [{ type: "text", text: conflictPrompt }] },
|
|
205
212
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
206
213
|
]
|
|
207
214
|
}, (reason) => { conflictStreamFailure = describeStreamFailure(reason); });
|
|
@@ -218,7 +225,10 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
218
225
|
// 预填作为防御性双保险——漏判读作"未裁决冲突"而非"冲突阶段整体失败"。
|
|
219
226
|
const covered = new Set();
|
|
220
227
|
for (const d of decisions) {
|
|
221
|
-
|
|
228
|
+
// Issue #126 review(Copilot):supersede 同样用 winner/loser 定位(没有 ids),
|
|
229
|
+
// 此前只认 conflict → 它的两个目标都没进 covered → 被补成 implicit keep → 与
|
|
230
|
+
// supersede 的 claim 冲突:严格模式整单拒绝,宽容模式把合成的 keep 记成 skipped。
|
|
231
|
+
if (d?.action === "conflict" || d?.action === "supersede") {
|
|
222
232
|
if (typeof d?.winner === "string") covered.add(d.winner);
|
|
223
233
|
if (typeof d?.loser === "string") covered.add(d.loser);
|
|
224
234
|
} else if (Array.isArray(d?.ids)) {
|
|
@@ -228,15 +238,43 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
228
238
|
for (const id of snapshot.keys()) {
|
|
229
239
|
if (!covered.has(id)) decisions.push({ action: "keep", ids: [id] });
|
|
230
240
|
}
|
|
231
|
-
|
|
232
|
-
|
|
241
|
+
// Issue #126:sleep 是多对批量裁决,严格模式"一票否决"太脆(一对非法就丢掉整轮
|
|
242
|
+
// 成果)→ 与 dream 对齐透传 skipInvalid:非法的那一对只跳过自己,合法子集照常
|
|
243
|
+
// 应用,run 记 degraded。update 上限与保护期同样取 dream 的同一组配置,保证两个
|
|
244
|
+
// 模块对"什么算合法"的判据一致。
|
|
245
|
+
const { ok, errors, skipped } = validateDecisions(decisions, snapshot, {
|
|
246
|
+
maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
|
|
247
|
+
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
248
|
+
skipInvalid: config.dreamSkipInvalid !== false,
|
|
249
|
+
allowCrossTypeMerge: config.allowCrossTypeMerge === true,
|
|
250
|
+
// Issue #126 review(Copilot):默认档必须真的只放行旧动作集——只换 prompt 挡不住
|
|
251
|
+
// 模型自发输出 supersede / differentiate,那样"opt-in 零行为变化"就不成立。
|
|
252
|
+
allowedActions: actionSet === "full" ? null : LEGACY_ACTIONS
|
|
253
|
+
});
|
|
254
|
+
if (!ok) return { status: "failed", error: `invalid decisions: ${errors.join("; ")}`, skipped };
|
|
233
255
|
const { applied, failures, conflicts } = applyDecisions(decisions, service, logger, snapshot, config);
|
|
256
|
+
// Issue #126 review(Copilot):事务内 service.update 会跳过 scheduleEmbed(txDepth>0),
|
|
257
|
+
// 而 sleep 路径此前没有 dream 那样的 post-apply 索引维护——differentiate 的差异注记与
|
|
258
|
+
// supersede 的归档都反映不到缓存向量,"注记进 embedding 防再判重复"就是空承诺。
|
|
259
|
+
if (applied > 0 && semantic?.embedder && semantic?.vectorIndex) {
|
|
260
|
+
try {
|
|
261
|
+
await maintainIndexAfterDream(decisions, service, semantic);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logger?.warn?.(`dsh-mneme sleep: index maintenance failed: ${String(error)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Issue #126 review(Copilot):有决策被跳过时不能报 ok——与 dream 的 degraded 契约
|
|
267
|
+
// 对齐("合法子集已应用"是 degraded 而非 ok),否则消费方分不清整轮与残轮。
|
|
268
|
+
const degraded = skipped.length > 0;
|
|
234
269
|
return {
|
|
235
|
-
status: applied > 0 ? "ok" : failures.length ? "failed" : "noop",
|
|
270
|
+
status: applied > 0 ? (degraded ? "degraded" : "ok") : failures.length ? "failed" : "noop",
|
|
236
271
|
pairs: selected.length,
|
|
237
272
|
applied,
|
|
238
273
|
failures,
|
|
239
|
-
conflicts
|
|
274
|
+
conflicts,
|
|
275
|
+
// Issue #126:#104 同向——被跳过的逐对明细要进审计行,否则事后无法判断
|
|
276
|
+
// 是哪一对、因何被跳过(此前只进 logger.warn,splice 后就地消失)。
|
|
277
|
+
skipped
|
|
240
278
|
};
|
|
241
279
|
}
|
|
242
280
|
|
|
@@ -295,6 +333,7 @@ function phaseDemotion(service, config, logger, runId, signal = null) {
|
|
|
295
333
|
* "every id claimed" invariant is trivially satisfied for pure-create lists.
|
|
296
334
|
*/
|
|
297
335
|
async function phasePatterns(ctx, service, config, logger, runId, signal = null) {
|
|
336
|
+
const language = langOf(config);
|
|
298
337
|
const route = resolveSleepRoute(ctx, config, logger);
|
|
299
338
|
if (!route) return { status: "skipped", reason: "no llm route" };
|
|
300
339
|
const limit = config.sleepPatternMinMemories ?? 100;
|
|
@@ -320,7 +359,7 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
320
359
|
maxTokens: 2048,
|
|
321
360
|
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
322
361
|
messages: [
|
|
323
|
-
{ role: "system", content: [{ type: "text", text:
|
|
362
|
+
{ role: "system", content: [{ type: "text", text: STR.prompts.pattern[language].replace("N", String(maxPatterns)) }] },
|
|
324
363
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
325
364
|
]
|
|
326
365
|
}, (reason) => { patternStreamFailure = describeStreamFailure(reason); });
|
|
@@ -417,7 +456,11 @@ function deriveStatus(phases) {
|
|
|
417
456
|
const list = Object.values(phases);
|
|
418
457
|
if (list.length === 0) return "noop";
|
|
419
458
|
const anyError = list.some((p) => p.status === "failed" || p.status === "error");
|
|
420
|
-
|
|
459
|
+
// Issue #126 review(Copilot):phase 自身可以是 degraded(合法子集已应用但有条目被
|
|
460
|
+
// 跳过),它同样算"有产出"——否则整轮会被误判成 noop,把真实变更报成空轮。
|
|
461
|
+
const anyWork = list.some((p) => p.status === "ok" || p.status === "degraded");
|
|
462
|
+
const anyDegraded = list.some((p) => p.status === "degraded");
|
|
463
|
+
if (anyDegraded) return "degraded";
|
|
421
464
|
if (anyWork && anyError) return "degraded";
|
|
422
465
|
if (anyError) return "failed";
|
|
423
466
|
if (anyWork) return "ok";
|
|
@@ -449,6 +492,12 @@ export async function runSleep(ctx, service, config, logger, semantic = null, si
|
|
|
449
492
|
const status = deriveStatus(phases);
|
|
450
493
|
const route = resolveSleepRoute(ctx, config, logger);
|
|
451
494
|
const totalApplied = Object.values(phases).reduce((n, p) => n + (Number.isInteger(p?.applied) ? p.applied : 0), 0);
|
|
495
|
+
// Issue #126(与 #104 同向):把各 phase 的跳过明细汇总成一行审计数据,带 phase
|
|
496
|
+
// 名(同一次 run 里 conflicts 与 patterns 都可能产出 skipped)。全空时写 NULL,
|
|
497
|
+
// 不落空数组——审计行只记真实发生过的跳过。
|
|
498
|
+
const skippedDetail = Object.entries(phases).flatMap(([phase, p]) =>
|
|
499
|
+
Array.isArray(p?.skipped) ? p.skipped.map((s) => ({ phase, ...s })) : []
|
|
500
|
+
);
|
|
452
501
|
const snapshotHash = createHash("sha256").update(JSON.stringify(phases)).digest("hex");
|
|
453
502
|
const receipt = buildReceipt({
|
|
454
503
|
runId,
|
|
@@ -473,7 +522,8 @@ export async function runSleep(ctx, service, config, logger, semantic = null, si
|
|
|
473
522
|
summary_stored: false,
|
|
474
523
|
receipt,
|
|
475
524
|
policy_epoch: config.policyEpoch ?? 0,
|
|
476
|
-
run_type: "sleep"
|
|
525
|
+
run_type: "sleep",
|
|
526
|
+
skipped: skippedDetail.length ? skippedDetail : undefined
|
|
477
527
|
});
|
|
478
528
|
} catch (error) {
|
|
479
529
|
logger?.warn?.(`dsh-mneme sleep: failed to record audit run: ${String(error)}`);
|