@modusensus/dsh-mneme 0.5.0 → 0.5.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 +419 -397
- package/lib/api.js +516 -516
- package/lib/commands.js +64 -64
- package/lib/config.js +252 -252
- package/lib/dream.js +817 -817
- package/lib/embedding.js +154 -154
- package/lib/hot-memory.js +7 -0
- package/lib/index.js +341 -341
- package/lib/inject.js +208 -208
- package/lib/local-embedder.js +282 -282
- package/lib/reranker.js +218 -215
- package/lib/service.js +1489 -1484
- package/lib/store.js +6 -2
- package/lib/summarize.js +5 -1
- package/lib/tools.js +7 -2
- package/package.json +1 -1
- package/scripts/sync-lib.js +7 -2
- package/src/hot-memory.js +53 -46
- package/src/reranker.js +218 -215
- package/src/service.js +1489 -1484
- package/src/store.js +6 -2
- package/src/summarize.js +5 -1
- package/src/tools.js +7 -2
- package/test/hot-memory.test.js +174 -145
- package/test/provenance.test.js +103 -0
- package/test/reranker.test.js +240 -197
- package/test/service-search.test.js +199 -174
package/lib/commands.js
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
// Custom slash-command manager: keeps the DSH command registry in sync with
|
|
2
|
-
// user-defined commands persisted in SQLite. Commands are registered on boot
|
|
3
|
-
// and (re)registered on add/remove through the API.
|
|
4
|
-
//
|
|
5
|
-
// Each custom command's handler returns the user-authored instruction as a
|
|
6
|
-
// success result; the DSH UI surfaces it as a model-directed instruction.
|
|
7
|
-
export function createCommandManager({ ctx, settings, logger }) {
|
|
8
|
-
const registered = new Map(); // name -> disposer
|
|
9
|
-
|
|
10
|
-
function registerOne(command) {
|
|
11
|
-
if (registered.has(command.name)) return;
|
|
12
|
-
let dispose;
|
|
13
|
-
try {
|
|
14
|
-
dispose = ctx.commands.register({
|
|
15
|
-
name: command.name,
|
|
16
|
-
description: command.description || `自定义指令 ${command.name}`,
|
|
17
|
-
handler: () => ({ kind: "success", text: command.instruction })
|
|
18
|
-
});
|
|
19
|
-
} catch (error) {
|
|
20
|
-
logger?.warn?.(`dsh-mneme: failed to register command /${command.name}: ${String(error)}`);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
registered.set(command.name, dispose);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function unregisterOne(name) {
|
|
27
|
-
const dispose = registered.get(name);
|
|
28
|
-
if (dispose) {
|
|
29
|
-
try {
|
|
30
|
-
dispose();
|
|
31
|
-
} catch {
|
|
32
|
-
/* ignore double-dispose */
|
|
33
|
-
}
|
|
34
|
-
registered.delete(name);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Register every stored command (boot-time sync). */
|
|
39
|
-
function sync() {
|
|
40
|
-
for (const command of settings.listCommands()) registerOne(command);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Add (or replace) a command and register it live. */
|
|
44
|
-
function add({ name, description, instruction }) {
|
|
45
|
-
const command = settings.addCommand({ name, description, instruction });
|
|
46
|
-
registerOne(command);
|
|
47
|
-
return command;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Remove a command by id and unregister it live. */
|
|
51
|
-
function remove(id) {
|
|
52
|
-
const existing = settings.listCommands().find((c) => c.id === id);
|
|
53
|
-
if (!existing) return false;
|
|
54
|
-
if (!settings.removeCommand(id)) return false;
|
|
55
|
-
unregisterOne(existing.name);
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function dispose() {
|
|
60
|
-
for (const name of [...registered.keys()]) unregisterOne(name);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { sync, add, remove, list: () => settings.listCommands(), dispose };
|
|
64
|
-
}
|
|
1
|
+
// Custom slash-command manager: keeps the DSH command registry in sync with
|
|
2
|
+
// user-defined commands persisted in SQLite. Commands are registered on boot
|
|
3
|
+
// and (re)registered on add/remove through the API.
|
|
4
|
+
//
|
|
5
|
+
// Each custom command's handler returns the user-authored instruction as a
|
|
6
|
+
// success result; the DSH UI surfaces it as a model-directed instruction.
|
|
7
|
+
export function createCommandManager({ ctx, settings, logger }) {
|
|
8
|
+
const registered = new Map(); // name -> disposer
|
|
9
|
+
|
|
10
|
+
function registerOne(command) {
|
|
11
|
+
if (registered.has(command.name)) return;
|
|
12
|
+
let dispose;
|
|
13
|
+
try {
|
|
14
|
+
dispose = ctx.commands.register({
|
|
15
|
+
name: command.name,
|
|
16
|
+
description: command.description || `自定义指令 ${command.name}`,
|
|
17
|
+
handler: () => ({ kind: "success", text: command.instruction })
|
|
18
|
+
});
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logger?.warn?.(`dsh-mneme: failed to register command /${command.name}: ${String(error)}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
registered.set(command.name, dispose);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function unregisterOne(name) {
|
|
27
|
+
const dispose = registered.get(name);
|
|
28
|
+
if (dispose) {
|
|
29
|
+
try {
|
|
30
|
+
dispose();
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore double-dispose */
|
|
33
|
+
}
|
|
34
|
+
registered.delete(name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Register every stored command (boot-time sync). */
|
|
39
|
+
function sync() {
|
|
40
|
+
for (const command of settings.listCommands()) registerOne(command);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Add (or replace) a command and register it live. */
|
|
44
|
+
function add({ name, description, instruction }) {
|
|
45
|
+
const command = settings.addCommand({ name, description, instruction });
|
|
46
|
+
registerOne(command);
|
|
47
|
+
return command;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Remove a command by id and unregister it live. */
|
|
51
|
+
function remove(id) {
|
|
52
|
+
const existing = settings.listCommands().find((c) => c.id === id);
|
|
53
|
+
if (!existing) return false;
|
|
54
|
+
if (!settings.removeCommand(id)) return false;
|
|
55
|
+
unregisterOne(existing.name);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dispose() {
|
|
60
|
+
for (const name of [...registered.keys()]) unregisterOne(name);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { sync, add, remove, list: () => settings.listCommands(), dispose };
|
|
64
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -1,252 +1,252 @@
|
|
|
1
|
-
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
|
|
3
|
-
export const Config = z.object({
|
|
4
|
-
memoryDir: z.string().default("~/.dsh/memory"),
|
|
5
|
-
autoInject: z.boolean().default(true),
|
|
6
|
-
autoSummarize: z.boolean().default(true),
|
|
7
|
-
// Optional model override for summarization. When both are non-empty, they
|
|
8
|
-
// take priority over the session's current model. Empty = use the session's
|
|
9
|
-
// active provider/model (same as before).
|
|
10
|
-
summarizeProvider: z.string().default(""),
|
|
11
|
-
summarizeModel: z.string().default(""),
|
|
12
|
-
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
13
|
-
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
14
|
-
autoDream: z.boolean().default(true),
|
|
15
|
-
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
16
|
-
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
17
|
-
dreamDelayMs: z.natural().min(0).max(60000).default(2000),
|
|
18
|
-
dreamProvider: z.string(),
|
|
19
|
-
dreamModel: z.string(),
|
|
20
|
-
dreamMaxTokens: z.natural().min(256).max(131072).default(8192),
|
|
21
|
-
// Pass-through reasoning effort for dream's LLM calls. 'none' (default)
|
|
22
|
-
// omits the field so the provider's own default applies; low/medium/high
|
|
23
|
-
// are forwarded verbatim. Useful to cap reasoning spend on thinking-type
|
|
24
|
-
// models that would otherwise drain the whole token budget and return an
|
|
25
|
-
// empty body ("no json array in llm output").
|
|
26
|
-
dreamReasoningEffort: z.union([
|
|
27
|
-
z.const("low"),
|
|
28
|
-
z.const("medium"),
|
|
29
|
-
z.const("high"),
|
|
30
|
-
z.const("none")
|
|
31
|
-
]).default("none"),
|
|
32
|
-
// 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
|
|
33
|
-
// 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
|
|
34
|
-
// 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
|
|
35
|
-
dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
|
|
36
|
-
// 隐式 keep(v0.4.4):LLM 未提及的 snapshot 记忆自动补 {action:"keep"},
|
|
37
|
-
// 避免"未覆盖即全拒"白白浪费整轮 run。设为 false 时保留旧的严格校验
|
|
38
|
-
// (未覆盖即拒绝整单)。
|
|
39
|
-
dreamImplicitKeep: z.boolean().default(true),
|
|
40
|
-
// 显式决策覆盖率下限(v0.4.4 fix):dreamImplicitKeep 开启时,LLM 输出被
|
|
41
|
-
// 截断只显式 claim 少量 snapshot 记忆(claimed.size / snapshot.size < 该阈值)
|
|
42
|
-
// → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
|
|
43
|
-
// 默认 0.5(至少显式覆盖一半 snapshot)。
|
|
44
|
-
dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
|
|
45
|
-
// Rule version for dream adjudication: when this bumps, older dream_runs
|
|
46
|
-
// degrade to historical evidence (their receipts no longer drive live
|
|
47
|
-
// decisions). Default 0 = no versioning in use yet.
|
|
48
|
-
policyEpoch: z.natural().min(0).max(1000000).default(0),
|
|
49
|
-
|
|
50
|
-
// --- API protection ------------------------------------------------------
|
|
51
|
-
// Optional shared token for the plugin's HTTP API. Empty (default) keeps
|
|
52
|
-
// the API open (DSH binds to 127.0.0.1 and has no built-in auth); when set,
|
|
53
|
-
// sensitive endpoints (vector-config, vector-reindex, and all write ops on
|
|
54
|
-
// profile/rules/commands) require `Authorization: Bearer <apiToken>` (or
|
|
55
|
-
// `X-DSH-Mneme-Token`). Read-only list/search/semantic stay open so the
|
|
56
|
-
// Web panel keeps working without the token.
|
|
57
|
-
apiToken: z.string(),
|
|
58
|
-
|
|
59
|
-
// --- semantic: local embedding provider (v0.2) --------------------------
|
|
60
|
-
// "openai" keeps the legacy external-API path (settings vector config);
|
|
61
|
-
// "local" runs an ONNX model in-process; "ollama" calls a local Ollama.
|
|
62
|
-
embedProvider: z.union([z.const("openai"), z.const("local"), z.const("ollama")]).default("openai"),
|
|
63
|
-
|
|
64
|
-
// Local ONNX embedder (transformers.js / onnxruntime).
|
|
65
|
-
localEmbedModel: z.string().default("Xenova/bge-small-zh-v1.5"),
|
|
66
|
-
localEmbedDimension: z.natural().default(512),
|
|
67
|
-
localEmbedDevice: z.union([z.const("cpu"), z.const("gpu")]).default("cpu"),
|
|
68
|
-
localEmbedBatchSize: z.natural().min(1).max(64).default(8),
|
|
69
|
-
|
|
70
|
-
// Ollama embedder.
|
|
71
|
-
ollamaBaseUrl: z.string().default("http://localhost:11434"),
|
|
72
|
-
ollamaModel: z.string().default("nomic-embed-text"),
|
|
73
|
-
|
|
74
|
-
// Model download/cache. When empty (default), models are cached under the
|
|
75
|
-
// user-level path ~/.dsh/mneme/models (resolved in local-embedder/reranker);
|
|
76
|
-
// a non-empty value is used verbatim.
|
|
77
|
-
embedModelCacheDir: z.string().default(""),
|
|
78
|
-
embedModelMirror: z.string().default("https://hf-mirror.com"),
|
|
79
|
-
|
|
80
|
-
// Vector search tuning.
|
|
81
|
-
vectorSearchTopK: z.natural().min(1).max(100).default(20),
|
|
82
|
-
vectorSearchThreshold: z.number().min(0).max(1).default(0.65),
|
|
83
|
-
hybridSearchVectorWeight: z.number().min(0).max(1).default(0.6),
|
|
84
|
-
hybridSearchKeywordWeight: z.number().min(0).max(1).default(0.4),
|
|
85
|
-
// Lazy auto-backfill of missing embeddings on boot (Bug2): when the vector
|
|
86
|
-
// API is configured and rows still lack an embedding, the index is rebuilt
|
|
87
|
-
// in the background after a short delay, rate-limited in batches. On by
|
|
88
|
-
// default; set false to keep the backfill manual only.
|
|
89
|
-
autoReindexOnBoot: z.boolean().default(true),
|
|
90
|
-
// Semantic-first injection (Bug4): when enabled, injectCandidates with a
|
|
91
|
-
// non-empty query recalls via the vector index first and falls back to the
|
|
92
|
-
// rule-based pick to fill/dedupe. Empty query / no vector → legacy behavior.
|
|
93
|
-
hybridInject: z.boolean().default(true),
|
|
94
|
-
|
|
95
|
-
// --- recall optimization (v0.5.0) ----------------------------------------
|
|
96
|
-
// BM25 third recall path beside vector + LIKE keyword (1.1): per-token IDF
|
|
97
|
-
// scoring recalls rows whose query terms are scattered — identifiers, code
|
|
98
|
-
// fragments, mixed CJK/ASCII — where substring LIKE cannot match.
|
|
99
|
-
bm25SearchEnabled: z.boolean().default(true),
|
|
100
|
-
// Query-aware vector cutoff (1.2) replacing the fixed 0.65: entity:/attr:
|
|
101
|
-
// prefixes loosen to 0.5, short queries tighten to 0.7, long queries loosen
|
|
102
|
-
// to 0.6, and a decisive top-1/top-5 score gap loosens to 0.5 so the tail
|
|
103
|
-
// still reaches the reranker. Off = legacy fixed threshold behavior.
|
|
104
|
-
adaptiveThresholdEnabled: z.boolean().default(true),
|
|
105
|
-
// Session-scoped hot memory (1.3): the latest N dialogue rounds rendered
|
|
106
|
-
// ahead of the long-term recall block — short-term context that never
|
|
107
|
-
// enters the memory store.
|
|
108
|
-
hotMemoryEnabled: z.boolean().default(true),
|
|
109
|
-
hotMemoryRounds: z.natural().min(1).max(50).default(5),
|
|
110
|
-
hotMemoryMaxTokens: z.natural().min(200).max(32000).default(2000),
|
|
111
|
-
// Topic-ranked injection (2.2): when a query vector is available the whole
|
|
112
|
-
// injection candidate list is re-ordered by similarity to the current
|
|
113
|
-
// query instead of keeping the rule-based order.
|
|
114
|
-
selectiveInjectEnabled: z.boolean().default(true),
|
|
115
|
-
// Search-time semantic dedup (2.3): greedy pass over the merged candidate
|
|
116
|
-
// list dropping rows whose embedding cosine-similarity to an already-kept
|
|
117
|
-
// row exceeds the threshold — duplicates are filtered at recall time
|
|
118
|
-
// instead of waiting for a dream consolidation. Opt-in aggressive mode:
|
|
119
|
-
// small embedding models can collapse legitimately distinct rows, so the
|
|
120
|
-
// default keeps every recalled row.
|
|
121
|
-
searchSemanticDedup: z.boolean().default(false),
|
|
122
|
-
searchSemanticDedupThreshold: z.number().min(0.5).max(1).default(0.95),
|
|
123
|
-
|
|
124
|
-
// --- semantic: rerank layer (v0.2) --------------------------------------
|
|
125
|
-
// Opt-in by default (item ⑥): the local cross-encoder pulls in onnxruntime
|
|
126
|
-
// (transformers.js) at init, so a bare install must not load it. Only an
|
|
127
|
-
// explicit rerankEnabled=true + rerankProvider="local" constructs LocalReranker.
|
|
128
|
-
rerankEnabled: z.boolean().default(false),
|
|
129
|
-
rerankProvider: z.union([z.const("local"), z.const("none")]).default("none"),
|
|
130
|
-
rerankModel: z.string().default("Xenova/bge-reranker-base"),
|
|
131
|
-
rerankBatchSize: z.natural().min(1).max(64).default(8),
|
|
132
|
-
rerankMaxCandidates: z.natural().min(5).max(100).default(30),
|
|
133
|
-
rerankScoreThreshold: z.number().min(0).max(1).default(0.1),
|
|
134
|
-
|
|
135
|
-
// --- reflection: update decision + failure tracking (v0.2.1) ------------
|
|
136
|
-
reflectionUpdateEnabled: z.boolean().default(true),
|
|
137
|
-
reflectionFailureTracking: z.boolean().default(true),
|
|
138
|
-
reflectionUpdateMaxPerRun: z.natural().min(0).max(5).default(2),
|
|
139
|
-
reflectionUpdateMinAgeHours: z.natural().min(0).max(168).default(24),
|
|
140
|
-
|
|
141
|
-
// --- conflict freeze: manual review for conflicting memories (v0.2.1) ---
|
|
142
|
-
// Opt-in by default: when true, conflicting memories are not auto-merged
|
|
143
|
-
// and are marked as pending manual review instead.
|
|
144
|
-
conflictFreezeEnabled: z.boolean().default(false),
|
|
145
|
-
// Maximum number of frozen conflicts to keep pending for manual review.
|
|
146
|
-
conflictFreezeMaxPending: z.natural().min(1).max(1000).default(100),
|
|
147
|
-
|
|
148
|
-
// --- entity gene (v0.3.0) -----------------------------------------------
|
|
149
|
-
// Opt-in: when false (default) nothing in the pipeline extracts entities.
|
|
150
|
-
// The storage layer (entities/entity_attrs/entity_relations tables + CRUD)
|
|
151
|
-
// is always available regardless of this flag.
|
|
152
|
-
entityExtractionEnabled: z.boolean().default(false),
|
|
153
|
-
// Optional model override for entity extraction; empty = use the caller's
|
|
154
|
-
// default provider/model.
|
|
155
|
-
entityExtractionModel: z.string().default(""),
|
|
156
|
-
// Cap on entities per extraction pass and attributes per entity.
|
|
157
|
-
entityExtractionMaxEntities: z.natural().min(1).max(20).default(10),
|
|
158
|
-
entityExtractionMaxAttrs: z.natural().min(1).max(50).default(20),
|
|
159
|
-
// Prefix/semantic search over entity names (used by recall).
|
|
160
|
-
entitySearchEnabled: z.boolean().default(true),
|
|
161
|
-
|
|
162
|
-
// --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
|
|
163
|
-
// Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
|
|
164
|
-
// sleep fires when the store has been quiet for sleepIdleMinutes and deep-
|
|
165
|
-
// maintains the whole library: conflict resolution, archival demotion,
|
|
166
|
-
// pattern discovery and entity relation completion. Abortable on user
|
|
167
|
-
// activity, audited into dream_runs (run_type='sleep'), and serialized with
|
|
168
|
-
// autoDream so the two never overlap.
|
|
169
|
-
sleepModeEnabled: z.boolean().default(false),
|
|
170
|
-
// Quiet window before a cycle fires (minutes).
|
|
171
|
-
sleepIdleMinutes: z.natural().min(1).max(60).default(5),
|
|
172
|
-
// Minimum gap between two sleep runs (hours) — a second idle window within
|
|
173
|
-
// this interval does not retrigger.
|
|
174
|
-
sleepMinIntervalHours: z.natural().min(1).max(168).default(8),
|
|
175
|
-
// Conflict adjudication strictness:
|
|
176
|
-
// gentle only high-confidence conflicts (threshold 0.92) are resolved
|
|
177
|
-
// normal standard dream-level (threshold 0.85)
|
|
178
|
-
// aggressive low-confidence pairs are also adjudicated (threshold 0.75)
|
|
179
|
-
sleepConflictStrictness: z.union([
|
|
180
|
-
z.const("gentle"),
|
|
181
|
-
z.const("normal"),
|
|
182
|
-
z.const("aggressive")
|
|
183
|
-
]).default("normal"),
|
|
184
|
-
// Archival demotion tiering (days since last access):
|
|
185
|
-
// >= sleepArchiveDays → shrink to summary, full body kept in _full_content
|
|
186
|
-
// >= sleepCompressDays → archived outright (entity relations preserved)
|
|
187
|
-
sleepArchiveDays: z.natural().min(7).max(365).default(30),
|
|
188
|
-
sleepCompressDays: z.natural().min(7).max(365).default(90),
|
|
189
|
-
// Pattern discovery scan window (most recent memories to scan).
|
|
190
|
-
sleepPatternMinMemories: z.natural().min(10).max(1000).default(100),
|
|
191
|
-
// How far back pattern discovery considers entity attr changes (days).
|
|
192
|
-
sleepPatternLookbackDays: z.natural().min(1).max(90).default(30),
|
|
193
|
-
// Max pattern memories minted per run (0 = disabled).
|
|
194
|
-
sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
|
|
195
|
-
// Optional LLM route override for sleep's bulk passes (empty = use dream
|
|
196
|
-
// route / agent default model).
|
|
197
|
-
sleepProvider: z.string().default(""),
|
|
198
|
-
sleepModel: z.string().default(""),
|
|
199
|
-
// Pass-through reasoning effort for sleep's LLM passes, same semantics as
|
|
200
|
-
// dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
|
|
201
|
-
// are forwarded verbatim.
|
|
202
|
-
sleepReasoningEffort: z.union([
|
|
203
|
-
z.const("low"),
|
|
204
|
-
z.const("medium"),
|
|
205
|
-
z.const("high"),
|
|
206
|
-
z.const("none")
|
|
207
|
-
]).default("none"),
|
|
208
|
-
|
|
209
|
-
// --- epistemic trust: memory source credibility (v0.4.5) -----------------
|
|
210
|
-
// Distinguish memories by source: observation (measured / witnessed),
|
|
211
|
-
// subjective (opinion / guess) and inferred (derived from other evidence).
|
|
212
|
-
// Opt-in by default: when false (default) retrieval ranking, injection
|
|
213
|
-
// marking and dream merge/conflict keepSource are untouched and
|
|
214
|
-
// epistemic_status stays inert data (still written + inferred on save, just
|
|
215
|
-
// never used to influence behavior).
|
|
216
|
-
trustEpistemicWeighting: z.boolean().default(false),
|
|
217
|
-
|
|
218
|
-
// --- memory quality filter (Bug7) ------------------------------------------
|
|
219
|
-
// Heuristic gate on what deserves the injection/recall surface. When enabled,
|
|
220
|
-
// saveWithDedupe scores each new memory after dedupe and before write:
|
|
221
|
-
// score >= degradeThreshold (60) → stored normally
|
|
222
|
-
// archiveThreshold (30) <= score < 60 → quality_score persisted and the
|
|
223
|
-
// injection sort re-ranks by importance * quality_score/100 (degraded)
|
|
224
|
-
// score < 30 → archived + tagged low_quality (still explicitly searchable)
|
|
225
|
-
// Meta-memory markers, near-duplicates and repetitive filler lose points.
|
|
226
|
-
memoryQualityFilter: z.object({
|
|
227
|
-
enabled: z.boolean().default(true),
|
|
228
|
-
archiveThreshold: z.natural().min(1).max(100).default(30),
|
|
229
|
-
degradeThreshold: z.natural().min(1).max(100).default(60),
|
|
230
|
-
minContentLength: z.natural().min(1).max(1000).default(10)
|
|
231
|
-
}).default({}),
|
|
232
|
-
|
|
233
|
-
// --- LLM audit trail (Bug8) ------------------------------------------------
|
|
234
|
-
// Records every background LLM call (autoDream consolidation + summary,
|
|
235
|
-
// autoSummarize compression) into llm_audit_logs: tokens, duration, status
|
|
236
|
-
// and which trigger produced it. Failures are recorded as status=error and
|
|
237
|
-
// never block the feature. retentionDays bounds the table: older rows are
|
|
238
|
-
// purged on boot.
|
|
239
|
-
llmAudit: z.object({
|
|
240
|
-
enabled: z.boolean().default(true),
|
|
241
|
-
retentionDays: z.natural().min(1).max(3650).default(90)
|
|
242
|
-
}).default({}),
|
|
243
|
-
|
|
244
|
-
// --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
|
|
245
|
-
// Separate retrieval evaluation snapshots from the production recall audit.
|
|
246
|
-
// When false (default) evaluateRetrieval still computes precision/recall/mrr
|
|
247
|
-
// and returns them to the caller, but writes nothing to recall_evals — the
|
|
248
|
-
// eval table only grows when the operator opts in. Production searchMemories
|
|
249
|
-
// audits to recall_runs and NEVER touches recall_evals, regardless of this
|
|
250
|
-
// flag (production isolation is unconditional).
|
|
251
|
-
evalPersistTestResults: z.boolean().default(false),
|
|
252
|
-
});
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
|
|
3
|
+
export const Config = z.object({
|
|
4
|
+
memoryDir: z.string().default("~/.dsh/memory"),
|
|
5
|
+
autoInject: z.boolean().default(true),
|
|
6
|
+
autoSummarize: z.boolean().default(true),
|
|
7
|
+
// Optional model override for summarization. When both are non-empty, they
|
|
8
|
+
// take priority over the session's current model. Empty = use the session's
|
|
9
|
+
// active provider/model (same as before).
|
|
10
|
+
summarizeProvider: z.string().default(""),
|
|
11
|
+
summarizeModel: z.string().default(""),
|
|
12
|
+
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
13
|
+
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
14
|
+
autoDream: z.boolean().default(true),
|
|
15
|
+
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
16
|
+
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
17
|
+
dreamDelayMs: z.natural().min(0).max(60000).default(2000),
|
|
18
|
+
dreamProvider: z.string(),
|
|
19
|
+
dreamModel: z.string(),
|
|
20
|
+
dreamMaxTokens: z.natural().min(256).max(131072).default(8192),
|
|
21
|
+
// Pass-through reasoning effort for dream's LLM calls. 'none' (default)
|
|
22
|
+
// omits the field so the provider's own default applies; low/medium/high
|
|
23
|
+
// are forwarded verbatim. Useful to cap reasoning spend on thinking-type
|
|
24
|
+
// models that would otherwise drain the whole token budget and return an
|
|
25
|
+
// empty body ("no json array in llm output").
|
|
26
|
+
dreamReasoningEffort: z.union([
|
|
27
|
+
z.const("low"),
|
|
28
|
+
z.const("medium"),
|
|
29
|
+
z.const("high"),
|
|
30
|
+
z.const("none")
|
|
31
|
+
]).default("none"),
|
|
32
|
+
// 滑动窗口上限(v0.4.4):autoDream 每次只对最近 dreamMaxSnapshotSize 条
|
|
33
|
+
// 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
|
|
34
|
+
// 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
|
|
35
|
+
dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
|
|
36
|
+
// 隐式 keep(v0.4.4):LLM 未提及的 snapshot 记忆自动补 {action:"keep"},
|
|
37
|
+
// 避免"未覆盖即全拒"白白浪费整轮 run。设为 false 时保留旧的严格校验
|
|
38
|
+
// (未覆盖即拒绝整单)。
|
|
39
|
+
dreamImplicitKeep: z.boolean().default(true),
|
|
40
|
+
// 显式决策覆盖率下限(v0.4.4 fix):dreamImplicitKeep 开启时,LLM 输出被
|
|
41
|
+
// 截断只显式 claim 少量 snapshot 记忆(claimed.size / snapshot.size < 该阈值)
|
|
42
|
+
// → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
|
|
43
|
+
// 默认 0.5(至少显式覆盖一半 snapshot)。
|
|
44
|
+
dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
|
|
45
|
+
// Rule version for dream adjudication: when this bumps, older dream_runs
|
|
46
|
+
// degrade to historical evidence (their receipts no longer drive live
|
|
47
|
+
// decisions). Default 0 = no versioning in use yet.
|
|
48
|
+
policyEpoch: z.natural().min(0).max(1000000).default(0),
|
|
49
|
+
|
|
50
|
+
// --- API protection ------------------------------------------------------
|
|
51
|
+
// Optional shared token for the plugin's HTTP API. Empty (default) keeps
|
|
52
|
+
// the API open (DSH binds to 127.0.0.1 and has no built-in auth); when set,
|
|
53
|
+
// sensitive endpoints (vector-config, vector-reindex, and all write ops on
|
|
54
|
+
// profile/rules/commands) require `Authorization: Bearer <apiToken>` (or
|
|
55
|
+
// `X-DSH-Mneme-Token`). Read-only list/search/semantic stay open so the
|
|
56
|
+
// Web panel keeps working without the token.
|
|
57
|
+
apiToken: z.string(),
|
|
58
|
+
|
|
59
|
+
// --- semantic: local embedding provider (v0.2) --------------------------
|
|
60
|
+
// "openai" keeps the legacy external-API path (settings vector config);
|
|
61
|
+
// "local" runs an ONNX model in-process; "ollama" calls a local Ollama.
|
|
62
|
+
embedProvider: z.union([z.const("openai"), z.const("local"), z.const("ollama")]).default("openai"),
|
|
63
|
+
|
|
64
|
+
// Local ONNX embedder (transformers.js / onnxruntime).
|
|
65
|
+
localEmbedModel: z.string().default("Xenova/bge-small-zh-v1.5"),
|
|
66
|
+
localEmbedDimension: z.natural().default(512),
|
|
67
|
+
localEmbedDevice: z.union([z.const("cpu"), z.const("gpu")]).default("cpu"),
|
|
68
|
+
localEmbedBatchSize: z.natural().min(1).max(64).default(8),
|
|
69
|
+
|
|
70
|
+
// Ollama embedder.
|
|
71
|
+
ollamaBaseUrl: z.string().default("http://localhost:11434"),
|
|
72
|
+
ollamaModel: z.string().default("nomic-embed-text"),
|
|
73
|
+
|
|
74
|
+
// Model download/cache. When empty (default), models are cached under the
|
|
75
|
+
// user-level path ~/.dsh/mneme/models (resolved in local-embedder/reranker);
|
|
76
|
+
// a non-empty value is used verbatim.
|
|
77
|
+
embedModelCacheDir: z.string().default(""),
|
|
78
|
+
embedModelMirror: z.string().default("https://hf-mirror.com"),
|
|
79
|
+
|
|
80
|
+
// Vector search tuning.
|
|
81
|
+
vectorSearchTopK: z.natural().min(1).max(100).default(20),
|
|
82
|
+
vectorSearchThreshold: z.number().min(0).max(1).default(0.65),
|
|
83
|
+
hybridSearchVectorWeight: z.number().min(0).max(1).default(0.6),
|
|
84
|
+
hybridSearchKeywordWeight: z.number().min(0).max(1).default(0.4),
|
|
85
|
+
// Lazy auto-backfill of missing embeddings on boot (Bug2): when the vector
|
|
86
|
+
// API is configured and rows still lack an embedding, the index is rebuilt
|
|
87
|
+
// in the background after a short delay, rate-limited in batches. On by
|
|
88
|
+
// default; set false to keep the backfill manual only.
|
|
89
|
+
autoReindexOnBoot: z.boolean().default(true),
|
|
90
|
+
// Semantic-first injection (Bug4): when enabled, injectCandidates with a
|
|
91
|
+
// non-empty query recalls via the vector index first and falls back to the
|
|
92
|
+
// rule-based pick to fill/dedupe. Empty query / no vector → legacy behavior.
|
|
93
|
+
hybridInject: z.boolean().default(true),
|
|
94
|
+
|
|
95
|
+
// --- recall optimization (v0.5.0) ----------------------------------------
|
|
96
|
+
// BM25 third recall path beside vector + LIKE keyword (1.1): per-token IDF
|
|
97
|
+
// scoring recalls rows whose query terms are scattered — identifiers, code
|
|
98
|
+
// fragments, mixed CJK/ASCII — where substring LIKE cannot match.
|
|
99
|
+
bm25SearchEnabled: z.boolean().default(true),
|
|
100
|
+
// Query-aware vector cutoff (1.2) replacing the fixed 0.65: entity:/attr:
|
|
101
|
+
// prefixes loosen to 0.5, short queries tighten to 0.7, long queries loosen
|
|
102
|
+
// to 0.6, and a decisive top-1/top-5 score gap loosens to 0.5 so the tail
|
|
103
|
+
// still reaches the reranker. Off = legacy fixed threshold behavior.
|
|
104
|
+
adaptiveThresholdEnabled: z.boolean().default(true),
|
|
105
|
+
// Session-scoped hot memory (1.3): the latest N dialogue rounds rendered
|
|
106
|
+
// ahead of the long-term recall block — short-term context that never
|
|
107
|
+
// enters the memory store.
|
|
108
|
+
hotMemoryEnabled: z.boolean().default(true),
|
|
109
|
+
hotMemoryRounds: z.natural().min(1).max(50).default(5),
|
|
110
|
+
hotMemoryMaxTokens: z.natural().min(200).max(32000).default(2000),
|
|
111
|
+
// Topic-ranked injection (2.2): when a query vector is available the whole
|
|
112
|
+
// injection candidate list is re-ordered by similarity to the current
|
|
113
|
+
// query instead of keeping the rule-based order.
|
|
114
|
+
selectiveInjectEnabled: z.boolean().default(true),
|
|
115
|
+
// Search-time semantic dedup (2.3): greedy pass over the merged candidate
|
|
116
|
+
// list dropping rows whose embedding cosine-similarity to an already-kept
|
|
117
|
+
// row exceeds the threshold — duplicates are filtered at recall time
|
|
118
|
+
// instead of waiting for a dream consolidation. Opt-in aggressive mode:
|
|
119
|
+
// small embedding models can collapse legitimately distinct rows, so the
|
|
120
|
+
// default keeps every recalled row.
|
|
121
|
+
searchSemanticDedup: z.boolean().default(false),
|
|
122
|
+
searchSemanticDedupThreshold: z.number().min(0.5).max(1).default(0.95),
|
|
123
|
+
|
|
124
|
+
// --- semantic: rerank layer (v0.2) --------------------------------------
|
|
125
|
+
// Opt-in by default (item ⑥): the local cross-encoder pulls in onnxruntime
|
|
126
|
+
// (transformers.js) at init, so a bare install must not load it. Only an
|
|
127
|
+
// explicit rerankEnabled=true + rerankProvider="local" constructs LocalReranker.
|
|
128
|
+
rerankEnabled: z.boolean().default(false),
|
|
129
|
+
rerankProvider: z.union([z.const("local"), z.const("none")]).default("none"),
|
|
130
|
+
rerankModel: z.string().default("Xenova/bge-reranker-base"),
|
|
131
|
+
rerankBatchSize: z.natural().min(1).max(64).default(8),
|
|
132
|
+
rerankMaxCandidates: z.natural().min(5).max(100).default(30),
|
|
133
|
+
rerankScoreThreshold: z.number().min(0).max(1).default(0.1),
|
|
134
|
+
|
|
135
|
+
// --- reflection: update decision + failure tracking (v0.2.1) ------------
|
|
136
|
+
reflectionUpdateEnabled: z.boolean().default(true),
|
|
137
|
+
reflectionFailureTracking: z.boolean().default(true),
|
|
138
|
+
reflectionUpdateMaxPerRun: z.natural().min(0).max(5).default(2),
|
|
139
|
+
reflectionUpdateMinAgeHours: z.natural().min(0).max(168).default(24),
|
|
140
|
+
|
|
141
|
+
// --- conflict freeze: manual review for conflicting memories (v0.2.1) ---
|
|
142
|
+
// Opt-in by default: when true, conflicting memories are not auto-merged
|
|
143
|
+
// and are marked as pending manual review instead.
|
|
144
|
+
conflictFreezeEnabled: z.boolean().default(false),
|
|
145
|
+
// Maximum number of frozen conflicts to keep pending for manual review.
|
|
146
|
+
conflictFreezeMaxPending: z.natural().min(1).max(1000).default(100),
|
|
147
|
+
|
|
148
|
+
// --- entity gene (v0.3.0) -----------------------------------------------
|
|
149
|
+
// Opt-in: when false (default) nothing in the pipeline extracts entities.
|
|
150
|
+
// The storage layer (entities/entity_attrs/entity_relations tables + CRUD)
|
|
151
|
+
// is always available regardless of this flag.
|
|
152
|
+
entityExtractionEnabled: z.boolean().default(false),
|
|
153
|
+
// Optional model override for entity extraction; empty = use the caller's
|
|
154
|
+
// default provider/model.
|
|
155
|
+
entityExtractionModel: z.string().default(""),
|
|
156
|
+
// Cap on entities per extraction pass and attributes per entity.
|
|
157
|
+
entityExtractionMaxEntities: z.natural().min(1).max(20).default(10),
|
|
158
|
+
entityExtractionMaxAttrs: z.natural().min(1).max(50).default(20),
|
|
159
|
+
// Prefix/semantic search over entity names (used by recall).
|
|
160
|
+
entitySearchEnabled: z.boolean().default(true),
|
|
161
|
+
|
|
162
|
+
// --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
|
|
163
|
+
// Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
|
|
164
|
+
// sleep fires when the store has been quiet for sleepIdleMinutes and deep-
|
|
165
|
+
// maintains the whole library: conflict resolution, archival demotion,
|
|
166
|
+
// pattern discovery and entity relation completion. Abortable on user
|
|
167
|
+
// activity, audited into dream_runs (run_type='sleep'), and serialized with
|
|
168
|
+
// autoDream so the two never overlap.
|
|
169
|
+
sleepModeEnabled: z.boolean().default(false),
|
|
170
|
+
// Quiet window before a cycle fires (minutes).
|
|
171
|
+
sleepIdleMinutes: z.natural().min(1).max(60).default(5),
|
|
172
|
+
// Minimum gap between two sleep runs (hours) — a second idle window within
|
|
173
|
+
// this interval does not retrigger.
|
|
174
|
+
sleepMinIntervalHours: z.natural().min(1).max(168).default(8),
|
|
175
|
+
// Conflict adjudication strictness:
|
|
176
|
+
// gentle only high-confidence conflicts (threshold 0.92) are resolved
|
|
177
|
+
// normal standard dream-level (threshold 0.85)
|
|
178
|
+
// aggressive low-confidence pairs are also adjudicated (threshold 0.75)
|
|
179
|
+
sleepConflictStrictness: z.union([
|
|
180
|
+
z.const("gentle"),
|
|
181
|
+
z.const("normal"),
|
|
182
|
+
z.const("aggressive")
|
|
183
|
+
]).default("normal"),
|
|
184
|
+
// Archival demotion tiering (days since last access):
|
|
185
|
+
// >= sleepArchiveDays → shrink to summary, full body kept in _full_content
|
|
186
|
+
// >= sleepCompressDays → archived outright (entity relations preserved)
|
|
187
|
+
sleepArchiveDays: z.natural().min(7).max(365).default(30),
|
|
188
|
+
sleepCompressDays: z.natural().min(7).max(365).default(90),
|
|
189
|
+
// Pattern discovery scan window (most recent memories to scan).
|
|
190
|
+
sleepPatternMinMemories: z.natural().min(10).max(1000).default(100),
|
|
191
|
+
// How far back pattern discovery considers entity attr changes (days).
|
|
192
|
+
sleepPatternLookbackDays: z.natural().min(1).max(90).default(30),
|
|
193
|
+
// Max pattern memories minted per run (0 = disabled).
|
|
194
|
+
sleepMaxPatternPerRun: z.natural().min(0).max(10).default(3),
|
|
195
|
+
// Optional LLM route override for sleep's bulk passes (empty = use dream
|
|
196
|
+
// route / agent default model).
|
|
197
|
+
sleepProvider: z.string().default(""),
|
|
198
|
+
sleepModel: z.string().default(""),
|
|
199
|
+
// Pass-through reasoning effort for sleep's LLM passes, same semantics as
|
|
200
|
+
// dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
|
|
201
|
+
// are forwarded verbatim.
|
|
202
|
+
sleepReasoningEffort: z.union([
|
|
203
|
+
z.const("low"),
|
|
204
|
+
z.const("medium"),
|
|
205
|
+
z.const("high"),
|
|
206
|
+
z.const("none")
|
|
207
|
+
]).default("none"),
|
|
208
|
+
|
|
209
|
+
// --- epistemic trust: memory source credibility (v0.4.5) -----------------
|
|
210
|
+
// Distinguish memories by source: observation (measured / witnessed),
|
|
211
|
+
// subjective (opinion / guess) and inferred (derived from other evidence).
|
|
212
|
+
// Opt-in by default: when false (default) retrieval ranking, injection
|
|
213
|
+
// marking and dream merge/conflict keepSource are untouched and
|
|
214
|
+
// epistemic_status stays inert data (still written + inferred on save, just
|
|
215
|
+
// never used to influence behavior).
|
|
216
|
+
trustEpistemicWeighting: z.boolean().default(false),
|
|
217
|
+
|
|
218
|
+
// --- memory quality filter (Bug7) ------------------------------------------
|
|
219
|
+
// Heuristic gate on what deserves the injection/recall surface. When enabled,
|
|
220
|
+
// saveWithDedupe scores each new memory after dedupe and before write:
|
|
221
|
+
// score >= degradeThreshold (60) → stored normally
|
|
222
|
+
// archiveThreshold (30) <= score < 60 → quality_score persisted and the
|
|
223
|
+
// injection sort re-ranks by importance * quality_score/100 (degraded)
|
|
224
|
+
// score < 30 → archived + tagged low_quality (still explicitly searchable)
|
|
225
|
+
// Meta-memory markers, near-duplicates and repetitive filler lose points.
|
|
226
|
+
memoryQualityFilter: z.object({
|
|
227
|
+
enabled: z.boolean().default(true),
|
|
228
|
+
archiveThreshold: z.natural().min(1).max(100).default(30),
|
|
229
|
+
degradeThreshold: z.natural().min(1).max(100).default(60),
|
|
230
|
+
minContentLength: z.natural().min(1).max(1000).default(10)
|
|
231
|
+
}).default({}),
|
|
232
|
+
|
|
233
|
+
// --- LLM audit trail (Bug8) ------------------------------------------------
|
|
234
|
+
// Records every background LLM call (autoDream consolidation + summary,
|
|
235
|
+
// autoSummarize compression) into llm_audit_logs: tokens, duration, status
|
|
236
|
+
// and which trigger produced it. Failures are recorded as status=error and
|
|
237
|
+
// never block the feature. retentionDays bounds the table: older rows are
|
|
238
|
+
// purged on boot.
|
|
239
|
+
llmAudit: z.object({
|
|
240
|
+
enabled: z.boolean().default(true),
|
|
241
|
+
retentionDays: z.natural().min(1).max(3650).default(90)
|
|
242
|
+
}).default({}),
|
|
243
|
+
|
|
244
|
+
// --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
|
|
245
|
+
// Separate retrieval evaluation snapshots from the production recall audit.
|
|
246
|
+
// When false (default) evaluateRetrieval still computes precision/recall/mrr
|
|
247
|
+
// and returns them to the caller, but writes nothing to recall_evals — the
|
|
248
|
+
// eval table only grows when the operator opts in. Production searchMemories
|
|
249
|
+
// audits to recall_runs and NEVER touches recall_evals, regardless of this
|
|
250
|
+
// flag (production isolation is unconditional).
|
|
251
|
+
evalPersistTestResults: z.boolean().default(false),
|
|
252
|
+
});
|