@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/lib/dream.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
|
-
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
2
|
+
import { clusterMemories, findPotentialConflicts, cosineSimilarity } from "./dream/clustering.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { STR, langOf } from "./lang.js";
|
|
4
5
|
export { validateDecisions, applyDecisions, withEffortFallback, describeStreamFailure, resolveDreamEffort, resolveRoute };
|
|
5
6
|
|
|
6
7
|
|
|
@@ -40,52 +41,6 @@ function extractJsonArray(text) {
|
|
|
40
41
|
}
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
43
|
-
const SUMMARY_PROMPT = `你是记忆库摘要助手。根据整理后的记忆,生成一段 150-200 字的记忆库总览,覆盖:用户偏好、活跃项目、关键决策。之后作为会话上下文注入。只输出摘要文本,不要其他内容。`;
|
|
44
|
-
|
|
45
|
-
const CONSOLIDATION_PROMPT = `你是记忆库整理助手。下面是全部记忆条目(id、类型、标题、内容、重要性、更新时间)。
|
|
46
|
-
请执行记忆巩固(consolidation),输出一个决策 JSON 数组。
|
|
47
|
-
|
|
48
|
-
【决策格式(必须严格遵守)】
|
|
49
|
-
每个决策必须是对象,字段固定:
|
|
50
|
-
- "action":必填。取值只能是 "keep" / "merge" / "archive" / "update" / "conflict" 之一(字段名必须是 action,严禁写成 type)
|
|
51
|
-
- "ids":必填,数组,本决策涉及的记忆 id 列表
|
|
52
|
-
- "reason":可选,字符串,决策理由
|
|
53
|
-
- "importance":可选,整数 1-5
|
|
54
|
-
- merge 额外字段:"keepSource"(单个 id 字符串,必须是 ids 之一)+ 合并后的 "title"、"content"
|
|
55
|
-
- conflict 额外字段:"winner" 与 "loser",都是【单个 id 字符串,不是数组】
|
|
56
|
-
- update 额外字段:修正后的 "title" 和/或 "content";"ids" 只能包含一个 id
|
|
57
|
-
|
|
58
|
-
【决策 JSON 示例】
|
|
59
|
-
[
|
|
60
|
-
{ "action": "merge", "ids": ["m1", "m2"], "keepSource": "m1", "title": "合并标题", "content": "合并后的摘要内容", "importance": 4, "reason": "主题相近" },
|
|
61
|
-
{ "action": "conflict", "winner": "m3", "loser": "m4", "reason": "内容矛盾,保留更新的信息" },
|
|
62
|
-
{ "action": "update", "ids": ["m5"], "content": "修正后的内容", "reason": "信息过时" },
|
|
63
|
-
{ "action": "archive", "ids": ["m6"], "reason": "重复或过时" }
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
【任务】
|
|
67
|
-
1. 识别主题相近的条目 → merge(合并为更精炼的摘要,保留信息最完整的 id 作为 keepSource)
|
|
68
|
-
2. 识别重复/过时信息 → archive
|
|
69
|
-
3. 识别内容矛盾的条目 → conflict(按时间新旧、来源完整性、信息具体程度判断 winner/loser)
|
|
70
|
-
4. 发现单条记忆中的信息过时、错误或遗漏 → update(直接修正内容)
|
|
71
|
-
- update 的 ids 只能包含一个 id
|
|
72
|
-
- 必须提供修正后的 title 和/或 content
|
|
73
|
-
- 仅当内容确实需要修正时才使用,不要滥用
|
|
74
|
-
- 每次整理最多输出 2 个 update
|
|
75
|
-
- 24 小时内新建的记忆不可 update
|
|
76
|
-
5. 无问题的条目无需输出(未提及的条目将自动保留 keep)
|
|
77
|
-
|
|
78
|
-
【硬性规则】
|
|
79
|
-
- 字段名必须精确为 "action",严禁写成 "type";字段名统一用双引号
|
|
80
|
-
- conflict 的 winner/loser、merge 的 keepSource 都是【单个 id 字符串,绝不是数组】
|
|
81
|
-
- 每条记忆最多被 claim 一次:同一个 id 不能出现在多个决策中(同一 id 不能被 merge 和 conflict/archive 等重复占用)
|
|
82
|
-
- 未在决策中提及的记忆将自动保留(keep),无需为每条记忆输出 keep
|
|
83
|
-
- merge 的 keepSource 必须是 ids 之一
|
|
84
|
-
- 仅合并同类型条目(type 相同)
|
|
85
|
-
- 不要编造 ids;只使用提供的 id
|
|
86
|
-
- 重要性 1-5,合并后取最高
|
|
87
|
-
- 只输出 JSON 数组,不要其他文字`;
|
|
88
|
-
|
|
89
44
|
function totalChars(memories) {
|
|
90
45
|
return memories.reduce((sum, m) => sum + (m.title?.length ?? 0) + (m.content?.length ?? 0), 0);
|
|
91
46
|
}
|
|
@@ -154,6 +109,13 @@ export function buildOutcome(decisions) {
|
|
|
154
109
|
byId[d.loser] = "conflict-archived";
|
|
155
110
|
} else if (d.action === "update") {
|
|
156
111
|
for (const id of d.ids) byId[id] = "updated";
|
|
112
|
+
} else if (d.action === "supersede") {
|
|
113
|
+
// Issue #126 review(Copilot):新动作此前在 outcome.byId 里没有 disposition,
|
|
114
|
+
// 审计行产了 receipt 却看不出这些目标被如何处置。
|
|
115
|
+
byId[d.winner] = "supersede-winner";
|
|
116
|
+
byId[d.loser] = "superseded-archived";
|
|
117
|
+
} else if (d.action === "differentiate") {
|
|
118
|
+
for (const id of d.ids ?? []) byId[id] = "differentiated";
|
|
157
119
|
}
|
|
158
120
|
}
|
|
159
121
|
return { byId };
|
|
@@ -226,6 +188,32 @@ function buildRecordReceipts({ runId, committed, snapshot, policyEpoch }) {
|
|
|
226
188
|
kind: "update",
|
|
227
189
|
input_digest: hashDecisionInput([at(c.ids[0])].filter(Boolean))
|
|
228
190
|
});
|
|
191
|
+
} else if (c.action === "supersede") {
|
|
192
|
+
// Issue #126:演进型裁决——record 记被取代的一方。winner 正文未被改动,
|
|
193
|
+
// 不需要自己的 receipt;loser 归档并追加"已被取代"注记。
|
|
194
|
+
receipts.push({
|
|
195
|
+
...base,
|
|
196
|
+
receipt_id: randomUUID(),
|
|
197
|
+
record_id: c.loser,
|
|
198
|
+
kind: "supersede",
|
|
199
|
+
input_digest: hashDecisionInput([at(c.winner), at(c.loser)].filter(Boolean)),
|
|
200
|
+
winner_id: c.winner,
|
|
201
|
+
loser_id: c.loser
|
|
202
|
+
});
|
|
203
|
+
} else if (c.action === "differentiate") {
|
|
204
|
+
// Issue #126:互补型裁决——两条都保留,各记一条 receipt。数量没有变化,
|
|
205
|
+
// count_before/after 固定 1/1(该记录只是被追加了差异注记)。
|
|
206
|
+
for (const id of c.ids ?? []) {
|
|
207
|
+
receipts.push({
|
|
208
|
+
...base,
|
|
209
|
+
receipt_id: randomUUID(),
|
|
210
|
+
record_id: id,
|
|
211
|
+
kind: "differentiate",
|
|
212
|
+
input_digest: hashDecisionInput([at(id)].filter(Boolean)),
|
|
213
|
+
count_before: 1,
|
|
214
|
+
count_after: 1
|
|
215
|
+
});
|
|
216
|
+
}
|
|
229
217
|
}
|
|
230
218
|
}
|
|
231
219
|
return receipts;
|
|
@@ -385,11 +373,48 @@ async function withEffortFallback(ctx, effort, attempt, fallback, getStreamError
|
|
|
385
373
|
* actually accepted — or omit the field entirely when the model declares no
|
|
386
374
|
* reasoning capability at all.
|
|
387
375
|
*
|
|
376
|
+
* Unset vs explicit 'none' (Issue #135 建议 4/5): these are different intents.
|
|
377
|
+
* Explicit 'none' = the user asked to omit the field (provider default
|
|
378
|
+
* applies). Unset is the out-of-the-box state — dream/sleep calls must produce
|
|
379
|
+
* JSON, and omission lets the harness substitute the model's defaultEffort
|
|
380
|
+
* (thinking-type models often default to high), which drains the token budget
|
|
381
|
+
* and returns an empty body. So unset resolves to the LOWEST effort the model
|
|
382
|
+
* declares instead of omitting; when the capability query is unavailable the
|
|
383
|
+
* field stays omitted (fail-safe, retry guard unchanged).
|
|
384
|
+
*
|
|
388
385
|
* @returns a supported effort id, or null when no effort should be sent, or the
|
|
389
386
|
* configured value unchanged when the capability query is unavailable.
|
|
390
387
|
*/
|
|
388
|
+
|
|
389
|
+
// 常识档位排序(越靠前推理开销越低);未知档位排最后(宁可交给显式配置)。
|
|
390
|
+
const EFFORT_RANK = { minimal: 0, off: 0, low: 1, medium: 2, high: 3, max: 4 };
|
|
391
|
+
|
|
392
|
+
function lowestSupportedEffort(reasoning) {
|
|
393
|
+
const ids = (reasoning?.efforts ?? []).map((e) => e?.id).filter(Boolean);
|
|
394
|
+
if (ids.length === 0) return null;
|
|
395
|
+
return [...ids].sort((a, b) => (EFFORT_RANK[a] ?? 99) - (EFFORT_RANK[b] ?? 99))[0];
|
|
396
|
+
}
|
|
397
|
+
|
|
391
398
|
async function resolveDreamEffort(ctx, route, configuredEffort, logger) {
|
|
392
|
-
|
|
399
|
+
// 显式 'none':用户要求省略字段(服务商自带默认生效),照旧。
|
|
400
|
+
if (configuredEffort === "none") return null;
|
|
401
|
+
if (!configuredEffort) {
|
|
402
|
+
// 未配置(开箱默认):取模型声明的最低档,避免 defaultEffort 顶上(建议 4/5)。
|
|
403
|
+
if (typeof ctx?.llm?.resolveModelInfo !== "function") return null;
|
|
404
|
+
try {
|
|
405
|
+
const info = await ctx.llm.resolveModelInfo(route.provider, route.model);
|
|
406
|
+
const reasoning = info?.reasoning;
|
|
407
|
+
if (!reasoning) return null; // 无推理声明:省略即安全(无 defaultEffort 可顶上)
|
|
408
|
+
const lowest = lowestSupportedEffort(reasoning);
|
|
409
|
+
if (!lowest) return null;
|
|
410
|
+
logger?.info?.(`dsh-mneme dream: no reasoningEffort configured for ${route.provider}:${route.model}; using lowest supported "${lowest}" instead of the model default "${reasoning.defaultEffort ?? "n/a"}"`);
|
|
411
|
+
return lowest;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
// 能力查询失败:省略字段(fail-safe),withEffortFallback 照旧兜底。
|
|
414
|
+
logger?.warn?.(`dsh-mneme dream: resolveModelInfo failed (${String(error?.message ?? error)}); omitting reasoningEffort`);
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
393
418
|
// Capability query unavailable (older harness / minimal mocks): forward the
|
|
394
419
|
// configured value as before — absence of the API proves nothing about the
|
|
395
420
|
// model, and withEffortFallback still guards against rejection.
|
|
@@ -480,12 +505,112 @@ async function collectVectors(memories, semantic) {
|
|
|
480
505
|
return vectors.some((v) => !v) ? null : vectors;
|
|
481
506
|
}
|
|
482
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Issue #125:把高相似对聚成**连通分量组**——一个簇 = 一个候选单元。直接按对展开
|
|
510
|
+
* 会把同一簇拆成互相抢占的多个决策(校验器有 one-claim 规则),成组才是正确粒度
|
|
511
|
+
* (与 sleep 冲突阶段的贪心"每记忆每轮最多进一对"同理)。
|
|
512
|
+
* 组内最小相似度用于组间排序:最该处理的簇先入选。
|
|
513
|
+
*/
|
|
514
|
+
function similarityGroups(pairs) {
|
|
515
|
+
const parent = new Map();
|
|
516
|
+
const find = (x) => {
|
|
517
|
+
let root = x;
|
|
518
|
+
while (parent.get(root) !== root) root = parent.get(root);
|
|
519
|
+
return root;
|
|
520
|
+
};
|
|
521
|
+
for (const p of pairs) {
|
|
522
|
+
if (!parent.has(p.a.id)) parent.set(p.a.id, p.a.id);
|
|
523
|
+
if (!parent.has(p.b.id)) parent.set(p.b.id, p.b.id);
|
|
524
|
+
const ra = find(p.a.id);
|
|
525
|
+
const rb = find(p.b.id);
|
|
526
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
527
|
+
}
|
|
528
|
+
const byRoot = new Map();
|
|
529
|
+
for (const p of pairs) {
|
|
530
|
+
for (const m of [p.a, p.b]) {
|
|
531
|
+
const root = find(m.id);
|
|
532
|
+
if (!byRoot.has(root)) byRoot.set(root, new Map());
|
|
533
|
+
byRoot.get(root).set(m.id, m);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return [...byRoot.values()].map((members) => {
|
|
537
|
+
const list = [...members.values()];
|
|
538
|
+
const ids = new Set(list.map((m) => m.id));
|
|
539
|
+
let minSim = 1;
|
|
540
|
+
for (const p of pairs) {
|
|
541
|
+
if (ids.has(p.a.id) && ids.has(p.b.id)) minSim = Math.min(minSim, p.similarity);
|
|
542
|
+
}
|
|
543
|
+
return { members: list, minSim };
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Issue #125:为候选构造收集向量。与 collectVectors 的差别是**允许部分缺失**——
|
|
549
|
+
* 全量库只要有几条嵌入失败就整场返回 null,会让 hybrid 在真实库上几乎永远回落;
|
|
550
|
+
* 拿不到向量的条目只是不参与比较(cosineSimilarity 对非数组返回 0,不会误判)。
|
|
551
|
+
*/
|
|
552
|
+
async function collectCandidateVectors(memories, semantic, logger) {
|
|
553
|
+
const { embedder, vectorIndex } = semantic;
|
|
554
|
+
const vectors = new Array(memories.length);
|
|
555
|
+
const missing = [];
|
|
556
|
+
for (let i = 0; i < memories.length; i++) {
|
|
557
|
+
const cached = vectorIndex.getEmbedding?.(memories[i].id);
|
|
558
|
+
if (cached) vectors[i] = cached;
|
|
559
|
+
else missing.push(i);
|
|
560
|
+
}
|
|
561
|
+
if (missing.length) {
|
|
562
|
+
try {
|
|
563
|
+
const texts = missing.map((i) => [memories[i].title, memories[i].content].filter(Boolean).join("\n"));
|
|
564
|
+
const rows = await embedder.embed(texts);
|
|
565
|
+
missing.forEach((mi, j) => {
|
|
566
|
+
if (rows[j]?.length) {
|
|
567
|
+
vectors[mi] = rows[j];
|
|
568
|
+
vectorIndex.saveEmbedding?.(memories[mi].id, rows[j]);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
} catch (error) {
|
|
572
|
+
logger?.warn?.(`[dsh-mneme] dream hybrid vector backfill failed: ${String(error)}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return vectors;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Issue #125:hybrid 候选成员,按"组内最小相似度"降序展平(高价值簇排前面,
|
|
580
|
+
* 受上限截断时先入选)。同类型比较沿用 findPotentialConflicts;仅当
|
|
581
|
+
* allowCrossTypeMerge 显式开启时再补跨类型高相似对。
|
|
582
|
+
* 任何失败都返回空数组——调用方回落纯窗口,dream 路径绝不因向量层而失败。
|
|
583
|
+
*/
|
|
584
|
+
async function hybridCandidateMembers(allMemories, config, semantic, logger) {
|
|
585
|
+
try {
|
|
586
|
+
const vectors = await collectCandidateVectors(allMemories, semantic, logger);
|
|
587
|
+
const minSim = config.dreamCandidateMinSim ?? 0.85;
|
|
588
|
+
const pairs = findPotentialConflicts(allMemories, vectors, minSim);
|
|
589
|
+
if (config.allowCrossTypeMerge === true) {
|
|
590
|
+
for (let i = 0; i < allMemories.length; i++) {
|
|
591
|
+
for (let j = i + 1; j < allMemories.length; j++) {
|
|
592
|
+
if (allMemories[i].type === allMemories[j].type) continue; // 同类型已由上面覆盖
|
|
593
|
+
const sim = cosineSimilarity(vectors[i], vectors[j]);
|
|
594
|
+
if (sim > minSim) pairs.push({ a: allMemories[i], b: allMemories[j], similarity: sim });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (pairs.length === 0) return [];
|
|
599
|
+
return similarityGroups(pairs)
|
|
600
|
+
.sort((g1, g2) => g2.minSim - g1.minSim)
|
|
601
|
+
.flatMap((g) => g.members);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
logger?.warn?.(`[dsh-mneme] dream hybrid candidate expansion failed (falling back to window): ${String(error)}`);
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
483
608
|
/**
|
|
484
609
|
* Rebuild the vector index after dream decisions so the store and the index
|
|
485
610
|
* stay in sync: merged-away/archived/conflict-loser rows lose their vectors,
|
|
486
611
|
* the merge keeper gets a fresh one.
|
|
487
612
|
*/
|
|
488
|
-
async function maintainIndexAfterDream(decisions, service, semantic) {
|
|
613
|
+
export async function maintainIndexAfterDream(decisions, service, semantic) {
|
|
489
614
|
const { embedder, vectorIndex } = semantic;
|
|
490
615
|
if (!embedder || !vectorIndex || typeof embedder.embedSingle !== "function") return;
|
|
491
616
|
const rebuild = new Map();
|
|
@@ -500,6 +625,18 @@ async function maintainIndexAfterDream(decisions, service, semantic) {
|
|
|
500
625
|
}
|
|
501
626
|
} else if (d.action === "archive" || d.action === "conflict") {
|
|
502
627
|
for (const id of d.ids ?? [d.loser]) vectorIndex.deleteEmbedding(id);
|
|
628
|
+
} else if (d.action === "supersede") {
|
|
629
|
+
// Issue #126 review(Copilot):被取代的一方已归档 → 移除其向量(与 conflict 的
|
|
630
|
+
// loser 同等处置)。winner 正文未变化,向量保持有效。
|
|
631
|
+
vectorIndex.deleteEmbedding(d.loser);
|
|
632
|
+
} else if (d.action === "differentiate") {
|
|
633
|
+
// Issue #126 review(Copilot):双方正文都被追加了差异注记,而 update 在事务内
|
|
634
|
+
// 会因 txDepth>0 跳过 scheduleEmbed —— 不在这里重嵌,注记就进不了 embedding,
|
|
635
|
+
// 注释里承诺的"下一轮不再判成重复"就不成立。
|
|
636
|
+
for (const id of d.ids ?? []) {
|
|
637
|
+
const mem = service.getById(id);
|
|
638
|
+
if (mem) rebuild.set(id, [mem.title, mem.content].filter(Boolean).join("\n"));
|
|
639
|
+
}
|
|
503
640
|
} else if (d.action === "update") {
|
|
504
641
|
const id = d.ids[0];
|
|
505
642
|
const mem = service.getById(id);
|
|
@@ -594,6 +731,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
594
731
|
}
|
|
595
732
|
|
|
596
733
|
async function runDream(ctx, service, config) {
|
|
734
|
+
const language = langOf(config);
|
|
597
735
|
const logger = ctx.logger;
|
|
598
736
|
let memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
599
737
|
if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
|
|
@@ -601,7 +739,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
601
739
|
// 窗口外的旧记忆不进 snapshot(大记忆量下全量快照会撑爆 LLM 输入,配合
|
|
602
740
|
// 隐式 keep 让 run 始终可收敛)。按 updated_at 倒序取前 maxSize 条。
|
|
603
741
|
const maxSize = Number.isInteger(config.dreamMaxSnapshotSize) ? config.dreamMaxSnapshotSize : 200;
|
|
604
|
-
|
|
742
|
+
const windowMemories = [...memories]
|
|
605
743
|
.sort((a, b) => {
|
|
606
744
|
const ta = String(a.updated_at ?? "");
|
|
607
745
|
const tb = String(b.updated_at ?? "");
|
|
@@ -610,6 +748,31 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
610
748
|
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
611
749
|
})
|
|
612
750
|
.slice(0, Math.max(1, maxSize));
|
|
751
|
+
// Issue #125:hybrid 档在窗口之外并入向量翻出的高相似组。候选**总量**仍由
|
|
752
|
+
// maxSize(或显式 dreamCandidateMax)封顶、与库总量解耦——用"按相似度取候选"
|
|
753
|
+
// 替代"调大窗口",输入成本才不随库增长。封顶截断时向量组优先于窗口尾部,
|
|
754
|
+
// 否则"该合并的一对"仍会被时间窗挤掉。
|
|
755
|
+
let selected = windowMemories;
|
|
756
|
+
if ((config.dreamCandidateMode ?? "window") === "hybrid" && semantic?.embedder && semantic?.vectorIndex) {
|
|
757
|
+
const extra = await hybridCandidateMembers(memories, config, semantic, logger);
|
|
758
|
+
if (extra.length > 0) {
|
|
759
|
+
const cap = Math.max(
|
|
760
|
+
Math.max(1, maxSize),
|
|
761
|
+
Number.isInteger(config.dreamCandidateMax) && config.dreamCandidateMax > 0 ? config.dreamCandidateMax : 0
|
|
762
|
+
);
|
|
763
|
+
const merged = [];
|
|
764
|
+
const seen = new Set();
|
|
765
|
+
for (const m of [...extra, ...windowMemories]) {
|
|
766
|
+
if (seen.has(m.id)) continue;
|
|
767
|
+
seen.add(m.id);
|
|
768
|
+
merged.push(m);
|
|
769
|
+
if (merged.length >= cap) break;
|
|
770
|
+
}
|
|
771
|
+
selected = merged;
|
|
772
|
+
logger?.info?.(`[dsh-mneme] dream hybrid candidates: ${merged.length} (window ${windowMemories.length}, vector ${extra.length}, cap ${cap})`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
memories = selected;
|
|
613
776
|
const snapshot = new Map(memories.map((m) => [m.id, m]));
|
|
614
777
|
const route = resolveRoute(ctx, config, logger);
|
|
615
778
|
const runId = randomUUID();
|
|
@@ -664,7 +827,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
664
827
|
outcome: result.outcome,
|
|
665
828
|
applied,
|
|
666
829
|
summary_stored: summaryStored,
|
|
667
|
-
receipt
|
|
830
|
+
receipt,
|
|
831
|
+
// Issue #104:degraded(合法子集已应用)轮被跳过的决策明细。写成独立字段
|
|
832
|
+
// 而非内联进 decisions——degraded 的 decisions 是合法子集,下游按「决策
|
|
833
|
+
// 数组」消费,内联标记会污染其它读者(失败轮的 _validationFailed 是整单
|
|
834
|
+
// 拒绝哨兵,语义与「部分应用」不同,也不复用)。
|
|
835
|
+
skipped: result.skipped
|
|
668
836
|
});
|
|
669
837
|
} catch (error) {
|
|
670
838
|
logger?.warn?.(`dsh-mneme dream: failed to record audit run: ${String(error)}`);
|
|
@@ -687,11 +855,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
687
855
|
const conflictIds = new Set(conflicts.flatMap((c) => [c.a.id, c.b.id]));
|
|
688
856
|
const parts = [];
|
|
689
857
|
clusters.forEach((cluster, ci) => {
|
|
690
|
-
parts.push(
|
|
858
|
+
parts.push(STR.clusterHeader[language](ci + 1));
|
|
691
859
|
for (const m of cluster) {
|
|
692
860
|
parts.push(
|
|
693
861
|
`id=${m.id} | type=${m.type} | importance=${m.importance} | updated=${m.updated_at} | title=${m.title} | content=${m.content}` +
|
|
694
|
-
(conflictIds.has(m.id) ?
|
|
862
|
+
(conflictIds.has(m.id) ? STR.conflictMark[language] : "")
|
|
695
863
|
);
|
|
696
864
|
}
|
|
697
865
|
});
|
|
@@ -712,8 +880,8 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
712
880
|
// winner/loser (validation requires them) but they are treated as tentative
|
|
713
881
|
// candidates — the human makes the final call, not the model.
|
|
714
882
|
const consolidationPrompt = freezeEnabled
|
|
715
|
-
?
|
|
716
|
-
:
|
|
883
|
+
? STR.prompts.consolidation[language] + STR.prompts.freezeSuffix[language]
|
|
884
|
+
: STR.prompts.consolidation[language];
|
|
717
885
|
let decisionText;
|
|
718
886
|
// 加固(v0.8.1):配置的 reasoningEffort 被 provider 拒收时回退重试一次
|
|
719
887
|
// (不带该字段),避免 thinking 模型配置 low/medium 直接整单失败。解析放
|
|
@@ -764,7 +932,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
764
932
|
logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0}; head: ${head})`);
|
|
765
933
|
return finish({ ok: false, error: "no json array in llm output", summary: false });
|
|
766
934
|
}
|
|
767
|
-
const { ok, errors, skipped } = validateDecisions(decisions, snapshot, {
|
|
935
|
+
const { ok, errors, skipped, resolvedShortIds } = validateDecisions(decisions, snapshot, {
|
|
768
936
|
maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
|
|
769
937
|
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
770
938
|
// v0.4.4 fix:显式透传,用户配 dreamImplicitKeep:false 时严格模式必须
|
|
@@ -776,9 +944,28 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
776
944
|
skipInvalid: config.dreamSkipInvalid !== false,
|
|
777
945
|
allowCrossTypeMerge: config.allowCrossTypeMerge === true
|
|
778
946
|
});
|
|
947
|
+
// Issue #135:模型把 UUID 缩写成前缀时,唯一前缀已在校验前被解析回完整 id。
|
|
948
|
+
// 解析量是模型输出质量的一个直接信号(>0 意味着模型在缩写 id),记一条 info
|
|
949
|
+
// 便于事后从日志侧观察该行为的分布。
|
|
950
|
+
if (resolvedShortIds > 0) {
|
|
951
|
+
logger?.info?.(`[dsh-mneme] dream: resolved ${resolvedShortIds} short id prefix(es) to full ids`);
|
|
952
|
+
}
|
|
779
953
|
if (!ok) {
|
|
954
|
+
// Issue #135(观测缺口):失败轮的逐条校验明细此前既不落库也基本不可见——
|
|
955
|
+
// skipped 的 warn 写在 return 之后(失败路径到不了),dream_runs.decisions
|
|
956
|
+
// 落 NULL,上百轮失败零现场。这里把明细前置到日志,并把
|
|
957
|
+
// { _validationFailed, errors, skipped } 随失败行持久化,事后可从审计行
|
|
958
|
+
// 直接定位「模型输出了什么、为何逐条非法」。
|
|
780
959
|
logger?.warn?.(`dsh-mneme dream: invalid decisions: ${errors.join("; ")}`);
|
|
781
|
-
|
|
960
|
+
if (skipped.length > 0) {
|
|
961
|
+
logger?.warn?.(`dsh-mneme dream: ${skipped.length} invalid decision(s) skipped: ${skipped.map((s) => `decision[${s.index}]: ${s.error}`).join("; ")}`);
|
|
962
|
+
}
|
|
963
|
+
return finish({
|
|
964
|
+
ok: false,
|
|
965
|
+
error: `invalid decisions: ${errors.length} errors`,
|
|
966
|
+
summary: false,
|
|
967
|
+
decisions: [{ _validationFailed: true, errors, skipped }]
|
|
968
|
+
});
|
|
782
969
|
}
|
|
783
970
|
const skippedInvalid = skipped.length > 0;
|
|
784
971
|
if (skippedInvalid) {
|
|
@@ -902,7 +1089,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
902
1089
|
maxTokens: config.dreamMaxTokens ?? 2048,
|
|
903
1090
|
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
904
1091
|
messages: [
|
|
905
|
-
{ role: "system", content: [{ type: "text", text:
|
|
1092
|
+
{ role: "system", content: [{ type: "text", text: STR.prompts.dreamSummary[language] }] },
|
|
906
1093
|
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
907
1094
|
]
|
|
908
1095
|
}, reportUsage, (reason) => { summaryStreamFailure = describeStreamFailure(reason); }));
|
|
@@ -920,7 +1107,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
920
1107
|
// must REPLACE the previous overview (not append — that would grow the
|
|
921
1108
|
// summary unboundedly). `_overwrite` still archives the old overview into
|
|
922
1109
|
// content_history before replacing it.
|
|
923
|
-
service.saveWithDedupe({ type: "summary", title:
|
|
1110
|
+
service.saveWithDedupe({ type: "summary", title: STR.summaryTitle[language], content: summaryText.trim(), importance: 5, source: "dream", _overwrite: true });
|
|
924
1111
|
summaryStored = true;
|
|
925
1112
|
// Re-embed the fresh summary so the index stays in sync with the store.
|
|
926
1113
|
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
@@ -966,7 +1153,10 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
966
1153
|
conflicts,
|
|
967
1154
|
failures,
|
|
968
1155
|
frozen: frozenCount,
|
|
969
|
-
summary: summaryStored
|
|
1156
|
+
summary: summaryStored,
|
|
1157
|
+
// Issue #104:只有真发生跳过时才落库(degraded 也可能仅因 summary 为空),
|
|
1158
|
+
// ok 轮留 NULL,避免「空数组」与「无此字段」两种假明细占据审计行。
|
|
1159
|
+
skipped: skippedInvalid ? skipped : undefined
|
|
970
1160
|
});
|
|
971
1161
|
}
|
|
972
1162
|
|
package/lib/embedding.js
CHANGED
|
@@ -81,9 +81,13 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
|
|
|
81
81
|
} catch { /* metadata write is best-effort */ }
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// issue #135: 「配置是否齐备」的唯一判据 —— embed()/embedFor() 的守卫与下面
|
|
85
|
+
// ready/configured 两个 getter 共用,避免以后改一处漏一处。
|
|
86
|
+
const configComplete = (cfg) => !!(cfg?.enabled && cfg.baseUrl && cfg.apiKey && cfg.model);
|
|
87
|
+
|
|
84
88
|
async function embedFor(id, title, content) {
|
|
85
89
|
const cfg = settings.getVectorConfig();
|
|
86
|
-
if (!cfg
|
|
90
|
+
if (!configComplete(cfg)) return;
|
|
87
91
|
const text = [title, content].filter(Boolean).join("\n");
|
|
88
92
|
const vector = await embedText(cfg, text);
|
|
89
93
|
if (vector) {
|
|
@@ -100,6 +104,22 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
|
|
|
100
104
|
// Display name for /semantic: a literal's constructor.name is "Object",
|
|
101
105
|
// which the status card would render verbatim.
|
|
102
106
|
name: "OpenAI",
|
|
107
|
+
/**
|
|
108
|
+
* issue #135: 这个 embedder 没有异步初始化,`"ready" in embedder` 的通用
|
|
109
|
+
* 分支会把它恒判为可用 —— 而 vector-config 四项有缺时它一条都嵌不出来。
|
|
110
|
+
* 对外报真实可用性,让 /semantic 的 ready 不再是「绿色假阳性」。
|
|
111
|
+
* 用 getter 而非快照字段:设置面板改完配置无需重启即生效(与 embed() 同源读取)。
|
|
112
|
+
*/
|
|
113
|
+
get ready() {
|
|
114
|
+
return configComplete(settings.getVectorConfig());
|
|
115
|
+
},
|
|
116
|
+
/**
|
|
117
|
+
* 与 ready 同源,分开两个名字是给 /semantic 用:local/ollama 的
|
|
118
|
+
* ready=false 表示「还在初始化」,而这里的 false 表示「根本没配」。
|
|
119
|
+
*/
|
|
120
|
+
get configured() {
|
|
121
|
+
return configComplete(settings.getVectorConfig());
|
|
122
|
+
},
|
|
103
123
|
/** Fire-and-forget re-embed of a memory after any write. */
|
|
104
124
|
schedule(memory) {
|
|
105
125
|
if (!memory?.id) return;
|
|
@@ -109,7 +129,7 @@ export function createEmbedder({ store, settings, logger, vectorIndex }) {
|
|
|
109
129
|
/** Embed one text and return its vector (null on failure/disabled). */
|
|
110
130
|
async embed(query) {
|
|
111
131
|
const cfg = settings.getVectorConfig();
|
|
112
|
-
if (!cfg
|
|
132
|
+
if (!configComplete(cfg)) return null;
|
|
113
133
|
const vector = await embedText(cfg, query);
|
|
114
134
|
if (vector) _dimension = vector.length;
|
|
115
135
|
return vector;
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createEmbedderByProvider } from "./local-embedder.js";
|
|
|
15
15
|
import { LocalReranker } from "./reranker.js";
|
|
16
16
|
import { createVectorIndex } from "./vector-index.js";
|
|
17
17
|
import { Config, applyLightModePreset } from "./config.js";
|
|
18
|
+
import { langOf } from "./lang.js";
|
|
18
19
|
import { extractEntities } from "./entities/extractor.js";
|
|
19
20
|
import { mkdirSync } from "node:fs";
|
|
20
21
|
import { join } from "node:path";
|
|
@@ -86,6 +87,44 @@ export function createEntityStreamAdapter({ llm, agentDefaultModel, logger }) {
|
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Issue #128: bounded backfill of rows still missing an embedding (active rows
|
|
92
|
+
* only — needsEmbedding filters archived/forgotten). Exported for tests.
|
|
93
|
+
*
|
|
94
|
+
* Runs regardless of the model fingerprint: the old call-site gate returned
|
|
95
|
+
* early when vector_meta already held the embedder's hash, permanently
|
|
96
|
+
* orphaning rows whose embed failed at write time (embedder not ready /
|
|
97
|
+
* provider rate limit) — one successful embed was enough to never backfill
|
|
98
|
+
* again. markModel is idempotent when the fingerprint already matches, so
|
|
99
|
+
* re-running costs nothing beyond the actually-missing rows.
|
|
100
|
+
*/
|
|
101
|
+
export async function backfillMissingEmbeddings({
|
|
102
|
+
store, embedder, vectorIndex, logger,
|
|
103
|
+
maxTotal = 500, batchSize = 10, rateLimitMs = 200
|
|
104
|
+
}) {
|
|
105
|
+
let indexed = 0;
|
|
106
|
+
for (let done = 0; done < maxTotal;) {
|
|
107
|
+
const rows = store.needsEmbedding(batchSize);
|
|
108
|
+
if (!rows.length) break;
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
try {
|
|
111
|
+
const text = [row.title, row.content].filter(Boolean).join("\n");
|
|
112
|
+
const vector = await embedder.embedSingle(text);
|
|
113
|
+
if (vector?.length) {
|
|
114
|
+
store.setEmbedding(row.id, vector);
|
|
115
|
+
indexed++;
|
|
116
|
+
}
|
|
117
|
+
} catch { /* skip the bad row */ }
|
|
118
|
+
}
|
|
119
|
+
done += rows.length;
|
|
120
|
+
// Rate limit: space out batches so the provider is not hammered.
|
|
121
|
+
if (store.needsEmbedding(1).length) await new Promise((r) => setTimeout(r, rateLimitMs));
|
|
122
|
+
}
|
|
123
|
+
if (indexed > 0 && embedder.modelHash) vectorIndex.markModel?.(embedder.modelHash, embedder.dimension);
|
|
124
|
+
logger?.info?.(`[dsh-mneme] auto-reindex backfilled ${indexed} embeddings on boot`);
|
|
125
|
+
return indexed;
|
|
126
|
+
}
|
|
127
|
+
|
|
89
128
|
export const apply = (ctx, config) => {
|
|
90
129
|
const rawCfg = Config(config);
|
|
91
130
|
|
|
@@ -148,7 +187,9 @@ export const apply = (ctx, config) => {
|
|
|
148
187
|
cfg[objKey] = { ...(cfg[objKey] ?? {}), ...sub };
|
|
149
188
|
}
|
|
150
189
|
|
|
151
|
-
|
|
190
|
+
// 记忆语言(memory.language):本实例逐层传入 inject / summarize / dream /
|
|
191
|
+
// sleep / mirror,多实例(如 agent preset 内挂载)互不影响。
|
|
192
|
+
const mirror = createMirror(memoryDir, langOf(cfg));
|
|
152
193
|
const service = createService({ store, mirror, config: cfg, logger: ctx.logger });
|
|
153
194
|
|
|
154
195
|
// F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
|
|
@@ -212,7 +253,18 @@ export const apply = (ctx, config) => {
|
|
|
212
253
|
// model fingerprint after each successful embed (Bug3).
|
|
213
254
|
embedder = createEmbedder({ store, settings, logger: ctx.logger, vectorIndex });
|
|
214
255
|
service.setEmbedder(embedder);
|
|
215
|
-
// legacy OpenAI embedder
|
|
256
|
+
// issue #135: 未配置时明确告警一次。此前 legacy OpenAI embedder 恒报
|
|
257
|
+
// ready=true,向量层「绿的但全哑」可以静默存在很久(本机持续了数周)。
|
|
258
|
+
// 只记日志、不阻断启动:轻量模式与「先跑起来再补配置」都是正当用法。
|
|
259
|
+
if (embedder.configured === false) {
|
|
260
|
+
ctx.logger?.warn?.(
|
|
261
|
+
"[dsh-mneme] 向量层未配置(vector-config 的 enabled/baseUrl/apiKey/model 有缺):"
|
|
262
|
+
+ "语义召回、语义去重、rerank、sleep 冲突检测将静默失效,"
|
|
263
|
+
+ "dream 的语义聚类会退化为全量窗口兜底。"
|
|
264
|
+
+ "请在设置面板补全 embedding 端点与模型,或把 embedProvider 改为 local/ollama。"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
// legacy OpenAI embedder needs no async init → human edits apply right away
|
|
216
268
|
applyHumanEdits();
|
|
217
269
|
} else {
|
|
218
270
|
try {
|
|
@@ -222,6 +274,7 @@ export const apply = (ctx, config) => {
|
|
|
222
274
|
device: cfg.localEmbedDevice,
|
|
223
275
|
batchSize: cfg.localEmbedBatchSize,
|
|
224
276
|
cacheDir: cfg.embedModelCacheDir,
|
|
277
|
+
runtimeDir: cfg.runtimeDir,
|
|
225
278
|
baseUrl: cfg.ollamaBaseUrl,
|
|
226
279
|
logger: ctx.logger
|
|
227
280
|
});
|
|
@@ -273,6 +326,7 @@ export const apply = (ctx, config) => {
|
|
|
273
326
|
scoreThreshold: cfg.rerankScoreThreshold,
|
|
274
327
|
device: cfg.localEmbedDevice,
|
|
275
328
|
cacheDir: cfg.embedModelCacheDir,
|
|
329
|
+
runtimeDir: cfg.runtimeDir,
|
|
276
330
|
logger: ctx.logger
|
|
277
331
|
});
|
|
278
332
|
service.setReranker(reranker);
|
|
@@ -287,10 +341,10 @@ export const apply = (ctx, config) => {
|
|
|
287
341
|
|
|
288
342
|
// Bug2: lazy auto-backfill of missing embeddings on boot. When the vector API
|
|
289
343
|
// is configured and rows still lack an embedding (e.g. written before vector
|
|
290
|
-
// search was enabled)
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
344
|
+
// search was enabled), the backfill runs in the background after a short
|
|
345
|
+
// delay. Gated on cfg.autoReindexOnBoot; rate-limited in small batches so a
|
|
346
|
+
// large backlog never floods the provider. Failures degrade silently —
|
|
347
|
+
// search stays keyword.
|
|
294
348
|
function scheduleAutoReindex() {
|
|
295
349
|
if (cfg.autoReindexOnBoot === false) return;
|
|
296
350
|
const attempt = (tries) => {
|
|
@@ -303,36 +357,13 @@ export const apply = (ctx, config) => {
|
|
|
303
357
|
return;
|
|
304
358
|
}
|
|
305
359
|
if (!store.needsEmbedding(1).length) return; // nothing to backfill
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
let indexed = 0;
|
|
314
|
-
for (let done = 0; done < MAX_TOTAL;) {
|
|
315
|
-
const rows = store.needsEmbedding(BATCH);
|
|
316
|
-
if (!rows.length) break;
|
|
317
|
-
for (const row of rows) {
|
|
318
|
-
try {
|
|
319
|
-
const text = [row.title, row.content].filter(Boolean).join("\n");
|
|
320
|
-
const vector = await embedder.embedSingle(text);
|
|
321
|
-
if (vector?.length) {
|
|
322
|
-
store.setEmbedding(row.id, vector);
|
|
323
|
-
indexed++;
|
|
324
|
-
}
|
|
325
|
-
} catch { /* skip the bad row */ }
|
|
326
|
-
}
|
|
327
|
-
done += rows.length;
|
|
328
|
-
// Rate limit: space out batches so the provider is not hammered.
|
|
329
|
-
if (store.needsEmbedding(1).length) await new Promise((r) => setTimeout(r, 200));
|
|
330
|
-
}
|
|
331
|
-
if (indexed > 0 && current) vectorIndex.markModel?.(current, embedder.dimension);
|
|
332
|
-
ctx.logger?.info?.(`[dsh-mneme] auto-reindex backfilled ${indexed} embeddings on boot`);
|
|
333
|
-
})().catch((error) => {
|
|
334
|
-
ctx.logger?.warn?.(`[dsh-mneme] auto-reindex failed: ${String(error)}`);
|
|
335
|
-
});
|
|
360
|
+
// Issue #128: no fingerprint gate here anymore — a matching fingerprint
|
|
361
|
+
// used to return early and permanently orphan rows whose embed failed
|
|
362
|
+
// at write time. See backfillMissingEmbeddings().
|
|
363
|
+
backfillMissingEmbeddings({ store, embedder, vectorIndex, logger: ctx.logger })
|
|
364
|
+
.catch((error) => {
|
|
365
|
+
ctx.logger?.warn?.(`[dsh-mneme] auto-reindex failed: ${String(error)}`);
|
|
366
|
+
});
|
|
336
367
|
} catch (error) {
|
|
337
368
|
ctx.logger?.warn?.(`[dsh-mneme] auto-reindex failed: ${String(error)}`);
|
|
338
369
|
}
|
|
@@ -345,7 +376,7 @@ export const apply = (ctx, config) => {
|
|
|
345
376
|
// on boot; add/remove re-register live through the API.
|
|
346
377
|
let commands = null;
|
|
347
378
|
if (ctx.commands) {
|
|
348
|
-
commands = createCommandManager({ ctx, settings, logger: ctx.logger });
|
|
379
|
+
commands = createCommandManager({ ctx, settings, logger: ctx.logger, language: langOf(cfg) });
|
|
349
380
|
commands.sync();
|
|
350
381
|
}
|
|
351
382
|
|