@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/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
+ });