openclaw-memory-alibaba-local 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.ts ADDED
@@ -0,0 +1,570 @@
1
+ import fs from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { MemoryCategory } from "./categories.js";
5
+ import {
6
+ ALL_CATEGORIES,
7
+ FULL_CONTEXT_SOURCE_CATEGORIES,
8
+ MEMORY_CATEGORY_LABEL_ZH,
9
+ SELF_IMPROVING_CATEGORIES,
10
+ USER_MEMORY_CATEGORIES,
11
+ USER_MEMORY_FACT,
12
+ } from "./categories.js";
13
+
14
+ /** OpenAI-compatible HTTP embeddings; all fields required — invalid/empty values fail on first embed, not at startup. */
15
+ export type EmbeddingConfigRemote = {
16
+ mode: "remote";
17
+ apiKey: string;
18
+ model: string;
19
+ baseUrl: string;
20
+ dimensions: number;
21
+ maxToken: number;
22
+ };
23
+
24
+ /** Local `llama-embedding` (stdin) or compatible CLI; defaults: commandPrefix, dimensions 768, maxToken 2048. */
25
+ export type EmbeddingConfigLocal = {
26
+ mode: "local";
27
+ commandPrefix?: string;
28
+ dimensions?: number;
29
+ maxToken?: number;
30
+ };
31
+
32
+ export type EmbeddingConfig = EmbeddingConfigRemote | EmbeddingConfigLocal;
33
+
34
+ export type LLMConfig = {
35
+ apiKey: string;
36
+ model: string;
37
+ baseUrl?: string;
38
+ };
39
+
40
+ /** 管理端列表「记忆类型」筛选项(category + 展示用中文名);可由插件配置覆盖。 */
41
+ export type AdminPanelMemoryTypeOption = {
42
+ category: MemoryCategory;
43
+ labelZh: string;
44
+ };
45
+
46
+ export type AdminPanelMemoryTypeOptionsResolved = {
47
+ user: AdminPanelMemoryTypeOption[];
48
+ self: AdminPanelMemoryTypeOption[];
49
+ full: AdminPanelMemoryTypeOption[];
50
+ };
51
+
52
+ const ADMIN_PANEL_TAB_KEYS = ["user", "self", "full"] as const;
53
+
54
+ const USER_MEMORY_CAT_SET = new Set<string>(USER_MEMORY_CATEGORIES);
55
+ const SELF_MEMORY_CAT_SET = new Set<string>(SELF_IMPROVING_CATEGORIES);
56
+ const FULL_SOURCE_CAT_SET = new Set<string>(FULL_CONTEXT_SOURCE_CATEGORIES);
57
+
58
+ function defaultAdminPanelMemoryTypeOptions(
59
+ enableFullContextMemory: boolean,
60
+ enableSelfImprovingMemory: boolean,
61
+ ): AdminPanelMemoryTypeOptionsResolved {
62
+ return {
63
+ user: USER_MEMORY_CATEGORIES.map((c) => ({ category: c, labelZh: MEMORY_CATEGORY_LABEL_ZH[c] })),
64
+ self: enableSelfImprovingMemory
65
+ ? SELF_IMPROVING_CATEGORIES.map((c) => ({ category: c, labelZh: MEMORY_CATEGORY_LABEL_ZH[c] }))
66
+ : [],
67
+ full: enableFullContextMemory
68
+ ? FULL_CONTEXT_SOURCE_CATEGORIES.map((c) => ({ category: c, labelZh: MEMORY_CATEGORY_LABEL_ZH[c] }))
69
+ : [],
70
+ };
71
+ }
72
+
73
+ function parseAdminPanelMemoryTypeOptions(
74
+ raw: unknown,
75
+ enableFullContextMemory: boolean,
76
+ enableSelfImprovingMemory: boolean,
77
+ ): AdminPanelMemoryTypeOptionsResolved {
78
+ const defaults = defaultAdminPanelMemoryTypeOptions(
79
+ enableFullContextMemory,
80
+ enableSelfImprovingMemory,
81
+ );
82
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
83
+ return defaults;
84
+ }
85
+ const o = raw as Record<string, unknown>;
86
+ assertAllowedKeys(o, [...ADMIN_PANEL_TAB_KEYS], "adminPanelMemoryTypeOptions");
87
+
88
+ const out: AdminPanelMemoryTypeOptionsResolved = {
89
+ user: defaults.user,
90
+ self: defaults.self,
91
+ full: defaults.full,
92
+ };
93
+
94
+ for (const key of ADMIN_PANEL_TAB_KEYS) {
95
+ if (!(key in o)) {
96
+ continue;
97
+ }
98
+ const arr = o[key];
99
+ if (arr == null) {
100
+ continue;
101
+ }
102
+ if (!Array.isArray(arr)) {
103
+ throw new Error(`adminPanelMemoryTypeOptions.${key} must be an array`);
104
+ }
105
+ const allowed =
106
+ key === "user" ? USER_MEMORY_CAT_SET : key === "self" ? SELF_MEMORY_CAT_SET : FULL_SOURCE_CAT_SET;
107
+ if (key === "self" && !enableSelfImprovingMemory && arr.length > 0) {
108
+ throw new Error("adminPanelMemoryTypeOptions.self: enableSelfImprovingMemory is false");
109
+ }
110
+ if (key === "full" && !enableFullContextMemory && arr.length > 0) {
111
+ throw new Error("adminPanelMemoryTypeOptions.full: enableFullContextMemory is false");
112
+ }
113
+ const parsed: AdminPanelMemoryTypeOption[] = [];
114
+ for (const item of arr) {
115
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
116
+ throw new Error(`adminPanelMemoryTypeOptions.${key}: invalid entry`);
117
+ }
118
+ const rec = item as Record<string, unknown>;
119
+ assertAllowedKeys(rec, ["category", "labelZh"], `adminPanelMemoryTypeOptions.${key}[]`);
120
+ const cat = pickNonEmptyString(rec.category);
121
+ const labelZh = pickNonEmptyString(rec.labelZh);
122
+ if (!cat || !labelZh) {
123
+ throw new Error(`adminPanelMemoryTypeOptions.${key}: category and labelZh must be non-empty strings`);
124
+ }
125
+ if (!ALL_CATEGORIES.includes(cat as MemoryCategory)) {
126
+ throw new Error(`adminPanelMemoryTypeOptions.${key}: unknown category "${cat}"`);
127
+ }
128
+ if (!allowed.has(cat)) {
129
+ throw new Error(`adminPanelMemoryTypeOptions.${key}: category "${cat}" is not valid for tab "${key}"`);
130
+ }
131
+ parsed.push({ category: cat as MemoryCategory, labelZh });
132
+ }
133
+ out[key] = parsed;
134
+ }
135
+ return out;
136
+ }
137
+
138
+ export type MemoryConfig = {
139
+ /** Omitted when plugin is loaded without embedding (e.g. npm install); required at runtime for memory ops. */
140
+ embedding?: EmbeddingConfig;
141
+ /** LanceDB directory; same default as OpenClaw memory-lancedb (~/.openclaw/memory/lancedb). */
142
+ dbPath?: string;
143
+ /** LLM decides insert vs update among similar memories; requires llm. Default true; set false to disable. */
144
+ memory_duplication_conflict_process: boolean;
145
+ /** Required when memory_duplication_conflict_process is true or memoryExtractionMethod is "llm". */
146
+ llm?: LLMConfig;
147
+ similarityThresholdUserMemory: number;
148
+ similarityThresholdSelfImproving: number;
149
+ /** Full-context snapshots per role per session. Default true; set false to disable. */
150
+ enableFullContextMemory: boolean;
151
+ /** self_improving_* capture + recall. Default true; set false to disable. */
152
+ enableSelfImprovingMemory: boolean;
153
+ memoryExtractionMethod: "regex" | "llm";
154
+ autoRecall: boolean;
155
+ autoCapture: boolean;
156
+ captureMaxChars: number;
157
+ /** Recall time decay. Default true; set false to disable. */
158
+ enableMemoryDecay: boolean;
159
+ memoryDecayHalfLifeDays: number;
160
+ memoryDecayStrategy: "exponential" | "linear" | "none";
161
+ /** 管理端各 Tab「记忆类型」筛选项(缺省为内置类别 + 中文名) */
162
+ adminPanelMemoryTypeOptions: AdminPanelMemoryTypeOptionsResolved;
163
+ };
164
+
165
+ /** Re-export for tools and DB (user_memory_* + full_context + self_improving_*) */
166
+ export { ALL_CATEGORIES, USER_MEMORY_FACT };
167
+ export type { MemoryCategory };
168
+
169
+ export const DEFAULT_CAPTURE_MAX_CHARS = 50000;
170
+
171
+ const LEGACY_STATE_DIRS: string[] = [];
172
+
173
+ function resolveDefaultDbPath(): string {
174
+ const home = homedir();
175
+ const preferred = join(home, ".openclaw", "memory", "lancedb");
176
+ try {
177
+ if (fs.existsSync(preferred)) {
178
+ return preferred;
179
+ }
180
+ } catch {
181
+ // best-effort
182
+ }
183
+
184
+ for (const legacy of LEGACY_STATE_DIRS) {
185
+ const candidate = join(home, legacy, "memory", "lancedb");
186
+ try {
187
+ if (fs.existsSync(candidate)) {
188
+ return candidate;
189
+ }
190
+ } catch {
191
+ // best-effort
192
+ }
193
+ }
194
+
195
+ return preferred;
196
+ }
197
+
198
+ export const DEFAULT_DB_PATH = resolveDefaultDbPath();
199
+
200
+ const EMBEDDING_DIMENSIONS: Record<string, number> = {
201
+ "text-embedding-v3": 1024,
202
+ "text-embedding-v2": 1536,
203
+ "text-embedding-3-small": 1536,
204
+ "text-embedding-3-large": 3072,
205
+ "text-embedding-ada-002": 1536,
206
+ "embed-english-v3.0": 1024,
207
+ "embed-multilingual-v3.0": 1024,
208
+ "embed-english-light-v3.0": 384,
209
+ "embed-multilingual-light-v3.0": 384,
210
+ "jina-embeddings-v3": 1024,
211
+ "jina-embeddings-v2-base-en": 768,
212
+ "jina-embeddings-v2-base-zh": 768,
213
+ "bge-large-zh-v1.5": 1024,
214
+ "bge-large-en-v1.5": 1024,
215
+ "bge-m3": 1024,
216
+ "nomic-embed-text": 768,
217
+ "text-embedding-004": 768,
218
+ };
219
+
220
+ const FLEX_DIMS_MODELS = new Set([
221
+ "text-embedding-v3",
222
+ "text-embedding-3-small",
223
+ "text-embedding-3-large",
224
+ "jina-embeddings-v3",
225
+ ]);
226
+
227
+ export function vectorDimsForModel(model: string, explicit?: number): number {
228
+ if (explicit && explicit > 0) {
229
+ return explicit;
230
+ }
231
+ return EMBEDDING_DIMENSIONS[model] ?? 1024;
232
+ }
233
+
234
+ export function embeddingVectorDim(cfg: EmbeddingConfig): number {
235
+ return cfg.mode === "remote" ? cfg.dimensions : (cfg.dimensions ?? 768);
236
+ }
237
+
238
+ export function modelSupportsFlexDimensions(model: string): boolean {
239
+ return FLEX_DIMS_MODELS.has(model);
240
+ }
241
+
242
+ function resolveEnvVars(value: string): string {
243
+ return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
244
+ const envValue = process.env[envVar];
245
+ if (!envValue) {
246
+ throw new Error(`Environment variable ${envVar} is not set`);
247
+ }
248
+ return envValue;
249
+ });
250
+ }
251
+
252
+ function assertAllowedKeys(value: Record<string, unknown>, allowed: string[], label: string) {
253
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
254
+ if (unknown.length > 0) {
255
+ throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
256
+ }
257
+ }
258
+
259
+ function requireString(obj: Record<string, unknown>, key: string, label: string): string {
260
+ const v = obj[key];
261
+ if (typeof v !== "string" || v.length === 0) {
262
+ throw new Error(`${label}.${key} is required and must be a non-empty string`);
263
+ }
264
+ return v;
265
+ }
266
+
267
+ const DEFAULT_REMOTE_EMBED_DIMENSIONS = 1024;
268
+ const DEFAULT_REMOTE_EMBED_MAX_TOKEN = 2048;
269
+
270
+ /**
271
+ * Normalize host JSON: missing mode → infer `remote` if any HTTP embedding fields present, else `local`.
272
+ */
273
+ export function normalizeEmbeddingInput(raw: unknown): Record<string, unknown> {
274
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
275
+ return { mode: "local" };
276
+ }
277
+ const e = { ...(raw as Record<string, unknown>) };
278
+ if (e.mode === "remote" || e.mode === "local") {
279
+ return e;
280
+ }
281
+ const hasRemoteHints =
282
+ (typeof e.apiKey === "string" && e.apiKey.trim().length > 0) ||
283
+ (typeof e.model === "string" && e.model.trim().length > 0) ||
284
+ (typeof e.baseUrl === "string" && e.baseUrl.trim().length > 0) ||
285
+ (typeof e.dimensions === "number" && Number.isFinite(e.dimensions));
286
+ if (hasRemoteHints) {
287
+ e.mode = "remote";
288
+ return e;
289
+ }
290
+ e.mode = "local";
291
+ return e;
292
+ }
293
+
294
+ function parseEmbeddingConfig(raw: unknown): EmbeddingConfig {
295
+ const e = normalizeEmbeddingInput(raw);
296
+ const mode = e.mode === "remote" ? "remote" : "local";
297
+ if (mode === "remote") {
298
+ assertAllowedKeys(e, ["mode", "apiKey", "model", "baseUrl", "dimensions", "maxToken"], "embedding");
299
+ const dimensions =
300
+ typeof e.dimensions === "number" && Number.isFinite(e.dimensions) && e.dimensions > 0
301
+ ? e.dimensions
302
+ : DEFAULT_REMOTE_EMBED_DIMENSIONS;
303
+ const maxToken =
304
+ typeof e.maxToken === "number" && Number.isFinite(e.maxToken) && e.maxToken > 0
305
+ ? e.maxToken
306
+ : DEFAULT_REMOTE_EMBED_MAX_TOKEN;
307
+ return {
308
+ mode: "remote",
309
+ apiKey: typeof e.apiKey === "string" ? e.apiKey : "",
310
+ model: typeof e.model === "string" ? e.model : "",
311
+ baseUrl: typeof e.baseUrl === "string" ? e.baseUrl : "",
312
+ dimensions,
313
+ maxToken,
314
+ };
315
+ }
316
+ assertAllowedKeys(e, ["mode", "commandPrefix", "dimensions", "maxToken"], "embedding");
317
+ return {
318
+ mode: "local",
319
+ commandPrefix: typeof e.commandPrefix === "string" && e.commandPrefix.trim() ? e.commandPrefix.trim() : undefined,
320
+ dimensions: typeof e.dimensions === "number" && Number.isFinite(e.dimensions) ? e.dimensions : undefined,
321
+ maxToken: typeof e.maxToken === "number" && Number.isFinite(e.maxToken) ? e.maxToken : undefined,
322
+ };
323
+ }
324
+
325
+ const DEFAULT_LLM_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
326
+
327
+ /** Resolve ~/.openclaw/openclaw.json (or OPENCLAW_CONFIG_PATH / OPENCLAW_STATE_DIR). */
328
+ function resolveOpenclawJsonPath(): string {
329
+ const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || join(homedir(), ".openclaw");
330
+ return process.env.OPENCLAW_CONFIG_PATH?.trim() || join(stateDir, "openclaw.json");
331
+ }
332
+
333
+ function isRecord(v: unknown): v is Record<string, unknown> {
334
+ return !!v && typeof v === "object" && !Array.isArray(v);
335
+ }
336
+
337
+ /**
338
+ * OpenClaw `agents.defaults.model` is `provider/model` string or `{ primary?: string, ... }`.
339
+ * Chat Completions (DashScope compat) expects the model id without the `bailian/` prefix.
340
+ */
341
+ export function normalizeAgentsPrimaryModelForLlmApi(primary: string): string {
342
+ const t = primary.trim();
343
+ const i = t.indexOf("/");
344
+ if (i <= 0) {
345
+ return t;
346
+ }
347
+ const provider = t.slice(0, i).toLowerCase();
348
+ const rest = t.slice(i + 1).trim();
349
+ if (!rest) {
350
+ return t;
351
+ }
352
+ if (provider === "bailian" || provider === "dashscope") {
353
+ return rest;
354
+ }
355
+ return rest;
356
+ }
357
+
358
+ function coerceProviderApiKey(value: unknown): string | undefined {
359
+ if (typeof value === "string" && value.trim()) {
360
+ return value.trim();
361
+ }
362
+ if (!isRecord(value)) {
363
+ return undefined;
364
+ }
365
+ const source = typeof value.source === "string" ? value.source : "";
366
+ const id = typeof value.id === "string" ? value.id : "";
367
+ if (source === "env" && id && process.env[id]) {
368
+ return process.env[id]!;
369
+ }
370
+ return undefined;
371
+ }
372
+
373
+ export type OpenclawJsonLlmDefaults = {
374
+ apiKey?: string;
375
+ baseUrl?: string;
376
+ /** Already normalized for Chat Completions `model` field when sourced from agents.defaults.model.primary */
377
+ model?: string;
378
+ };
379
+
380
+ /**
381
+ * Read defaults for plugin `llm` from host OpenClaw config:
382
+ * - models.providers.bailian.apiKey / baseUrl
383
+ * - agents.defaults.model (string or { primary }) → normalized model id for API
384
+ */
385
+ export function readOpenclawJsonLlmDefaults(): OpenclawJsonLlmDefaults | null {
386
+ try {
387
+ const p = resolveOpenclawJsonPath();
388
+ if (!fs.existsSync(p)) {
389
+ return null;
390
+ }
391
+ const raw = fs.readFileSync(p, "utf8");
392
+ const json = JSON.parse(raw) as unknown;
393
+ if (!isRecord(json)) {
394
+ return null;
395
+ }
396
+ const models = json.models;
397
+ const providers = isRecord(models) && isRecord(models.providers) ? models.providers : undefined;
398
+ const bailian = providers && isRecord(providers.bailian) ? providers.bailian : undefined;
399
+
400
+ const apiKey = bailian ? coerceProviderApiKey(bailian.apiKey) : undefined;
401
+ const baseUrl =
402
+ bailian && typeof bailian.baseUrl === "string" && bailian.baseUrl.trim()
403
+ ? bailian.baseUrl.trim()
404
+ : undefined;
405
+
406
+ const agents = isRecord(json.agents) ? json.agents : undefined;
407
+ const defaults = agents && isRecord(agents.defaults) ? agents.defaults : undefined;
408
+ const modelCfg = defaults?.model;
409
+ let primaryRaw: string | undefined;
410
+ if (typeof modelCfg === "string" && modelCfg.trim()) {
411
+ primaryRaw = modelCfg.trim();
412
+ } else if (isRecord(modelCfg) && typeof modelCfg.primary === "string" && modelCfg.primary.trim()) {
413
+ primaryRaw = modelCfg.primary.trim();
414
+ }
415
+ const model = primaryRaw ? normalizeAgentsPrimaryModelForLlmApi(primaryRaw) : undefined;
416
+
417
+ if (!apiKey && !baseUrl && !model) {
418
+ return null;
419
+ }
420
+ return { apiKey, baseUrl, model };
421
+ } catch {
422
+ return null;
423
+ }
424
+ }
425
+
426
+ function pickNonEmptyString(v: unknown): string | undefined {
427
+ return typeof v === "string" && v.trim() ? v.trim() : undefined;
428
+ }
429
+
430
+ function parseLLMConfig(
431
+ raw: unknown,
432
+ openclawDefaults: OpenclawJsonLlmDefaults | null,
433
+ ): LLMConfig {
434
+ const l =
435
+ raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};
436
+ assertAllowedKeys(l, ["apiKey", "model", "baseUrl"], "llm");
437
+
438
+ const apiKeyRaw = pickNonEmptyString(l.apiKey) ?? openclawDefaults?.apiKey;
439
+ if (!apiKeyRaw) {
440
+ throw new Error(
441
+ "llm.apiKey is required (set in plugin config or models.providers.bailian.apiKey in openclaw.json)",
442
+ );
443
+ }
444
+ const apiKey = resolveEnvVars(apiKeyRaw);
445
+
446
+ const modelRaw = pickNonEmptyString(l.model) ?? openclawDefaults?.model;
447
+ if (!modelRaw) {
448
+ throw new Error(
449
+ 'llm.model is required (set in plugin config or agents.defaults.model.primary in openclaw.json as "bailian/your-model")',
450
+ );
451
+ }
452
+ const model = normalizeAgentsPrimaryModelForLlmApi(modelRaw);
453
+
454
+ const baseUrlRaw = pickNonEmptyString(l.baseUrl) ?? openclawDefaults?.baseUrl;
455
+ const baseUrl = baseUrlRaw ? resolveEnvVars(baseUrlRaw) : DEFAULT_LLM_BASE_URL;
456
+
457
+ return { apiKey, model, baseUrl };
458
+ }
459
+
460
+ export const memoryConfigSchema = {
461
+ parse(value: unknown): MemoryConfig {
462
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
463
+ value = {};
464
+ }
465
+ const cfg = value as Record<string, unknown>;
466
+
467
+ assertAllowedKeys(
468
+ cfg,
469
+ [
470
+ "embedding",
471
+ "dbPath",
472
+ "memory_duplication_conflict_process",
473
+ "llm",
474
+ "similarityThresholdUserMemory",
475
+ "similarityThresholdSelfImproving",
476
+ "enableFullContextMemory",
477
+ "enableSelfImprovingMemory",
478
+ "memoryExtractionMethod",
479
+ "autoRecall",
480
+ "autoCapture",
481
+ "captureMaxChars",
482
+ "enableMemoryDecay",
483
+ "memoryDecayHalfLifeDays",
484
+ "memoryDecayStrategy",
485
+ "adminPanelMemoryTypeOptions",
486
+ ],
487
+ "memory config",
488
+ );
489
+
490
+ const dbPath =
491
+ typeof cfg.dbPath === "string" && cfg.dbPath.length > 0 ? cfg.dbPath : DEFAULT_DB_PATH;
492
+
493
+ /** Default on: LLM resolves insert vs update when similar memories exist (opt out with false). */
494
+ let memory_duplication_conflict_process = cfg.memory_duplication_conflict_process !== false;
495
+ const rawMethod =
496
+ typeof cfg.memoryExtractionMethod === "string" ? cfg.memoryExtractionMethod.trim().toLowerCase() : "";
497
+ let memoryExtractionMethod: "regex" | "llm" = rawMethod === "regex" ? "regex" : "llm";
498
+ const openclawLlmDefaults = readOpenclawJsonLlmDefaults();
499
+ const hasPluginLlm = cfg.llm && typeof cfg.llm === "object" && !Array.isArray(cfg.llm);
500
+ const canFillFromOpenclaw =
501
+ !!openclawLlmDefaults && !!(openclawLlmDefaults.apiKey && openclawLlmDefaults.model);
502
+ let needsLlm = memory_duplication_conflict_process || memoryExtractionMethod === "llm";
503
+ if (needsLlm && !hasPluginLlm && !canFillFromOpenclaw) {
504
+ memoryExtractionMethod = "regex";
505
+ memory_duplication_conflict_process = false;
506
+ needsLlm = false;
507
+ }
508
+
509
+ const similarityThresholdUserMemory =
510
+ typeof cfg.similarityThresholdUserMemory === "number" ? cfg.similarityThresholdUserMemory : 0.65;
511
+ const similarityThresholdSelfImproving =
512
+ typeof cfg.similarityThresholdSelfImproving === "number" ? cfg.similarityThresholdSelfImproving : 0.62;
513
+ if (
514
+ similarityThresholdUserMemory < 0 ||
515
+ similarityThresholdUserMemory > 1 ||
516
+ similarityThresholdSelfImproving < 0 ||
517
+ similarityThresholdSelfImproving > 1
518
+ ) {
519
+ throw new Error("similarityThresholdUserMemory and similarityThresholdSelfImproving must be between 0 and 1");
520
+ }
521
+
522
+ /** Default on: always persist per-role full_context_* per session when autoCapture runs (opt out with false). */
523
+ const enableFullContextMemory = cfg.enableFullContextMemory !== false;
524
+ /** Default on: self_improving_* write + recall (opt out with false). */
525
+ const enableSelfImprovingMemory = cfg.enableSelfImprovingMemory !== false;
526
+
527
+ const captureMaxChars =
528
+ typeof cfg.captureMaxChars === "number" ? Math.floor(cfg.captureMaxChars) : undefined;
529
+ if (typeof captureMaxChars === "number" && (captureMaxChars < 100 || captureMaxChars > 100_000)) {
530
+ throw new Error("captureMaxChars must be between 100 and 100000");
531
+ }
532
+
533
+ /** Default on: time decay on recall scores (opt out with false). */
534
+ const enableMemoryDecay = cfg.enableMemoryDecay !== false;
535
+ const memoryDecayHalfLifeDaysRaw =
536
+ typeof cfg.memoryDecayHalfLifeDays === "number" && Number.isFinite(cfg.memoryDecayHalfLifeDays)
537
+ ? Math.floor(cfg.memoryDecayHalfLifeDays)
538
+ : 30;
539
+ const memoryDecayHalfLifeDays = Math.max(1, Math.min(3650, memoryDecayHalfLifeDaysRaw));
540
+ const decayRaw =
541
+ typeof cfg.memoryDecayStrategy === "string" ? cfg.memoryDecayStrategy.trim().toLowerCase() : "";
542
+ const memoryDecayStrategy: "exponential" | "linear" | "none" =
543
+ decayRaw === "linear" ? "linear" : decayRaw === "none" ? "none" : "exponential";
544
+
545
+ const adminPanelMemoryTypeOptions = parseAdminPanelMemoryTypeOptions(
546
+ cfg.adminPanelMemoryTypeOptions,
547
+ enableFullContextMemory,
548
+ enableSelfImprovingMemory,
549
+ );
550
+
551
+ return {
552
+ embedding: parseEmbeddingConfig(cfg.embedding),
553
+ dbPath,
554
+ memory_duplication_conflict_process,
555
+ llm: needsLlm ? parseLLMConfig(cfg.llm, openclawLlmDefaults) : undefined,
556
+ similarityThresholdUserMemory,
557
+ similarityThresholdSelfImproving,
558
+ enableFullContextMemory,
559
+ enableSelfImprovingMemory,
560
+ memoryExtractionMethod,
561
+ autoRecall: cfg.autoRecall !== false,
562
+ autoCapture: cfg.autoCapture !== false,
563
+ captureMaxChars: captureMaxChars ?? DEFAULT_CAPTURE_MAX_CHARS,
564
+ enableMemoryDecay,
565
+ memoryDecayHalfLifeDays,
566
+ memoryDecayStrategy,
567
+ adminPanelMemoryTypeOptions,
568
+ };
569
+ },
570
+ };