@modusensus/dsh-mneme 0.7.31 → 0.8.0
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 +65 -27
- package/lib/api.js +139 -6
- package/lib/client.js +369 -32
- package/lib/commands.js +36 -5
- package/lib/config.js +101 -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 +54 -13
- package/lib/lang.js +168 -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/scope.js +83 -0
- package/lib/service.js +374 -55
- package/lib/settings.js +43 -2
- package/lib/store.js +126 -13
- package/lib/summarize.js +108 -35
- package/lib/tools.js +205 -12
- 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 +139 -6
- package/src/commands.js +36 -5
- package/src/config.js +101 -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 +54 -13
- package/src/lang.js +168 -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/scope.js +83 -0
- package/src/service.js +374 -55
- package/src/settings.js +43 -2
- package/src/store.js +126 -13
- package/src/summarize.js +108 -35
- package/src/tools.js +205 -12
- package/test/api.test.js +175 -9
- package/test/client.test.js +152 -10
- package/test/commands.test.js +40 -3
- 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 +130 -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/scope-retrieval.test.js +197 -0
- package/test/scope-storage.test.js +247 -0
- package/test/scope-strict.test.js +165 -0
- package/test/service.test.js +76 -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/decisions.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { STR, langOf } from "../lang.js";
|
|
2
|
+
|
|
3
|
+
// Issue #126:sleepActionSet="full" 时新增两个动作——supersede(演进:新版取代
|
|
4
|
+
// 旧版)与 differentiate(互补:两条都留、各追加差异注记)。两者的校验与执行都
|
|
5
|
+
// 复用既有 merge/conflict 的机制(winner/loser、casGuard、transaction、receipt)。
|
|
6
|
+
// 调用方(sleep)按档位派生「本档允许的动作集合」时需要读它,故导出。
|
|
7
|
+
export const ACTIONS = new Set(["keep", "merge", "archive", "conflict", "update", "create", "supersede", "differentiate"]);
|
|
2
8
|
|
|
3
9
|
// Epistemic trust (v0.4.5): when config.trustEpistemicWeighting is on, merge
|
|
4
10
|
// keepSource and conflict winners prefer the higher-trust memory. Higher value
|
|
@@ -11,7 +17,8 @@ const EPISTEMIC_PRIORITY = { observation: 3, inferred: 2, subjective: 1 };
|
|
|
11
17
|
* entries are spliced out of this array in place (the caller reuses the same
|
|
12
18
|
* reference downstream for apply/audit); implicit keeps are appended here too.
|
|
13
19
|
* @param snapshot - Map<id, memory> of eligible (non-archived, non-summary) entries.
|
|
14
|
-
* @returns {{ok: boolean, errors: string[], skipped?: Array<{index, action, ids, error}
|
|
20
|
+
* @returns {{ok: boolean, errors: string[], skipped?: Array<{index, action, ids, error}>,
|
|
21
|
+
* resolvedShortIds?: number}}
|
|
15
22
|
*/
|
|
16
23
|
export function validateDecisions(decisions, snapshot, options = {}) {
|
|
17
24
|
const errors = [];
|
|
@@ -25,15 +32,52 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
25
32
|
const skipInvalid = options.skipInvalid === true;
|
|
26
33
|
const maxUpdatePerRun = options.maxUpdatePerRun ?? 2;
|
|
27
34
|
const minAgeHours = options.minAgeHours ?? 24;
|
|
35
|
+
// Issue #126 review(Copilot):档位白名单。sleepActionSet 默认档只换 prompt 是
|
|
36
|
+
// 不够的——ACTIONS 是两个模块共用的,模型自发出 supersede / differentiate 时校验器
|
|
37
|
+
// 照样放行,"opt-in 零行为变化"就成了空话。省略 / null = 不限制(沿用全局 ACTIONS)。
|
|
38
|
+
const allowedActions = Array.isArray(options.allowedActions) ? new Set(options.allowedActions) : null;
|
|
28
39
|
if (!Array.isArray(decisions)) {
|
|
29
|
-
return { ok: false, errors: ["decision list must be an array"] };
|
|
40
|
+
return { ok: false, errors: ["decision list must be an array"], resolvedShortIds: 0 };
|
|
30
41
|
}
|
|
31
42
|
// 空决策 = 模型完整评估后确认无需操作(CONSOLIDATION_PROMPT 明确允许"无问题的
|
|
32
43
|
// 条目无需输出")。合法 JSON [] 不是空体(那是无输出/截断),也不是"残缺输出"
|
|
33
44
|
// ——显式短路直接 ok,避免隐式 keep 的覆盖率检查把 0% 误判为模型坏了。与
|
|
34
45
|
// sleep 的空模式(skipped no-op)语义对齐:下游 applied=0、audit 记 ok。
|
|
35
46
|
if (decisions.length === 0) {
|
|
36
|
-
return { ok: true, errors: [], skipped: [] };
|
|
47
|
+
return { ok: true, errors: [], skipped: [], resolvedShortIds: 0 };
|
|
48
|
+
}
|
|
49
|
+
// Issue #135:模型常把 36 位 UUID 回填成前缀(实测 78/78 个 8 位前缀都能唯一
|
|
50
|
+
// 对应窗口内真实记忆,且 few-shot 示例的 "m1"/"m2" 占位符在诱导这种缩写),而
|
|
51
|
+
// 下方校验用 snapshot.get(id) 整串精确匹配 → 全部 unknown id → claimed=0 →
|
|
52
|
+
// 覆盖率闸整单拒绝,一整轮语义成果被丢。校验前先把「唯一前缀」解析回完整 id
|
|
53
|
+
// (git 短哈希式):仅在 snapshot 内解析,有歧义或匹配不到就原样保留,仍由下
|
|
54
|
+
// 方 unknown id 校验如实报错。就地改写 ids / keepSource / winner / loser,下
|
|
55
|
+
// 游 applyDecisions 与审计拿到的同样是完整 id。
|
|
56
|
+
let resolvedShortIds = 0;
|
|
57
|
+
const resolveShortId = (value) => {
|
|
58
|
+
if (typeof value !== "string" || snapshot.has(value)) return value;
|
|
59
|
+
// 太短不猜(歧义概率高);长度 ≥36 说明不是前缀缩写,交给校验如实报错。
|
|
60
|
+
if (value.length < 8 || value.length >= 36) return value;
|
|
61
|
+
let hit = null;
|
|
62
|
+
let matches = 0;
|
|
63
|
+
for (const id of snapshot.keys()) {
|
|
64
|
+
if (id.startsWith(value)) {
|
|
65
|
+
hit = id;
|
|
66
|
+
if (++matches > 1) return value; // 有歧义,不解析
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (matches === 1) {
|
|
70
|
+
resolvedShortIds++;
|
|
71
|
+
return hit;
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
};
|
|
75
|
+
for (const d of decisions) {
|
|
76
|
+
if (!d || typeof d !== "object") continue;
|
|
77
|
+
if (Array.isArray(d.ids)) d.ids = d.ids.map(resolveShortId);
|
|
78
|
+
if (typeof d.keepSource === "string") d.keepSource = resolveShortId(d.keepSource);
|
|
79
|
+
if (typeof d.winner === "string") d.winner = resolveShortId(d.winner);
|
|
80
|
+
if (typeof d.loser === "string") d.loser = resolveShortId(d.loser);
|
|
37
81
|
}
|
|
38
82
|
const claimed = new Set();
|
|
39
83
|
for (const [index, d] of decisions.entries()) {
|
|
@@ -41,12 +85,26 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
41
85
|
// 单条校验错误先进 local:skipInvalid 模式下整条跳过,严格模式下才并入
|
|
42
86
|
// 全局 errors(沿用 v0.6.9 的双轨结构)。
|
|
43
87
|
const local = [];
|
|
44
|
-
|
|
88
|
+
// Issue #126:supersede 与 conflict 一样用 winner/loser 定位双方(沿用同一套
|
|
89
|
+
// 字段,校验与 receipt 逻辑因此可以直接复用)。
|
|
90
|
+
const ids = d && (d.action === "conflict" || d.action === "supersede") ? [d.winner, d.loser] : (d?.ids ?? []);
|
|
45
91
|
if (!d || typeof d !== "object" || !ACTIONS.has(d.action)) {
|
|
46
92
|
local.push(`${at}: invalid action ${JSON.stringify(d?.action)}`);
|
|
47
|
-
} else if (d.action
|
|
93
|
+
} else if (allowedActions && !allowedActions.has(d.action)) {
|
|
94
|
+
// 动作本身合法,但不在当前档位允许的集合里(例如默认档收到 supersede)。
|
|
95
|
+
local.push(`${at}: action ${JSON.stringify(d.action)} not allowed in this mode`);
|
|
96
|
+
} else if (d.action === "conflict" || d.action === "supersede") {
|
|
48
97
|
if (!d.winner || !d.loser || d.winner === d.loser) {
|
|
49
|
-
local.push(`${at}:
|
|
98
|
+
local.push(`${at}: ${d.action} needs distinct winner and loser`);
|
|
99
|
+
}
|
|
100
|
+
} else if (d.action === "differentiate") {
|
|
101
|
+
// Issue #126:互补型——两条都留下、各追加差异注记。ids 必须 ≥2(随后走下方
|
|
102
|
+
// 通用 id 校验:unknown / archived / summary / 重复 claim),distinctions 必须
|
|
103
|
+
// 非空,否则会写出空注记(写了等于没写,还会污染正文)。
|
|
104
|
+
if (!Array.isArray(d.ids) || d.ids.length < 2) {
|
|
105
|
+
local.push(`${at}: differentiate needs at least two ids`);
|
|
106
|
+
} else if (!Array.isArray(d.distinctions) || !d.distinctions.some((s) => typeof s === "string" && s.trim())) {
|
|
107
|
+
local.push(`${at}: differentiate needs a non-empty distinctions array`);
|
|
50
108
|
}
|
|
51
109
|
} else if (d.action === "create") {
|
|
52
110
|
// Mint a fresh memory (sleep pattern discovery). Claims no existing id,
|
|
@@ -170,14 +228,14 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
170
228
|
// snapshot(claimed.size / snapshot.size < dreamMinExplicitCoverage)时整单拒绝,
|
|
171
229
|
// 而不是用 keep 把绝大部分 snapshot 全部"通过"。
|
|
172
230
|
if (errors.length > 0) {
|
|
173
|
-
return { ok: false, errors, skipped };
|
|
231
|
+
return { ok: false, errors, skipped, resolvedShortIds };
|
|
174
232
|
}
|
|
175
233
|
const minCoverage = options.dreamMinExplicitCoverage ?? 0.5;
|
|
176
234
|
if (options.dreamImplicitKeep !== false) {
|
|
177
235
|
const coverage = snapshot.size > 0 ? claimed.size / snapshot.size : 1;
|
|
178
236
|
if (coverage < minCoverage) {
|
|
179
237
|
errors.push(`explicit decision coverage ${Math.round(coverage * 100)}% < minimum ${Math.round(minCoverage * 100)}%`);
|
|
180
|
-
return { ok: false, errors, skipped };
|
|
238
|
+
return { ok: false, errors, skipped, resolvedShortIds };
|
|
181
239
|
}
|
|
182
240
|
for (const id of snapshot.keys()) {
|
|
183
241
|
if (!claimed.has(id)) survivors.push({ action: "keep", ids: [id] });
|
|
@@ -186,7 +244,7 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
186
244
|
for (const id of snapshot.keys()) {
|
|
187
245
|
if (!claimed.has(id)) errors.push(`memory ${JSON.stringify(id)} missing from decisions`);
|
|
188
246
|
}
|
|
189
|
-
if (errors.length > 0) return { ok: false, errors, skipped };
|
|
247
|
+
if (errors.length > 0) return { ok: false, errors, skipped, resolvedShortIds };
|
|
190
248
|
}
|
|
191
249
|
// 调用方下游(apply/audit)复用同一 decisions 引用:就地同步为 survivors——
|
|
192
250
|
// 在 skipInvalid 模式下去掉被跳过的非法决策;在隐式 keep 下追加补齐的 keep。
|
|
@@ -194,7 +252,7 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
194
252
|
// 当"被跳过的非法决策数 == 隐式补齐的 keep 数"时长度回到相等但内容已变,
|
|
195
253
|
// 被跳过的决策会残留进 apply/audit。一律无条件 splice 最安全。
|
|
196
254
|
decisions.splice(0, decisions.length, ...survivors);
|
|
197
|
-
return { ok: true, errors, skipped };
|
|
255
|
+
return { ok: true, errors, skipped, resolvedShortIds };
|
|
198
256
|
}
|
|
199
257
|
|
|
200
258
|
/** Marker thrown when a decision target changed since the run snapshot. */
|
|
@@ -208,7 +266,7 @@ export class CasConflictError extends Error {
|
|
|
208
266
|
}
|
|
209
267
|
|
|
210
268
|
function decisionIds(d) {
|
|
211
|
-
return d.action === "conflict" ? [d.winner, d.loser] : (d.ids ?? []);
|
|
269
|
+
return d.action === "conflict" || d.action === "supersede" ? [d.winner, d.loser] : (d.ids ?? []);
|
|
212
270
|
}
|
|
213
271
|
|
|
214
272
|
/**
|
|
@@ -297,6 +355,8 @@ function applyOne(d, service, snapshot, config = {}) {
|
|
|
297
355
|
case "archive": return applyArchive(d, service, snapshot);
|
|
298
356
|
case "merge": return applyMerge(d, service, snapshot, config);
|
|
299
357
|
case "conflict": return applyConflict(d, service, snapshot, config);
|
|
358
|
+
case "supersede": return applySupersede(d, service, snapshot, config);
|
|
359
|
+
case "differentiate": return applyDifferentiate(d, service, snapshot, config);
|
|
300
360
|
case "create": return applyCreate(d, service, config);
|
|
301
361
|
default: return applyUpdate(d, service, snapshot, config);
|
|
302
362
|
}
|
|
@@ -330,6 +390,7 @@ function pickBestKeeper(ids, preferred, service) {
|
|
|
330
390
|
* memories. saveWithDedupe dedupes identical mints (idempotent replay-safe).
|
|
331
391
|
*/
|
|
332
392
|
function applyCreate(d, service, config = {}) {
|
|
393
|
+
const language = langOf(config);
|
|
333
394
|
const title = String(d.title ?? "").trim();
|
|
334
395
|
const content = String(d.content ?? "").trim();
|
|
335
396
|
const importance = Number.isInteger(d.importance) ? d.importance : 3;
|
|
@@ -338,7 +399,7 @@ function applyCreate(d, service, config = {}) {
|
|
|
338
399
|
? d.evidence.filter((id) => typeof id === "string")
|
|
339
400
|
: [];
|
|
340
401
|
const body = evidence.length > 0
|
|
341
|
-
?
|
|
402
|
+
? STR.evidenceSuffix[language](content, evidence)
|
|
342
403
|
: content;
|
|
343
404
|
const created = service.saveWithDedupe({ type, title, content: body, importance });
|
|
344
405
|
const memory = created?.memory;
|
|
@@ -408,6 +469,7 @@ function applyMerge(d, service, snapshot, config = {}) {
|
|
|
408
469
|
}
|
|
409
470
|
|
|
410
471
|
function applyConflict(d, service, snapshot, config = {}) {
|
|
472
|
+
const language = langOf(config);
|
|
411
473
|
// Epistemic trust (v0.4.5): when enabled, the observation side of a conflict
|
|
412
474
|
// is preferred as winner over a subjective/inferred one.
|
|
413
475
|
if (config.trustEpistemicWeighting === true) {
|
|
@@ -428,13 +490,84 @@ function applyConflict(d, service, snapshot, config = {}) {
|
|
|
428
490
|
const loserNow = service.getById(d.loser);
|
|
429
491
|
if (!winnerNow || !loserNow || loserNow.archived) return;
|
|
430
492
|
service.update(d.winner, {
|
|
431
|
-
content:
|
|
493
|
+
content: STR.supersededSuffix[language](winnerNow.content, [...loserNow.content].slice(0, 100).join(""))
|
|
432
494
|
});
|
|
433
495
|
service.setArchived(d.loser, true);
|
|
434
496
|
});
|
|
435
497
|
return { applied: 1, committed: { action: "conflict", winner: d.winner, loser: d.loser, count_before: 2, count_after: 1 } };
|
|
436
498
|
}
|
|
437
499
|
|
|
500
|
+
/**
|
|
501
|
+
* supersede(Issue #126,sleepActionSet="full"):演进型重复——新版本取代旧版本。
|
|
502
|
+
* 与 conflict 的关键差别:**注记追加到 loser(被取代方)而不是 winner**。conflict
|
|
503
|
+
* 把"已否决旧信息"写进赢家正文,赢家因此被污染——这正是 #126 的原始抱怨;
|
|
504
|
+
* supersede 把可追溯性留在被归档的那一侧,赢家正文一字不动。
|
|
505
|
+
*
|
|
506
|
+
* 幂等:loser 已归档即视为该决策已落地(重放/并发不会重复追加注记或重复归档)。
|
|
507
|
+
*/
|
|
508
|
+
function applySupersede(d, service, snapshot, config = {}) {
|
|
509
|
+
const language = langOf(config);
|
|
510
|
+
// Epistemic trust(与 conflict 同款):可信度更高的一侧优先作为取代方。
|
|
511
|
+
if (config.trustEpistemicWeighting === true) {
|
|
512
|
+
const pw = EPISTEMIC_PRIORITY[service.getById(d.winner)?.epistemic_status] ?? 0;
|
|
513
|
+
const pl = EPISTEMIC_PRIORITY[service.getById(d.loser)?.epistemic_status] ?? 0;
|
|
514
|
+
if (pl > pw) [d.winner, d.loser] = [d.loser, d.winner];
|
|
515
|
+
}
|
|
516
|
+
const winner = service.getById(d.winner);
|
|
517
|
+
const loser = service.getById(d.loser);
|
|
518
|
+
if (!winner || !loser) return "skipped";
|
|
519
|
+
if (loser.archived) return "skipped"; // 幂等重放:已经取代过了
|
|
520
|
+
service.transaction(() => {
|
|
521
|
+
casGuard(service, snapshot, [d.winner, d.loser]);
|
|
522
|
+
const winnerNow = service.getById(d.winner);
|
|
523
|
+
const loserNow = service.getById(d.loser);
|
|
524
|
+
if (!winnerNow || !loserNow || loserNow.archived) return;
|
|
525
|
+
service.update(d.loser, {
|
|
526
|
+
content: STR.supersededBySuffix[language](loserNow.content, winnerNow.title)
|
|
527
|
+
});
|
|
528
|
+
service.setArchived(d.loser, true);
|
|
529
|
+
});
|
|
530
|
+
return { applied: 1, committed: { action: "supersede", winner: d.winner, loser: d.loser, count_before: 2, count_after: 1 } };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* differentiate(Issue #126,sleepActionSet="full"):互补型重复——两条各自成立、
|
|
535
|
+
* 覆盖不同侧面,不该判输赢。双方都保留(不归档),各追加同一段差异注记;注记进
|
|
536
|
+
* 正文 → 进注入与 embedding → 下一轮不会再被判成重复(这是本动作存在的意义)。
|
|
537
|
+
*
|
|
538
|
+
* 注记由插件按 distinctions 渲染,**不让模型整段重写 content**(重写有篡改/丢失
|
|
539
|
+
* 风险)。幂等:注记已在正文里就不重复追加,否则同一决策重放会不断堆叠。
|
|
540
|
+
*/
|
|
541
|
+
function applyDifferentiate(d, service, snapshot, config = {}) {
|
|
542
|
+
const language = langOf(config);
|
|
543
|
+
const ids = Array.isArray(d.ids) ? d.ids : [];
|
|
544
|
+
const notes = (Array.isArray(d.distinctions) ? d.distinctions : [])
|
|
545
|
+
.filter((s) => typeof s === "string" && s.trim())
|
|
546
|
+
.map((s) => s.trim());
|
|
547
|
+
const marker = STR.differentiatedMarker[language](notes);
|
|
548
|
+
const alive = ids.filter((id) => {
|
|
549
|
+
const m = service.getById(id);
|
|
550
|
+
return m && !m.archived;
|
|
551
|
+
});
|
|
552
|
+
if (alive.length < 2) return "skipped"; // 缺一条即已处理/不可用(幂等)
|
|
553
|
+
if (alive.every((id) => String(service.getById(id)?.content ?? "").includes(marker))) {
|
|
554
|
+
return "skipped"; // 同一条决策重放:注记已存在
|
|
555
|
+
}
|
|
556
|
+
service.transaction(() => {
|
|
557
|
+
casGuard(service, snapshot, ids);
|
|
558
|
+
for (const id of ids) {
|
|
559
|
+
const m = service.getById(id);
|
|
560
|
+
if (!m || m.archived) continue;
|
|
561
|
+
if (String(m.content).includes(marker)) continue; // 单条已注记过
|
|
562
|
+
service.update(id, { content: `${m.content}${marker}` });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
return {
|
|
566
|
+
applied: 1,
|
|
567
|
+
committed: { action: "differentiate", ids, count_before: ids.length, count_after: ids.length }
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
438
571
|
function applyUpdate(d, service, snapshot, config = {}) {
|
|
439
572
|
const id = d.ids[0];
|
|
440
573
|
const mem = service.getById(id);
|
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)}`);
|