@modusensus/dsh-mneme 0.8.1 → 0.8.2
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 +11 -3
- package/lib/api-standalone.js +12 -0
- package/lib/api.js +12 -2
- package/lib/config.js +8 -0
- package/lib/dream/decisions.js +55 -8
- package/lib/dream/sleep.js +36 -5
- package/lib/dream.js +18 -7
- package/lib/entities/extractor.js +2 -2
- package/lib/index.js +6 -0
- package/lib/lang.js +2 -2
- package/lib/local-embedder.js +9 -1
- package/lib/reranker.js +38 -8
- package/lib/service.js +16 -0
- package/lib/store.js +85 -25
- package/lib/summarize.js +2 -2
- package/lib/tools.js +6 -19
- package/lib/vector-index.js +2 -8
- package/package.json +2 -1
- package/scripts/sync-test-badge.mjs +61 -0
- package/src/api-standalone.js +12 -0
- package/src/api.js +12 -2
- package/src/config.js +8 -0
- package/src/dream/decisions.js +55 -8
- package/src/dream/sleep.js +36 -5
- package/src/dream.js +18 -7
- package/src/entities/extractor.js +2 -2
- package/src/index.js +6 -0
- package/src/lang.js +2 -2
- package/src/local-embedder.js +9 -1
- package/src/reranker.js +38 -8
- package/src/service.js +16 -0
- package/src/store.js +85 -25
- package/src/summarize.js +2 -2
- package/src/tools.js +6 -19
- package/src/vector-index.js +2 -8
- package/test/api.test.js +5 -0
- package/test/dream-archive-guardrail.test.js +116 -0
- package/test/dream-coverage-degrade.test.js +86 -0
- package/test/dream.test.js +15 -11
- package/test/inject-first-round-bm25.test.js +89 -0
- package/test/llm-message-source.test.js +177 -0
- package/test/reranker-single-logit.test.js +73 -0
- package/test/skip-invalid-restore.test.js +7 -5
- package/test/sleep-scheduler-rearm.test.js +89 -0
- package/test/sleep.test.js +2 -2
- package/test/standalone-api.test.js +34 -1
- package/test/store-vector-cache.test.js +98 -0
- package/test/tools-memory-get-schema.test.js +65 -0
package/src/dream.js
CHANGED
|
@@ -910,8 +910,8 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
910
910
|
maxTokens: config.dreamMaxTokens ?? 4096,
|
|
911
911
|
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
912
912
|
messages: [
|
|
913
|
-
{ role: "system", content: [{ type: "text", text: consolidationPrompt }] },
|
|
914
|
-
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
913
|
+
{ role: "system", content: [{ type: "text", text: consolidationPrompt }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
914
|
+
{ role: "user", content: [{ type: "text", text: listText }], source: { kind: "plugin", plugin: "dsh-mneme" } }
|
|
915
915
|
]
|
|
916
916
|
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); }));
|
|
917
917
|
};
|
|
@@ -933,8 +933,10 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
933
933
|
logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0}; head: ${head})`);
|
|
934
934
|
return finish({ ok: false, error: "no json array in llm output", summary: false });
|
|
935
935
|
}
|
|
936
|
-
const { ok, errors, skipped, resolvedShortIds } = validateDecisions(decisions, snapshot, {
|
|
936
|
+
const { ok, errors, skipped, resolvedShortIds, coverageShortfall } = validateDecisions(decisions, snapshot, {
|
|
937
937
|
maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
|
|
938
|
+
// Issue #104 方向 2:archive 批量上限(全局闸门,超限整单拒绝)。
|
|
939
|
+
maxArchivePerRun: config.dreamMaxArchivePerRun,
|
|
938
940
|
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
939
941
|
// v0.4.4 fix:显式透传,用户配 dreamImplicitKeep:false 时严格模式必须
|
|
940
942
|
// 真正生效,dreamMinExplicitCoverage 决定隐式 keep 下的覆盖率下限。
|
|
@@ -972,6 +974,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
972
974
|
if (skippedInvalid) {
|
|
973
975
|
logger?.warn?.(`dsh-mneme dream: ${skipped.length} invalid decision(s) skipped (run degrades): ${skipped.map((s) => s.error).join("; ")}`);
|
|
974
976
|
}
|
|
977
|
+
// Issue #104 方向 1:覆盖率不足降级为 degraded(不再整单拒绝)——合法子集
|
|
978
|
+
// 照常应用、未提及条目隐式 keep,理由随 outcome.degradations 落库供离线定位。
|
|
979
|
+
const coverageDegraded = typeof coverageShortfall === "string" && coverageShortfall.length > 0;
|
|
980
|
+
if (coverageDegraded) {
|
|
981
|
+
logger?.warn?.(`dsh-mneme dream: ${coverageShortfall} (run degrades: valid subset applied, unclaimed ids kept implicitly)`);
|
|
982
|
+
}
|
|
975
983
|
|
|
976
984
|
// Capture pre-update snapshots so the audit records what each update changed.
|
|
977
985
|
const updateSnapshots = {};
|
|
@@ -1087,6 +1095,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1087
1095
|
// claim "merge-archived" (item ②). Conflicts/failures ride along so the
|
|
1088
1096
|
// audit row records why the run diverged.
|
|
1089
1097
|
const outcome = { ...buildOutcome(committed), conflicts, failures };
|
|
1098
|
+
if (coverageDegraded) outcome.degradations = [coverageShortfall];
|
|
1090
1099
|
// Frozen conflicts were not adjudicated: mark both sides pending in the
|
|
1091
1100
|
// per-id outcome so the audit row shows they were parked, not skipped.
|
|
1092
1101
|
if (frozenIds.length) {
|
|
@@ -1130,8 +1139,8 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1130
1139
|
maxTokens: config.dreamMaxTokens ?? 2048,
|
|
1131
1140
|
...(withEffort && effort ? { reasoningEffort: effort } : {}),
|
|
1132
1141
|
messages: [
|
|
1133
|
-
{ role: "system", content: [{ type: "text", text: STR.prompts.dreamSummary[language] }] },
|
|
1134
|
-
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
1142
|
+
{ role: "system", content: [{ type: "text", text: STR.prompts.dreamSummary[language] }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
1143
|
+
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }], source: { kind: "plugin", plugin: "dsh-mneme" } }
|
|
1135
1144
|
]
|
|
1136
1145
|
}, reportUsage, (reason) => { summaryStreamFailure = describeStreamFailure(reason); }));
|
|
1137
1146
|
};
|
|
@@ -1171,7 +1180,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1171
1180
|
// degraded — real consolidation landed but the run did not produce its
|
|
1172
1181
|
// full output: the summary came back empty/missing, or
|
|
1173
1182
|
// skipInvalid dropped individually-invalid decisions
|
|
1174
|
-
// (Issue #89 — marked, not faked)
|
|
1183
|
+
// (Issue #89 — marked, not faked), or the explicit-decision
|
|
1184
|
+
// coverage fell below dreamMinExplicitCoverage (Issue #104
|
|
1185
|
+
// 方向 1 — degrade, don't reject). The valid subset was
|
|
1175
1186
|
// absorbed (ok for the baseline).
|
|
1176
1187
|
let status;
|
|
1177
1188
|
let okResult;
|
|
@@ -1182,7 +1193,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1182
1193
|
status = summaryStored ? "ok" : "noop";
|
|
1183
1194
|
okResult = summaryStored;
|
|
1184
1195
|
} else {
|
|
1185
|
-
status = summaryStored && !skippedInvalid ? "ok" : "degraded";
|
|
1196
|
+
status = summaryStored && !skippedInvalid && !coverageDegraded ? "ok" : "degraded";
|
|
1186
1197
|
okResult = true;
|
|
1187
1198
|
}
|
|
1188
1199
|
return finish({
|
|
@@ -150,8 +150,8 @@ export async function extractEntities(memory, { store, config, callLLM, logger }
|
|
|
150
150
|
const userText = buildUserMessage(memory.content);
|
|
151
151
|
|
|
152
152
|
const messages = [
|
|
153
|
-
{ role: "system", content: [{ type: "text", text: systemPrompt }] },
|
|
154
|
-
{ role: "user", content: [{ type: "text", text: userText }] }
|
|
153
|
+
{ role: "system", content: [{ type: "text", text: systemPrompt }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
154
|
+
{ role: "user", content: [{ type: "text", text: userText }], source: { kind: "plugin", plugin: "dsh-mneme" } }
|
|
155
155
|
];
|
|
156
156
|
|
|
157
157
|
// Issue #109: optional provider/model override + reasoning effort are
|
package/src/index.js
CHANGED
|
@@ -275,6 +275,8 @@ export const apply = (ctx, config) => {
|
|
|
275
275
|
batchSize: cfg.localEmbedBatchSize,
|
|
276
276
|
cacheDir: cfg.embedModelCacheDir,
|
|
277
277
|
runtimeDir: cfg.runtimeDir,
|
|
278
|
+
// #188:embedModelMirror 接成 transformers 的下载镜像(此前死配置)。
|
|
279
|
+
remoteHost: cfg.embedModelMirror,
|
|
278
280
|
baseUrl: cfg.ollamaBaseUrl,
|
|
279
281
|
logger: ctx.logger
|
|
280
282
|
});
|
|
@@ -327,6 +329,10 @@ export const apply = (ctx, config) => {
|
|
|
327
329
|
device: cfg.localEmbedDevice,
|
|
328
330
|
cacheDir: cfg.embedModelCacheDir,
|
|
329
331
|
runtimeDir: cfg.runtimeDir,
|
|
332
|
+
// #188:量化档默认 q8(此前不传 dtype 会去要 1GB 级 fp32 模型);
|
|
333
|
+
// embedModelMirror 此前是死配置,现接成 transformers 的下载镜像。
|
|
334
|
+
useDtype: cfg.rerankDtype,
|
|
335
|
+
remoteHost: cfg.embedModelMirror,
|
|
330
336
|
logger: ctx.logger
|
|
331
337
|
});
|
|
332
338
|
service.setReranker(reranker);
|
package/src/lang.js
CHANGED
|
@@ -23,8 +23,8 @@ const PROMPTS = {
|
|
|
23
23
|
"en": "You are the memory overview assistant. From the consolidated memories below, produce a 150-200 word memory-library overview covering: user preferences, active projects, key decisions. This will be injected as conversation context. Output only the summary text, nothing else."
|
|
24
24
|
},
|
|
25
25
|
"consolidation": {
|
|
26
|
-
"zh": "你是记忆库整理助手。下面是全部记忆条目(id、类型、标题、内容、重要性、更新时间)。\n请执行记忆巩固(consolidation),输出一个决策 JSON 数组。\n\n【决策格式(必须严格遵守)】\n每个决策必须是对象,字段固定:\n- \"action\":必填。取值只能是 \"keep\" / \"merge\" / \"archive\" / \"update\" / \"conflict\" 之一(字段名必须是 action,严禁写成 type)\n- \"ids\":必填,数组,本决策涉及的记忆 id 列表\n- \"reason\":可选,字符串,决策理由\n- \"importance\":可选,整数 1-5\n- merge 额外字段:\"keepSource\"(单个 id 字符串,必须是 ids 之一)+ 合并后的 \"title\"、\"content\"\n- conflict 额外字段:\"winner\" 与 \"loser\",都是【单个 id 字符串,不是数组】\n- update 额外字段:修正后的 \"title\" 和/或 \"content\";\"ids\" 只能包含一个 id\n\n【决策 JSON 示例】\n[\n { \"action\": \"merge\", \"ids\": [\"m1\", \"m2\"], \"keepSource\": \"m1\", \"title\": \"合并标题\", \"content\": \"合并后的摘要内容\", \"importance\": 4, \"reason\": \"主题相近\" },\n { \"action\": \"conflict\", \"winner\": \"m3\", \"loser\": \"m4\", \"reason\": \"内容矛盾,保留更新的信息\" },\n { \"action\": \"update\", \"ids\": [\"m5\"], \"content\": \"修正后的内容\", \"reason\": \"信息过时\" },\n { \"action\": \"archive\", \"ids\": [\"m6\"], \"reason\": \"重复或过时\" }\n]\n\n【任务】\n1. 识别主题相近的条目 → merge(合并为更精炼的摘要,保留信息最完整的 id 作为 keepSource)\n2. 识别重复/过时信息 → archive\n3. 识别内容矛盾的条目 → conflict(按时间新旧、来源完整性、信息具体程度判断 winner/loser)\n4. 发现单条记忆中的信息过时、错误或遗漏 → update(直接修正内容)\n - update 的 ids 只能包含一个 id\n - 必须提供修正后的 title 和/或 content\n - 仅当内容确实需要修正时才使用,不要滥用\n - 每次整理最多输出 2 个 update\n - 24 小时内新建的记忆不可 update\n5. 无问题的条目无需输出(未提及的条目将自动保留 keep)\n\n【硬性规则】\n- 字段名必须精确为 \"action\",严禁写成 \"type\";字段名统一用双引号\n- conflict 的 winner/loser、merge 的 keepSource 都是【单个 id 字符串,绝不是数组】\n- 每条记忆最多被 claim 一次:同一个 id 不能出现在多个决策中(同一 id 不能被 merge 和 conflict/archive 等重复占用)\n- 未在决策中提及的记忆将自动保留(keep),无需为每条记忆输出 keep\n- merge 的 keepSource 必须是 ids 之一\n- 仅合并同类型条目(type 相同)\n- 不要编造 ids;只使用提供的 id\n- 重要性 1-5,合并后取最高\n- 只输出 JSON 数组,不要其他文字",
|
|
27
|
-
"en": "You are the memory consolidation assistant. Below are all memory entries (id, type, title, content, importance, updated time).\nPerform memory consolidation and output a JSON array of decisions.\n\n[Decision format (must be followed strictly)]\nEach decision must be an object with fixed fields:\n- \"action\": required. Value must be one of \"keep\" / \"merge\" / \"archive\" / \"update\" / \"conflict\" (the field name must be action, never type)\n- \"ids\": required, array, the list of memory ids involved in this decision\n- \"reason\": optional, string, rationale for the decision\n- \"importance\": optional, integer 1-5\n- merge extra fields: \"keepSource\" (a single id string, must be one of ids) plus the merged \"title\" and \"content\"\n- conflict extra fields: \"winner\" and \"loser\", each a [single id string, never an array]\n- update extra fields: the corrected \"title\" and/or \"content\"; \"ids\" may contain only one id\n\n[Decision JSON examples]\n[\n { \"action\": \"merge\", \"ids\": [\"m1\", \"m2\"], \"keepSource\": \"m1\", \"title\": \"Merged title\", \"content\": \"Merged summary content\", \"importance\": 4, \"reason\": \"similar topics\" },\n { \"action\": \"conflict\", \"winner\": \"m3\", \"loser\": \"m4\", \"reason\": \"content contradicts; keep the newer information\" },\n { \"action\": \"update\", \"ids\": [\"m5\"], \"content\": \"corrected content\", \"reason\": \"information outdated\" },\n { \"action\": \"archive\", \"ids\": [\"m6\"], \"reason\": \"duplicate or outdated\" }\n]\n\n[Task]\n1. Identify entries with similar topics -> merge (merge into a more refined summary; keep the id with the most complete information as keepSource)\n2. Identify duplicate/outdated information -> archive\n3. Identify entries with contradictory content -> conflict (judge winner/loser by recency, source completeness, and information specificity)\n4. Find outdated, wrong, or missing information in a single memory -> update (correct the content directly)\n - update's ids may contain only one id\n - must provide the corrected title and/or content\n - use only when the content truly needs correction; do not abuse\n - output at most 2 updates per consolidation run\n - memories created within the last 24 hours must not be updated\n5. Entries without issues need no output (unmentioned entries are automatically kept)\n\n[Hard rules]\n- Field names must be exactly \"action\", never \"type\"; use double quotes for all field names\n- conflict winner/loser and merge keepSource are [single id strings, never arrays]\n- Each memory may be claimed at most once: the same id must not appear in multiple decisions (an id must not be claimed by both merge and conflict/archive, etc.)\n- Memories not mentioned in any decision are automatically kept (keep); no need to output keep for every memory\n- merge keepSource must be one of ids\n- Only merge entries of the same type (identical type)\n- Never invent ids; use only the provided ids\n- Importance is 1-5; after a merge take the highest\n- Write every title, content, and reason in English\n- Output only the JSON array, nothing else"
|
|
26
|
+
"zh": "你是记忆库整理助手。下面是全部记忆条目(id、类型、标题、内容、重要性、更新时间)。\n请执行记忆巩固(consolidation),输出一个决策 JSON 数组。\n\n【决策格式(必须严格遵守)】\n每个决策必须是对象,字段固定:\n- \"action\":必填。取值只能是 \"keep\" / \"merge\" / \"archive\" / \"update\" / \"conflict\" 之一(字段名必须是 action,严禁写成 type)\n- \"ids\":必填,数组,本决策涉及的记忆 id 列表\n- \"reason\":可选,字符串,决策理由\n- \"importance\":可选,整数 1-5\n- merge 额外字段:\"keepSource\"(单个 id 字符串,必须是 ids 之一)+ 合并后的 \"title\"、\"content\"\n- conflict 额外字段:\"winner\" 与 \"loser\",都是【单个 id 字符串,不是数组】\n- update 额外字段:修正后的 \"title\" 和/或 \"content\";\"ids\" 只能包含一个 id\n\n【决策 JSON 示例】\n[\n { \"action\": \"merge\", \"ids\": [\"m1\", \"m2\"], \"keepSource\": \"m1\", \"title\": \"合并标题\", \"content\": \"合并后的摘要内容\", \"importance\": 4, \"reason\": \"主题相近\" },\n { \"action\": \"conflict\", \"winner\": \"m3\", \"loser\": \"m4\", \"reason\": \"内容矛盾,保留更新的信息\" },\n { \"action\": \"update\", \"ids\": [\"m5\"], \"content\": \"修正后的内容\", \"reason\": \"信息过时\" },\n { \"action\": \"archive\", \"ids\": [\"m6\"], \"reason\": \"重复或过时\" }\n]\n\n【任务】\n1. 识别主题相近的条目 → merge(合并为更精炼的摘要,保留信息最完整的 id 作为 keepSource)\n2. 识别重复/过时信息 → archive\n3. 识别内容矛盾的条目 → conflict(按时间新旧、来源完整性、信息具体程度判断 winner/loser)\n4. 发现单条记忆中的信息过时、错误或遗漏 → update(直接修正内容)\n - update 的 ids 只能包含一个 id\n - 必须提供修正后的 title 和/或 content\n - 仅当内容确实需要修正时才使用,不要滥用\n - 每次整理最多输出 2 个 update\n - 24 小时内新建的记忆不可 update\n5. 无问题的条目无需输出(未提及的条目将自动保留 keep)\n\n【硬性规则】\n- 字段名必须精确为 \"action\",严禁写成 \"type\";字段名统一用双引号\n- conflict 的 winner/loser、merge 的 keepSource 都是【单个 id 字符串,绝不是数组】\n- 每条记忆最多被 claim 一次:同一个 id 不能出现在多个决策中(同一 id 不能被 merge 和 conflict/archive 等重复占用)\n- 未在决策中提及的记忆将自动保留(keep),无需为每条记忆输出 keep\n- merge 的 keepSource 必须是 ids 之一\n- 仅合并同类型条目(type 相同)\n- 不要编造 ids;只使用提供的 id\n- 重要性 1-5,合并后取最高\n- archive 仅用于确属「重复或已过时」的条目;rejected_solution / constraint / pitfall / preference / pattern 属长保留类型,除非确属重复或过时,否则不要归档\n- 每次整理最多输出 8 个 archive\n- 只输出 JSON 数组,不要其他文字",
|
|
27
|
+
"en": "You are the memory consolidation assistant. Below are all memory entries (id, type, title, content, importance, updated time).\nPerform memory consolidation and output a JSON array of decisions.\n\n[Decision format (must be followed strictly)]\nEach decision must be an object with fixed fields:\n- \"action\": required. Value must be one of \"keep\" / \"merge\" / \"archive\" / \"update\" / \"conflict\" (the field name must be action, never type)\n- \"ids\": required, array, the list of memory ids involved in this decision\n- \"reason\": optional, string, rationale for the decision\n- \"importance\": optional, integer 1-5\n- merge extra fields: \"keepSource\" (a single id string, must be one of ids) plus the merged \"title\" and \"content\"\n- conflict extra fields: \"winner\" and \"loser\", each a [single id string, never an array]\n- update extra fields: the corrected \"title\" and/or \"content\"; \"ids\" may contain only one id\n\n[Decision JSON examples]\n[\n { \"action\": \"merge\", \"ids\": [\"m1\", \"m2\"], \"keepSource\": \"m1\", \"title\": \"Merged title\", \"content\": \"Merged summary content\", \"importance\": 4, \"reason\": \"similar topics\" },\n { \"action\": \"conflict\", \"winner\": \"m3\", \"loser\": \"m4\", \"reason\": \"content contradicts; keep the newer information\" },\n { \"action\": \"update\", \"ids\": [\"m5\"], \"content\": \"corrected content\", \"reason\": \"information outdated\" },\n { \"action\": \"archive\", \"ids\": [\"m6\"], \"reason\": \"duplicate or outdated\" }\n]\n\n[Task]\n1. Identify entries with similar topics -> merge (merge into a more refined summary; keep the id with the most complete information as keepSource)\n2. Identify duplicate/outdated information -> archive\n3. Identify entries with contradictory content -> conflict (judge winner/loser by recency, source completeness, and information specificity)\n4. Find outdated, wrong, or missing information in a single memory -> update (correct the content directly)\n - update's ids may contain only one id\n - must provide the corrected title and/or content\n - use only when the content truly needs correction; do not abuse\n - output at most 2 updates per consolidation run\n - memories created within the last 24 hours must not be updated\n5. Entries without issues need no output (unmentioned entries are automatically kept)\n\n[Hard rules]\n- Field names must be exactly \"action\", never \"type\"; use double quotes for all field names\n- conflict winner/loser and merge keepSource are [single id strings, never arrays]\n- Each memory may be claimed at most once: the same id must not appear in multiple decisions (an id must not be claimed by both merge and conflict/archive, etc.)\n- Memories not mentioned in any decision are automatically kept (keep); no need to output keep for every memory\n- merge keepSource must be one of ids\n- Only merge entries of the same type (identical type)\n- Never invent ids; use only the provided ids\n- Importance is 1-5; after a merge take the highest\n- Write every title, content, and reason in English\n- archive is only for entries that are genuinely \"duplicate or outdated\"; rejected_solution / constraint / pitfall / preference / pattern are long-retention types — do not archive them unless they are truly duplicate or outdated\n- output at most 8 archives per consolidation run\n- Output only the JSON array, nothing else"
|
|
28
28
|
},
|
|
29
29
|
"conflict": {
|
|
30
30
|
"zh": "你是记忆库冲突仲裁助手。下面是检测到的高相似度记忆对,可能内容矛盾或重复。\n对每一对输出一个 decision 对象:\n- 两条确实矛盾/重复 → { \"action\": \"conflict\", \"winner\": <保留的id>, \"loser\": <归档的id>, \"reason\": \"理由\" }\n- 两条只是主题相近、并无矛盾 → { \"action\": \"keep\", \"ids\": [<两个id>] }\n规则:\n- winner 应为信息更完整、更新或更可信的一条\n- 只使用提供的 id,不要编造\n- 每对必须输出一个 decision\n- 只输出 JSON 数组,不要其他文字",
|
package/src/local-embedder.js
CHANGED
|
@@ -30,7 +30,7 @@ function modelHash(model) {
|
|
|
30
30
|
* transformers 的选项里。
|
|
31
31
|
*/
|
|
32
32
|
async function defaultPipelineLoader(task, model, options) {
|
|
33
|
-
const { runtimeDir, ...pipelineOptions } = options ?? {};
|
|
33
|
+
const { runtimeDir, remoteHost, ...pipelineOptions } = options ?? {};
|
|
34
34
|
const { module } = await loadTransformers({
|
|
35
35
|
runtimeDir: runtimeDir || defaultRuntimeDir()
|
|
36
36
|
});
|
|
@@ -41,6 +41,11 @@ async function defaultPipelineLoader(task, model, options) {
|
|
|
41
41
|
// model is fully cached locally. Mirroring the cache_dir onto env.cacheDir
|
|
42
42
|
// makes that pre-check resolve locally too — fully offline loading.
|
|
43
43
|
if (pipelineOptions.cache_dir) env.cacheDir = pipelineOptions.cache_dir;
|
|
44
|
+
// issue #188 补充:embedModelMirror 此前是死配置(定义了但全仓无人读取)。
|
|
45
|
+
// transformers.js 不认 HF_ENDPOINT(那是 Python huggingface_hub 的变量),
|
|
46
|
+
// 模型下载源由 env.remoteHost 决定——把镜像接到真正的开关上,否则
|
|
47
|
+
// huggingface.co 不可达的网络连模型都下不下来。
|
|
48
|
+
if (remoteHost) env.remoteHost = remoteHost;
|
|
44
49
|
return pipeline(task, model, pipelineOptions);
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -72,6 +77,8 @@ export class LocalEmbedder {
|
|
|
72
77
|
// 自管运行时根目录(issue #131):空表示用默认位置,具体解析交给 loader。
|
|
73
78
|
this.runtimeDir = String(opts.runtimeDir ?? "").trim();
|
|
74
79
|
this.useDtype = opts.useDtype || "q8";
|
|
80
|
+
// 模型镜像下载源(embedModelMirror,#188 补充):空 = transformers 默认。
|
|
81
|
+
this.remoteHost = String(opts.remoteHost ?? "").trim();
|
|
75
82
|
this.logger = opts.logger ?? null;
|
|
76
83
|
// Test hook: replace the pipeline factory without touching modules.
|
|
77
84
|
this.engineFactory = opts.engineFactory || defaultPipelineLoader;
|
|
@@ -91,6 +98,7 @@ export class LocalEmbedder {
|
|
|
91
98
|
if (this.cacheDir) options.cache_dir = this.cacheDir;
|
|
92
99
|
// 传给注入的 engineFactory,由 defaultPipelineLoader 取走后交给三层解析。
|
|
93
100
|
if (this.runtimeDir) options.runtimeDir = this.runtimeDir;
|
|
101
|
+
if (this.remoteHost) options.remoteHost = this.remoteHost;
|
|
94
102
|
this.extractor = await this.engineFactory("feature-extraction", this.model, options);
|
|
95
103
|
this.ready = true; // service reads this to flush queued re-embeds
|
|
96
104
|
this.logger?.info?.(
|
package/src/reranker.js
CHANGED
|
@@ -27,7 +27,7 @@ function modelHash(model) {
|
|
|
27
27
|
* runtimeDir 取走后不混进 transformers 的选项。
|
|
28
28
|
*/
|
|
29
29
|
async function defaultPipelineLoader(task, model, options) {
|
|
30
|
-
const { runtimeDir, ...pipelineOptions } = options ?? {};
|
|
30
|
+
const { runtimeDir, remoteHost, ...pipelineOptions } = options ?? {};
|
|
31
31
|
const { module } = await loadTransformers({
|
|
32
32
|
runtimeDir: runtimeDir || defaultRuntimeDir()
|
|
33
33
|
});
|
|
@@ -36,6 +36,10 @@ async function defaultPipelineLoader(task, model, options) {
|
|
|
36
36
|
// 的 cache_dir,回退到 env.cacheDir —— 即使模型已完全缓存在本地也会去请求网络。
|
|
37
37
|
// embedder 侧早就镜像了,reranker 侧一直漏着,于是「已缓存 + 离线」时重排初始化会失败。
|
|
38
38
|
if (pipelineOptions.cache_dir) env.cacheDir = pipelineOptions.cache_dir;
|
|
39
|
+
// issue #188 补充:embedModelMirror 此前是死配置——transformers.js 不认
|
|
40
|
+
// HF_ENDPOINT(那是 Python huggingface_hub 的变量),下载源由 env.remoteHost
|
|
41
|
+
// 决定。把镜像接到真正的开关上,huggingface.co 不可达的网络才能自动下载。
|
|
42
|
+
if (remoteHost) env.remoteHost = remoteHost;
|
|
39
43
|
return pipeline(task, model, pipelineOptions);
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -85,6 +89,11 @@ export class LocalReranker {
|
|
|
85
89
|
this.cacheDir = String(opts.cacheDir ?? "").trim() || defaultModelCacheDir();
|
|
86
90
|
// 自管运行时根目录(issue #131):空表示用默认位置,解析交给 loader。
|
|
87
91
|
this.runtimeDir = String(opts.runtimeDir ?? "").trim();
|
|
92
|
+
// 与嵌入层同约定:默认 q8(4 倍小的 model_quantized.onnx),"fp32" 可关。
|
|
93
|
+
// 此前不传 dtype,即使缓存里有量化文件也会去要 1GB 级的 model.onnx(#188)。
|
|
94
|
+
this.useDtype = String(opts.useDtype ?? "q8").trim() || "q8";
|
|
95
|
+
// 模型镜像下载源(embedModelMirror,#188 补充):空 = transformers 默认。
|
|
96
|
+
this.remoteHost = String(opts.remoteHost ?? "").trim();
|
|
88
97
|
this.logger = opts.logger ?? null;
|
|
89
98
|
this.engineFactory = opts.engineFactory || defaultPipelineLoader;
|
|
90
99
|
// Injectable seam: async (query, passage) => number. When set, init()
|
|
@@ -94,6 +103,7 @@ export class LocalReranker {
|
|
|
94
103
|
this._batchScorer = null;
|
|
95
104
|
this._queryVec = null;
|
|
96
105
|
this._queryKey = null;
|
|
106
|
+
this._constantScoreWarned = false;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
/** Load the model; throws when no strategy can be bound. */
|
|
@@ -119,9 +129,10 @@ export class LocalReranker {
|
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
_engineOptions() {
|
|
122
|
-
const options = { device: this.device };
|
|
132
|
+
const options = { device: this.device, dtype: this.useDtype };
|
|
123
133
|
if (this.cacheDir) options.cache_dir = this.cacheDir;
|
|
124
134
|
if (this.runtimeDir) options.runtimeDir = this.runtimeDir;
|
|
135
|
+
if (this.remoteHost) options.remoteHost = this.remoteHost;
|
|
125
136
|
return options;
|
|
126
137
|
}
|
|
127
138
|
|
|
@@ -136,8 +147,14 @@ export class LocalReranker {
|
|
|
136
147
|
return out.map((r) => clamp01(typeof r?.score === "number" ? r.score : 0));
|
|
137
148
|
};
|
|
138
149
|
} else if (strategy === "tc") {
|
|
139
|
-
// Classification head:
|
|
140
|
-
//
|
|
150
|
+
// Classification head: score so relevance lands in [0, 1]. Branch on the
|
|
151
|
+
// output width (issue #188): a dual-logit head (num_labels=2 exports,
|
|
152
|
+
// e.g. bge-reranker-v2-m3) gives the positive class as the softmax
|
|
153
|
+
// probability sigmoid(l1 - l0); a SINGLE-logit head (e.g.
|
|
154
|
+
// Xenova/bge-reranker-base) emits one raw relevance logit per pair, and
|
|
155
|
+
// the positive class is simply sigmoid(logit) — reading l1 as l0 there
|
|
156
|
+
// collapses every score to sigmoid(0) = 0.5 and silently disables
|
|
157
|
+
// reranking (no error, status still "ready").
|
|
141
158
|
this._batchScorer = async (query, passages) => {
|
|
142
159
|
const { tokenizer, model } = this.pipeline;
|
|
143
160
|
const inputs = tokenizer(passages.map(() => query), {
|
|
@@ -151,10 +168,10 @@ export class LocalReranker {
|
|
|
151
168
|
const rows = [];
|
|
152
169
|
for (let i = 0; i < dims[0]; i++) {
|
|
153
170
|
const base = i * cols;
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
rows.push(clamp01(
|
|
171
|
+
const score = cols > 1
|
|
172
|
+
? 1 / (1 + Math.exp(logits.data[base] - logits.data[base + 1]))
|
|
173
|
+
: 1 / (1 + Math.exp(-logits.data[base]));
|
|
174
|
+
rows.push(clamp01(score));
|
|
158
175
|
}
|
|
159
176
|
return rows;
|
|
160
177
|
};
|
|
@@ -208,6 +225,19 @@ export class LocalReranker {
|
|
|
208
225
|
results.push({ id: chunk[j].id, score: clamp01(Number(scores[j]) || 0) });
|
|
209
226
|
}
|
|
210
227
|
}
|
|
228
|
+
// Silent-failure tripwire (issue #188): a reranker whose every candidate
|
|
229
|
+
// scores identically is not ranking anything — e.g. a single-logit head
|
|
230
|
+
// read through the dual-logit formula. Warn once so the degradation is
|
|
231
|
+
// observable instead of a constant 0.5 column hiding behind a "ready"
|
|
232
|
+
// status forever.
|
|
233
|
+
if (results.length > 1 && results.every((r) => r.score === results[0].score)) {
|
|
234
|
+
if (!this._constantScoreWarned) {
|
|
235
|
+
this._constantScoreWarned = true;
|
|
236
|
+
this.logger?.warn?.(
|
|
237
|
+
`[dsh-mneme] reranker scores degenerated to a constant (${results[0].score}) — check the model's output dims (single vs dual logit head)`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
211
241
|
return results
|
|
212
242
|
.filter((r) => r.score >= this.scoreThreshold)
|
|
213
243
|
.sort((a, b) => b.score - a.score);
|
package/src/service.js
CHANGED
|
@@ -1292,6 +1292,22 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1292
1292
|
if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten && codingGate(m)) semanticItems.push(m);
|
|
1293
1293
|
}
|
|
1294
1294
|
}
|
|
1295
|
+
// Issue #198:首轮(无向量、无缓存召回)的同步兜底——BM25 词法召回领位。
|
|
1296
|
+
// 注入渲染是同步的(宿主 systemPrompt 的 contexts 不支持异步 text),
|
|
1297
|
+
// 查询向量只能异步 prefetch 给下一轮,首轮必 miss;此时纯静态排序会让
|
|
1298
|
+
// 老的高重要性 summary/preference 占满槽位,当前话题的 decision/project
|
|
1299
|
+
// 进不来。查询文本本身就在手上,BM25 是纯进程内同步计算,词法相关性
|
|
1300
|
+
// 足以把当前话题顶到前排;门控与向量路径一致(含 importance 阈值)。
|
|
1301
|
+
// 语义向量命中时本分支不参与,行为不变。
|
|
1302
|
+
if (!semanticItems.length) {
|
|
1303
|
+
for (const hit of bm25Recall(q, maxItems * 2)) {
|
|
1304
|
+
if (hit && !hit.archived && INJECT_TYPES.has(hit.type) && !hit.forgotten &&
|
|
1305
|
+
codingGate(hit) &&
|
|
1306
|
+
(hit.type === "summary" || hit.type === "preference" || hit.importance >= threshold)) {
|
|
1307
|
+
semanticItems.push(hit);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1295
1311
|
if (semanticItems.length) {
|
|
1296
1312
|
const seen = new Set();
|
|
1297
1313
|
const merged = [];
|
package/src/store.js
CHANGED
|
@@ -22,6 +22,9 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
22
22
|
);
|
|
23
23
|
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
24
24
|
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
25
|
+
-- Issue #202:all()/list() 按 updated_at 排序,无索引时走临时 B 树全表排序
|
|
26
|
+
-- (5k 行实测 all() 231ms → 135ms)。IF NOT EXISTS 幂等,存量库启动即建。
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at);
|
|
25
28
|
|
|
26
29
|
-- autoDream audit trail: one row per consolidation run, capturing the exact
|
|
27
30
|
-- input snapshot digest + the LLM decision list + per-id outcome + a compact
|
|
@@ -717,11 +720,51 @@ export function createStore(path) {
|
|
|
717
720
|
}
|
|
718
721
|
}
|
|
719
722
|
|
|
723
|
+
// Issue #202:embedding 解析缓存。检索路径此前每次调用都对全部带向量行
|
|
724
|
+
// JSON.parse(993 × 512 维 ≈ 48 ms/次,且 SQL 拖着 10 MB 的 TEXT),余弦本身
|
|
725
|
+
// 只占 3%。向量只经 setEmbedding 写入(单一失效点),把解析结果按 id 缓存,
|
|
726
|
+
// 命中即零解析;上限 FIFO 驱逐防大库内存无界(4000 × 512 维 double ≈ 16 MB)。
|
|
727
|
+
const embeddingCache = new Map();
|
|
728
|
+
const EMBEDDING_CACHE_MAX = 4000;
|
|
729
|
+
|
|
730
|
+
function cacheEmbedding(id, vector) {
|
|
731
|
+
if (embeddingCache.size >= EMBEDDING_CACHE_MAX && !embeddingCache.has(id)) {
|
|
732
|
+
const oldest = embeddingCache.keys().next().value;
|
|
733
|
+
embeddingCache.delete(oldest);
|
|
734
|
+
}
|
|
735
|
+
embeddingCache.set(id, vector);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Parsed embedding for one id (cache-first); undefined when none/parse
|
|
739
|
+
* failure. Single read path shared by searchVector / getEmbedding(s). */
|
|
740
|
+
function getParsedEmbedding(id) {
|
|
741
|
+
const cached = embeddingCache.get(id);
|
|
742
|
+
if (cached) return cached;
|
|
743
|
+
const row = db.prepare("SELECT embedding FROM memories WHERE id = ?").get(id);
|
|
744
|
+
if (!row?.embedding) return undefined;
|
|
745
|
+
try {
|
|
746
|
+
const v = JSON.parse(row.embedding);
|
|
747
|
+
if (Array.isArray(v) && v.length) {
|
|
748
|
+
cacheEmbedding(id, v);
|
|
749
|
+
return v;
|
|
750
|
+
}
|
|
751
|
+
} catch { /* corrupt embedding text: treated as absent */ }
|
|
752
|
+
return undefined;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// all()/list() 的显式列清单:从实际 schema 派生(迁移后),只排除 embedding
|
|
756
|
+
// 列——consolidation/sleep/inject 的消费方都不读向量,而它是全表里最重的一列
|
|
757
|
+
// (5k 行库 ≈ 9.4 MB TEXT),每次全表调用都白搬。经 PRAGMA 派生而非硬编码,
|
|
758
|
+
// schema 演进时自动跟随。
|
|
759
|
+
const memoryColumns = db.prepare("PRAGMA table_info(memories)").all()
|
|
760
|
+
.map((c) => c.name)
|
|
761
|
+
.filter((n) => n !== "embedding");
|
|
762
|
+
const memoryColumnList = memoryColumns.join(", ");
|
|
763
|
+
|
|
720
764
|
// Per-instance monotonic timestamp guard: consecutive writes within the same
|
|
721
765
|
// millisecond must still produce strictly increasing timestamps (test asserts
|
|
722
766
|
// updated_at != created_at). State lives in the store closure, not module scope.
|
|
723
|
-
let lastTs = "";
|
|
724
|
-
function nowIso() {
|
|
767
|
+
let lastTs = ""; function nowIso() {
|
|
725
768
|
let ts = new Date().toISOString();
|
|
726
769
|
if (lastTs && ts <= lastTs) {
|
|
727
770
|
const d = new Date(lastTs);
|
|
@@ -925,6 +968,10 @@ export function createStore(path) {
|
|
|
925
968
|
now,
|
|
926
969
|
id
|
|
927
970
|
);
|
|
971
|
+
// Issue #202:update 可直写 embedding(patch.embedding),缓存条目失效,
|
|
972
|
+
// 下次读取按库值重解析。宁滥勿缺——即便本 patch 未含 embedding,丢一条
|
|
973
|
+
// 缓存只多一次点查重解析。
|
|
974
|
+
embeddingCache.delete(id);
|
|
928
975
|
// Desired generation bumped in the same transaction as the update (peer
|
|
929
976
|
// blocker 1: crash between write and sync must still be recoverable).
|
|
930
977
|
incrementGeneration();
|
|
@@ -995,6 +1042,8 @@ export function createStore(path) {
|
|
|
995
1042
|
expectedUpdatedAt
|
|
996
1043
|
);
|
|
997
1044
|
if (result.changes === 0) return; // CAS miss: a concurrent write won
|
|
1045
|
+
// Issue #202:同 update——CAS 命中即可能改写 embedding,失效缓存条目。
|
|
1046
|
+
embeddingCache.delete(id);
|
|
998
1047
|
// Only bump desired generation on a successful CAS — a miss writes nothing.
|
|
999
1048
|
incrementGeneration();
|
|
1000
1049
|
applied = true;
|
|
@@ -1177,7 +1226,9 @@ export function createStore(path) {
|
|
|
1177
1226
|
}
|
|
1178
1227
|
|
|
1179
1228
|
function all() {
|
|
1180
|
-
|
|
1229
|
+
// Issue #202:显式列清单排除 embedding——toRow 本就不输出向量,SELECT *
|
|
1230
|
+
// 纯属把全表最重的列(9.4 MB TEXT/5k 行)拖进内存再丢弃。
|
|
1231
|
+
const rows = db.prepare(`SELECT ${memoryColumnList} FROM memories ORDER BY updated_at DESC`).all();
|
|
1181
1232
|
return rows.map(toRow);
|
|
1182
1233
|
}
|
|
1183
1234
|
|
|
@@ -1185,6 +1236,17 @@ export function createStore(path) {
|
|
|
1185
1236
|
function setEmbedding(id, vector) {
|
|
1186
1237
|
const json = Array.isArray(vector) && vector.length ? JSON.stringify(vector) : null;
|
|
1187
1238
|
db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(json, id);
|
|
1239
|
+
// Issue #202:单一写入口即单一失效点——重写缓存条目(向量在手上不必重解析),
|
|
1240
|
+
// 清空则移除,删除过的 id 不会被下一次 searchVector 的 id 集合选中。
|
|
1241
|
+
if (json) {
|
|
1242
|
+
try {
|
|
1243
|
+
const parsed = JSON.parse(json);
|
|
1244
|
+
if (Array.isArray(parsed) && parsed.length) cacheEmbedding(id, parsed);
|
|
1245
|
+
else embeddingCache.delete(id);
|
|
1246
|
+
} catch { embeddingCache.delete(id); }
|
|
1247
|
+
} else {
|
|
1248
|
+
embeddingCache.delete(id);
|
|
1249
|
+
}
|
|
1188
1250
|
}
|
|
1189
1251
|
|
|
1190
1252
|
/** Batch fetch stored embeddings by id (v0.5.0 search-time semantic dedup).
|
|
@@ -1193,19 +1255,10 @@ export function createStore(path) {
|
|
|
1193
1255
|
function getEmbeddings(ids) {
|
|
1194
1256
|
const out = new Map();
|
|
1195
1257
|
const list = (Array.isArray(ids) ? ids : []).filter(Boolean);
|
|
1196
|
-
for (
|
|
1197
|
-
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
WHERE embedding IS NOT NULL AND embedding != ''
|
|
1201
|
-
AND id IN (${chunk.map(() => "?").join(",")})`
|
|
1202
|
-
).all(...chunk);
|
|
1203
|
-
for (const row of rows) {
|
|
1204
|
-
try {
|
|
1205
|
-
const vec = JSON.parse(row.embedding);
|
|
1206
|
-
if (Array.isArray(vec) && vec.length) out.set(row.id, vec);
|
|
1207
|
-
} catch { /* corrupt row: skip */ }
|
|
1208
|
-
}
|
|
1258
|
+
for (const id of list) {
|
|
1259
|
+
// Issue #202:走同一解析缓存(语义去重的批量读也免重复 parse)。
|
|
1260
|
+
const vec = getParsedEmbedding(id);
|
|
1261
|
+
if (vec) out.set(id, vec);
|
|
1209
1262
|
}
|
|
1210
1263
|
return out;
|
|
1211
1264
|
}
|
|
@@ -1280,24 +1333,30 @@ export function createStore(path) {
|
|
|
1280
1333
|
function searchVector(vector, { limit = 20, includeArchived = false, threshold = 0 } = {}) {
|
|
1281
1334
|
if (!Array.isArray(vector) || !vector.length) return [];
|
|
1282
1335
|
const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
|
|
1336
|
+
// Issue #202:只取 id 列(~40 KB)而非 SELECT *(~11 MB)——向量从缓存取,
|
|
1337
|
+
// 缓存未命中时按 id 点查 embedding 再解析回填。SQL 里保留 embedding 的
|
|
1338
|
+
// IS NOT NULL 过滤,保证「有向量的行」集合始终以库为准(删除/清空即时生效)。
|
|
1283
1339
|
const rows = db.prepare(
|
|
1284
|
-
`SELECT
|
|
1340
|
+
`SELECT id FROM memories
|
|
1285
1341
|
WHERE ${archivedFilter}forgotten = 0 AND embedding IS NOT NULL AND embedding != ''`
|
|
1286
1342
|
).all();
|
|
1287
1343
|
const scored = [];
|
|
1288
1344
|
for (const row of rows) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
v = JSON.parse(row.embedding);
|
|
1292
|
-
} catch {
|
|
1293
|
-
continue;
|
|
1294
|
-
}
|
|
1345
|
+
const v = getParsedEmbedding(row.id);
|
|
1346
|
+
if (!v) continue;
|
|
1295
1347
|
const score = cosine(vector, v);
|
|
1296
|
-
if (score >= threshold) scored.push({ row, score });
|
|
1348
|
+
if (score >= threshold) scored.push({ row: { id: row.id }, score });
|
|
1297
1349
|
}
|
|
1298
1350
|
scored.sort((a, b) => b.score - a.score);
|
|
1299
1351
|
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
1300
|
-
|
|
1352
|
+
// Top-N 回表:只对入选行取完整字段(主键点查 ≤ lim 行),评分别拖着正文走。
|
|
1353
|
+
const getFull = db.prepare("SELECT * FROM memories WHERE id = ?");
|
|
1354
|
+
const out = [];
|
|
1355
|
+
for (const { row, score } of scored.slice(0, lim)) {
|
|
1356
|
+
const full = getFull.get(row.id);
|
|
1357
|
+
if (full) out.push({ ...toRow(full), score });
|
|
1358
|
+
}
|
|
1359
|
+
return out;
|
|
1301
1360
|
}
|
|
1302
1361
|
|
|
1303
1362
|
// --- autoDream audit trail ----------------------------------------------
|
|
@@ -2245,6 +2304,7 @@ export function createStore(path) {
|
|
|
2245
2304
|
search,
|
|
2246
2305
|
setEmbedding,
|
|
2247
2306
|
getEmbeddings,
|
|
2307
|
+
getParsedEmbedding,
|
|
2248
2308
|
embeddedCount,
|
|
2249
2309
|
needsEmbedding,
|
|
2250
2310
|
searchVector,
|
package/src/summarize.js
CHANGED
|
@@ -144,7 +144,7 @@ function collectMessages(session, maxChars = 8000, language = "zh") {
|
|
|
144
144
|
break;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
return lines.length ? [createUserMessage({ content: [{ type: "text", text: trim(lines.join("\n"), maxChars) }] })] : [];
|
|
147
|
+
return lines.length ? [createUserMessage({ content: [{ type: "text", text: trim(lines.join("\n"), maxChars) }], source: { kind: "plugin", plugin: "dsh-mneme" } })] : [];
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// ── 智能调速器(429 保护)─────────────────────────────────────────────
|
|
@@ -265,7 +265,7 @@ export function createSummarizer(ctx, service, config) {
|
|
|
265
265
|
model: route.model,
|
|
266
266
|
purpose: "summarization",
|
|
267
267
|
messages: [
|
|
268
|
-
{ role: "system", content: [{ type: "text", text: config.codingRetrospect ? STR.prompts.codingSummary[langOf(config)] : STR.prompts.summary[langOf(config)] }] },
|
|
268
|
+
{ role: "system", content: [{ type: "text", text: config.codingRetrospect ? STR.prompts.codingSummary[langOf(config)] : STR.prompts.summary[langOf(config)] }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
269
269
|
...messages
|
|
270
270
|
],
|
|
271
271
|
signal: controller.signal
|
package/src/tools.js
CHANGED
|
@@ -39,6 +39,8 @@ const MEMORY_ITEM_SCHEMA = {
|
|
|
39
39
|
updated_at: { type: "string", required: true }
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
|
+
// 导出供回归测试断言 memory_get 与共享 schema 同源(#184 防漂移护栏)。
|
|
43
|
+
export { MEMORY_ITEM_SCHEMA };
|
|
42
44
|
|
|
43
45
|
// v0.8.0 A2:occurred 时间窗参数 → store 过滤选项(list/count 同口径);
|
|
44
46
|
// 未传参返回空对象,既有调用方不受影响。(A3 复核:此处入参仅含 occurred 边界。)
|
|
@@ -277,25 +279,10 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
277
279
|
type: "object",
|
|
278
280
|
additionalProperties: false,
|
|
279
281
|
properties: {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
id: { type: "string", required: true },
|
|
285
|
-
title: { type: "string", required: true },
|
|
286
|
-
type: { type: "string", required: true },
|
|
287
|
-
importance: { type: "integer", required: true },
|
|
288
|
-
tags: { type: "array", items: { type: "string" } },
|
|
289
|
-
content: { type: "string", required: true },
|
|
290
|
-
source: { type: "string" },
|
|
291
|
-
agent_scope: { type: "string" },
|
|
292
|
-
workspace_scope: { type: "string" },
|
|
293
|
-
sensitivity: { type: "string" },
|
|
294
|
-
occurred_at: { type: "string" },
|
|
295
|
-
created_at: { type: "string" },
|
|
296
|
-
updated_at: { type: "string" }
|
|
297
|
-
}
|
|
298
|
-
}
|
|
282
|
+
// 与 memory_search / memory_list 共用同一份 item schema——DTO 由
|
|
283
|
+
// toApiList 统一产出,内联副本曾漏掉 v0.8.1 的 scope 来源三键,
|
|
284
|
+
// 任何被标注过的行都过不了 in-process 校验(#184)。
|
|
285
|
+
memory: MEMORY_ITEM_SCHEMA
|
|
299
286
|
}
|
|
300
287
|
},
|
|
301
288
|
render: (_args, value) => {
|
package/src/vector-index.js
CHANGED
|
@@ -56,14 +56,8 @@ export function createVectorIndex({ store, logger }) {
|
|
|
56
56
|
|
|
57
57
|
/** Read one memory's cached vector; undefined when none/parse failure. */
|
|
58
58
|
getEmbedding(id) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const v = JSON.parse(row.embedding);
|
|
63
|
-
return Array.isArray(v) && v.length ? v : undefined;
|
|
64
|
-
} catch {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
59
|
+
// Issue #202:走 store 的解析缓存(与 searchVector/getEmbeddings 同源)。
|
|
60
|
+
return store.getParsedEmbedding(id);
|
|
67
61
|
},
|
|
68
62
|
|
|
69
63
|
/** Cosine search over embedded rows; results carry a 0..1 score. */
|
package/test/api.test.js
CHANGED
|
@@ -1394,6 +1394,11 @@ test("POST /api/dsh-mneme/test-model succeeds and reports reply + latency", asyn
|
|
|
1394
1394
|
assert.equal(data.modelId, "deepseek:deepseek-chat", "modelId reports the probed route");
|
|
1395
1395
|
assert.equal(MOCK_LLM.lastOptions.provider, "deepseek");
|
|
1396
1396
|
assert.equal(MOCK_LLM.lastOptions.purpose, "dsh-mneme-connectivity-test");
|
|
1397
|
+
// 回归:探测必须是一条 user 消息(带 source)。system-only 对话自
|
|
1398
|
+
// 0.1.6-alpha.1 起被官方 API 整单拒绝(「messages: at least one message
|
|
1399
|
+
// is required」),探测按钮会 502。
|
|
1400
|
+
assert.equal(MOCK_LLM.lastOptions.messages[0].role, "user");
|
|
1401
|
+
assert.equal(MOCK_LLM.lastOptions.messages[0].source?.kind, "plugin");
|
|
1397
1402
|
assert.equal("reasoningEffort" in MOCK_LLM.lastOptions, false, "no effort configured -> field omitted");
|
|
1398
1403
|
});
|
|
1399
1404
|
|