@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/inject.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createScopeResolver } from "./scope.js";
|
|
1
2
|
import { createHotMemory } from "./hot-memory.js";
|
|
3
|
+
import { STR, langOf } from "./lang.js";
|
|
2
4
|
|
|
3
5
|
// Best-effort extraction of the current user's latest message text from the
|
|
4
6
|
// live session, for semantic-first injection (Bug4). The system-prompt
|
|
@@ -43,10 +45,23 @@ function extractRounds(ctx, maxRounds) {
|
|
|
43
45
|
const rounds = [];
|
|
44
46
|
let pendingQuery = null;
|
|
45
47
|
const textOf = (event) => {
|
|
46
|
-
|
|
48
|
+
// user/message 的正文就在 data 上,而 assistant/message(以及 tool/result)的消息体
|
|
49
|
+
// 嵌在 data.message 下 —— DSH 侧由 @deepseek-ai/dsh-session 的
|
|
50
|
+
// assertMessageEventShape 一行写死:const message = type === "user/message" ? record : record?.["message"]。
|
|
51
|
+
// 只读 data.content 会让每一轮的 response 恒为空(Issue #129)。
|
|
52
|
+
// 这里用 ?? 兜底而不是直接改成 data.message.content:user 消息与旧的扁平形状都照旧可用。
|
|
53
|
+
const parts = event?.data?.content ?? event?.data?.message?.content;
|
|
47
54
|
if (!Array.isArray(parts)) return "";
|
|
55
|
+
// 只取正文 part(type === "text" 或字符串),跳过 reasoning(issue #162):
|
|
56
|
+
// reasoning 里出现 Go template / Vue / Handlebars / 正则 / 日志原文的概率远高于
|
|
57
|
+
// 正文,把它带进 hot memory 会让 `{{...}}` 模板片段注入 prompt。旧的扁平形状
|
|
58
|
+
// (字符串 part)照旧取用,无 type 字段的兜底视为正文。
|
|
48
59
|
return parts
|
|
49
|
-
.map((p) =>
|
|
60
|
+
.map((p) => {
|
|
61
|
+
if (typeof p === "string") return p;
|
|
62
|
+
if (p?.type && p.type !== "text") return "";
|
|
63
|
+
return p?.text ?? "";
|
|
64
|
+
})
|
|
50
65
|
.filter(Boolean)
|
|
51
66
|
.join("\n")
|
|
52
67
|
.trim();
|
|
@@ -76,8 +91,13 @@ function extractRounds(ctx, maxRounds) {
|
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
export function createInjector(ctx, service, settings, config) {
|
|
94
|
+
const language = langOf(config);
|
|
79
95
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
80
96
|
const threshold = config.importanceThreshold ?? 3;
|
|
97
|
+
// v0.8.0 A3(issue #17):注入路径的会话 scope 解析——strictScope 硬过滤
|
|
98
|
+
// 需要。渲染 ctx 与工具 exec 同形(agent.session),解析器直接复用。
|
|
99
|
+
// logger 透传:registry 反查失败时 warnOnce 才有出口。
|
|
100
|
+
const resolveSessionScope = createScopeResolver({ ctx, config, logger: ctx.logger });
|
|
81
101
|
|
|
82
102
|
// Bug6: bound the injected memory block. Each entry's content is truncated to
|
|
83
103
|
// MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
|
|
@@ -96,6 +116,24 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
96
116
|
return text.length <= maxLength ? text : `${text.slice(0, maxLength)}…`;
|
|
97
117
|
}
|
|
98
118
|
|
|
119
|
+
// Prompt-variable brace escaping (issue #162, restored from v0.7.4 #40):
|
|
120
|
+
// DSH's interpolate() scans every injected section for `{{...}}` and throws
|
|
121
|
+
// unless the variable name matches /^[a-z][a-z0-9_]*$/ — memory/profile
|
|
122
|
+
// content carrying legal template syntax (docker `--format "{{.Server.Version}}"`,
|
|
123
|
+
// Obsidian-style `{{hl|}}`, `{{挖空}}`, `{{关键词}}`) would hit an illegal
|
|
124
|
+
// variable name and throw, crashing the whole turn. At the injection boundary
|
|
125
|
+
// we escape every run of 2+ consecutive braces, inserting a `\` between each
|
|
126
|
+
// pair, so no `{{`/`}}` substring survives into the prompt: `{{a}}` → `{\{a\}\}`,
|
|
127
|
+
// and odd runs like `{{{a}}}` (which pair-wise escaping would leave with a
|
|
128
|
+
// literal `{{`) are handled too. The text keeps its readable template form,
|
|
129
|
+
// the transform is idempotent (escaped braces are single + `\`, never two
|
|
130
|
+
// adjacent), and single braces pass through untouched — interpolate only
|
|
131
|
+
// scans `{{`. Off via config.escapePromptVariables=false (default true).
|
|
132
|
+
function escapePromptVars(text) {
|
|
133
|
+
if (config.escapePromptVariables === false) return String(text);
|
|
134
|
+
return String(text).replace(/[{}]{2,}/g, (run) => run.split("").join("\\"));
|
|
135
|
+
}
|
|
136
|
+
|
|
99
137
|
// Hot memory (v0.5.0 1.3): the latest rounds of THIS session, rebuilt from
|
|
100
138
|
// the materialized event log on every render — stateless, so it survives
|
|
101
139
|
// session switches and never persists anywhere.
|
|
@@ -112,23 +150,23 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
112
150
|
for (const r of rounds) hot.add(r);
|
|
113
151
|
const body = hot.getContext();
|
|
114
152
|
if (!body) return "";
|
|
115
|
-
return
|
|
153
|
+
return escapePromptVars(STR.hotHeader[language](rounds.length, body));
|
|
116
154
|
}
|
|
117
155
|
|
|
118
156
|
function render(candidates) {
|
|
119
157
|
if (!candidates.length) return "";
|
|
120
|
-
const header =
|
|
158
|
+
const header = STR.memoryHeader[language];
|
|
121
159
|
const lines = [header];
|
|
122
160
|
let budget = MAX_BLOCK - header.length;
|
|
123
161
|
for (const m of candidates) {
|
|
124
162
|
// Epistemic trust (v0.4.5): when enabled, measured observations are
|
|
125
163
|
// flagged so the agent can weigh them above guesses/opinions.
|
|
126
164
|
const verified = config.trustEpistemicWeighting === true && m.epistemic_status === "observation"
|
|
127
|
-
?
|
|
165
|
+
? STR.verified[language]
|
|
128
166
|
: "";
|
|
129
|
-
const title =
|
|
167
|
+
const title = STR.entryTitle[language](m.title, m.importance);
|
|
130
168
|
const content = injectMemory(m);
|
|
131
|
-
const full =
|
|
169
|
+
const full = STR.entryLine[language](m.type, verified, title, content);
|
|
132
170
|
if (budget - full.length >= 0) {
|
|
133
171
|
lines.push(full);
|
|
134
172
|
budget -= full.length;
|
|
@@ -136,7 +174,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
136
174
|
lines.push(`- [${m.type}] ${verified}${title}`);
|
|
137
175
|
}
|
|
138
176
|
}
|
|
139
|
-
return lines.join("\n");
|
|
177
|
+
return escapePromptVars(lines.join("\n"));
|
|
140
178
|
}
|
|
141
179
|
|
|
142
180
|
// Bug4: the system-prompt render is synchronous, so the semantic query vector
|
|
@@ -167,10 +205,10 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
167
205
|
const profile = settings.getProfile().trim();
|
|
168
206
|
const rules = settings.getRules();
|
|
169
207
|
if (!profile && !rules.length) return "";
|
|
170
|
-
const lines = [
|
|
171
|
-
if (profile) lines.push(
|
|
172
|
-
for (const rule of rules) lines.push(
|
|
173
|
-
return lines.join("\n");
|
|
208
|
+
const lines = [STR.userSettingsHeader[language]];
|
|
209
|
+
if (profile) lines.push(STR.profileLine[language](profile));
|
|
210
|
+
for (const rule of rules) lines.push(STR.ruleLine[language](rule));
|
|
211
|
+
return escapePromptVars(lines.join("\n"));
|
|
174
212
|
}
|
|
175
213
|
|
|
176
214
|
const disposers = [
|
|
@@ -185,7 +223,10 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
185
223
|
const query = lastUserQuery(ctx);
|
|
186
224
|
if (query) prefetchQueryVector(query);
|
|
187
225
|
const queryVector = queryVectorCache.get(query);
|
|
188
|
-
|
|
226
|
+
// v0.8.0 A3:scope 随请求解析(strict 开启时 injectCandidates 内硬过滤;
|
|
227
|
+
// flag 关闭时解析器返回 null,注入行为不变)。
|
|
228
|
+
const scope = resolveSessionScope(ctx);
|
|
229
|
+
const candidates = service.injectCandidates({ query, queryVector, maxItems, threshold, scope });
|
|
189
230
|
// Hot memory (v0.5.0 1.3) leads the single memory block: the agent
|
|
190
231
|
// sees the short-term rounds first, then the cross-session recall —
|
|
191
232
|
// the documented injection order 1→2. Folding it here (instead of a
|
package/lib/lang.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// 记忆语言(memory.language,src/config.js):生成记忆、注入标题与后台 LLM
|
|
2
|
+
// 提示词所用语言,'zh'(默认,上游行为不变)/ 'en'。本模块无全局状态:每个
|
|
3
|
+
// 插件实例从自己的 config 取语言(langOf),逐层传给 inject / summarize /
|
|
4
|
+
// dream / sleep / mirror,多实例(如 agent preset 内挂载)互不影响。
|
|
5
|
+
// 各语言对经 STR.<key>[language] 索引取值。
|
|
6
|
+
|
|
7
|
+
/** 解析实例语言:config.language === "en" 用英文,其余(含缺席)一律中文。 */
|
|
8
|
+
export function langOf(config) {
|
|
9
|
+
return config?.language === "en" ? "en" : "zh";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PROMPTS = {
|
|
13
|
+
"summary": {
|
|
14
|
+
"zh": "你是记忆库提炼助手。根据下面的会话内容,提炼值得跨会话记住的原子记忆。\n原子记忆原则:每条记忆只装一个独立事实/偏好/决策,短小、自带完整上下文(把数字、名字、路径、结论等原始细节保留在 content 里,不要抽象概括);宁可拆成多条也绝不合并丢细节。信息量一般提 2-4 条,信息密集的对话可提 4-8 条。\n只输出 JSON 数组,每项形如 {\"type\":\"preference|project|decision|history\",\"title\":\"简短标题\",\"content\":\"保留原始细节的一句话\",\"importance\":1-5}。\n不要输出任何其他文字。",
|
|
15
|
+
"en": "You are the memory curation assistant. From the conversation content below, distill atomic memories worth remembering across sessions.\nAtomic-memory principle: each memory holds exactly one standalone fact/preference/decision — short and self-contained (keep raw details like numbers, names, paths, and conclusions verbatim in content; do not abstract or summarize them away). Prefer splitting into multiple entries over merging and losing detail. Extract 2-4 entries normally, 4-8 for information-dense conversations.\nWrite every title and content in English.\nOutput only a JSON array, each item shaped {\"type\":\"preference|project|decision|history\",\"title\":\"short title\",\"content\":\"one sentence preserving original detail\",\"importance\":1-5}.\nOutput no other text."
|
|
16
|
+
},
|
|
17
|
+
"codingSummary": {
|
|
18
|
+
"zh": "你是记忆库提炼助手。根据下面的会话内容(含用户输入、助手回答、工具调用与结果),提炼值得跨会话记住的原子记忆。\n原子记忆原则:每条记忆只装一个独立事实/偏好/决策,短小、自带完整上下文(把数字、报错信息、命令、路径、结论等原始细节保留在 content 里,不要抽象概括);宁可拆成多条也绝不合并丢细节。信息量一般提 2-4 条,信息密集的对话可提 4-8 条。\n只输出 JSON 数组,每项形如 {\"type\":\"preference|project|decision|history|rejected_solution|pitfall|constraint\",\"title\":\"简短标题\",\"content\":\"保留原始细节的一句话\",\"importance\":1-5}。\n若对话涉及编码/调试,可额外提取编码类记忆:\n- rejected_solution:被否决/废弃的实现方案(content 含方案简述 + 被否决原因 + 最终采用方案)\n- pitfall:调试踩坑记录(content 含现象/报错 + 根因 + 解决/规避方法)\n- constraint:项目工程约束(content 含约束描述 + 来源)\n普通闲聊、临时无关对话一律不提取编码类记忆。不要输出任何其他文字。",
|
|
19
|
+
"en": "You are the memory curation assistant. From the conversation content below (including user input, assistant replies, tool calls and results), distill atomic memories worth remembering across sessions.\nAtomic-memory principle: each memory holds exactly one standalone fact/preference/decision — short and self-contained (keep raw details like numbers, error messages, commands, paths, and conclusions verbatim in content; do not abstract them away). Prefer splitting into multiple entries over merging and losing detail. Extract 2-4 entries normally, 4-8 for information-dense conversations.\nWrite every title and content in English.\nOutput only a JSON array, each item shaped {\"type\":\"preference|project|decision|history|rejected_solution|pitfall|constraint\",\"title\":\"short title\",\"content\":\"one sentence preserving original detail\",\"importance\":1-5}.\nIf the conversation involves coding/debugging, additionally extract coding-specific memories:\n- rejected_solution: implementation approaches that were rejected or abandoned (content: brief approach + why rejected + what was adopted instead)\n- pitfall: debugging pitfalls (content: symptom/error + root cause + fix or workaround)\n- constraint: project engineering constraints (content: constraint description + source)\nNever extract coding-specific memories from casual small talk or temporary unrelated conversations. Output no other text."
|
|
20
|
+
},
|
|
21
|
+
"dreamSummary": {
|
|
22
|
+
"zh": "你是记忆库摘要助手。根据整理后的记忆,生成一段 150-200 字的记忆库总览,覆盖:用户偏好、活跃项目、关键决策。之后作为会话上下文注入。只输出摘要文本,不要其他内容。",
|
|
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
|
+
},
|
|
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"
|
|
28
|
+
},
|
|
29
|
+
"conflict": {
|
|
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 数组,不要其他文字",
|
|
31
|
+
"en": "You are the memory conflict arbiter. Below are detected high-similarity memory pairs that may contradict or duplicate each other.\nFor each pair output one decision object:\n- The two entries genuinely contradict/duplicate -> { \"action\": \"conflict\", \"winner\": <id to keep>, \"loser\": <id to archive>, \"reason\": \"rationale\" }\n- The two entries are merely topically similar, no contradiction -> { \"action\": \"keep\", \"ids\": [<both ids>] }\nRules:\n- winner should be the more complete, newer, or more trustworthy entry\n- Use only the provided ids; never invent them\n- Every pair must produce exactly one decision\n- Write every reason in English\n- Output only a JSON array, no other text"
|
|
32
|
+
},
|
|
33
|
+
// Issue #126:sleepActionSet="full" 的六分支裁决 prompt。默认档("conflict")
|
|
34
|
+
// 仍用上面只有 conflict/keep 的窄 prompt——后台自动流程不静默扩张行为。
|
|
35
|
+
"conflictFull": {
|
|
36
|
+
"zh": "你是记忆库冲突仲裁助手。下面是检测到的高相似度记忆对。请对每一对**恰好输出一个** decision。\n\n先判断属于哪一类,再按对应格式输出:\n\n1. 同义重复(说的是同一件事,合并后不丢信息)→ { \"action\": \"merge\", \"ids\": [<两个id>], \"keepSource\": <保留的id>, \"title\": \"合并后标题\", \"content\": \"合并后内容\", \"importance\": 1-5 }\n2. 演进(旧版整体被新版取代、旧版作废)→ { \"action\": \"supersede\", \"winner\": <取代方id>, \"loser\": <被取代方id>, \"reason\": \"理由\" }\n3. 互补(两条各自成立、覆盖不同侧面,都不能丢)→ { \"action\": \"differentiate\", \"ids\": [<两个id>], \"distinctions\": [\"差异点1\", \"差异点2\"] }\n4. 信息部分过时(只需就地修正其中一条)→ { \"action\": \"update\", \"ids\": [<要修正的id>], \"title\": \"修正后标题\", \"content\": \"修正后内容\", \"importance\": 1-5, \"reason\": \"理由\" }\n5. 真矛盾(同一事实给出互斥结论,必须留一个)→ { \"action\": \"conflict\", \"winner\": <保留的id>, \"loser\": <归档的id>, \"reason\": \"理由\" }\n6. 主题相近但既无矛盾也不需合并 → { \"action\": \"keep\", \"ids\": [<两个id>] }\n\n判据先后:先看能否 merge(同义),再看是否只是旧版被取代(supersede),再看是否互补(differentiate),最后才考虑 conflict。拿不准时倾向 keep 或 differentiate——两条都留下比错误归档一条安全。\n规则:\n- 只使用提供的 id,不要编造\n- 每对必须输出一个 decision\n- winner/loser 与 keepSource 都是【单个 id 字符串,不是数组】\n- differentiate 的 distinctions 必须非空,写清两条各自覆盖的不同侧面\n- update 的 ids 只能含一个 id,且只能指向 24 小时以前创建的记忆(保护期内的新记忆不要 update)\n- 只输出 JSON 数组,不要其他文字",
|
|
37
|
+
"en": "You are the memory conflict arbiter. Below are detected high-similarity memory pairs. For each pair output **exactly one** decision.\n\nClassify first, then use the matching shape:\n\n1. Same-fact duplicate (same thing, merging loses no information) -> { \"action\": \"merge\", \"ids\": [<both ids>], \"keepSource\": <id to keep>, \"title\": \"merged title\", \"content\": \"merged content\", \"importance\": 1-5 }\n2. Evolution (the newer version supersedes and invalidates the older one) -> { \"action\": \"supersede\", \"winner\": <superseding id>, \"loser\": <superseded id>, \"reason\": \"rationale\" }\n3. Complementary (both hold, covering different aspects — neither can be dropped) -> { \"action\": \"differentiate\", \"ids\": [<both ids>], \"distinctions\": [\"difference 1\", \"difference 2\"] }\n4. Partially outdated (only one entry needs an in-place correction) -> { \"action\": \"update\", \"ids\": [<id to correct>], \"title\": \"corrected title\", \"content\": \"corrected content\", \"importance\": 1-5, \"reason\": \"rationale\" }\n5. Genuine contradiction (mutually exclusive conclusions on the same fact — one must go) -> { \"action\": \"conflict\", \"winner\": <id to keep>, \"loser\": <id to archive>, \"reason\": \"rationale\" }\n6. Topically similar but neither contradictory nor mergeable -> { \"action\": \"keep\", \"ids\": [<both ids>] }\n\nOrder of judgment: try merge first (same fact), then supersede (older version replaced), then differentiate (complementary), and only then conflict. When unsure, prefer keep or differentiate — keeping both is safer than wrongly archiving one.\nRules:\n- Use only the provided ids; never invent them\n- Every pair must produce exactly one decision\n- winner/loser and keepSource are each a [single id string, never an array]\n- distinctions must be a non-empty array spelling out what each entry covers\n- update may target only memories created more than 24 hours ago, and ids may contain only one id\n- Write every reason in English\n- Output only a JSON array, no other text"
|
|
38
|
+
},
|
|
39
|
+
"pattern": {
|
|
40
|
+
"zh": "你是记忆库模式发现助手。下面是最近的记忆条目(id、类型、标题、内容)。\n请发现跨条目的稳定模式:用户偏好的规律、反复出现的主题、可复用的工作流或项目规律。\n对每个模式输出一个 create decision:\n{ \"action\": \"create\", \"type\": \"pattern\", \"title\": \"模式一句话标题\", \"content\": \"模式详细描述(2-4句)\", \"importance\": 1-5, \"evidence\": [\"支持该模式的记忆id\"] }\n规则:\n- 只输出有据可依的模式,宁缺毋滥\n- evidence 必须是列表中真实存在的 id\n- 最多输出 N 个模式\n- 只输出 JSON 数组,不要其他文字",
|
|
41
|
+
"en": "You are the memory pattern-discovery assistant. Below are recent memory entries (id, type, title, content).\nDiscover stable cross-entry patterns: regularities in user preferences, recurring themes, reusable workflows or project patterns.\nFor each pattern output one create decision:\n{ \"action\": \"create\", \"type\": \"pattern\", \"title\": \"one-sentence pattern title\", \"content\": \"detailed pattern description (2-4 sentences)\", \"importance\": 1-5, \"evidence\": [\"ids of memories supporting this pattern\"] }\nRules:\n- Only output well-evidenced patterns; prefer fewer over filler\n- evidence must contain ids that actually exist in the list\n- Output at most N patterns\n- Write every title, content, and reason in English\n- Output only a JSON array, no other text"
|
|
42
|
+
},
|
|
43
|
+
"freezeSuffix": {
|
|
44
|
+
"zh": "\n\n当前为「冲突冻结」模式:检测到内容矛盾的条目时,仍请输出 conflict,并以 winner/loser 作为候选、reason 说明理由;冲突不会被自动裁决,而会冻结待人工确认。",
|
|
45
|
+
"en": "\n\nConflict-freeze mode is active: when you detect entries with contradictory content, still output conflict — winner/loser are treated as candidates and reason explains the rationale; conflicts are not auto-adjudicated but frozen for human confirmation."
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const STR = {
|
|
50
|
+
// --- inject.js:注入块标题 / 条目行 ---------------------------------------
|
|
51
|
+
hotHeader: {
|
|
52
|
+
zh: (rounds, body) => `[短期上下文] 最近对话(共 ${rounds} 轮):\n${body}`,
|
|
53
|
+
en: (rounds, body) => `[Short-term context] Recent conversation (${rounds} rounds):\n${body}`
|
|
54
|
+
},
|
|
55
|
+
memoryHeader: {
|
|
56
|
+
zh: "[记忆库] 来自 dsh-mneme 的跨会话记忆(用户偏好与高优先级项目/决策):",
|
|
57
|
+
en: "[Memory] Cross-session memories from dsh-mneme (user preferences and high-priority project/decision entries):"
|
|
58
|
+
},
|
|
59
|
+
verified: { zh: "[verified] ", en: "[verified] " },
|
|
60
|
+
entryTitle: {
|
|
61
|
+
zh: (title, importance) => `${title}(重要性 ${importance})`,
|
|
62
|
+
en: (title, importance) => `${title} (importance ${importance})`
|
|
63
|
+
},
|
|
64
|
+
entryLine: {
|
|
65
|
+
zh: (type, verified, title, content) => `- [${type}] ${verified}${title}:${content}`,
|
|
66
|
+
en: (type, verified, title, content) => `- [${type}] ${verified}${title}: ${content}`
|
|
67
|
+
},
|
|
68
|
+
userSettingsHeader: {
|
|
69
|
+
zh: "[用户设置] 来自 dsh-mneme 的用户画像与规则:",
|
|
70
|
+
en: "[User settings] Profile and rules from dsh-mneme:"
|
|
71
|
+
},
|
|
72
|
+
profileLine: {
|
|
73
|
+
zh: (profile) => `- 用户画像:${profile}`,
|
|
74
|
+
en: (profile) => `- Profile: ${profile}`
|
|
75
|
+
},
|
|
76
|
+
ruleLine: {
|
|
77
|
+
zh: (rule) => `- 规则:${rule}`,
|
|
78
|
+
en: (rule) => `- Rule: ${rule}`
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// --- summarize.js:蒸馏转录标签(进入 LLM 上下文) --------------------------
|
|
82
|
+
transcriptUser: { zh: (t) => `用户:${t}`, en: (t) => `User: ${t}` },
|
|
83
|
+
transcriptAssistant: { zh: (t) => `助手:${t}`, en: (t) => `Assistant: ${t}` },
|
|
84
|
+
transcriptToolCall: {
|
|
85
|
+
zh: (name, args) => `工具调用:${name}(${args})`,
|
|
86
|
+
en: (name, args) => `Tool call: ${name}(${args})`
|
|
87
|
+
},
|
|
88
|
+
statusOk: { zh: "成功", en: "success" },
|
|
89
|
+
statusFail: { zh: "失败", en: "failed" },
|
|
90
|
+
transcriptToolResult: {
|
|
91
|
+
zh: (status, out) => `工具结果(${status}):${out}`,
|
|
92
|
+
en: (status, out) => `Tool result (${status}): ${out}`
|
|
93
|
+
},
|
|
94
|
+
transcriptCode: {
|
|
95
|
+
zh: (status, out) => `代码执行(${status}):${out}`,
|
|
96
|
+
en: (status, out) => `Code execution (${status}): ${out}`
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// --- dream.js:聚类快照标记 -------------------------------------------------
|
|
100
|
+
clusterHeader: { zh: (n) => `# 聚类 ${n}`, en: (n) => `# Cluster ${n}` },
|
|
101
|
+
conflictMark: { zh: " | [潜在冲突]", en: " | [potential conflict]" },
|
|
102
|
+
summaryTitle: { zh: "记忆库总览", en: "Memory library overview" },
|
|
103
|
+
|
|
104
|
+
// --- dream/sleep.js:冲突候选列表 --------------------------------------------
|
|
105
|
+
similarityReason: { zh: (s) => `相似度 ${s}`, en: (s) => `similarity ${s}` },
|
|
106
|
+
candidateConflicts: {
|
|
107
|
+
zh: (p) => `候选冲突:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(相似度 ${p.similarity.toFixed(2)})`,
|
|
108
|
+
en: (p) => `Candidate conflicts:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(similarity ${p.similarity.toFixed(2)})`
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// --- dream/decisions.js:写回记忆的来源批注 ------------------------------------
|
|
112
|
+
evidenceSuffix: {
|
|
113
|
+
zh: (content, evidence) => `${content}\n\n[证据: ${evidence.join(", ")}]`,
|
|
114
|
+
en: (content, evidence) => `${content}\n\n[Evidence: ${evidence.join(", ")}]`
|
|
115
|
+
},
|
|
116
|
+
supersededSuffix: {
|
|
117
|
+
zh: (content, oldInfo) => `${content}\n\n(已否决旧信息:${oldInfo})`,
|
|
118
|
+
en: (content, oldInfo) => `${content}\n\n(superseded outdated info: ${oldInfo})`
|
|
119
|
+
},
|
|
120
|
+
// Issue #126:supersede 与 conflict 的关键差别——注记写在 loser(被取代方)
|
|
121
|
+
// 而不是 winner,赢家正文因此保持干净(这正是 #126 对 conflict 的原抱怨)。
|
|
122
|
+
supersededBySuffix: {
|
|
123
|
+
zh: (content, winnerTitle) => `${content}\n\n(已被取代:${winnerTitle})`,
|
|
124
|
+
en: (content, winnerTitle) => `${content}\n\n(superseded by: ${winnerTitle})`
|
|
125
|
+
},
|
|
126
|
+
// Issue #126:differentiate 的差异注记,双方都要追加(进正文 → 进注入与
|
|
127
|
+
// embedding,下一轮就不会再被判成重复)。
|
|
128
|
+
differentiatedMarker: {
|
|
129
|
+
zh: (notes) => `\n\n(差异注记:${notes.join(";")})`,
|
|
130
|
+
en: (notes) => `\n\n(differences: ${notes.join("; ")})`
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// --- service.js:三方合并冲突批注(写入记忆内容) -----------------------------
|
|
134
|
+
serviceConflictMarker: {
|
|
135
|
+
zh: (ts, content) => `\n\n> ⚠️ 并发冲突:人工编辑 vs 记忆库并发更新(${ts})\n> 记忆库版本:${content}`,
|
|
136
|
+
en: (ts, content) => `\n\n> ⚠️ Concurrent conflict: human edit vs concurrent memory-library update (${ts})\n> memory-library version: ${content}`
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// --- commands.js:自定义指令缺省描述(进命令注册表,LLM 可见) -----------------
|
|
140
|
+
commandFallbackDesc: {
|
|
141
|
+
zh: (name) => `自定义指令 ${name}`,
|
|
142
|
+
en: (name) => `Custom command ${name}`
|
|
143
|
+
},
|
|
144
|
+
commandInputHint: {
|
|
145
|
+
zh: "可选:追加给模型的补充说明",
|
|
146
|
+
en: "Optional: extra note to append for the model"
|
|
147
|
+
},
|
|
148
|
+
commandSubmitted: {
|
|
149
|
+
zh: (name) => `已提交指令:${name}`,
|
|
150
|
+
en: (name) => `Submitted command: ${name}`
|
|
151
|
+
},
|
|
152
|
+
commandSubmitFailed: {
|
|
153
|
+
zh: (name) => `指令 ${name} 提交失败,详见日志`,
|
|
154
|
+
en: (name) => `Failed to submit command ${name}; see logs`
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// --- mirror.js:镜像文件标签(渲染随实例语言;解析两种语言都认) ----------------
|
|
158
|
+
mirrorLabel: {
|
|
159
|
+
zh: { type: "类型", importance: "重要性", tags: "标签", updated: "更新时间", source: "来源" },
|
|
160
|
+
en: { type: "Type", importance: "Importance", tags: "Tags", updated: "Updated", source: "Source" }
|
|
161
|
+
},
|
|
162
|
+
mirrorHeader: {
|
|
163
|
+
zh: (name) => `# ${name} — dsh-mneme 镜像\n\n<!-- 手工编辑此文件会被合并回记忆库(人工优先)。 -->\n\n`,
|
|
164
|
+
en: (name) => `# ${name} — dsh-mneme mirror\n\n<!-- Manual edits to this file are merged back into the memory store (human edits win). -->\n\n`
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
prompts: PROMPTS
|
|
168
|
+
};
|
package/lib/local-embedder.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// old embedding.js logic). All classes share one interface so the orchestrator
|
|
4
4
|
// can pick a backend by provider name and degrade gracefully on failure.
|
|
5
5
|
// Methods throw on error — the caller decides the fallback chain.
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import { defaultModelCacheDir, defaultRuntimeDir } from "./runtime/layout.js";
|
|
7
|
+
import { loadTransformers } from "./runtime/loader.js";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
10
10
|
|
|
@@ -20,16 +20,28 @@ function modelHash(model) {
|
|
|
20
20
|
return `${model}#${hashString(model)}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Lazy default loader: dynamic import keeps module load cheap.
|
|
25
|
+
*
|
|
26
|
+
* 经 runtime/loader.js 的三层解析取运行时(自管 payload 优先,其次宿主裸 specifier):
|
|
27
|
+
* 这样 PR-B 把 @huggingface/transformers 从 dependencies 摘掉、pnpm 把宿主那份 prune
|
|
28
|
+
* 掉之后,本地嵌入仍然可用;而在那之前,宿主那份照样能被第 ② 层兜住。
|
|
29
|
+
* runtimeDir 从 options 里取走后再传 pipeline,免得把插件自己的配置项混进
|
|
30
|
+
* transformers 的选项里。
|
|
31
|
+
*/
|
|
24
32
|
async function defaultPipelineLoader(task, model, options) {
|
|
25
|
-
const {
|
|
33
|
+
const { runtimeDir, ...pipelineOptions } = options ?? {};
|
|
34
|
+
const { module } = await loadTransformers({
|
|
35
|
+
runtimeDir: runtimeDir || defaultRuntimeDir()
|
|
36
|
+
});
|
|
37
|
+
const { env, pipeline } = module;
|
|
26
38
|
// issue #13: transformers.js's get_tokenizer_files() drops the caller's
|
|
27
39
|
// cache_dir when it pre-checks tokenizer_config.json metadata, so the HEAD
|
|
28
40
|
// request falls back to env.cacheDir and hits the network even when the
|
|
29
41
|
// model is fully cached locally. Mirroring the cache_dir onto env.cacheDir
|
|
30
42
|
// makes that pre-check resolve locally too — fully offline loading.
|
|
31
|
-
if (
|
|
32
|
-
return pipeline(task, model,
|
|
43
|
+
if (pipelineOptions.cache_dir) env.cacheDir = pipelineOptions.cache_dir;
|
|
44
|
+
return pipeline(task, model, pipelineOptions);
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
/** Flatten a transformers.js Tensor [batch, dim] into number[][]. */
|
|
@@ -56,9 +68,9 @@ export class LocalEmbedder {
|
|
|
56
68
|
this._dimension = opts.dimension || 512;
|
|
57
69
|
this.device = opts.device || "cpu";
|
|
58
70
|
this.batchSize = opts.batchSize || 8;
|
|
59
|
-
this.cacheDir =
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
this.cacheDir = String(opts.cacheDir ?? "").trim() || defaultModelCacheDir();
|
|
72
|
+
// 自管运行时根目录(issue #131):空表示用默认位置,具体解析交给 loader。
|
|
73
|
+
this.runtimeDir = String(opts.runtimeDir ?? "").trim();
|
|
62
74
|
this.useDtype = opts.useDtype || "q8";
|
|
63
75
|
this.logger = opts.logger ?? null;
|
|
64
76
|
// Test hook: replace the pipeline factory without touching modules.
|
|
@@ -77,6 +89,8 @@ export class LocalEmbedder {
|
|
|
77
89
|
device: this.device
|
|
78
90
|
};
|
|
79
91
|
if (this.cacheDir) options.cache_dir = this.cacheDir;
|
|
92
|
+
// 传给注入的 engineFactory,由 defaultPipelineLoader 取走后交给三层解析。
|
|
93
|
+
if (this.runtimeDir) options.runtimeDir = this.runtimeDir;
|
|
80
94
|
this.extractor = await this.engineFactory("feature-extraction", this.model, options);
|
|
81
95
|
this.ready = true; // service reads this to flush queued re-embeds
|
|
82
96
|
this.logger?.info?.(
|
package/lib/mirror.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { STR, langOf } from "./lang.js";
|
|
4
5
|
|
|
5
6
|
export const TYPE_FILE = {
|
|
6
7
|
preference: "preferences.md",
|
|
@@ -21,7 +22,7 @@ function unescape(text) {
|
|
|
21
22
|
return String(text).replace(UNESCAPE, "$1");
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
function renderMemory(m) {
|
|
25
|
+
function renderMemory(m, language = "zh") {
|
|
25
26
|
// last-rendered digest baseline: sha256(title \x00 content). service.js
|
|
26
27
|
// compares the file hash against this to tell "untouched by a human" (machine
|
|
27
28
|
// write wins) apart from a real human edit, so a not-yet-re-rendered store
|
|
@@ -33,11 +34,12 @@ function renderMemory(m) {
|
|
|
33
34
|
lines.push(`## ${esc(m.title)}`);
|
|
34
35
|
lines.push("");
|
|
35
36
|
lines.push(`- **ID**: \`${m.id}\``);
|
|
36
|
-
|
|
37
|
-
lines.push(`-
|
|
38
|
-
lines.push(`-
|
|
39
|
-
lines.push(`-
|
|
40
|
-
|
|
37
|
+
const ML = STR.mirrorLabel[language];
|
|
38
|
+
lines.push(`- **${ML.type}**: ${m.type}`);
|
|
39
|
+
lines.push(`- **${ML.importance}**: ${m.importance}`);
|
|
40
|
+
lines.push(`- **${ML.tags}**: ${m.tags.map((t) => `\`${esc(t)}\``).join(" ")}`);
|
|
41
|
+
lines.push(`- **${ML.updated}**: ${m.updated_at}`);
|
|
42
|
+
if (m.source) lines.push(`- **${ML.source}**: ${esc(m.source)}`);
|
|
41
43
|
lines.push("");
|
|
42
44
|
lines.push(`<!-- mirror-digest: ${digest} -->`);
|
|
43
45
|
lines.push(m.content);
|
|
@@ -54,14 +56,14 @@ function renderMemory(m) {
|
|
|
54
56
|
* byte-compatible with a mirror file and can be fed straight back through
|
|
55
57
|
* parseHumanEdits → mergeHumanEdits. Unknown type → undefined.
|
|
56
58
|
*/
|
|
57
|
-
export function renderMirrorText(type, memories) {
|
|
59
|
+
export function renderMirrorText(type, memories, language = "zh") {
|
|
58
60
|
const name = TYPE_FILE[type];
|
|
59
61
|
if (!name) return undefined;
|
|
60
62
|
const items = (memories ?? [])
|
|
61
63
|
.slice()
|
|
62
64
|
.sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1));
|
|
63
|
-
const header =
|
|
64
|
-
const body = items.map(renderMemory).join("\n");
|
|
65
|
+
const header = STR.mirrorHeader[language](name);
|
|
66
|
+
const body = items.map((m) => renderMemory(m, language)).join("\n");
|
|
65
67
|
return header + body;
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -88,7 +90,8 @@ export function parseHumanEdits(text) {
|
|
|
88
90
|
// machine-rendered ID line is always followed by the "- **类型**:" line.
|
|
89
91
|
// A body line like "- **ID**: `x`" is not, so it never splits the block
|
|
90
92
|
// or produces a ghost entry.
|
|
91
|
-
|
|
93
|
+
// 解析同时接受两种语言的标签:切换语言前渲染的镜像文件仍能合并。
|
|
94
|
+
const anchors = [...normalized.matchAll(/^- \*\*ID\*\*: `([^`]+)`\n- \*\*(?:类型|Type)\*\*:/gm)];
|
|
92
95
|
let prevEnd = 0;
|
|
93
96
|
for (let i = 0; i < anchors.length; i++) {
|
|
94
97
|
const anchor = anchors[i];
|
|
@@ -106,7 +109,7 @@ export function parseHumanEdits(text) {
|
|
|
106
109
|
let body = normalized
|
|
107
110
|
.slice(blockStart, blockEnd)
|
|
108
111
|
.replace(/^- \*\*ID\*\*: `[^`]+`\n?/, "")
|
|
109
|
-
.replace(/^(- \*\*(
|
|
112
|
+
.replace(/^(- \*\*(?:类型|重要性|标签|更新时间|来源|Type|Importance|Tags|Updated|Source)\*\*:.*\n?)+/, "")
|
|
110
113
|
.replace(/^<!-- mirror-digest: [a-f0-9]+ -->\n?/m, "");
|
|
111
114
|
const separators = [...body.matchAll(/^---\s*$/gm)];
|
|
112
115
|
const lastSep = separators[separators.length - 1];
|
|
@@ -117,7 +120,7 @@ export function parseHumanEdits(text) {
|
|
|
117
120
|
// render time — the version token for detecting a concurrent store write
|
|
118
121
|
// during a three-way merge of human edits (see service.syncMirror).
|
|
119
122
|
const block = normalized.slice(blockStart, blockEnd);
|
|
120
|
-
const updatedMatch = block.match(/-
|
|
123
|
+
const updatedMatch = block.match(/- \*\*(?:更新时间|Updated)\*\*: ([^\n]+)/);
|
|
121
124
|
const digestMatch = block.match(/<!-- mirror-digest: ([a-f0-9]+) -->/);
|
|
122
125
|
edits.push({
|
|
123
126
|
id: anchor[1],
|
|
@@ -133,7 +136,7 @@ export function parseHumanEdits(text) {
|
|
|
133
136
|
return edits;
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
export function createMirror(dir) {
|
|
139
|
+
export function createMirror(dir, language = "zh") {
|
|
137
140
|
mkdirSync(dir, { recursive: true });
|
|
138
141
|
|
|
139
142
|
function filePath(type) {
|
|
@@ -181,7 +184,7 @@ export function createMirror(dir) {
|
|
|
181
184
|
} else {
|
|
182
185
|
// 渲染走 renderMirrorText(与 /export 共用同一条渲染路径),磁盘镜像
|
|
183
186
|
// 与导出文本永远同构。
|
|
184
|
-
writeFileSync(file, renderMirrorText(type, items), "utf8");
|
|
187
|
+
writeFileSync(file, renderMirrorText(type, items, language), "utf8");
|
|
185
188
|
}
|
|
186
189
|
results[type] = { ok: true };
|
|
187
190
|
} catch (error) {
|
package/lib/reranker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { defaultModelCacheDir, defaultRuntimeDir } from "./runtime/layout.js";
|
|
2
|
+
import { loadTransformers } from "./runtime/loader.js";
|
|
3
3
|
|
|
4
4
|
// Cross-encoder re-ranker for dsh-mneme recall candidates. Uses
|
|
5
5
|
// bge-reranker-base through transformers.js: tries the native `rerank` task
|
|
@@ -19,10 +19,24 @@ function modelHash(model) {
|
|
|
19
19
|
return `${model}#${hashString(model)}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Lazy default pipeline factory: dynamic import keeps module load cheap.
|
|
24
|
+
*
|
|
25
|
+
* 与 local-embedder 一样经 runtime/loader.js 的三层解析取运行时(自管 payload 优先,
|
|
26
|
+
* 其次宿主裸 specifier),这样 PR-B 把依赖从 profile 摘掉后 rerank 也还能用。
|
|
27
|
+
* runtimeDir 取走后不混进 transformers 的选项。
|
|
28
|
+
*/
|
|
23
29
|
async function defaultPipelineLoader(task, model, options) {
|
|
24
|
-
const {
|
|
25
|
-
|
|
30
|
+
const { runtimeDir, ...pipelineOptions } = options ?? {};
|
|
31
|
+
const { module } = await loadTransformers({
|
|
32
|
+
runtimeDir: runtimeDir || defaultRuntimeDir()
|
|
33
|
+
});
|
|
34
|
+
const { env, pipeline } = module;
|
|
35
|
+
// issue #13 同款处理:transformers.js 在预检 tokenizer_config.json 元数据时会丢掉调用方
|
|
36
|
+
// 的 cache_dir,回退到 env.cacheDir —— 即使模型已完全缓存在本地也会去请求网络。
|
|
37
|
+
// embedder 侧早就镜像了,reranker 侧一直漏着,于是「已缓存 + 离线」时重排初始化会失败。
|
|
38
|
+
if (pipelineOptions.cache_dir) env.cacheDir = pipelineOptions.cache_dir;
|
|
39
|
+
return pipeline(task, model, pipelineOptions);
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
/** Flatten a transformers.js Tensor [batch, seq, dim] into number[][] rows. */
|
|
@@ -68,9 +82,9 @@ export class LocalReranker {
|
|
|
68
82
|
this.maxCandidates = opts.maxCandidates || 30;
|
|
69
83
|
this.scoreThreshold = opts.scoreThreshold ?? 0.1;
|
|
70
84
|
this.device = opts.device || "cpu";
|
|
71
|
-
this.cacheDir =
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
this.cacheDir = String(opts.cacheDir ?? "").trim() || defaultModelCacheDir();
|
|
86
|
+
// 自管运行时根目录(issue #131):空表示用默认位置,解析交给 loader。
|
|
87
|
+
this.runtimeDir = String(opts.runtimeDir ?? "").trim();
|
|
74
88
|
this.logger = opts.logger ?? null;
|
|
75
89
|
this.engineFactory = opts.engineFactory || defaultPipelineLoader;
|
|
76
90
|
// Injectable seam: async (query, passage) => number. When set, init()
|
|
@@ -107,6 +121,7 @@ export class LocalReranker {
|
|
|
107
121
|
_engineOptions() {
|
|
108
122
|
const options = { device: this.device };
|
|
109
123
|
if (this.cacheDir) options.cache_dir = this.cacheDir;
|
|
124
|
+
if (this.runtimeDir) options.runtimeDir = this.runtimeDir;
|
|
110
125
|
return options;
|
|
111
126
|
}
|
|
112
127
|
|