@modusensus/dsh-mneme 0.7.9 → 0.7.11
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.en.md +368 -0
- package/README.md +298 -167
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/{dsh-mneme/lib → lib}/api.js +58 -284
- package/lib/client.js +2086 -0
- package/{dsh-mneme/src → lib}/config.js +6 -100
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/src → lib}/index.js +6 -34
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/lib → lib}/settings.js +0 -70
- package/{dsh-mneme/lib → lib}/store.js +35 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +41 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -3
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/{dsh-mneme/src → src}/api.js +58 -284
- package/{dsh-mneme/lib → src}/config.js +6 -100
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → src}/index.js +6 -34
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/src → src}/settings.js +0 -70
- package/{dsh-mneme/src → src}/store.js +35 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +1 -156
- package/{dsh-mneme/test → test}/client.test.js +22 -207
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +0 -43
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -291
- package/SECURITY.md +0 -718
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -406
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -498
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/lib/client.js +0 -2050
- package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2050
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
3
|
-
import { runAutoTag } from "./dream/tag-extractor.js";
|
|
4
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
export { validateDecisions, applyDecisions
|
|
4
|
+
export { validateDecisions, applyDecisions };
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
@@ -41,95 +40,6 @@ function extractJsonArray(text) {
|
|
|
41
40
|
}
|
|
42
41
|
return null;
|
|
43
42
|
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Field-name drift guard (v0.5.3): thinking-type models (deepseek-v4-flash
|
|
47
|
-
* etc.) occasionally ignore the prompt's exact decision schema and emit alias
|
|
48
|
-
* keys —实测方案 A 输出 "consolidation"/"target_ids"、方案 B 输出
|
|
49
|
-
* "action"/"targetIds",都不是插件要求的 "action"/"ids"。normalizeDecisions
|
|
50
|
-
* 在 validateDecisions 之前把常见变体重写回规范字段名,让"字段名不听话但
|
|
51
|
-
* 语义正确"的输出仍可被应用,而不是整单被拒。覆盖三类漂移:
|
|
52
|
-
* 1. 整个 body 是包在对象里的数组({consolidation:[...]} /
|
|
53
|
-
* {decisions:[...]} / {actions:[...]});
|
|
54
|
-
* 2. 决策对象用了别名键(target_ids→ids、keep_source→keepSource、
|
|
55
|
-
* winner_id→winner、targetIds→ids 等);
|
|
56
|
-
* 3. action 值用了同义词(archived→archive、consolidation→merge 等)。
|
|
57
|
-
* 无归一化必要时原样返回(null→null,调用方"no json array"分支不受影响)。
|
|
58
|
-
*/
|
|
59
|
-
const FIELD_ALIASES = {
|
|
60
|
-
// "consolidation" 也可作 action 键(实测 deepseek-v4-flash 这么写过);
|
|
61
|
-
// 但 normalizeOne 对 action 键做字符串过滤,{consolidation:[...]} 这种
|
|
62
|
-
// wrapper 数组不会被误当成 action(顶层 WRAPPER_KEYS 负责解包)。
|
|
63
|
-
action: ["action", "decision", "operation", "op", "action_type", "actionType", "mode", "consolidation"],
|
|
64
|
-
// 注意:action 别名不含 "type"——create 决策的 type 是合法的记忆类型字段,
|
|
65
|
-
// 不能误当 action。
|
|
66
|
-
ids: ["ids", "target_ids", "targetIds", "targets", "memory_ids", "memoryIds", "id_list", "idList", "memories"],
|
|
67
|
-
reason: ["reason", "rationale", "why", "comment", "note", "explanation"],
|
|
68
|
-
importance: ["importance", "priority", "weight", "level", "score"],
|
|
69
|
-
keepSource: ["keepSource", "keep_source", "source_id", "sourceId", "keeper", "keep_id", "keepId"],
|
|
70
|
-
winner: ["winner", "winner_id", "winnerId", "win_id", "winId", "preferred", "primary"],
|
|
71
|
-
loser: ["loser", "loser_id", "loserId", "lose_id", "loseId", "drop_id", "dropId", "archive_id", "archiveId"],
|
|
72
|
-
title: ["title", "new_title", "newTitle", "merged_title", "mergedTitle", "merge_title", "mergeTitle"],
|
|
73
|
-
content: ["content", "new_content", "newContent", "merged_content", "mergedContent", "merge_content", "mergeContent"],
|
|
74
|
-
evidence: ["evidence", "evidence_ids", "evidenceIds"]
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// 保守的 action 值同义词:只收语义无歧义、不可能被误认为记忆类型/其他 action 的映射。
|
|
78
|
-
const ACTION_SYNONYMS = {
|
|
79
|
-
archived: "archive",
|
|
80
|
-
remove: "archive",
|
|
81
|
-
delete: "archive",
|
|
82
|
-
combine: "merge",
|
|
83
|
-
consolidation: "merge",
|
|
84
|
-
consolidate: "merge",
|
|
85
|
-
modify: "update",
|
|
86
|
-
edit: "update"
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const WRAPPER_KEYS = ["consolidation", "decisions", "actions", "results", "updates", "list", "data"];
|
|
90
|
-
|
|
91
|
-
/** 把单个决策对象重写到规范字段名;非对象原样返回。 */
|
|
92
|
-
function normalizeOne(raw) {
|
|
93
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
94
|
-
const d = {};
|
|
95
|
-
const consumed = new Set();
|
|
96
|
-
for (const [canon, aliases] of Object.entries(FIELD_ALIASES)) {
|
|
97
|
-
for (const key of aliases) {
|
|
98
|
-
if (key in raw && raw[key] !== undefined && raw[key] !== null) {
|
|
99
|
-
// action 必须是字符串——{consolidation:[...]} 的 wrapper 数组跳过。
|
|
100
|
-
if (canon === "action" && Array.isArray(raw[key])) continue;
|
|
101
|
-
d[canon] = raw[key];
|
|
102
|
-
consumed.add(key);
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// 保留未被别名消费的原始键(create 的 type、未来字段)原样透传。
|
|
108
|
-
for (const key of Object.keys(raw)) {
|
|
109
|
-
if (!consumed.has(key)) d[key] = raw[key];
|
|
110
|
-
}
|
|
111
|
-
if (typeof d.action === "string") {
|
|
112
|
-
const low = d.action.toLowerCase();
|
|
113
|
-
if (ACTION_SYNONYMS[low] !== undefined) d.action = ACTION_SYNONYMS[low];
|
|
114
|
-
}
|
|
115
|
-
// ids 若被写成单个字符串则包成数组(validateDecisions 要求数组)。
|
|
116
|
-
if (d.ids !== undefined && typeof d.ids === "string") d.ids = [d.ids];
|
|
117
|
-
return d;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** 归一化 LLM 决策 body(数组 / 包裹对象 / 单个决策对象),null 原样返回。 */
|
|
121
|
-
function normalizeDecisions(raw) {
|
|
122
|
-
if (!raw) return raw;
|
|
123
|
-
if (Array.isArray(raw)) return raw.map(normalizeOne).filter((d) => d && typeof d === "object");
|
|
124
|
-
if (typeof raw === "object") {
|
|
125
|
-
for (const key of WRAPPER_KEYS) {
|
|
126
|
-
if (Array.isArray(raw[key])) return normalizeDecisions(raw[key]);
|
|
127
|
-
}
|
|
128
|
-
// 单个决策对象 → 包成单元素数组(validateDecisions 要求数组)。
|
|
129
|
-
return [normalizeOne(raw)];
|
|
130
|
-
}
|
|
131
|
-
return raw;
|
|
132
|
-
}
|
|
133
43
|
const SUMMARY_PROMPT = `你是记忆库摘要助手。根据整理后的记忆,生成一段 150-200 字的记忆库总览,覆盖:用户偏好、活跃项目、关键决策。之后作为会话上下文注入。只输出摘要文本,不要其他内容。`;
|
|
134
44
|
|
|
135
45
|
const CONSOLIDATION_PROMPT = `你是记忆库整理助手。下面是全部记忆条目(id、类型、标题、内容、重要性、更新时间)。
|
|
@@ -401,26 +311,20 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
401
311
|
}
|
|
402
312
|
|
|
403
313
|
/**
|
|
404
|
-
* Resolve the LLM route (
|
|
405
|
-
* (dreamProvider/dreamModel)
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* this ordering the config route would be dead code and dreamProvider/dreamModel
|
|
409
|
-
* could never take effect. Falls through to undefined when no route exists —
|
|
410
|
-
* runDream then fails safe. A config→default switch is logged so it is observable.
|
|
314
|
+
* Resolve the LLM route: agent default model (deployment) first, plugin config
|
|
315
|
+
* (dreamProvider/dreamModel) as fallback. Falls through to undefined when no
|
|
316
|
+
* route exists — runDream then fails safe. Fallback is logged so a silent
|
|
317
|
+
* route switch is observable.
|
|
411
318
|
*/
|
|
412
319
|
function resolveRoute(ctx, config, logger) {
|
|
413
|
-
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
414
320
|
try {
|
|
415
321
|
const sel = ctx.agentDefaultModel?.currentSelection?.();
|
|
416
|
-
if (sel?.provider && sel?.model) {
|
|
417
|
-
|
|
418
|
-
return { provider: sel.provider, model: sel.model };
|
|
419
|
-
}
|
|
420
|
-
logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, no config route either");
|
|
322
|
+
if (sel?.provider && sel?.model) return { provider: sel.provider, model: sel.model };
|
|
323
|
+
logger?.warn?.("dsh-mneme dream: agentDefaultModel unavailable, falling back to config route");
|
|
421
324
|
} catch (error) {
|
|
422
|
-
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed: ${String(error)}`);
|
|
325
|
+
logger?.warn?.(`dsh-mneme dream: agentDefaultModel lookup failed, falling back to config route: ${String(error)}`);
|
|
423
326
|
}
|
|
327
|
+
if (config.dreamProvider && config.dreamModel) return { provider: config.dreamProvider, model: config.dreamModel };
|
|
424
328
|
return undefined;
|
|
425
329
|
}
|
|
426
330
|
|
|
@@ -502,7 +406,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
502
406
|
let inFlight = null;
|
|
503
407
|
|
|
504
408
|
function shouldTrigger(service) {
|
|
505
|
-
const memories = service.all().filter((m) => !m.archived &&
|
|
409
|
+
const memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
506
410
|
const count = memories.length;
|
|
507
411
|
const chars = totalChars(memories);
|
|
508
412
|
const overBase = count >= baseline.count + thresholdCount || chars >= baseline.chars + thresholdChars;
|
|
@@ -559,7 +463,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
559
463
|
if (inFlight) await inFlight.catch(() => {});
|
|
560
464
|
}
|
|
561
465
|
|
|
562
|
-
async function runDream(ctx, service, config
|
|
466
|
+
async function runDream(ctx, service, config) {
|
|
563
467
|
const logger = ctx.logger;
|
|
564
468
|
let memories = service.all().filter((m) => !m.archived && m.type !== "summary");
|
|
565
469
|
if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
|
|
@@ -712,35 +616,23 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
712
616
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
713
617
|
}
|
|
714
618
|
|
|
715
|
-
|
|
716
|
-
// 在 validateDecisions 之前重写到规范字段名,语义正确但 schema 不听话的
|
|
717
|
-
// 输出不再整单被拒。
|
|
718
|
-
const decisions = normalizeDecisions(extractJsonArray(decisionText));
|
|
619
|
+
const decisions = extractJsonArray(decisionText);
|
|
719
620
|
if (!Array.isArray(decisions)) {
|
|
720
621
|
logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0})`);
|
|
721
622
|
return finish({ ok: false, error: "no json array in llm output", summary: false });
|
|
722
623
|
}
|
|
723
|
-
const { ok, errors
|
|
624
|
+
const { ok, errors } = validateDecisions(decisions, snapshot, {
|
|
724
625
|
maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
|
|
725
626
|
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
726
627
|
// v0.4.4 fix:显式透传,用户配 dreamImplicitKeep:false 时严格模式必须
|
|
727
628
|
// 真正生效,dreamMinExplicitCoverage 决定隐式 keep 下的覆盖率下限。
|
|
728
629
|
dreamImplicitKeep: config.dreamImplicitKeep,
|
|
729
|
-
dreamMinExplicitCoverage: config.dreamMinExplicitCoverage
|
|
730
|
-
// Issue #26 (P0):跨类型 merge 等"单条非法"决策不再拖垮整批——默认开启
|
|
731
|
-
// skipInvalid,非法决策跳过、合法子集照常应用(run 记为 degraded)。
|
|
732
|
-
// dreamSkipInvalid:false 可恢复旧的"任意非法即整单拒绝"。
|
|
733
|
-
skipInvalid: config.dreamSkipInvalid !== false,
|
|
734
|
-
// Issue #26 (P1):显式开启 allowCrossTypeMerge 后放宽跨类型合并检查。
|
|
735
|
-
allowCrossTypeMerge: config.allowCrossTypeMerge === true
|
|
630
|
+
dreamMinExplicitCoverage: config.dreamMinExplicitCoverage
|
|
736
631
|
});
|
|
737
632
|
if (!ok) {
|
|
738
633
|
logger?.warn?.(`dsh-mneme dream: invalid decisions: ${errors.join("; ")}`);
|
|
739
634
|
return finish({ ok: false, error: `invalid decisions: ${errors.length} errors`, summary: false });
|
|
740
635
|
}
|
|
741
|
-
for (const s of skipped) {
|
|
742
|
-
logger?.warn?.(`dsh-mneme dream: skipping invalid decision[${s.index}] (${s.action}): ${s.error}`);
|
|
743
|
-
}
|
|
744
636
|
|
|
745
637
|
// Capture pre-update snapshots so the audit records what each update changed.
|
|
746
638
|
const updateSnapshots = {};
|
|
@@ -813,11 +705,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
813
705
|
);
|
|
814
706
|
// Outcome is derived from the ACTUALLY committed sub-steps, never from the
|
|
815
707
|
// raw LLM decision list — a merge whose archive step rolled back must not
|
|
816
|
-
// claim "merge-archived" (item ②). Conflicts/failures
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
// e.g. cross-type merge).
|
|
820
|
-
const outcome = { ...buildOutcome(committed), conflicts, failures, skipped };
|
|
708
|
+
// claim "merge-archived" (item ②). Conflicts/failures ride along so the
|
|
709
|
+
// audit row records why the run diverged.
|
|
710
|
+
const outcome = { ...buildOutcome(committed), conflicts, failures };
|
|
821
711
|
// Frozen conflicts were not adjudicated: mark both sides pending in the
|
|
822
712
|
// per-id outcome so the audit row shows they were parked, not skipped.
|
|
823
713
|
if (frozenIds.length) {
|
|
@@ -833,30 +723,6 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
833
723
|
// that only froze conflicts is not a noop.
|
|
834
724
|
const noChange = frozenCount === 0 && applied === 0 && committed.every((c) => c.action === "keep");
|
|
835
725
|
|
|
836
|
-
// v0.6.2 auto-tag: a light LLM pass over the retained (post-consolidation)
|
|
837
|
-
// memories. Opt-in via config.autoTagEnabled, bounded by autoTagMaxPerRun,
|
|
838
|
-
// and always fail-safe — a tag failure must never change the consolidation
|
|
839
|
-
// outcome reported below (it is logged and counted, nothing more).
|
|
840
|
-
let autoTagged = 0;
|
|
841
|
-
// Effective opt-in (issue #31): the Web panel's settings toggle
|
|
842
|
-
// (user_settings) overrides the zod plugin config when explicitly stored;
|
|
843
|
-
// unset → fall back to config.autoTagEnabled. settings is null-safe (absent
|
|
844
|
-
// in tests / direct calls) so behaviour is unchanged without it.
|
|
845
|
-
const autoTagEnabled = settings?.getAutoTagConfig?.().autoTagEnabled ?? config.autoTagEnabled;
|
|
846
|
-
if (autoTagEnabled === true) {
|
|
847
|
-
try {
|
|
848
|
-
const tagResult = await runAutoTag({ ctx, service, config, route });
|
|
849
|
-
autoTagged = tagResult?.tagged ?? 0;
|
|
850
|
-
if (tagResult?.ok === false && tagResult?.skippedBy && tagResult.skippedBy !== "empty") {
|
|
851
|
-
logger?.warn?.(`dsh-mneme dream: auto-tag skipped (${tagResult.skippedBy})`);
|
|
852
|
-
} else if (autoTagged > 0) {
|
|
853
|
-
logger?.info?.(`[dsh-mneme] auto-tag: ${autoTagged} memory(ies) tagged`);
|
|
854
|
-
}
|
|
855
|
-
} catch (error) {
|
|
856
|
-
logger?.warn?.(`dsh-mneme dream: auto-tag failed: ${String(error)}`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
726
|
// Keep the vector index consistent with the post-dream store state.
|
|
861
727
|
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
862
728
|
try {
|
|
@@ -886,7 +752,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
886
752
|
: {}),
|
|
887
753
|
messages: [
|
|
888
754
|
{ role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
|
|
889
|
-
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived &&
|
|
755
|
+
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
890
756
|
]
|
|
891
757
|
}, reportUsage));
|
|
892
758
|
} catch (error) {
|
|
@@ -919,12 +785,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
919
785
|
// run. ok:false keeps the scheduler from moving the baseline.
|
|
920
786
|
// ok — either real changes landed, or a fresh summary was stored
|
|
921
787
|
// (all-keep + summary is a substantive summary refresh).
|
|
922
|
-
// degraded — real consolidation landed but the
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
// skipped by skipInvalid. The valid subset was absorbed (ok
|
|
926
|
-
// for the baseline — a future run won't re-fail on the same
|
|
927
|
-
// permanently-invalid pairs), but the run is marked, not faked.
|
|
788
|
+
// degraded — real consolidation landed but the summary came back empty/
|
|
789
|
+
// missing: the store was absorbed (ok for the baseline) but
|
|
790
|
+
// the run did not produce its full output (marked, not faked).
|
|
928
791
|
let status;
|
|
929
792
|
let okResult;
|
|
930
793
|
if (partial) {
|
|
@@ -933,13 +796,6 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
933
796
|
} else if (noChange) {
|
|
934
797
|
status = summaryStored ? "ok" : "noop";
|
|
935
798
|
okResult = summaryStored;
|
|
936
|
-
} else if (skipped.length > 0) {
|
|
937
|
-
// Issue #26: valid subset landed but at least one decision was dropped as
|
|
938
|
-
// invalid (e.g. cross-type merge). degraded (not ok) — never a fake ok.
|
|
939
|
-
// autoTag still runs on degraded runs, so it is not blocked behind a
|
|
940
|
-
// "success" that a skipped-invalid run can never reach.
|
|
941
|
-
status = "degraded";
|
|
942
|
-
okResult = true;
|
|
943
799
|
} else {
|
|
944
800
|
status = summaryStored ? "ok" : "degraded";
|
|
945
801
|
okResult = true;
|
|
@@ -952,10 +808,8 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
952
808
|
outcome,
|
|
953
809
|
conflicts,
|
|
954
810
|
failures,
|
|
955
|
-
skipped,
|
|
956
811
|
frozen: frozenCount,
|
|
957
|
-
summary: summaryStored
|
|
958
|
-
error: skipped.length > 0 ? `skipped ${skipped.length} invalid decision(s)` : undefined
|
|
812
|
+
summary: summaryStored
|
|
959
813
|
});
|
|
960
814
|
}
|
|
961
815
|
|
|
@@ -21,13 +21,6 @@ export function estimateTokens(text) {
|
|
|
21
21
|
* rounds(): Array, clear(): void}}
|
|
22
22
|
*/
|
|
23
23
|
export function createHotMemory({ maxRounds = 5, maxTokens = 2000 } = {}) {
|
|
24
|
-
// Entry defense: a non-positive or non-integer maxRounds (0, -1, 1.5, NaN,
|
|
25
|
-
// null, "2") would make the eviction while-loop unbounded — the buffer can
|
|
26
|
-
// never shrink below `buffer.length > maxRounds`, so `add` would spin forever.
|
|
27
|
-
// Fall back to the defaults so a hostile/buggy caller can never wedge the
|
|
28
|
-
// hot-memory buffer in an infinite loop.
|
|
29
|
-
maxRounds = (Number.isInteger(maxRounds) && maxRounds > 0) ? maxRounds : 5;
|
|
30
|
-
maxTokens = (Number.isFinite(maxTokens) && maxTokens > 0) ? maxTokens : 2000;
|
|
31
24
|
const buffer = [];
|
|
32
25
|
|
|
33
26
|
function totalTokens() {
|
|
@@ -52,20 +52,8 @@ export const apply = (ctx, config) => {
|
|
|
52
52
|
store.deleteOldLlmAudits(new Date(Date.now() - retentionMs * 86400000).toISOString());
|
|
53
53
|
}
|
|
54
54
|
} catch { /* non-fatal */ }
|
|
55
|
-
// v0.7.0: recall_runs 滚动清理 —— recordRecall 默认开且注入路径也记账,表随
|
|
56
|
-
// 活跃度增长;启动时按 recallRetentionDays(默认 90 天)清一次,防膨胀。
|
|
57
|
-
// 与 llm_audit 同策略:纯 bookkeeping,清理失败绝不阻塞插件启动。
|
|
58
|
-
try {
|
|
59
|
-
const recallRetentionDays = Number.isInteger(cfg.recallRetentionDays) ? cfg.recallRetentionDays : 90;
|
|
60
|
-
store.purgeRecallRunsOlderThan(recallRetentionDays);
|
|
61
|
-
} catch { /* non-fatal */ }
|
|
62
55
|
const mirror = createMirror(memoryDir);
|
|
63
|
-
|
|
64
|
-
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
65
|
-
// Created before the service so the tag-config resolver (issue #31) can merge
|
|
66
|
-
// the Web panel's persisted toggles over the plugin config at runtime.
|
|
67
|
-
const settings = createSettings(store.db);
|
|
68
|
-
const service = createService({ store, mirror, config: cfg, logger: ctx.logger, settings });
|
|
56
|
+
const service = createService({ store, mirror, config: cfg, logger: ctx.logger });
|
|
69
57
|
|
|
70
58
|
// F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
|
|
71
59
|
// a safe re-render at boot so a stale mirror converges without needing a
|
|
@@ -90,6 +78,10 @@ export const apply = (ctx, config) => {
|
|
|
90
78
|
} catch { /* non-fatal: recall recording is bookkeeping */ }
|
|
91
79
|
});
|
|
92
80
|
|
|
81
|
+
// User-configurable settings (profile, rules) and custom commands share the
|
|
82
|
+
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
83
|
+
const settings = createSettings(store.db);
|
|
84
|
+
|
|
93
85
|
// Semantic pipeline: a local/ollama embedder when configured, otherwise the
|
|
94
86
|
// legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
|
|
95
87
|
// the store's embedding column and tracks the active model fingerprint. A
|
|
@@ -252,7 +244,7 @@ export const apply = (ctx, config) => {
|
|
|
252
244
|
delayMs: cfg.dreamDelayMs,
|
|
253
245
|
logger: ctx.logger,
|
|
254
246
|
semantic: { embedder, vectorIndex },
|
|
255
|
-
onRun: () => (dream ? dream.runDream(ctx, service, cfg
|
|
247
|
+
onRun: () => (dream ? dream.runDream(ctx, service, cfg) : Promise.resolve({ ok: true, skipped: true }))
|
|
256
248
|
});
|
|
257
249
|
service.setDreamHook(() => dream.maybeSchedule(service));
|
|
258
250
|
}
|
|
@@ -325,26 +317,6 @@ export const apply = (ctx, config) => {
|
|
|
325
317
|
const summarizer = createSummarizer(ctx, service, cfg);
|
|
326
318
|
disposers.push(summarizer.dispose);
|
|
327
319
|
|
|
328
|
-
// Session lifecycle (v0.6.0): when a session leaves the store and the toggle
|
|
329
|
-
// is enabled, mark every memory born in it as session-disposed (hidden from
|
|
330
|
-
// injection/search/dream but never destroyed — recoverable via
|
|
331
|
-
// restoreBySession). Default off, so a disposed session leaves its memories
|
|
332
|
-
// active (legacy behavior). Every path is guarded: a failure inside the
|
|
333
|
-
// callback must never propagate into DSH's session teardown (that would crash
|
|
334
|
-
// the plugin on the very delete action it serves).
|
|
335
|
-
if (cfg.sessionLifecycleEnabled) {
|
|
336
|
-
disposers.push(ctx.on("session/disposed", (session) => {
|
|
337
|
-
const sessionId = session?.id;
|
|
338
|
-
if (!sessionId) return;
|
|
339
|
-
try {
|
|
340
|
-
const { disposed } = service.disposeBySession(sessionId);
|
|
341
|
-
ctx.logger?.info?.(`[dsh-mneme] session disposed, hid ${disposed} memory(s) for ${sessionId}`);
|
|
342
|
-
} catch (error) {
|
|
343
|
-
ctx.logger?.warn?.(`[dsh-mneme] session dispose failed for ${sessionId}: ${String(error)}`);
|
|
344
|
-
}
|
|
345
|
-
}));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
320
|
if (ctx.webServer) {
|
|
349
321
|
const api = createApi(ctx, service, settings, commands ?? {
|
|
350
322
|
add: () => { throw new Error("commands unavailable"); },
|
|
@@ -8,7 +8,10 @@ import { createHotMemory } from "./hot-memory.js";
|
|
|
8
8
|
// falls back to the legacy rule-based pick, never breaking the render.
|
|
9
9
|
function lastUserQuery(ctx) {
|
|
10
10
|
try {
|
|
11
|
-
const
|
|
11
|
+
const session = ctx?.agent?.session;
|
|
12
|
+
// DSH ≥0.1.2-rc only exposes events via snapshotEvents(); older builds
|
|
13
|
+
// still have the .events property, so fall back to it.
|
|
14
|
+
const events = session?.snapshotEvents?.() ?? session?.events;
|
|
12
15
|
if (!Array.isArray(events) || events.length === 0) return "";
|
|
13
16
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
14
17
|
const event = events[i];
|
|
@@ -34,7 +37,8 @@ function lastUserQuery(ctx) {
|
|
|
34
37
|
// returns [] on any failure, and the hot block simply does not render.
|
|
35
38
|
function extractRounds(ctx, maxRounds) {
|
|
36
39
|
try {
|
|
37
|
-
const
|
|
40
|
+
const session = ctx?.agent?.session;
|
|
41
|
+
const events = session?.snapshotEvents?.() ?? session?.events;
|
|
38
42
|
if (!Array.isArray(events) || events.length === 0) return [];
|
|
39
43
|
const rounds = [];
|
|
40
44
|
let pendingQuery = null;
|
|
@@ -75,49 +79,6 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
75
79
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
76
80
|
const threshold = config.importanceThreshold ?? 3;
|
|
77
81
|
|
|
78
|
-
// Prompt-variable escaping (issue #40): DSH's interpolate() treats `{{name}}`
|
|
79
|
-
// as a prompt variable and strictly validates the name against
|
|
80
|
-
// /^[a-z][a-z0-9_]*$/ — memory/profile content carrying legal template syntax
|
|
81
|
-
// (Obsidian-style `{{hl|}}`, `{{挖空}}`, `{{关键词}}`) would hit an illegal
|
|
82
|
-
// variable name and throw, crashing the whole turn. At the injection boundary
|
|
83
|
-
// we escape every run of 2+ consecutive braces, inserting a `\` between each
|
|
84
|
-
// pair, so no `{{`/`}}` substring survives into the prompt: `{{a}}` → `{\{a\}\}`,
|
|
85
|
-
// and odd runs like `{{{a}}}` (which pair-wise escaping would leave with a
|
|
86
|
-
// literal `{{`) are handled too. The text keeps its readable template form,
|
|
87
|
-
// the transform is idempotent (escaped braces are single + `\`, never two
|
|
88
|
-
// adjacent), and single braces pass through untouched — interpolate only
|
|
89
|
-
// scans `{{`. Off via config.escapePromptVariables=false (default true).
|
|
90
|
-
function escapePromptVars(text) {
|
|
91
|
-
if (config.escapePromptVariables === false) return String(text);
|
|
92
|
-
return String(text).replace(/[{}]{2,}/g, (run) => run.split("").join("\\"));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Time-prefix injection (issue #34): opt-in, off by default. When enabled the
|
|
96
|
-
// current date/time is injected once per conversation — at the first prompt
|
|
97
|
-
// assembly of a new session — so the model can sense what time/day it is.
|
|
98
|
-
// A per-session latch means later assemblies in the same session never
|
|
99
|
-
// re-inject; a bare render ctx (no session id) falls back to once per
|
|
100
|
-
// injector lifetime.
|
|
101
|
-
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
|
102
|
-
let timePrefixSession = null;
|
|
103
|
-
|
|
104
|
-
function renderTimePrefix(ctx) {
|
|
105
|
-
if (config.injectTimePrefix !== true) return "";
|
|
106
|
-
const sessionId = ctx?.agent?.session?.id;
|
|
107
|
-
if (sessionId === undefined) {
|
|
108
|
-
if (timePrefixSession !== null) return "";
|
|
109
|
-
timePrefixSession = true;
|
|
110
|
-
} else {
|
|
111
|
-
if (sessionId === timePrefixSession) return "";
|
|
112
|
-
timePrefixSession = sessionId;
|
|
113
|
-
}
|
|
114
|
-
const now = new Date();
|
|
115
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
116
|
-
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
117
|
-
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
118
|
-
return `[当前时间: ${date} ${WEEKDAYS[now.getDay()]} ${time}]`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
82
|
// Bug6: bound the injected memory block. Each entry's content is truncated to
|
|
122
83
|
// MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
|
|
123
84
|
// and an entry that would exceed it collapses to its title only, so a long
|
|
@@ -151,7 +112,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
151
112
|
for (const r of rounds) hot.add(r);
|
|
152
113
|
const body = hot.getContext();
|
|
153
114
|
if (!body) return "";
|
|
154
|
-
return
|
|
115
|
+
return `[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}`;
|
|
155
116
|
}
|
|
156
117
|
|
|
157
118
|
function render(candidates) {
|
|
@@ -175,7 +136,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
175
136
|
lines.push(`- [${m.type}] ${verified}${title}`);
|
|
176
137
|
}
|
|
177
138
|
}
|
|
178
|
-
return
|
|
139
|
+
return lines.join("\n");
|
|
179
140
|
}
|
|
180
141
|
|
|
181
142
|
// Bug4: the system-prompt render is synchronous, so the semantic query vector
|
|
@@ -209,7 +170,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
209
170
|
const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
|
|
210
171
|
if (profile) lines.push(`- 用户画像:${profile}`);
|
|
211
172
|
for (const rule of rules) lines.push(`- 规则:${rule}`);
|
|
212
|
-
return
|
|
173
|
+
return lines.join("\n");
|
|
213
174
|
}
|
|
214
175
|
|
|
215
176
|
const disposers = [
|
|
@@ -231,11 +192,8 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
231
192
|
// separate context) keeps the prompt assembly stable at two blocks.
|
|
232
193
|
const hotText = renderHotContext(ctx);
|
|
233
194
|
const body = render(candidates);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const timePrefix = renderTimePrefix(ctx);
|
|
237
|
-
if (!timePrefix) return out;
|
|
238
|
-
return out ? `${timePrefix}\n\n${out}` : timePrefix;
|
|
195
|
+
if (!hotText) return body;
|
|
196
|
+
return body ? `${hotText}\n\n${body}` : hotText;
|
|
239
197
|
}
|
|
240
198
|
}),
|
|
241
199
|
ctx.systemPrompt.context({
|
|
@@ -7,9 +7,7 @@ export const TYPE_FILE = {
|
|
|
7
7
|
project: "projects.md",
|
|
8
8
|
decision: "decisions.md",
|
|
9
9
|
history: "history.md",
|
|
10
|
-
summary: "summary.md"
|
|
11
|
-
user: "user.md",
|
|
12
|
-
fact: "facts.md"
|
|
10
|
+
summary: "summary.md"
|
|
13
11
|
};
|
|
14
12
|
|
|
15
13
|
const ESCAPE = /([\\`*_[\]{}()#+.!|>~-])/g;
|
|
@@ -33,15 +31,6 @@ function renderMemory(m) {
|
|
|
33
31
|
.digest("hex");
|
|
34
32
|
const lines = [];
|
|
35
33
|
lines.push(`## ${esc(m.title)}`);
|
|
36
|
-
// v0.6.2 tag line: entity_attrs-backed tags (attached by the service as
|
|
37
|
-
// `entityTags`) rendered as `#tag` space-separated under the title. No tags
|
|
38
|
-
// → no line. Legacy `- **标签**:` metadata below keeps the memories.tags
|
|
39
|
-
// column (still written by save/update) readable.
|
|
40
|
-
const entityTags = Array.isArray(m.entityTags) ? m.entityTags : [];
|
|
41
|
-
if (entityTags.length) {
|
|
42
|
-
lines.push("");
|
|
43
|
-
lines.push(entityTags.map((t) => `#${esc(t)}`).join(" "));
|
|
44
|
-
}
|
|
45
34
|
lines.push("");
|
|
46
35
|
lines.push(`- **ID**: \`${m.id}\``);
|
|
47
36
|
lines.push(`- **类型**: ${m.type}`);
|
|
@@ -34,9 +34,7 @@ const TYPE_LABELS = {
|
|
|
34
34
|
decision: ["decision", "决策", "决定"],
|
|
35
35
|
history: ["history", "历史", "事件"],
|
|
36
36
|
summary: ["summary", "总结", "摘要", "总览"],
|
|
37
|
-
pattern: ["pattern", "模式", "规律"]
|
|
38
|
-
user: ["user", "用户"],
|
|
39
|
-
fact: ["fact", "事实", "原子事实"]
|
|
37
|
+
pattern: ["pattern", "模式", "规律"]
|
|
40
38
|
};
|
|
41
39
|
|
|
42
40
|
/** Normalized bigram-overlap similarity in [0,1]; 0 for tiny/empty inputs. */
|
|
@@ -21,10 +21,7 @@ function modelHash(model) {
|
|
|
21
21
|
|
|
22
22
|
/** Lazy default pipeline factory: dynamic import keeps module load cheap. */
|
|
23
23
|
async function defaultPipelineLoader(task, model, options) {
|
|
24
|
-
const {
|
|
25
|
-
// issue #13: mirror cache_dir onto env.cacheDir so the tokenizer pre-check
|
|
26
|
-
// resolves locally too (same fix as local-embedder.js).
|
|
27
|
-
if (options?.cache_dir) env.cacheDir = options.cache_dir;
|
|
24
|
+
const { pipeline } = await import("@huggingface/transformers");
|
|
28
25
|
return pipeline(task, model, options);
|
|
29
26
|
}
|
|
30
27
|
|