dsh-tdai-memory 0.2.13 → 0.2.14

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.
Files changed (69) hide show
  1. package/LICENSE +21 -21
  2. package/THIRD-PARTY-NOTICES.md +20 -20
  3. package/client.js +8 -4
  4. package/cordis.patch.yml +8 -8
  5. package/index.js +414 -414
  6. package/package.json +1 -1
  7. package/recall-inject.js +86 -86
  8. package/vendor/tdai/adapters/index.js +15 -15
  9. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  10. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  11. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  12. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  13. package/vendor/tdai/adapters/standalone/index.js +5 -5
  14. package/vendor/tdai/adapters/standalone/llm-runner.js +246 -246
  15. package/vendor/tdai/config.js +363 -363
  16. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  17. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  18. package/vendor/tdai/core/hooks/auto-recall.js +653 -653
  19. package/vendor/tdai/core/index.js +8 -8
  20. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  21. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  22. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  23. package/vendor/tdai/core/prompts/l1-dedup.js +136 -136
  24. package/vendor/tdai/core/prompts/l1-extraction.js +132 -132
  25. package/vendor/tdai/core/prompts/persona-generation.js +160 -160
  26. package/vendor/tdai/core/prompts/scene-extraction.js +269 -269
  27. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  28. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  29. package/vendor/tdai/core/record/l1-reader.js +178 -178
  30. package/vendor/tdai/core/record/l1-writer.js +141 -141
  31. package/vendor/tdai/core/report/reporter.js +86 -86
  32. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  33. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  34. package/vendor/tdai/core/scene/scene-format.js +53 -53
  35. package/vendor/tdai/core/scene/scene-index.js +82 -82
  36. package/vendor/tdai/core/scene/scene-navigation.js +60 -60
  37. package/vendor/tdai/core/seed/input.js +404 -404
  38. package/vendor/tdai/core/seed/seed-runtime.js +314 -314
  39. package/vendor/tdai/core/seed/types.js +9 -9
  40. package/vendor/tdai/core/store/bm25-client.js +117 -117
  41. package/vendor/tdai/core/store/bm25-local.js +68 -68
  42. package/vendor/tdai/core/store/embedding.js +632 -632
  43. package/vendor/tdai/core/store/factory.js +95 -95
  44. package/vendor/tdai/core/store/search-utils.js +54 -54
  45. package/vendor/tdai/core/store/sqlite.js +1822 -1822
  46. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  47. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  48. package/vendor/tdai/core/store/types.js +17 -17
  49. package/vendor/tdai/core/tdai-core.js +446 -446
  50. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  51. package/vendor/tdai/core/tools/memory-search.js +207 -207
  52. package/vendor/tdai/core/types.js +12 -12
  53. package/vendor/tdai/utils/backup.js +189 -189
  54. package/vendor/tdai/utils/checkpoint.js +360 -360
  55. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  56. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  57. package/vendor/tdai/utils/env.js +13 -13
  58. package/vendor/tdai/utils/managed-timer.js +127 -127
  59. package/vendor/tdai/utils/manifest.js +91 -91
  60. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  61. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  62. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  63. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  64. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  65. package/vendor/tdai/utils/sanitize.js +360 -360
  66. package/vendor/tdai/utils/serial-queue.js +103 -103
  67. package/vendor/tdai/utils/session-filter.js +88 -88
  68. package/vendor/tdai/utils/text-utils.js +29 -29
  69. package/vendor/tdai/utils/time.js +252 -252
@@ -1,363 +1,363 @@
1
- /**
2
- * Plugin configuration types and parser (v3).
3
- *
4
- * Config is organized into flat functional groups:
5
- * capture, extraction, persona, pipeline, recall, embedding
6
- *
7
- * Minimal config (zero config): {} — all fields have sensible defaults.
8
- */
9
- import { normalizeDisableThinking } from "./utils/no-think-fetch.js";
10
- // ============================
11
- // Parser
12
- // ============================
13
- /**
14
- * Parse plugin config from raw user input.
15
- * All fields have sensible defaults — minimal config is just {}.
16
- */
17
- export function parseConfig(raw) {
18
- const c = raw ?? {};
19
- // --- Capture (L0) ---
20
- const captureGroup = obj(c, "capture");
21
- // --- Retention days validation (from capture.l0l1RetentionDays) ---
22
- const rawRetentionDays = num(captureGroup, "l0l1RetentionDays") ?? 0;
23
- const allowAggressiveCleanup = bool(captureGroup, "allowAggressiveCleanup") ?? false;
24
- let retentionDays;
25
- if (rawRetentionDays <= 0) {
26
- retentionDays = undefined;
27
- }
28
- else if (rawRetentionDays >= 3) {
29
- retentionDays = rawRetentionDays;
30
- }
31
- else if (allowAggressiveCleanup) {
32
- retentionDays = rawRetentionDays;
33
- }
34
- else {
35
- retentionDays = undefined;
36
- }
37
- // --- Extraction (L1) ---
38
- const extractionGroup = obj(c, "extraction");
39
- // --- Persona (L2/L3) ---
40
- const personaGroup = obj(c, "persona");
41
- // --- Pipeline ---
42
- const pipelineGroup = obj(c, "pipeline");
43
- // --- Recall ---
44
- const recallGroup = obj(c, "recall");
45
- // --- Embedding ---
46
- const embeddingGroup = obj(c, "embedding");
47
- let embeddingConfigError;
48
- // Embedding config: determine provider based on user input and apiKey availability
49
- const embeddingApiKey = str(embeddingGroup, "apiKey") ?? "";
50
- const embeddingBaseUrl = str(embeddingGroup, "baseUrl") ?? "";
51
- const embeddingProviderRaw = str(embeddingGroup, "provider") ?? "none";
52
- const embeddingModelRaw = str(embeddingGroup, "model") ?? "";
53
- const embeddingDimensionsRaw = num(embeddingGroup, "dimensions");
54
- const embeddingProxyUrl = str(embeddingGroup, "proxyUrl");
55
- // provider="none" → embedding disabled (default for zero-config users)
56
- // provider="local" → no longer exposed to users; treated as disabled at entry level
57
- // provider="qclaw" → requires proxyUrl for local proxy forwarding
58
- // Any other value → remote mode (requires apiKey, baseUrl, model, dimensions)
59
- let embeddingProvider;
60
- let embeddingEnabled = bool(embeddingGroup, "enabled") ?? true;
61
- if (embeddingProviderRaw === "none") {
62
- // Explicitly disabled (default): no embedding, no vector search
63
- embeddingProvider = "none";
64
- embeddingEnabled = false;
65
- }
66
- else if (embeddingProviderRaw === "local") {
67
- // Local embedding is not exposed to users; treat as disabled at entry level.
68
- // Internal LocalEmbeddingService code is preserved but not reachable from config.
69
- embeddingProvider = "none";
70
- embeddingEnabled = false;
71
- embeddingConfigError =
72
- "Local embedding provider is not available in user config. " +
73
- "Please configure a remote embedding provider (e.g. openai, deepseek). Embedding has been disabled.";
74
- }
75
- else if (embeddingProviderRaw === "qclaw") {
76
- // qclaw provider: requires proxyUrl for local proxy forwarding
77
- const missingFields = [];
78
- if (!embeddingProxyUrl)
79
- missingFields.push("proxyUrl");
80
- if (!embeddingBaseUrl)
81
- missingFields.push("baseUrl");
82
- if (!embeddingApiKey)
83
- missingFields.push("apiKey");
84
- if (!embeddingModelRaw)
85
- missingFields.push("model");
86
- if (embeddingDimensionsRaw == null || embeddingDimensionsRaw <= 0)
87
- missingFields.push("dimensions");
88
- if (missingFields.length > 0) {
89
- const errorMsg = `Embedding provider 'qclaw' requires 'proxyUrl', 'baseUrl', 'apiKey', 'model', and 'dimensions' to be set. ` +
90
- `Missing: ${missingFields.join(", ")}. Embedding has been disabled.`;
91
- embeddingConfigError = errorMsg;
92
- embeddingEnabled = false;
93
- embeddingProvider = embeddingProviderRaw;
94
- }
95
- else {
96
- embeddingProvider = embeddingProviderRaw;
97
- }
98
- }
99
- else {
100
- // Remote mode — validate all required fields
101
- const missingFields = [];
102
- if (!embeddingApiKey)
103
- missingFields.push("apiKey");
104
- if (!embeddingBaseUrl)
105
- missingFields.push("baseUrl");
106
- if (!embeddingModelRaw)
107
- missingFields.push("model");
108
- if (embeddingDimensionsRaw == null || embeddingDimensionsRaw <= 0)
109
- missingFields.push("dimensions");
110
- if (missingFields.length > 0) {
111
- // Configuration error: disable embedding and log detailed error
112
- // This does NOT throw — the plugin continues running without vector search
113
- const errorMsg = `Remote embedding provider '${embeddingProviderRaw}' requires 'apiKey', 'baseUrl', 'model', and 'dimensions' to be set. ` +
114
- `Missing: ${missingFields.join(", ")}. Embedding has been disabled.`;
115
- // We store the error message so the caller (index.ts) can log it
116
- embeddingConfigError = errorMsg;
117
- embeddingEnabled = false;
118
- embeddingProvider = embeddingProviderRaw; // preserve original for error context
119
- }
120
- else {
121
- embeddingProvider = embeddingProviderRaw;
122
- }
123
- }
124
- // When provider="none", dimensions=0 signals VectorStore to skip vec0 table
125
- // creation entirely (deferred until a real embedding provider is configured).
126
- // This avoids creating vec0 tables with a placeholder dimension that would
127
- // mismatch if the user later enables a different-dimensional provider.
128
- const defaultDimensions = embeddingProvider === "none" ? 0 :
129
- embeddingDimensionsRaw ?? 0;
130
- const defaultModel = embeddingProvider === "none" ? "" : embeddingModelRaw;
131
- const cleanTime = normalizeCleanTime(str(captureGroup, "cleanTime")) ?? "03:00";
132
- // --- BM25 (local @tencentdb-agent-memory/tcvdb-text encoder) ---
133
- const bm25Group = obj(c, "bm25");
134
- // --- Store backend ---
135
- const storeBackendRaw = str(c, "storeBackend") ?? "sqlite";
136
- const storeBackend = storeBackendRaw === "tcvdb" ? "tcvdb" : "sqlite";
137
- // --- TCVDB config ---
138
- const tcvdbGroup = obj(c, "tcvdb");
139
- const memoryCleanup = {
140
- retentionDays,
141
- enabled: retentionDays != null,
142
- cleanTime,
143
- };
144
- // --- Offload ---
145
- const offloadGroup = obj(c, "offload");
146
- const offloadMode = (() => {
147
- const raw = optStr(offloadGroup, "mode");
148
- if (raw === "local" || raw === "backend" || raw === "collect")
149
- return raw;
150
- return optStr(offloadGroup, "backendUrl") ? "backend" : "local";
151
- })();
152
- const offload = {
153
- enabled: bool(offloadGroup, "enabled") ?? false,
154
- mode: offloadMode,
155
- model: optStr(offloadGroup, "model"),
156
- temperature: num(offloadGroup, "temperature") ?? 0.2,
157
- disableThinking: normalizeDisableThinking(boolOrStr(offloadGroup, "disableThinking")),
158
- forceTriggerThreshold: num(offloadGroup, "forceTriggerThreshold") ?? 4,
159
- dataDir: optStr(offloadGroup, "dataDir"),
160
- defaultContextWindow: num(offloadGroup, "defaultContextWindow") ?? 200000,
161
- maxPairsPerBatch: num(offloadGroup, "maxPairsPerBatch") ?? 20,
162
- l2NullThreshold: num(offloadGroup, "l2NullThreshold") ?? 4,
163
- l2TimeoutSeconds: num(offloadGroup, "l2TimeoutSeconds") ?? 300,
164
- mildOffloadRatio: num(offloadGroup, "mildOffloadRatio") ?? 0.5,
165
- aggressiveCompressRatio: num(offloadGroup, "aggressiveCompressRatio") ?? 0.85,
166
- mmdMaxTokenRatio: num(offloadGroup, "mmdMaxTokenRatio") ?? 0.2,
167
- backendUrl: optStr(offloadGroup, "backendUrl"),
168
- backendApiKey: optStr(offloadGroup, "backendApiKey"),
169
- backendTimeoutMs: num(offloadGroup, "backendTimeoutMs") ?? 120000,
170
- offloadRetentionDays: normalizeOffloadRetentionDays(num(offloadGroup, "offloadRetentionDays") ?? 0),
171
- logMaxSizeMb: num(offloadGroup, "logMaxSizeMb") ?? 50,
172
- userId: optStr(offloadGroup, "userId"),
173
- };
174
- return {
175
- timezone: str(c, "timezone") ?? "system",
176
- capture: {
177
- enabled: bool(captureGroup, "enabled") ?? true,
178
- excludeAgents: strArray(captureGroup, "excludeAgents") ?? [],
179
- l0l1RetentionDays: retentionDays ?? 0,
180
- allowAggressiveCleanup,
181
- },
182
- extraction: {
183
- enabled: bool(extractionGroup, "enabled") ?? true,
184
- enableDedup: bool(extractionGroup, "enableDedup") ?? true,
185
- maxMemoriesPerSession: num(extractionGroup, "maxMemoriesPerSession") ?? 20,
186
- model: optStr(extractionGroup, "model"),
187
- },
188
- persona: {
189
- triggerEveryN: num(personaGroup, "triggerEveryN") ?? 50,
190
- maxScenes: num(personaGroup, "maxScenes") ?? 15,
191
- backupCount: num(personaGroup, "backupCount") ?? 3,
192
- sceneBackupCount: num(personaGroup, "sceneBackupCount") ?? 10,
193
- model: optStr(personaGroup, "model"),
194
- },
195
- pipeline: {
196
- everyNConversations: num(pipelineGroup, "everyNConversations") ?? 5,
197
- enableWarmup: bool(pipelineGroup, "enableWarmup") ?? true,
198
- l1IdleTimeoutSeconds: num(pipelineGroup, "l1IdleTimeoutSeconds") ?? 600,
199
- l2DelayAfterL1Seconds: num(pipelineGroup, "l2DelayAfterL1Seconds") ?? 10,
200
- l2MinIntervalSeconds: num(pipelineGroup, "l2MinIntervalSeconds") ?? 900,
201
- l2MaxIntervalSeconds: num(pipelineGroup, "l2MaxIntervalSeconds") ?? 3600,
202
- sessionActiveWindowHours: num(pipelineGroup, "sessionActiveWindowHours") ?? 24,
203
- },
204
- recall: {
205
- enabled: bool(recallGroup, "enabled") ?? true,
206
- maxResults: num(recallGroup, "maxResults") ?? 5,
207
- maxCharsPerMemory: num(recallGroup, "maxCharsPerMemory") ?? 0,
208
- maxTotalRecallChars: num(recallGroup, "maxTotalRecallChars") ?? 0,
209
- scoreThreshold: num(recallGroup, "scoreThreshold") ?? 0.3,
210
- strategy: validateStrategy(str(recallGroup, "strategy")) ?? "hybrid",
211
- timeoutMs: num(recallGroup, "timeoutMs") ?? 5000,
212
- },
213
- embedding: {
214
- enabled: embeddingEnabled,
215
- provider: embeddingProvider,
216
- baseUrl: embeddingBaseUrl,
217
- apiKey: embeddingApiKey,
218
- model: str(embeddingGroup, "model") ?? defaultModel,
219
- dimensions: num(embeddingGroup, "dimensions") ?? defaultDimensions,
220
- sendDimensions: bool(embeddingGroup, "sendDimensions") ?? true,
221
- conflictRecallTopK: num(embeddingGroup, "conflictRecallTopK") ?? 5,
222
- proxyUrl: embeddingProxyUrl,
223
- maxInputChars: num(embeddingGroup, "maxInputChars") ?? 5000,
224
- timeoutMs: num(embeddingGroup, "timeoutMs") ?? 10_000,
225
- recallTimeoutMs: num(embeddingGroup, "recallTimeoutMs") ?? undefined,
226
- captureTimeoutMs: num(embeddingGroup, "captureTimeoutMs") ?? undefined,
227
- modelCacheDir: optStr(embeddingGroup, "modelCacheDir"),
228
- configError: embeddingConfigError,
229
- },
230
- storeBackend,
231
- tcvdb: {
232
- url: str(tcvdbGroup, "url") ?? "",
233
- username: str(tcvdbGroup, "username") ?? "root",
234
- apiKey: str(tcvdbGroup, "apiKey") ?? "",
235
- database: str(tcvdbGroup, "database") ?? "",
236
- alias: str(tcvdbGroup, "alias") ?? "",
237
- embeddingModel: str(tcvdbGroup, "embeddingModel") ?? "bge-large-zh",
238
- timeout: num(tcvdbGroup, "timeout") ?? 10000,
239
- caPemPath: str(tcvdbGroup, "caPemPath") || undefined,
240
- },
241
- bm25: {
242
- enabled: bool(bm25Group, "enabled") ?? true,
243
- language: (str(bm25Group, "language") === "en" ? "en" : "zh"),
244
- },
245
- memoryCleanup,
246
- report: {
247
- enabled: bool(obj(c, "report"), "enabled") ?? false,
248
- type: str(obj(c, "report"), "type") ?? "local",
249
- },
250
- llm: (() => {
251
- const llmGroup = obj(c, "llm");
252
- return {
253
- enabled: bool(llmGroup, "enabled") ?? false,
254
- baseUrl: str(llmGroup, "baseUrl") ?? "https://api.openai.com/v1",
255
- apiKey: str(llmGroup, "apiKey") ?? "",
256
- model: str(llmGroup, "model") ?? "gpt-4o",
257
- maxTokens: num(llmGroup, "maxTokens") ?? 4096,
258
- timeoutMs: num(llmGroup, "timeoutMs") ?? 120_000,
259
- disableThinking: normalizeDisableThinking(boolOrStr(llmGroup, "disableThinking")),
260
- };
261
- })(),
262
- offload,
263
- };
264
- }
265
- // ============================
266
- // Helper functions
267
- // ============================
268
- /** Get sub-object by key, or empty object if missing. */
269
- function obj(c, key) {
270
- const v = c[key];
271
- return v && typeof v === "object" && !Array.isArray(v) ? v : {};
272
- }
273
- function str(src, key) {
274
- const v = src[key];
275
- return typeof v === "string" && v.trim() ? v.trim() : undefined;
276
- }
277
- function optStr(src, key) {
278
- const v = src[key];
279
- return typeof v === "string" ? v : undefined;
280
- }
281
- function num(src, key) {
282
- const v = src[key];
283
- return typeof v === "number" && Number.isFinite(v) ? v : undefined;
284
- }
285
- function bool(src, key) {
286
- const v = src[key];
287
- return typeof v === "boolean" ? v : undefined;
288
- }
289
- /** Read a field that may be boolean or string. */
290
- function boolOrStr(src, key) {
291
- const v = src[key];
292
- if (typeof v === "boolean")
293
- return v;
294
- if (typeof v === "string" && v.trim())
295
- return v.trim();
296
- return undefined;
297
- }
298
- function strArray(src, key) {
299
- const v = src[key];
300
- if (!Array.isArray(v))
301
- return undefined;
302
- return v.filter((item) => typeof item === "string" && item.trim().length > 0);
303
- }
304
- const VALID_STRATEGIES = ["embedding", "keyword", "hybrid"];
305
- /**
306
- * Validate recall strategy against whitelist.
307
- * Returns the strategy if valid, undefined otherwise (caller falls back to default).
308
- */
309
- function validateStrategy(value) {
310
- if (!value)
311
- return undefined;
312
- return VALID_STRATEGIES.includes(value)
313
- ? value
314
- : undefined;
315
- }
316
- /**
317
- * Normalize a cleanup time string.
318
- *
319
- * The input must follow "HH:MM" or "H:MM" format (24-hour clock).
320
- * If the time is valid, it returns the normalized format "HH:MM"
321
- * with leading zeros added when necessary.
322
- * If the format is invalid or the time is out of range
323
- * (hour: 0–23, minute: 0–59), it returns undefined.
324
- *
325
- * Examples:
326
- * normalizeCleanTime("3:05") -> "03:05"
327
- * normalizeCleanTime("03:05") -> "03:05"
328
- * normalizeCleanTime("23:59") -> "23:59"
329
- *
330
- * normalizeCleanTime("24:00") -> undefined // hour out of range
331
- * normalizeCleanTime("12:60") -> undefined // minute out of range
332
- * normalizeCleanTime("3:5") -> undefined // minute must have two digits
333
- * normalizeCleanTime("abc") -> undefined // invalid format
334
- */
335
- function normalizeCleanTime(input) {
336
- if (!input)
337
- return undefined;
338
- const trimmed = input.trim();
339
- const m = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
340
- if (!m)
341
- return undefined;
342
- const hh = Number(m[1]);
343
- const mm = Number(m[2]);
344
- if (!Number.isInteger(hh) || !Number.isInteger(mm))
345
- return undefined;
346
- if (hh < 0 || hh > 23 || mm < 0 || mm > 59)
347
- return undefined;
348
- return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
349
- }
350
- /**
351
- * Normalize offload retention days.
352
- *
353
- * - `<= 0` → 0 (disabled)
354
- * - `(0, 3)` → 0 (invalid, force disabled)
355
- * - `>= 3` → as-is
356
- */
357
- function normalizeOffloadRetentionDays(value) {
358
- if (value <= 0)
359
- return 0;
360
- if (value < 3)
361
- return 0;
362
- return value;
363
- }
1
+ /**
2
+ * Plugin configuration types and parser (v3).
3
+ *
4
+ * Config is organized into flat functional groups:
5
+ * capture, extraction, persona, pipeline, recall, embedding
6
+ *
7
+ * Minimal config (zero config): {} — all fields have sensible defaults.
8
+ */
9
+ import { normalizeDisableThinking } from "./utils/no-think-fetch.js";
10
+ // ============================
11
+ // Parser
12
+ // ============================
13
+ /**
14
+ * Parse plugin config from raw user input.
15
+ * All fields have sensible defaults — minimal config is just {}.
16
+ */
17
+ export function parseConfig(raw) {
18
+ const c = raw ?? {};
19
+ // --- Capture (L0) ---
20
+ const captureGroup = obj(c, "capture");
21
+ // --- Retention days validation (from capture.l0l1RetentionDays) ---
22
+ const rawRetentionDays = num(captureGroup, "l0l1RetentionDays") ?? 0;
23
+ const allowAggressiveCleanup = bool(captureGroup, "allowAggressiveCleanup") ?? false;
24
+ let retentionDays;
25
+ if (rawRetentionDays <= 0) {
26
+ retentionDays = undefined;
27
+ }
28
+ else if (rawRetentionDays >= 3) {
29
+ retentionDays = rawRetentionDays;
30
+ }
31
+ else if (allowAggressiveCleanup) {
32
+ retentionDays = rawRetentionDays;
33
+ }
34
+ else {
35
+ retentionDays = undefined;
36
+ }
37
+ // --- Extraction (L1) ---
38
+ const extractionGroup = obj(c, "extraction");
39
+ // --- Persona (L2/L3) ---
40
+ const personaGroup = obj(c, "persona");
41
+ // --- Pipeline ---
42
+ const pipelineGroup = obj(c, "pipeline");
43
+ // --- Recall ---
44
+ const recallGroup = obj(c, "recall");
45
+ // --- Embedding ---
46
+ const embeddingGroup = obj(c, "embedding");
47
+ let embeddingConfigError;
48
+ // Embedding config: determine provider based on user input and apiKey availability
49
+ const embeddingApiKey = str(embeddingGroup, "apiKey") ?? "";
50
+ const embeddingBaseUrl = str(embeddingGroup, "baseUrl") ?? "";
51
+ const embeddingProviderRaw = str(embeddingGroup, "provider") ?? "none";
52
+ const embeddingModelRaw = str(embeddingGroup, "model") ?? "";
53
+ const embeddingDimensionsRaw = num(embeddingGroup, "dimensions");
54
+ const embeddingProxyUrl = str(embeddingGroup, "proxyUrl");
55
+ // provider="none" → embedding disabled (default for zero-config users)
56
+ // provider="local" → no longer exposed to users; treated as disabled at entry level
57
+ // provider="qclaw" → requires proxyUrl for local proxy forwarding
58
+ // Any other value → remote mode (requires apiKey, baseUrl, model, dimensions)
59
+ let embeddingProvider;
60
+ let embeddingEnabled = bool(embeddingGroup, "enabled") ?? true;
61
+ if (embeddingProviderRaw === "none") {
62
+ // Explicitly disabled (default): no embedding, no vector search
63
+ embeddingProvider = "none";
64
+ embeddingEnabled = false;
65
+ }
66
+ else if (embeddingProviderRaw === "local") {
67
+ // Local embedding is not exposed to users; treat as disabled at entry level.
68
+ // Internal LocalEmbeddingService code is preserved but not reachable from config.
69
+ embeddingProvider = "none";
70
+ embeddingEnabled = false;
71
+ embeddingConfigError =
72
+ "Local embedding provider is not available in user config. " +
73
+ "Please configure a remote embedding provider (e.g. openai, deepseek). Embedding has been disabled.";
74
+ }
75
+ else if (embeddingProviderRaw === "qclaw") {
76
+ // qclaw provider: requires proxyUrl for local proxy forwarding
77
+ const missingFields = [];
78
+ if (!embeddingProxyUrl)
79
+ missingFields.push("proxyUrl");
80
+ if (!embeddingBaseUrl)
81
+ missingFields.push("baseUrl");
82
+ if (!embeddingApiKey)
83
+ missingFields.push("apiKey");
84
+ if (!embeddingModelRaw)
85
+ missingFields.push("model");
86
+ if (embeddingDimensionsRaw == null || embeddingDimensionsRaw <= 0)
87
+ missingFields.push("dimensions");
88
+ if (missingFields.length > 0) {
89
+ const errorMsg = `Embedding provider 'qclaw' requires 'proxyUrl', 'baseUrl', 'apiKey', 'model', and 'dimensions' to be set. ` +
90
+ `Missing: ${missingFields.join(", ")}. Embedding has been disabled.`;
91
+ embeddingConfigError = errorMsg;
92
+ embeddingEnabled = false;
93
+ embeddingProvider = embeddingProviderRaw;
94
+ }
95
+ else {
96
+ embeddingProvider = embeddingProviderRaw;
97
+ }
98
+ }
99
+ else {
100
+ // Remote mode — validate all required fields
101
+ const missingFields = [];
102
+ if (!embeddingApiKey)
103
+ missingFields.push("apiKey");
104
+ if (!embeddingBaseUrl)
105
+ missingFields.push("baseUrl");
106
+ if (!embeddingModelRaw)
107
+ missingFields.push("model");
108
+ if (embeddingDimensionsRaw == null || embeddingDimensionsRaw <= 0)
109
+ missingFields.push("dimensions");
110
+ if (missingFields.length > 0) {
111
+ // Configuration error: disable embedding and log detailed error
112
+ // This does NOT throw — the plugin continues running without vector search
113
+ const errorMsg = `Remote embedding provider '${embeddingProviderRaw}' requires 'apiKey', 'baseUrl', 'model', and 'dimensions' to be set. ` +
114
+ `Missing: ${missingFields.join(", ")}. Embedding has been disabled.`;
115
+ // We store the error message so the caller (index.ts) can log it
116
+ embeddingConfigError = errorMsg;
117
+ embeddingEnabled = false;
118
+ embeddingProvider = embeddingProviderRaw; // preserve original for error context
119
+ }
120
+ else {
121
+ embeddingProvider = embeddingProviderRaw;
122
+ }
123
+ }
124
+ // When provider="none", dimensions=0 signals VectorStore to skip vec0 table
125
+ // creation entirely (deferred until a real embedding provider is configured).
126
+ // This avoids creating vec0 tables with a placeholder dimension that would
127
+ // mismatch if the user later enables a different-dimensional provider.
128
+ const defaultDimensions = embeddingProvider === "none" ? 0 :
129
+ embeddingDimensionsRaw ?? 0;
130
+ const defaultModel = embeddingProvider === "none" ? "" : embeddingModelRaw;
131
+ const cleanTime = normalizeCleanTime(str(captureGroup, "cleanTime")) ?? "03:00";
132
+ // --- BM25 (local @tencentdb-agent-memory/tcvdb-text encoder) ---
133
+ const bm25Group = obj(c, "bm25");
134
+ // --- Store backend ---
135
+ const storeBackendRaw = str(c, "storeBackend") ?? "sqlite";
136
+ const storeBackend = storeBackendRaw === "tcvdb" ? "tcvdb" : "sqlite";
137
+ // --- TCVDB config ---
138
+ const tcvdbGroup = obj(c, "tcvdb");
139
+ const memoryCleanup = {
140
+ retentionDays,
141
+ enabled: retentionDays != null,
142
+ cleanTime,
143
+ };
144
+ // --- Offload ---
145
+ const offloadGroup = obj(c, "offload");
146
+ const offloadMode = (() => {
147
+ const raw = optStr(offloadGroup, "mode");
148
+ if (raw === "local" || raw === "backend" || raw === "collect")
149
+ return raw;
150
+ return optStr(offloadGroup, "backendUrl") ? "backend" : "local";
151
+ })();
152
+ const offload = {
153
+ enabled: bool(offloadGroup, "enabled") ?? false,
154
+ mode: offloadMode,
155
+ model: optStr(offloadGroup, "model"),
156
+ temperature: num(offloadGroup, "temperature") ?? 0.2,
157
+ disableThinking: normalizeDisableThinking(boolOrStr(offloadGroup, "disableThinking")),
158
+ forceTriggerThreshold: num(offloadGroup, "forceTriggerThreshold") ?? 4,
159
+ dataDir: optStr(offloadGroup, "dataDir"),
160
+ defaultContextWindow: num(offloadGroup, "defaultContextWindow") ?? 200000,
161
+ maxPairsPerBatch: num(offloadGroup, "maxPairsPerBatch") ?? 20,
162
+ l2NullThreshold: num(offloadGroup, "l2NullThreshold") ?? 4,
163
+ l2TimeoutSeconds: num(offloadGroup, "l2TimeoutSeconds") ?? 300,
164
+ mildOffloadRatio: num(offloadGroup, "mildOffloadRatio") ?? 0.5,
165
+ aggressiveCompressRatio: num(offloadGroup, "aggressiveCompressRatio") ?? 0.85,
166
+ mmdMaxTokenRatio: num(offloadGroup, "mmdMaxTokenRatio") ?? 0.2,
167
+ backendUrl: optStr(offloadGroup, "backendUrl"),
168
+ backendApiKey: optStr(offloadGroup, "backendApiKey"),
169
+ backendTimeoutMs: num(offloadGroup, "backendTimeoutMs") ?? 120000,
170
+ offloadRetentionDays: normalizeOffloadRetentionDays(num(offloadGroup, "offloadRetentionDays") ?? 0),
171
+ logMaxSizeMb: num(offloadGroup, "logMaxSizeMb") ?? 50,
172
+ userId: optStr(offloadGroup, "userId"),
173
+ };
174
+ return {
175
+ timezone: str(c, "timezone") ?? "system",
176
+ capture: {
177
+ enabled: bool(captureGroup, "enabled") ?? true,
178
+ excludeAgents: strArray(captureGroup, "excludeAgents") ?? [],
179
+ l0l1RetentionDays: retentionDays ?? 0,
180
+ allowAggressiveCleanup,
181
+ },
182
+ extraction: {
183
+ enabled: bool(extractionGroup, "enabled") ?? true,
184
+ enableDedup: bool(extractionGroup, "enableDedup") ?? true,
185
+ maxMemoriesPerSession: num(extractionGroup, "maxMemoriesPerSession") ?? 20,
186
+ model: optStr(extractionGroup, "model"),
187
+ },
188
+ persona: {
189
+ triggerEveryN: num(personaGroup, "triggerEveryN") ?? 50,
190
+ maxScenes: num(personaGroup, "maxScenes") ?? 15,
191
+ backupCount: num(personaGroup, "backupCount") ?? 3,
192
+ sceneBackupCount: num(personaGroup, "sceneBackupCount") ?? 10,
193
+ model: optStr(personaGroup, "model"),
194
+ },
195
+ pipeline: {
196
+ everyNConversations: num(pipelineGroup, "everyNConversations") ?? 5,
197
+ enableWarmup: bool(pipelineGroup, "enableWarmup") ?? true,
198
+ l1IdleTimeoutSeconds: num(pipelineGroup, "l1IdleTimeoutSeconds") ?? 600,
199
+ l2DelayAfterL1Seconds: num(pipelineGroup, "l2DelayAfterL1Seconds") ?? 10,
200
+ l2MinIntervalSeconds: num(pipelineGroup, "l2MinIntervalSeconds") ?? 900,
201
+ l2MaxIntervalSeconds: num(pipelineGroup, "l2MaxIntervalSeconds") ?? 3600,
202
+ sessionActiveWindowHours: num(pipelineGroup, "sessionActiveWindowHours") ?? 24,
203
+ },
204
+ recall: {
205
+ enabled: bool(recallGroup, "enabled") ?? true,
206
+ maxResults: num(recallGroup, "maxResults") ?? 5,
207
+ maxCharsPerMemory: num(recallGroup, "maxCharsPerMemory") ?? 0,
208
+ maxTotalRecallChars: num(recallGroup, "maxTotalRecallChars") ?? 0,
209
+ scoreThreshold: num(recallGroup, "scoreThreshold") ?? 0.3,
210
+ strategy: validateStrategy(str(recallGroup, "strategy")) ?? "hybrid",
211
+ timeoutMs: num(recallGroup, "timeoutMs") ?? 5000,
212
+ },
213
+ embedding: {
214
+ enabled: embeddingEnabled,
215
+ provider: embeddingProvider,
216
+ baseUrl: embeddingBaseUrl,
217
+ apiKey: embeddingApiKey,
218
+ model: str(embeddingGroup, "model") ?? defaultModel,
219
+ dimensions: num(embeddingGroup, "dimensions") ?? defaultDimensions,
220
+ sendDimensions: bool(embeddingGroup, "sendDimensions") ?? true,
221
+ conflictRecallTopK: num(embeddingGroup, "conflictRecallTopK") ?? 5,
222
+ proxyUrl: embeddingProxyUrl,
223
+ maxInputChars: num(embeddingGroup, "maxInputChars") ?? 5000,
224
+ timeoutMs: num(embeddingGroup, "timeoutMs") ?? 10_000,
225
+ recallTimeoutMs: num(embeddingGroup, "recallTimeoutMs") ?? undefined,
226
+ captureTimeoutMs: num(embeddingGroup, "captureTimeoutMs") ?? undefined,
227
+ modelCacheDir: optStr(embeddingGroup, "modelCacheDir"),
228
+ configError: embeddingConfigError,
229
+ },
230
+ storeBackend,
231
+ tcvdb: {
232
+ url: str(tcvdbGroup, "url") ?? "",
233
+ username: str(tcvdbGroup, "username") ?? "root",
234
+ apiKey: str(tcvdbGroup, "apiKey") ?? "",
235
+ database: str(tcvdbGroup, "database") ?? "",
236
+ alias: str(tcvdbGroup, "alias") ?? "",
237
+ embeddingModel: str(tcvdbGroup, "embeddingModel") ?? "bge-large-zh",
238
+ timeout: num(tcvdbGroup, "timeout") ?? 10000,
239
+ caPemPath: str(tcvdbGroup, "caPemPath") || undefined,
240
+ },
241
+ bm25: {
242
+ enabled: bool(bm25Group, "enabled") ?? true,
243
+ language: (str(bm25Group, "language") === "en" ? "en" : "zh"),
244
+ },
245
+ memoryCleanup,
246
+ report: {
247
+ enabled: bool(obj(c, "report"), "enabled") ?? false,
248
+ type: str(obj(c, "report"), "type") ?? "local",
249
+ },
250
+ llm: (() => {
251
+ const llmGroup = obj(c, "llm");
252
+ return {
253
+ enabled: bool(llmGroup, "enabled") ?? false,
254
+ baseUrl: str(llmGroup, "baseUrl") ?? "https://api.openai.com/v1",
255
+ apiKey: str(llmGroup, "apiKey") ?? "",
256
+ model: str(llmGroup, "model") ?? "gpt-4o",
257
+ maxTokens: num(llmGroup, "maxTokens") ?? 4096,
258
+ timeoutMs: num(llmGroup, "timeoutMs") ?? 120_000,
259
+ disableThinking: normalizeDisableThinking(boolOrStr(llmGroup, "disableThinking")),
260
+ };
261
+ })(),
262
+ offload,
263
+ };
264
+ }
265
+ // ============================
266
+ // Helper functions
267
+ // ============================
268
+ /** Get sub-object by key, or empty object if missing. */
269
+ function obj(c, key) {
270
+ const v = c[key];
271
+ return v && typeof v === "object" && !Array.isArray(v) ? v : {};
272
+ }
273
+ function str(src, key) {
274
+ const v = src[key];
275
+ return typeof v === "string" && v.trim() ? v.trim() : undefined;
276
+ }
277
+ function optStr(src, key) {
278
+ const v = src[key];
279
+ return typeof v === "string" ? v : undefined;
280
+ }
281
+ function num(src, key) {
282
+ const v = src[key];
283
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
284
+ }
285
+ function bool(src, key) {
286
+ const v = src[key];
287
+ return typeof v === "boolean" ? v : undefined;
288
+ }
289
+ /** Read a field that may be boolean or string. */
290
+ function boolOrStr(src, key) {
291
+ const v = src[key];
292
+ if (typeof v === "boolean")
293
+ return v;
294
+ if (typeof v === "string" && v.trim())
295
+ return v.trim();
296
+ return undefined;
297
+ }
298
+ function strArray(src, key) {
299
+ const v = src[key];
300
+ if (!Array.isArray(v))
301
+ return undefined;
302
+ return v.filter((item) => typeof item === "string" && item.trim().length > 0);
303
+ }
304
+ const VALID_STRATEGIES = ["embedding", "keyword", "hybrid"];
305
+ /**
306
+ * Validate recall strategy against whitelist.
307
+ * Returns the strategy if valid, undefined otherwise (caller falls back to default).
308
+ */
309
+ function validateStrategy(value) {
310
+ if (!value)
311
+ return undefined;
312
+ return VALID_STRATEGIES.includes(value)
313
+ ? value
314
+ : undefined;
315
+ }
316
+ /**
317
+ * Normalize a cleanup time string.
318
+ *
319
+ * The input must follow "HH:MM" or "H:MM" format (24-hour clock).
320
+ * If the time is valid, it returns the normalized format "HH:MM"
321
+ * with leading zeros added when necessary.
322
+ * If the format is invalid or the time is out of range
323
+ * (hour: 0–23, minute: 0–59), it returns undefined.
324
+ *
325
+ * Examples:
326
+ * normalizeCleanTime("3:05") -> "03:05"
327
+ * normalizeCleanTime("03:05") -> "03:05"
328
+ * normalizeCleanTime("23:59") -> "23:59"
329
+ *
330
+ * normalizeCleanTime("24:00") -> undefined // hour out of range
331
+ * normalizeCleanTime("12:60") -> undefined // minute out of range
332
+ * normalizeCleanTime("3:5") -> undefined // minute must have two digits
333
+ * normalizeCleanTime("abc") -> undefined // invalid format
334
+ */
335
+ function normalizeCleanTime(input) {
336
+ if (!input)
337
+ return undefined;
338
+ const trimmed = input.trim();
339
+ const m = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
340
+ if (!m)
341
+ return undefined;
342
+ const hh = Number(m[1]);
343
+ const mm = Number(m[2]);
344
+ if (!Number.isInteger(hh) || !Number.isInteger(mm))
345
+ return undefined;
346
+ if (hh < 0 || hh > 23 || mm < 0 || mm > 59)
347
+ return undefined;
348
+ return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
349
+ }
350
+ /**
351
+ * Normalize offload retention days.
352
+ *
353
+ * - `<= 0` → 0 (disabled)
354
+ * - `(0, 3)` → 0 (invalid, force disabled)
355
+ * - `>= 3` → as-is
356
+ */
357
+ function normalizeOffloadRetentionDays(value) {
358
+ if (value <= 0)
359
+ return 0;
360
+ if (value < 3)
361
+ return 0;
362
+ return value;
363
+ }