openclaw-cortex-memory 0.1.0-Alpha.3 → 0.1.0-Alpha.30

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 (82) hide show
  1. package/README.md +263 -204
  2. package/SKILL.md +77 -268
  3. package/dist/index.d.ts +92 -22
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1062 -1207
  6. package/dist/index.js.map +1 -1
  7. package/dist/openclaw.plugin.json +384 -15
  8. package/dist/src/dedup/three_stage_deduplicator.d.ts +25 -0
  9. package/dist/src/dedup/three_stage_deduplicator.d.ts.map +1 -0
  10. package/dist/src/dedup/three_stage_deduplicator.js +224 -0
  11. package/dist/src/dedup/three_stage_deduplicator.js.map +1 -0
  12. package/dist/src/engine/memory_engine.d.ts +2 -1
  13. package/dist/src/engine/memory_engine.d.ts.map +1 -1
  14. package/dist/src/engine/ts_engine.d.ts +126 -0
  15. package/dist/src/engine/ts_engine.d.ts.map +1 -1
  16. package/dist/src/engine/ts_engine.js +1145 -44
  17. package/dist/src/engine/ts_engine.js.map +1 -1
  18. package/dist/src/engine/types.d.ts +12 -0
  19. package/dist/src/engine/types.d.ts.map +1 -1
  20. package/dist/src/graph/ontology.d.ts +103 -0
  21. package/dist/src/graph/ontology.d.ts.map +1 -0
  22. package/dist/src/graph/ontology.js +564 -0
  23. package/dist/src/graph/ontology.js.map +1 -0
  24. package/dist/src/net/http_post.d.ts +17 -0
  25. package/dist/src/net/http_post.d.ts.map +1 -0
  26. package/dist/src/net/http_post.js +56 -0
  27. package/dist/src/net/http_post.js.map +1 -0
  28. package/dist/src/quality/llm_output_validator.d.ts +48 -0
  29. package/dist/src/quality/llm_output_validator.d.ts.map +1 -0
  30. package/dist/src/quality/llm_output_validator.js +404 -0
  31. package/dist/src/quality/llm_output_validator.js.map +1 -0
  32. package/dist/src/reflect/reflector.d.ts +7 -0
  33. package/dist/src/reflect/reflector.d.ts.map +1 -1
  34. package/dist/src/reflect/reflector.js +352 -8
  35. package/dist/src/reflect/reflector.js.map +1 -1
  36. package/dist/src/rules/rule_store.d.ts.map +1 -1
  37. package/dist/src/rules/rule_store.js +75 -16
  38. package/dist/src/rules/rule_store.js.map +1 -1
  39. package/dist/src/session/session_end.d.ts +33 -0
  40. package/dist/src/session/session_end.d.ts.map +1 -1
  41. package/dist/src/session/session_end.js +67 -64
  42. package/dist/src/session/session_end.js.map +1 -1
  43. package/dist/src/store/archive_store.d.ts +128 -0
  44. package/dist/src/store/archive_store.d.ts.map +1 -0
  45. package/dist/src/store/archive_store.js +475 -0
  46. package/dist/src/store/archive_store.js.map +1 -0
  47. package/dist/src/store/embedding_utils.d.ts +32 -0
  48. package/dist/src/store/embedding_utils.d.ts.map +1 -0
  49. package/dist/src/store/embedding_utils.js +173 -0
  50. package/dist/src/store/embedding_utils.js.map +1 -0
  51. package/dist/src/store/graph_memory_store.d.ts +44 -0
  52. package/dist/src/store/graph_memory_store.d.ts.map +1 -0
  53. package/dist/src/store/graph_memory_store.js +168 -0
  54. package/dist/src/store/graph_memory_store.js.map +1 -0
  55. package/dist/src/store/read_store.d.ts +86 -0
  56. package/dist/src/store/read_store.d.ts.map +1 -1
  57. package/dist/src/store/read_store.js +1661 -25
  58. package/dist/src/store/read_store.js.map +1 -1
  59. package/dist/src/store/vector_store.d.ts +44 -0
  60. package/dist/src/store/vector_store.d.ts.map +1 -0
  61. package/dist/src/store/vector_store.js +201 -0
  62. package/dist/src/store/vector_store.js.map +1 -0
  63. package/dist/src/store/write_store.d.ts +52 -0
  64. package/dist/src/store/write_store.d.ts.map +1 -1
  65. package/dist/src/store/write_store.js +239 -3
  66. package/dist/src/store/write_store.js.map +1 -1
  67. package/dist/src/sync/session_sync.d.ts +100 -2
  68. package/dist/src/sync/session_sync.d.ts.map +1 -1
  69. package/dist/src/sync/session_sync.js +725 -28
  70. package/dist/src/sync/session_sync.js.map +1 -1
  71. package/dist/src/utils/runtime_env.d.ts +4 -0
  72. package/dist/src/utils/runtime_env.d.ts.map +1 -0
  73. package/dist/src/utils/runtime_env.js +51 -0
  74. package/dist/src/utils/runtime_env.js.map +1 -0
  75. package/openclaw.plugin.json +384 -15
  76. package/package.json +53 -7
  77. package/schema/graph.schema.yaml +175 -0
  78. package/scripts/cli.js +19 -14
  79. package/scripts/repair-memory.js +321 -0
  80. package/scripts/uninstall.js +22 -5
  81. package/index.ts +0 -2142
  82. package/scripts/install.js +0 -27
package/dist/index.js CHANGED
@@ -39,28 +39,21 @@ exports.getStatus = getStatus;
39
39
  exports.unregister = unregister;
40
40
  exports.register = register;
41
41
  /// <reference types="node" />
42
- const child_process_1 = require("child_process");
43
42
  const path = __importStar(require("path"));
44
43
  const fs = __importStar(require("fs"));
45
- const net = __importStar(require("net"));
46
44
  const ts_engine_1 = require("./src/engine/ts_engine");
47
45
  const read_store_1 = require("./src/store/read_store");
48
46
  const write_store_1 = require("./src/store/write_store");
47
+ const archive_store_1 = require("./src/store/archive_store");
48
+ const vector_store_1 = require("./src/store/vector_store");
49
+ const graph_memory_store_1 = require("./src/store/graph_memory_store");
49
50
  const session_sync_1 = require("./src/sync/session_sync");
50
51
  const session_end_1 = require("./src/session/session_end");
51
52
  const rule_store_1 = require("./src/rules/rule_store");
52
53
  const reflector_1 = require("./src/reflect/reflector");
54
+ const three_stage_deduplicator_1 = require("./src/dedup/three_stage_deduplicator");
55
+ const runtime_env_1 = require("./src/utils/runtime_env");
53
56
  const ERROR_CODES = {
54
- CONNECTION_REFUSED: {
55
- code: "E001",
56
- message: "Cannot connect to the memory service",
57
- suggestion: "The Python backend may not be running. Try restarting the OpenClaw gateway."
58
- },
59
- TIMEOUT: {
60
- code: "E002",
61
- message: "The memory service is not responding",
62
- suggestion: "The service may be overloaded. Wait a moment and try again."
63
- },
64
57
  NOT_FOUND: {
65
58
  code: "E003",
66
59
  message: "Memory not found",
@@ -71,11 +64,6 @@ const ERROR_CODES = {
71
64
  message: "Invalid input provided",
72
65
  suggestion: "Please check your input parameters and try again."
73
66
  },
74
- SERVICE_ERROR: {
75
- code: "E005",
76
- message: "The memory service encountered an error",
77
- suggestion: "Check the service logs for details or try restarting the gateway."
78
- },
79
67
  PLUGIN_DISABLED: {
80
68
  code: "E006",
81
69
  message: "Cortex Memory plugin is disabled",
@@ -84,28 +72,123 @@ const ERROR_CODES = {
84
72
  };
85
73
  const SENSITIVE_KEYS = ["API_KEY", "SECRET", "TOKEN", "PASSWORD", "APIKEY"];
86
74
  const PLUGIN_ID = "openclaw-cortex-memory";
87
- const MIN_OPENCLAW_VERSION = "2026.3.8";
88
- const MAX_OPENCLAW_VERSION = "2027.0.0";
75
+ const MIN_OPENCLAW_GATEWAY_VERSION = "2026.4.5";
76
+ const MAX_OPENCLAW_GATEWAY_VERSION = "2027.0.0";
89
77
  const defaultConfig = {
90
78
  autoSync: true,
79
+ llmRequiredForWrite: true,
91
80
  autoReflect: false,
81
+ autoReflectIntervalMinutes: 30,
82
+ graphQualityMode: "warn",
83
+ readFusion: {
84
+ enabled: true,
85
+ maxCandidates: 10,
86
+ authoritative: true,
87
+ channelWeights: {
88
+ rules: 1,
89
+ archive: 1.15,
90
+ vector: 1.2,
91
+ graph: 1,
92
+ },
93
+ channelTopK: {
94
+ rules: 8,
95
+ archive: 20,
96
+ vector: 20,
97
+ graph: 12,
98
+ },
99
+ minLexicalHits: 1,
100
+ minSemanticHits: 1,
101
+ lengthNorm: {
102
+ enabled: true,
103
+ pivotChars: 1200,
104
+ strength: 0.75,
105
+ minFactor: 0.45,
106
+ },
107
+ },
108
+ vectorChunking: {
109
+ chunkSize: 600,
110
+ chunkOverlap: 100,
111
+ evidenceMaxChunks: 2,
112
+ },
113
+ writePolicy: {
114
+ archiveMinConfidence: 0.35,
115
+ archiveMinQualityScore: 0.4,
116
+ activeMinQualityScore: 0.45,
117
+ activeDedupTailLines: 200,
118
+ activeTextMaxChars: 4000,
119
+ archiveSourceTextMaxChars: 8000,
120
+ },
121
+ memoryDecay: {
122
+ enabled: true,
123
+ minFloor: 0.15,
124
+ defaultHalfLifeDays: 90,
125
+ antiDecay: {
126
+ enabled: true,
127
+ maxBoost: 1.6,
128
+ hitWeight: 0.08,
129
+ recentWindowDays: 30,
130
+ },
131
+ halfLifeByEventType: {
132
+ issue: 30,
133
+ fix: 30,
134
+ action_item: 30,
135
+ blocker: 30,
136
+ plan: 60,
137
+ milestone: 60,
138
+ follow_up: 60,
139
+ decision: 120,
140
+ insight: 120,
141
+ retrospective: 120,
142
+ preference: 240,
143
+ constraint: 240,
144
+ requirement: 240,
145
+ dependency: 240,
146
+ assumption: 240,
147
+ },
148
+ },
149
+ readTuning: {
150
+ scoring: {
151
+ lexicalWeight: 0.2,
152
+ bm25Scale: 2,
153
+ semanticWeight: 0.3,
154
+ recencyWeight: 0.1,
155
+ qualityWeight: 0.15,
156
+ typeMatchWeight: 0.15,
157
+ graphMatchWeight: 0.1,
158
+ },
159
+ rrf: {
160
+ k: 60,
161
+ weight: 1.5,
162
+ },
163
+ recency: {
164
+ buckets: [
165
+ { maxAgeHours: 12, score: 1, bonus: 0.6 },
166
+ { maxAgeHours: 24, score: 0.8, bonus: 0.6 },
167
+ { maxAgeHours: 72, score: 0.6, bonus: 0.3 },
168
+ { maxAgeHours: 168, score: 0.4, bonus: 0.3 },
169
+ { maxAgeHours: 720, score: 0.2, bonus: 0 },
170
+ { maxAgeHours: Number.POSITIVE_INFINITY, score: 0.05, bonus: 0 },
171
+ ],
172
+ },
173
+ autoContext: {
174
+ queryMaxChars: 80,
175
+ lightweightSearch: true,
176
+ },
177
+ },
92
178
  enabled: true,
93
- fallbackToBuiltin: true,
94
- engineMode: "ts",
95
179
  };
96
180
  let autoSearchCacheBySession = new Map();
97
181
  const AUTO_SEARCH_CACHE_TTL = 60000;
98
182
  const MAX_AUTO_SEARCH_CACHE_SESSIONS = 200;
99
183
  const HOOK_GUARD_TIMEOUT_MS = 2000;
184
+ const SYNC_DEBOUNCE_WINDOW_MS = 120000;
100
185
  let config = null;
101
186
  let logger;
102
- let pythonProcess = null;
103
187
  let isShuttingDown = false;
104
188
  let isInitializing = false;
105
189
  let isRegistered = false;
106
190
  let isEnabled = false;
107
191
  let api = null;
108
- let builtinMemory = null;
109
192
  let registeredTools = [];
110
193
  let registeredHooks = [];
111
194
  let registeredFallbackTools = [];
@@ -115,13 +198,10 @@ let autoReflectInterval = null;
115
198
  let lastAutoReflectArchiveMarker = "";
116
199
  let lastAutoReflectRunAt = 0;
117
200
  let configPath = null;
118
- let pythonStartPromise = null;
119
201
  let processHandlersRegistered = false;
120
- let pythonPidFilePath = null;
121
202
  let memoryEngine = null;
122
- function shouldUsePythonRuntime() {
123
- return false;
124
- }
203
+ let builtinMemory = null;
204
+ const TOOL_TRACE_PAYLOAD_MAX_CHARS = 1500;
125
205
  function getMemoryRoot() {
126
206
  const projectRoot = findProjectRoot();
127
207
  return config?.dbPath ? path.resolve(config.dbPath) : path.join(projectRoot, "data", "memory");
@@ -182,8 +262,7 @@ function resolveEngine() {
182
262
  if (!config) {
183
263
  throw new Error("Configuration not loaded");
184
264
  }
185
- const selectedMode = "ts";
186
- if (memoryEngine && memoryEngine.mode === "ts") {
265
+ if (memoryEngine) {
187
266
  return memoryEngine;
188
267
  }
189
268
  const projectRoot = findProjectRoot();
@@ -192,23 +271,88 @@ function resolveEngine() {
192
271
  projectRoot,
193
272
  dbPath: config.dbPath,
194
273
  logger,
274
+ embedding: config.embedding,
275
+ reranker: config.reranker,
276
+ llm: config.llm,
277
+ fusion: config.readFusion,
278
+ memoryDecay: config.memoryDecay,
279
+ readTuning: config.readTuning,
280
+ });
281
+ const vectorStore = (0, vector_store_1.createVectorStore)({
282
+ memoryRoot,
283
+ logger,
195
284
  });
196
285
  const writeStore = (0, write_store_1.createWriteStore)({
197
286
  projectRoot,
198
287
  dbPath: config.dbPath,
199
288
  logger,
289
+ embedding: config.embedding,
290
+ vectorChunking: config.vectorChunking,
291
+ writePolicy: config.writePolicy,
292
+ vectorStore,
293
+ });
294
+ const deduplicator = (0, three_stage_deduplicator_1.createThreeStageDeduplicator)({
295
+ memoryRoot,
296
+ logger,
297
+ });
298
+ const archiveStore = (0, archive_store_1.createArchiveStore)({
299
+ projectRoot,
300
+ memoryRoot,
301
+ logger,
302
+ embedding: config.embedding,
303
+ vectorChunking: config.vectorChunking,
304
+ writePolicy: config.writePolicy,
305
+ deduplicator,
306
+ vectorStore,
307
+ });
308
+ const graphMemoryStore = (0, graph_memory_store_1.createGraphMemoryStore)({
309
+ projectRoot,
310
+ memoryRoot,
311
+ logger,
312
+ qualityMode: config.graphQualityMode || "warn",
200
313
  });
201
314
  const sessionSync = (0, session_sync_1.createSessionSync)({
202
315
  projectRoot,
203
316
  dbPath: config.dbPath,
204
317
  logger,
318
+ llm: config.llm,
319
+ graphQualityMode: config.graphQualityMode || "warn",
320
+ requireLlmForWrite: config.llmRequiredForWrite ?? true,
321
+ writePolicy: config.writePolicy,
322
+ archiveStore,
323
+ graphMemoryStore,
205
324
  writeStore,
206
325
  });
326
+ let syncInFlight = null;
327
+ let lastSyncFinishedAt = 0;
328
+ const dedupedSyncMemory = async () => {
329
+ const now = Date.now();
330
+ if (syncInFlight) {
331
+ logger.info("sync_memory dedup: join in-flight run");
332
+ return syncInFlight;
333
+ }
334
+ if ((now - lastSyncFinishedAt) < SYNC_DEBOUNCE_WINDOW_MS) {
335
+ const waitMs = SYNC_DEBOUNCE_WINDOW_MS - (now - lastSyncFinishedAt);
336
+ logger.info(`sync_memory dedup: skip due to debounce window (${waitMs}ms remaining)`);
337
+ return { imported: 0, skipped: 0, filesProcessed: 0 };
338
+ }
339
+ syncInFlight = sessionSync.syncMemory();
340
+ try {
341
+ const result = await syncInFlight;
342
+ lastSyncFinishedAt = Date.now();
343
+ return result;
344
+ }
345
+ finally {
346
+ syncInFlight = null;
347
+ }
348
+ };
207
349
  const sessionEnd = (0, session_end_1.createSessionEnd)({
208
350
  projectRoot,
209
351
  dbPath: config.dbPath,
210
352
  logger,
211
- syncMemory: sessionSync.syncMemory,
353
+ syncMemory: dedupedSyncMemory,
354
+ syncDailySummaries: sessionSync.syncDailySummaries,
355
+ routeTranscript: sessionSync.routeTranscript,
212
356
  });
213
357
  const ruleStore = (0, rule_store_1.createRuleStore)({
214
358
  projectRoot,
@@ -220,14 +364,27 @@ function resolveEngine() {
220
364
  dbPath: config.dbPath,
221
365
  logger,
222
366
  ruleStore,
367
+ llm: config.llm,
223
368
  });
369
+ const sessionSyncBridge = {
370
+ ...sessionSync,
371
+ syncMemory: dedupedSyncMemory,
372
+ };
224
373
  memoryEngine = (0, ts_engine_1.createTsEngine)({
225
374
  readStore,
226
375
  writeStore,
227
- sessionSync,
376
+ vectorStore,
377
+ archiveStore,
378
+ graphMemoryStore,
379
+ sessionSync: sessionSyncBridge,
228
380
  sessionEnd,
229
381
  reflector,
230
382
  memoryRoot,
383
+ projectRoot,
384
+ embedding: config.embedding,
385
+ llm: config.llm,
386
+ reranker: config.reranker,
387
+ vectorChunking: config.vectorChunking,
231
388
  getCachedAutoSearch: getSessionCachedAutoSearch,
232
389
  resolveSessionId: (context, payload) => resolveSessionId(context, payload),
233
390
  normalizeIncomingMessage,
@@ -238,6 +395,84 @@ function resolveEngine() {
238
395
  });
239
396
  return memoryEngine;
240
397
  }
398
+ function normalizeToolNameList(input) {
399
+ if (!input)
400
+ return [];
401
+ if (Array.isArray(input)) {
402
+ const names = input
403
+ .map((item) => {
404
+ if (typeof item === "string")
405
+ return item.trim();
406
+ if (item && typeof item === "object") {
407
+ const name = firstString([item.name]);
408
+ return name || "";
409
+ }
410
+ return "";
411
+ })
412
+ .filter(Boolean);
413
+ return [...new Set(names)].sort();
414
+ }
415
+ if (typeof input === "object") {
416
+ return Object.keys(input).sort();
417
+ }
418
+ return [];
419
+ }
420
+ async function getApiVisibleToolNames() {
421
+ if (!api)
422
+ return [];
423
+ const apiObj = api;
424
+ const readers = [];
425
+ if (typeof apiObj.listTools === "function") {
426
+ readers.push(() => apiObj.listTools());
427
+ }
428
+ if (typeof apiObj.getTools === "function") {
429
+ readers.push(() => apiObj.getTools());
430
+ }
431
+ if (typeof apiObj.tools === "object" && apiObj.tools !== null) {
432
+ readers.push(() => apiObj.tools);
433
+ }
434
+ if (typeof apiObj.registeredTools === "object" && apiObj.registeredTools !== null) {
435
+ readers.push(() => apiObj.registeredTools);
436
+ }
437
+ for (const read of readers) {
438
+ try {
439
+ const value = await Promise.resolve(read());
440
+ const names = normalizeToolNameList(value);
441
+ if (names.length > 0) {
442
+ return names;
443
+ }
444
+ }
445
+ catch (error) {
446
+ logger.debug(`Failed to read visible tools from API: ${error instanceof Error ? error.message : String(error)}`);
447
+ }
448
+ }
449
+ return [];
450
+ }
451
+ async function withToolVisibilityDiagnostics(result, context) {
452
+ if (!result.success || !result.data || typeof result.data !== "object" || Array.isArray(result.data)) {
453
+ return result;
454
+ }
455
+ const visible = await getApiVisibleToolNames();
456
+ const registered = [...registeredTools].sort();
457
+ const missingFromVisibility = registered.filter((name) => !visible.includes(name));
458
+ const data = result.data;
459
+ return {
460
+ ...result,
461
+ data: {
462
+ ...data,
463
+ tool_visibility: {
464
+ lane_context: {
465
+ agent_id: context?.agentId || "unknown-agent",
466
+ session_id: context?.sessionId || null,
467
+ workspace_id: context?.workspaceId || "default",
468
+ },
469
+ registered_tools: registered,
470
+ api_visible_tools: visible,
471
+ missing_from_visible: missingFromVisibility,
472
+ },
473
+ },
474
+ };
475
+ }
241
476
  function clearStaleAutoSearchCache(now = Date.now()) {
242
477
  for (const [sessionId, cache] of autoSearchCacheBySession.entries()) {
243
478
  if ((now - cache.timestamp) >= AUTO_SEARCH_CACHE_TTL) {
@@ -269,1137 +504,443 @@ function createConsoleLogger() {
269
504
  error: (message, ...args) => console.error(`[CortexMemory] ${message}`, ...args),
270
505
  };
271
506
  }
272
- function sanitizeForLogging(obj) {
273
- const sanitized = {};
274
- for (const [key, value] of Object.entries(obj)) {
275
- const isSensitive = SENSITIVE_KEYS.some(k => key.toUpperCase().includes(k));
276
- if (isSensitive) {
277
- sanitized[key] = "***REDACTED***";
278
- }
279
- else if (typeof value === "object" && value !== null) {
280
- sanitized[key] = sanitizeForLogging(value);
281
- }
282
- else {
283
- sanitized[key] = value;
284
- }
285
- }
286
- return sanitized;
287
- }
288
- function asRecord(value) {
289
- if (typeof value === "object" && value !== null) {
507
+ function toTextContent(value) {
508
+ if (typeof value === "string") {
290
509
  return value;
291
510
  }
292
- return null;
511
+ try {
512
+ return JSON.stringify(value, null, 2);
513
+ }
514
+ catch {
515
+ return String(value);
516
+ }
293
517
  }
294
- function firstString(values) {
295
- for (const value of values) {
296
- if (typeof value === "string" && value.trim()) {
297
- return value.trim();
298
- }
518
+ function truncateForLog(value, maxChars = TOOL_TRACE_PAYLOAD_MAX_CHARS) {
519
+ if (value.length <= maxChars) {
520
+ return value;
299
521
  }
300
- return undefined;
522
+ return `${value.slice(0, maxChars)}...<truncated>`;
301
523
  }
302
- function normalizeIncomingMessage(payload) {
303
- const data = asRecord(payload);
304
- if (!data) {
305
- return null;
524
+ function formatUnknownForLog(value) {
525
+ if (typeof value === "string") {
526
+ return truncateForLog(value);
306
527
  }
307
- const message = asRecord(data.message);
308
- const eventData = asRecord(data.data);
309
- const update = asRecord(data.update);
310
- const updateMessage = update ? asRecord(update.message) : null;
311
- const role = firstString([
312
- data.role,
313
- data.fromRole,
314
- data.senderRole,
315
- message?.role,
316
- eventData?.role,
317
- updateMessage?.role,
318
- ]) || "user";
319
- const source = firstString([
320
- data.source,
321
- data.platform,
322
- data.channel,
323
- data.provider,
324
- message?.source,
325
- eventData?.source,
326
- ]) || "message";
327
- let text = firstString([
328
- data.content,
329
- data.text,
330
- data.body,
331
- data.prompt,
332
- data.message,
333
- message?.content,
334
- message?.text,
335
- message?.body,
336
- eventData?.content,
337
- eventData?.text,
338
- updateMessage?.text,
339
- updateMessage?.caption,
340
- ]);
341
- if (!text && Array.isArray(data.messages)) {
342
- const merged = data.messages
343
- .map(item => {
344
- if (typeof item === "string")
345
- return item;
346
- const msgObj = asRecord(item);
347
- if (!msgObj)
348
- return "";
349
- return firstString([msgObj.content, msgObj.text, msgObj.body]) || "";
350
- })
351
- .filter(Boolean)
352
- .join("\n");
353
- text = merged.trim() || undefined;
528
+ try {
529
+ return truncateForLog(JSON.stringify(value));
354
530
  }
355
- if (!text) {
356
- return null;
531
+ catch {
532
+ return truncateForLog(String(value));
357
533
  }
358
- return { text, role, source };
359
534
  }
360
- function resolveSessionId(context, payload) {
361
- const fromContext = typeof context.sessionId === "string" ? context.sessionId.trim() : "";
362
- if (fromContext)
363
- return fromContext;
364
- const data = asRecord(payload);
365
- const dataChat = data ? asRecord(data.chat) : null;
366
- const message = data ? asRecord(data.message) : null;
367
- const messageChat = message ? asRecord(message.chat) : null;
368
- const eventData = data ? asRecord(data.data) : null;
369
- const eventChat = eventData ? asRecord(eventData.chat) : null;
370
- const update = data ? asRecord(data.update) : null;
371
- const updateMessage = update ? asRecord(update.message) : null;
372
- const updateChat = updateMessage ? asRecord(updateMessage.chat) : null;
373
- const direct = firstString([
374
- data?.sessionId,
375
- data?.session_id,
376
- data?.conversationId,
377
- data?.conversation_id,
378
- data?.threadId,
379
- data?.thread_id,
380
- message?.sessionId,
381
- eventData?.sessionId,
382
- ]);
383
- if (direct)
384
- return direct;
385
- const chatId = firstString([
386
- data?.chatId,
387
- data?.chat_id,
388
- dataChat?.id,
389
- messageChat?.id,
390
- eventChat?.id,
391
- updateChat?.id,
392
- ]);
393
- if (chatId)
394
- return `chat:${chatId}`;
395
- return `fallback:${context.workspaceId || "default"}:${context.agentId || "agent"}`;
535
+ function createToolTraceId(toolName) {
536
+ return `${toolName}:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
396
537
  }
397
- function compareVersions(a, b) {
398
- const partsA = a.split(".").map(Number);
399
- const partsB = b.split(".").map(Number);
400
- for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
401
- const valA = partsA[i] || 0;
402
- const valB = partsB[i] || 0;
403
- if (valA < valB)
404
- return -1;
405
- if (valA > valB)
406
- return 1;
407
- }
408
- return 0;
538
+ function toErrorMessage(error) {
539
+ if (error instanceof Error) {
540
+ return error.message;
541
+ }
542
+ return String(error);
409
543
  }
410
- async function checkOpenClawVersion() {
411
- try {
412
- const version = process.env.OPENCLAW_VERSION;
413
- if (version) {
414
- if (compareVersions(version, MIN_OPENCLAW_VERSION) < 0) {
415
- throw new Error(`Incompatible OpenClaw version: ${version}. Minimum required: ${MIN_OPENCLAW_VERSION}`);
416
- }
417
- if (compareVersions(version, MAX_OPENCLAW_VERSION) >= 0) {
418
- throw new Error(`Incompatible OpenClaw version: ${version}. Maximum supported: <${MAX_OPENCLAW_VERSION}`);
419
- }
420
- logger.info(`OpenClaw version check passed: ${version}`);
544
+ function toAgentToolResult(result, traceId) {
545
+ if (!result.success) {
546
+ const errorText = result.error || "Tool execution failed";
547
+ return {
548
+ content: [{ type: "text", text: errorText }],
549
+ details: {
550
+ status: "error",
551
+ error: errorText,
552
+ ...(traceId ? { traceId } : {}),
553
+ ...(result.errorCode ? { errorCode: result.errorCode } : {}),
554
+ },
555
+ };
556
+ }
557
+ const payloadText = toTextContent(result.data ?? { ok: true });
558
+ const detailsData = result.data && typeof result.data === "object" && !Array.isArray(result.data)
559
+ ? result.data
560
+ : { value: result.data ?? null };
561
+ return {
562
+ content: [{ type: "text", text: payloadText }],
563
+ details: {
564
+ status: "ok",
565
+ ...(traceId ? { traceId } : {}),
566
+ ...detailsData,
567
+ },
568
+ };
569
+ }
570
+ function sanitizeForLogging(obj) {
571
+ const result = {};
572
+ for (const [key, value] of Object.entries(obj)) {
573
+ if (SENSITIVE_KEYS.some(k => key.toUpperCase().includes(k))) {
574
+ result[key] = "***REDACTED***";
575
+ }
576
+ else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
577
+ result[key] = sanitizeForLogging(value);
421
578
  }
422
579
  else {
423
- logger.warn("Could not determine OpenClaw version, proceeding with caution");
580
+ result[key] = value;
424
581
  }
425
582
  }
426
- catch (e) {
427
- const message = e instanceof Error ? e.message : String(e);
428
- logger.warn(`Version check warning: ${message}`);
429
- }
583
+ return result;
430
584
  }
431
585
  function findProjectRoot() {
432
- if (api && api.rootDir) {
433
- return api.rootDir;
434
- }
435
- let current = process.cwd();
436
- while (current !== path.dirname(current)) {
437
- if (fs.existsSync(path.join(current, "openclaw.plugin.json")) &&
438
- fs.existsSync(path.join(current, "package.json"))) {
439
- return current;
586
+ let dir = __dirname;
587
+ while (dir !== path.dirname(dir)) {
588
+ const pkgPath = path.join(dir, "package.json");
589
+ if (fs.existsSync(pkgPath)) {
590
+ return dir;
440
591
  }
441
- current = path.dirname(current);
592
+ dir = path.dirname(dir);
442
593
  }
443
- throw new Error("Cannot find project root directory");
594
+ return process.cwd();
444
595
  }
445
- function findOpenClawConfig() {
446
- const possiblePaths = [
447
- path.join(process.cwd(), "openclaw.json"),
448
- path.join(process.env.USERPROFILE || process.env.HOME || "", ".openclaw", "openclaw.json"),
449
- path.join(process.env.OPENCLAW_BASE_PATH || "", "openclaw.json"),
596
+ function resolveSessionId(context, payload) {
597
+ const contextObj = (context || {});
598
+ const payloadObj = (payload || {});
599
+ const candidates = [
600
+ contextObj.sessionId,
601
+ contextObj.session_id,
602
+ payloadObj.sessionId,
603
+ payloadObj.session_id,
604
+ payloadObj.id,
450
605
  ];
451
- for (const p of possiblePaths) {
452
- if (fs.existsSync(p)) {
453
- return p;
606
+ for (const c of candidates) {
607
+ if (typeof c === "string" && c.trim()) {
608
+ return c.trim();
454
609
  }
455
610
  }
456
- return null;
611
+ return `fallback:${Date.now().toString(36)}`;
457
612
  }
458
- function loadPluginEnabledState() {
459
- if (!configPath || !fs.existsSync(configPath)) {
460
- return true;
461
- }
462
- try {
463
- const content = fs.readFileSync(configPath, "utf-8");
464
- const openclawConfig = JSON.parse(content);
465
- const pluginEntry = openclawConfig?.plugins?.entries?.[PLUGIN_ID];
466
- if (pluginEntry && typeof pluginEntry === "object") {
467
- return pluginEntry.enabled !== false;
468
- }
469
- const legacyPluginConfig = openclawConfig?.plugins?.["cortex-memory"];
470
- return legacyPluginConfig?.enabled !== false;
471
- }
472
- catch (e) {
473
- logger.warn(`Failed to load config state: ${e}`);
474
- return true;
475
- }
476
- }
477
- function startConfigWatcher() {
478
- if (configWatchInterval) {
479
- clearInterval(configWatchInterval);
480
- }
481
- let lastEnabledState = isEnabled;
482
- configWatchInterval = setInterval(() => {
483
- const newState = loadPluginEnabledState();
484
- if (newState !== lastEnabledState) {
485
- lastEnabledState = newState;
486
- if (newState && !isEnabled) {
487
- logger.info("Detected config change: enabling Cortex Memory plugin");
488
- enable();
489
- }
490
- else if (!newState && isEnabled) {
491
- logger.info("Detected config change: disabling Cortex Memory plugin");
492
- disable();
493
- }
494
- }
495
- }, 5000);
613
+ function normalizeIncomingMessage(payload) {
614
+ const p = payload;
615
+ const text = typeof p.text === "string" ? p.text : (typeof p.content === "string" ? p.content : "");
616
+ if (!text)
617
+ return null;
618
+ const role = typeof p.role === "string" ? p.role : "unknown";
619
+ const source = typeof p.source === "string" ? p.source : "unknown";
620
+ return { text, role, source };
496
621
  }
497
- function stopConfigWatcher() {
498
- if (configWatchInterval) {
499
- clearInterval(configWatchInterval);
500
- configWatchInterval = null;
622
+ function asRecord(value) {
623
+ if (typeof value === "object" && value !== null) {
624
+ return value;
501
625
  }
626
+ return null;
502
627
  }
503
- function startAutoReflectScheduler() {
504
- if (!config?.autoReflect || autoReflectInterval) {
505
- return;
506
- }
507
- autoReflectInterval = setInterval(() => {
508
- if (!isEnabled) {
509
- return;
510
- }
511
- const schedulerContext = {
512
- agentId: "cortex-memory-scheduler",
513
- workspaceId: "system",
514
- };
515
- const marker = getArchiveMarker();
516
- const now = Date.now();
517
- if (marker === "missing" || marker === "error") {
518
- return;
519
- }
520
- if (marker === lastAutoReflectArchiveMarker) {
521
- return;
522
- }
523
- if (now - lastAutoReflectRunAt < 5 * 60 * 1000) {
524
- return;
628
+ function firstString(values) {
629
+ for (const v of values) {
630
+ if (typeof v === "string" && v.trim()) {
631
+ return v.trim();
525
632
  }
526
- resolveEngine().reflectMemory({}, schedulerContext)
527
- .then(result => {
528
- if (result.success) {
529
- lastAutoReflectArchiveMarker = marker;
530
- lastAutoReflectRunAt = now;
531
- logger.info("Scheduled reflection complete");
532
- }
533
- else {
534
- logger.warn(`Auto-reflect failed: ${result.error ?? "unknown_error"}`);
535
- }
536
- })
537
- .catch(error => logger.warn(`Auto-reflect failed: ${String(error)}`));
538
- }, 5 * 60 * 1000);
539
- }
540
- function stopAutoReflectScheduler() {
541
- if (autoReflectInterval) {
542
- clearInterval(autoReflectInterval);
543
- autoReflectInterval = null;
544
633
  }
634
+ return undefined;
545
635
  }
546
636
  function validateConfig(cfg) {
547
637
  const errors = [];
548
- if (!cfg.embedding?.provider || !cfg.embedding?.model) {
549
- errors.push("embedding.provider and embedding.model are required. Please configure them in openclaw.json");
550
- }
551
- if (!cfg.embedding?.apiKey || !cfg.embedding?.baseURL) {
552
- errors.push("embedding.apiKey and embedding.baseURL are required. Please configure third-party embedding endpoint credentials.");
553
- }
554
- if (!cfg.llm?.provider || !cfg.llm?.model) {
555
- errors.push("llm.provider and llm.model are required. Please configure them in openclaw.json");
638
+ if (!cfg.embedding?.provider) {
639
+ errors.push("embedding.provider is required.");
556
640
  }
557
- if (!cfg.llm?.apiKey || !cfg.llm?.baseURL) {
558
- errors.push("llm.apiKey and llm.baseURL are required. Please configure third-party LLM endpoint credentials.");
641
+ if (!cfg.embedding?.model) {
642
+ errors.push("embedding.model is required.");
559
643
  }
560
- if (!cfg.reranker?.model) {
561
- errors.push("reranker.model is required. Please configure it in openclaw.json");
644
+ if (!cfg.llm?.provider) {
645
+ errors.push("llm.provider is required.");
562
646
  }
563
- if (!cfg.reranker?.apiKey || !cfg.reranker?.baseURL) {
564
- errors.push("reranker.apiKey and reranker.baseURL are required. Please configure third-party reranker endpoint credentials.");
647
+ if (!cfg.llm?.model) {
648
+ errors.push("llm.model is required.");
565
649
  }
566
- return errors;
567
- }
568
- function getApiHostAndPort() {
569
- const parsed = new URL(getBaseUrl());
570
- const host = parsed.hostname || "127.0.0.1";
571
- const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
572
- return { host, port };
573
- }
574
- function sleep(ms) {
575
- return new Promise(resolve => setTimeout(resolve, ms));
576
- }
577
- function isPortListening(host, port, timeoutMs = 700) {
578
- return new Promise(resolve => {
579
- const socket = new net.Socket();
580
- let settled = false;
581
- const finalize = (value) => {
582
- if (settled)
583
- return;
584
- settled = true;
585
- socket.destroy();
586
- resolve(value);
587
- };
588
- socket.setTimeout(timeoutMs);
589
- socket.once("connect", () => finalize(true));
590
- socket.once("timeout", () => finalize(false));
591
- socket.once("error", () => finalize(false));
592
- socket.connect(port, host);
593
- });
594
- }
595
- function writePythonPid(pid) {
596
- if (!pythonPidFilePath)
597
- return;
598
- try {
599
- fs.writeFileSync(pythonPidFilePath, String(pid), "utf-8");
600
- }
601
- catch (e) {
602
- logger.warn(`Failed to write Python pid file: ${e instanceof Error ? e.message : String(e)}`);
603
- }
604
- }
605
- function clearPythonPidFile() {
606
- if (!pythonPidFilePath)
607
- return;
608
- try {
609
- if (fs.existsSync(pythonPidFilePath)) {
610
- fs.unlinkSync(pythonPidFilePath);
650
+ if (cfg.autoReflectIntervalMinutes !== undefined) {
651
+ if (!Number.isFinite(cfg.autoReflectIntervalMinutes) || cfg.autoReflectIntervalMinutes < 5) {
652
+ errors.push("autoReflectIntervalMinutes must be >= 5.");
611
653
  }
612
654
  }
613
- catch (e) {
614
- logger.warn(`Failed to clear Python pid file: ${e instanceof Error ? e.message : String(e)}`);
615
- }
616
- }
617
- function readPythonPid() {
618
- if (!pythonPidFilePath || !fs.existsSync(pythonPidFilePath))
619
- return null;
620
- try {
621
- const raw = fs.readFileSync(pythonPidFilePath, "utf-8").trim();
622
- const pid = Number(raw);
623
- return Number.isInteger(pid) && pid > 0 ? pid : null;
624
- }
625
- catch {
626
- return null;
627
- }
628
- }
629
- function killProcessByPid(pid) {
630
- if (!pid)
631
- return;
632
- try {
633
- if (process.platform === "win32") {
634
- (0, child_process_1.execSync)(`taskkill /pid ${pid} /f /t`, { stdio: "ignore" });
635
- return;
636
- }
637
- try {
638
- process.kill(pid, "SIGTERM");
655
+ if (cfg.graphQualityMode !== undefined) {
656
+ if (!["off", "warn", "strict"].includes(cfg.graphQualityMode)) {
657
+ errors.push("graphQualityMode must be one of: off, warn, strict.");
639
658
  }
640
- catch { }
641
- try {
642
- process.kill(-pid, "SIGTERM");
643
- }
644
- catch { }
645
- setTimeout(() => {
646
- try {
647
- process.kill(pid, "SIGKILL");
648
- }
649
- catch { }
650
- try {
651
- process.kill(-pid, "SIGKILL");
652
- }
653
- catch { }
654
- }, 2000);
655
- }
656
- catch (e) {
657
- logger.warn(`Failed to kill process ${pid}: ${e instanceof Error ? e.message : String(e)}`);
658
659
  }
659
- }
660
- function freePortWithSystemTools(port) {
661
- try {
662
- if (process.platform === "win32") {
663
- const output = (0, child_process_1.execSync)(`netstat -ano | findstr :${port}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
664
- const pids = output
665
- .split(/\r?\n/)
666
- .filter(line => line.includes("LISTENING"))
667
- .map(line => {
668
- const parts = line.trim().split(/\s+/);
669
- return Number(parts[parts.length - 1]);
670
- })
671
- .filter(pid => Number.isInteger(pid) && pid > 0);
672
- for (const pid of pids) {
673
- killProcessByPid(pid);
660
+ if (cfg.readFusion?.channelWeights) {
661
+ const weights = cfg.readFusion.channelWeights;
662
+ for (const [key, value] of Object.entries(weights)) {
663
+ if (typeof value === "number" && (!Number.isFinite(value) || value < 0)) {
664
+ errors.push(`readFusion.channelWeights.${key} must be >= 0.`);
674
665
  }
675
- return;
676
- }
677
- const output = (0, child_process_1.execSync)(`sh -lc "lsof -ti tcp:${port} 2>/dev/null || true"`, { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
678
- const pids = output
679
- .split(/\r?\n/)
680
- .map(line => Number(line.trim()))
681
- .filter(pid => Number.isInteger(pid) && pid > 0);
682
- for (const pid of pids) {
683
- killProcessByPid(pid);
684
666
  }
685
667
  }
686
- catch {
687
- // ignore
688
- }
689
- }
690
- async function checkPortInUse() {
691
- const apiUrl = getBaseUrl();
692
- try {
693
- const response = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(1000) });
694
- return response.ok;
695
- }
696
- catch {
697
- return false;
698
- }
699
- }
700
- async function startPythonService() {
701
- if (pythonStartPromise) {
702
- return pythonStartPromise;
703
- }
704
- pythonStartPromise = startPythonServiceInternal().finally(() => {
705
- pythonStartPromise = null;
706
- });
707
- return pythonStartPromise;
708
- }
709
- async function startPythonServiceInternal() {
710
- if (!config) {
711
- throw new Error("Configuration not loaded");
712
- }
713
- const projectRoot = findProjectRoot();
714
- pythonPidFilePath = path.join(projectRoot, ".cortex-memory-python.pid");
715
- const { host, port } = getApiHostAndPort();
716
- const stalePid = readPythonPid();
717
- if (stalePid && (!pythonProcess || pythonProcess.pid !== stalePid)) {
718
- logger.info(`Found stale Python pid ${stalePid}, trying to stop it...`);
719
- killProcessByPid(stalePid);
720
- await sleep(800);
721
- clearPythonPidFile();
722
- }
723
- const healthyRunning = await checkPortInUse();
724
- if (healthyRunning) {
725
- logger.info("Python service already running, shutting down old instance...");
726
- await shutdownPythonApi();
727
- await sleep(1000);
728
- }
729
- const occupied = await isPortListening(host, port);
730
- if (occupied) {
731
- logger.warn(`Port ${port} is still occupied after graceful shutdown, forcing cleanup...`);
732
- freePortWithSystemTools(port);
733
- await sleep(1000);
734
- }
735
- if (await isPortListening(host, port)) {
736
- throw new Error(`Port ${port} is already in use by another process. Please stop that process and retry.`);
737
- }
738
- const venvDir = path.join(projectRoot, "venv");
739
- const pythonCmd = process.platform === "win32"
740
- ? path.join(venvDir, "Scripts", "python.exe")
741
- : path.join(venvDir, "bin", "python");
742
- if (!fs.existsSync(pythonCmd)) {
743
- throw new Error("Python environment not found. Please run 'npm install' first.");
744
- }
745
- const errors = validateConfig(config);
746
- if (errors.length > 0) {
747
- throw new Error(`Configuration errors:\n${errors.join("\n")}`);
748
- }
749
- logger.info("Starting Cortex Memory Python service...");
750
- const env = {
751
- ...process.env,
752
- CORTEX_MEMORY_EMBEDDING_PROVIDER: config.embedding.provider,
753
- CORTEX_MEMORY_EMBEDDING_MODEL: config.embedding.model,
754
- CORTEX_MEMORY_LLM_PROVIDER: config.llm.provider,
755
- CORTEX_MEMORY_LLM_MODEL: config.llm.model,
756
- CORTEX_MEMORY_RERANKER_PROVIDER: config.reranker.provider || "",
757
- CORTEX_MEMORY_RERANKER_MODEL: config.reranker.model,
758
- CORTEX_MEMORY_DB_PATH: config.dbPath || path.join(process.env.USERPROFILE || process.env.HOME || "", ".openclaw", "agents", "main", "lancedb_store"),
759
- };
760
- if (config.embedding.apiKey) {
761
- env.CORTEX_MEMORY_EMBEDDING_API_KEY = config.embedding.apiKey;
762
- }
763
- if (config.embedding.baseURL) {
764
- env.CORTEX_MEMORY_EMBEDDING_BASE_URL = config.embedding.baseURL;
765
- }
766
- if (config.embedding.dimensions) {
767
- env.CORTEX_MEMORY_EMBEDDING_DIMENSIONS = String(config.embedding.dimensions);
768
- }
769
- if (config.llm.apiKey) {
770
- env.CORTEX_MEMORY_LLM_API_KEY = config.llm.apiKey;
771
- }
772
- if (config.llm.baseURL) {
773
- env.CORTEX_MEMORY_LLM_BASE_URL = config.llm.baseURL;
774
- }
775
- if (config.reranker.apiKey) {
776
- env.CORTEX_MEMORY_RERANKER_API_KEY = config.reranker.apiKey;
777
- }
778
- if (config.reranker.baseURL) {
779
- env.CORTEX_MEMORY_RERANKER_ENDPOINT = config.reranker.baseURL;
780
- }
781
- return new Promise((resolve, reject) => {
782
- pythonProcess = (0, child_process_1.spawn)(pythonCmd, ["-m", "api.server"], {
783
- cwd: projectRoot,
784
- detached: false,
785
- windowsHide: true,
786
- env: { ...env, PYTHONWARNINGS: "ignore::RuntimeWarning" },
787
- });
788
- if (pythonProcess.pid) {
789
- writePythonPid(pythonProcess.pid);
790
- }
791
- let started = false;
792
- let stderrBuffer = "";
793
- let settled = false;
794
- let startupTimeout = null;
795
- const resolveOnce = () => {
796
- if (settled)
797
- return;
798
- settled = true;
799
- started = true;
800
- if (startupTimeout) {
801
- clearTimeout(startupTimeout);
802
- startupTimeout = null;
803
- }
804
- resolve();
805
- };
806
- const rejectOnce = (error) => {
807
- if (settled)
808
- return;
809
- settled = true;
810
- if (startupTimeout) {
811
- clearTimeout(startupTimeout);
812
- startupTimeout = null;
813
- }
814
- reject(error);
815
- };
816
- pythonProcess.stdout?.on("data", (data) => {
817
- const output = data.toString();
818
- if (!output.toLowerCase().includes("key") && !output.toLowerCase().includes("token")) {
819
- logger.info(`[Python] ${output.trim()}`);
820
- }
821
- if (output.includes("Cortex Memory API started") || output.includes("Application startup complete")) {
822
- resolveOnce();
668
+ if (cfg.readFusion?.channelTopK) {
669
+ const topK = cfg.readFusion.channelTopK;
670
+ for (const [key, value] of Object.entries(topK)) {
671
+ if (typeof value === "number" && (!Number.isFinite(value) || value < 1)) {
672
+ errors.push(`readFusion.channelTopK.${key} must be >= 1.`);
823
673
  }
824
- });
825
- pythonProcess.stderr?.on("data", (data) => {
826
- const output = data.toString();
827
- stderrBuffer += output;
828
- if (!output.toLowerCase().includes("key") && !output.toLowerCase().includes("token")) {
829
- logger.warn(`[Python] ${output.trim()}`);
830
- }
831
- if (output.includes("Cortex Memory API started") ||
832
- output.includes("Application startup complete") ||
833
- output.includes("Uvicorn running on")) {
834
- resolveOnce();
835
- }
836
- });
837
- pythonProcess.on("error", (error) => {
838
- logger.error("Failed to start Python service:", error.message);
839
- rejectOnce(error);
840
- });
841
- pythonProcess.on("exit", (code) => {
842
- clearPythonPidFile();
843
- pythonProcess = null;
844
- if (!started && code !== 0 && !isShuttingDown) {
845
- rejectOnce(new Error(`Python service exited with code ${code}. Stderr: ${stderrBuffer.slice(-500)}`));
846
- }
847
- });
848
- startupTimeout = setTimeout(() => {
849
- if (!started) {
850
- const tail = stderrBuffer ? `\nLast stderr: ${stderrBuffer.slice(-500)}` : "";
851
- killPythonProcess();
852
- rejectOnce(new Error(`Timeout waiting for Python service to start (300s)${tail}`));
853
- }
854
- }, 300000);
855
- });
856
- }
857
- async function shutdownPythonApi() {
858
- const apiUrl = getBaseUrl();
859
- try {
860
- await fetch(`${apiUrl}/shutdown`, { method: "POST", signal: AbortSignal.timeout(2000) });
861
- await new Promise(resolve => setTimeout(resolve, 500));
862
- }
863
- catch {
864
- // ignore
865
- }
866
- }
867
- function killPythonProcess() {
868
- const directPid = pythonProcess?.pid ?? null;
869
- const pidFromFile = readPythonPid();
870
- const pid = directPid || pidFromFile;
871
- if (!pid)
872
- return;
873
- try {
874
- killProcessByPid(pid);
875
- }
876
- catch (e) {
877
- const message = e instanceof Error ? e.message : String(e);
878
- logger.warn(`Failed to kill Python process: ${message}`);
879
- }
880
- finally {
881
- pythonProcess = null;
882
- clearPythonPidFile();
883
- }
884
- }
885
- async function stopPythonServiceAsync() {
886
- if (pythonStartPromise) {
887
- try {
888
- await pythonStartPromise;
889
- }
890
- catch {
891
- // ignore
892
674
  }
893
675
  }
894
- await shutdownPythonApi();
895
- killPythonProcess();
896
- }
897
- function stopPythonService() {
898
- stopPythonServiceAsync();
899
- }
900
- function getBaseUrl() {
901
- return config?.apiUrl ?? "http://127.0.0.1:8765";
902
- }
903
- async function waitForService(maxAttempts = 30) {
904
- const apiUrl = getBaseUrl();
905
- for (let i = 0; i < maxAttempts; i++) {
906
- try {
907
- const response = await fetch(`${apiUrl}/health`, { signal: AbortSignal.timeout(1000) });
908
- if (response.ok)
909
- return;
676
+ if (cfg.vectorChunking?.chunkSize !== undefined) {
677
+ if (!Number.isFinite(cfg.vectorChunking.chunkSize) || cfg.vectorChunking.chunkSize < 100) {
678
+ errors.push("vectorChunking.chunkSize must be >= 100.");
910
679
  }
911
- catch { }
912
- await new Promise(resolve => setTimeout(resolve, 1000));
913
- }
914
- throw new Error("Service failed to become ready");
915
- }
916
- function formatApiError(error) {
917
- const message = error instanceof Error ? error.message : String(error);
918
- const lower = message.toLowerCase();
919
- if (lower.includes("econnrefused") || lower.includes("enotfound") || lower.includes("fetch failed")) {
920
- const err = ERROR_CODES.CONNECTION_REFUSED;
921
- return `${err.message} (${err.code}). ${err.suggestion}`;
922
- }
923
- if (lower.includes("abort") || lower.includes("timed out")) {
924
- const err = ERROR_CODES.TIMEOUT;
925
- return `${err.message} (${err.code}). ${err.suggestion}`;
926
- }
927
- if (lower.includes("404") || lower.includes("not found")) {
928
- const err = ERROR_CODES.NOT_FOUND;
929
- return `${err.message} (${err.code}). ${err.suggestion}`;
930
- }
931
- if (lower.includes("400") || lower.includes("invalid")) {
932
- const err = ERROR_CODES.INVALID_INPUT;
933
- return `${err.message} (${err.code}). ${err.suggestion}`;
934
- }
935
- const err = ERROR_CODES.SERVICE_ERROR;
936
- return `${err.message} (${err.code}). Details: ${message}`;
937
- }
938
- const pendingRequests = new Map();
939
- const requestDebounceMs = 100;
940
- function getRequestKey(endpoint, method, body) {
941
- const bodyHash = body ? JSON.stringify(body).slice(0, 100) : "";
942
- return `${method}:${endpoint}:${bodyHash}`;
943
- }
944
- async function apiCallWithRetry(endpoint, method = "GET", body, options) {
945
- const { maxRetries = 3, baseDelay = 1000, timeout = 30000, skipDebounce = false } = options || {};
946
- if (!skipDebounce) {
947
- const requestKey = getRequestKey(endpoint, method, body);
948
- const pending = pendingRequests.get(requestKey);
949
- if (pending) {
950
- logger.debug(`Reusing pending request for ${endpoint}`);
951
- return pending.promise;
952
- }
953
- const requestPromise = apiCallInternal(endpoint, method, body, maxRetries, baseDelay, timeout)
954
- .finally(() => {
955
- setTimeout(() => pendingRequests.delete(requestKey), requestDebounceMs);
956
- });
957
- pendingRequests.set(requestKey, {
958
- promise: requestPromise
959
- });
960
- return await requestPromise;
961
680
  }
962
- return apiCallInternal(endpoint, method, body, maxRetries, baseDelay, timeout);
963
- }
964
- async function apiCallInternal(endpoint, method, body, maxRetries, baseDelay, timeout) {
965
- let lastError = null;
966
- for (let attempt = 0; attempt < maxRetries; attempt++) {
967
- try {
968
- return await apiCall(endpoint, method, body, timeout);
681
+ if (cfg.vectorChunking?.chunkOverlap !== undefined) {
682
+ if (!Number.isFinite(cfg.vectorChunking.chunkOverlap) || cfg.vectorChunking.chunkOverlap < 0) {
683
+ errors.push("vectorChunking.chunkOverlap must be >= 0.");
969
684
  }
970
- catch (error) {
971
- lastError = error instanceof Error ? error : new Error(String(error));
972
- const isRetryable = lastError.message.includes("E001") ||
973
- lastError.message.includes("E002") ||
974
- lastError.message.includes("timeout") ||
975
- lastError.message.includes("ECONNREFUSED") ||
976
- lastError.message.includes("ENOTFOUND");
977
- if (attempt < maxRetries - 1 && isRetryable) {
978
- const delay = baseDelay * Math.pow(2, attempt);
979
- logger.warn(`API call failed (attempt ${attempt + 1}/${maxRetries}), retrying in ${delay}ms: ${lastError.message.split(".")[0]}`);
980
- await new Promise(resolve => setTimeout(resolve, delay));
981
- }
685
+ }
686
+ if (cfg.vectorChunking?.evidenceMaxChunks !== undefined) {
687
+ if (!Number.isFinite(cfg.vectorChunking.evidenceMaxChunks) || cfg.vectorChunking.evidenceMaxChunks < 0) {
688
+ errors.push("vectorChunking.evidenceMaxChunks must be >= 0.");
982
689
  }
983
690
  }
984
- throw lastError;
985
- }
986
- async function apiCall(endpoint, method = "GET", body, timeout = 30000) {
987
- const url = `${getBaseUrl()}${endpoint}`;
988
- const controller = new AbortController();
989
- const timeoutId = setTimeout(() => controller.abort(), timeout);
990
- const options = {
991
- method,
992
- headers: { "Content-Type": "application/json" },
993
- signal: controller.signal,
994
- };
995
- if (body)
996
- options.body = JSON.stringify(body);
997
- try {
998
- const response = await fetch(url, options);
999
- const text = await response.text();
1000
- if (!response.ok) {
1001
- try {
1002
- const errorData = JSON.parse(text);
1003
- throw new Error(errorData.error || errorData.detail || `HTTP ${response.status}`);
1004
- }
1005
- catch {
1006
- throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
691
+ if (cfg.writePolicy) {
692
+ const wp = cfg.writePolicy;
693
+ const bounded01 = ["archiveMinConfidence", "archiveMinQualityScore", "activeMinQualityScore"];
694
+ for (const key of bounded01) {
695
+ const value = wp[key];
696
+ if (value !== undefined && (!Number.isFinite(value) || value < 0 || value > 1)) {
697
+ errors.push(`writePolicy.${key} must be between 0 and 1.`);
1007
698
  }
1008
699
  }
1009
- if (!text)
1010
- return {};
1011
- try {
1012
- return JSON.parse(text);
700
+ if (wp.activeDedupTailLines !== undefined && (!Number.isFinite(wp.activeDedupTailLines) || wp.activeDedupTailLines < 20)) {
701
+ errors.push("writePolicy.activeDedupTailLines must be >= 20.");
1013
702
  }
1014
- catch {
1015
- throw new Error("Invalid JSON response");
703
+ if (wp.activeTextMaxChars !== undefined && (!Number.isFinite(wp.activeTextMaxChars) || wp.activeTextMaxChars < 500)) {
704
+ errors.push("writePolicy.activeTextMaxChars must be >= 500.");
705
+ }
706
+ if (wp.archiveSourceTextMaxChars !== undefined && (!Number.isFinite(wp.archiveSourceTextMaxChars) || wp.archiveSourceTextMaxChars < 1000)) {
707
+ errors.push("writePolicy.archiveSourceTextMaxChars must be >= 1000.");
1016
708
  }
1017
709
  }
1018
- catch (error) {
1019
- throw new Error(formatApiError(error));
1020
- }
1021
- finally {
1022
- clearTimeout(timeoutId);
1023
- }
1024
- }
1025
- async function searchMemoryWithFallback(args, context) {
1026
- if (!args || !args.query) {
1027
- logger.error(`search_memory called with invalid args: ${JSON.stringify(args)}`);
1028
- return { success: false, error: ERROR_CODES.INVALID_INPUT.message + " Missing 'query' parameter.", errorCode: ERROR_CODES.INVALID_INPUT.code };
1029
- }
1030
- if (!isEnabled) {
1031
- if (config?.fallbackToBuiltin && builtinMemory) {
1032
- logger.info("Using builtin memory (plugin disabled)");
1033
- try {
1034
- const results = await builtinMemory.search(args.query, args.top_k || 3);
1035
- return { success: true, data: results };
710
+ if (cfg.memoryDecay) {
711
+ if (typeof cfg.memoryDecay.minFloor === "number" && (!Number.isFinite(cfg.memoryDecay.minFloor) || cfg.memoryDecay.minFloor < 0 || cfg.memoryDecay.minFloor > 1)) {
712
+ errors.push("memoryDecay.minFloor must be between 0 and 1.");
713
+ }
714
+ if (typeof cfg.memoryDecay.defaultHalfLifeDays === "number" && (!Number.isFinite(cfg.memoryDecay.defaultHalfLifeDays) || cfg.memoryDecay.defaultHalfLifeDays <= 0)) {
715
+ errors.push("memoryDecay.defaultHalfLifeDays must be > 0.");
716
+ }
717
+ if (cfg.memoryDecay.antiDecay) {
718
+ const anti = cfg.memoryDecay.antiDecay;
719
+ if (typeof anti.maxBoost === "number" && (!Number.isFinite(anti.maxBoost) || anti.maxBoost < 1)) {
720
+ errors.push("memoryDecay.antiDecay.maxBoost must be >= 1.");
1036
721
  }
1037
- catch (error) {
1038
- const message = error instanceof Error ? error.message : String(error);
1039
- return { success: false, error: `Builtin memory error: ${message}` };
722
+ if (typeof anti.hitWeight === "number" && (!Number.isFinite(anti.hitWeight) || anti.hitWeight < 0)) {
723
+ errors.push("memoryDecay.antiDecay.hitWeight must be >= 0.");
724
+ }
725
+ if (typeof anti.recentWindowDays === "number" && (!Number.isFinite(anti.recentWindowDays) || anti.recentWindowDays <= 0)) {
726
+ errors.push("memoryDecay.antiDecay.recentWindowDays must be > 0.");
1040
727
  }
1041
728
  }
1042
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1043
- }
1044
- try {
1045
- const result = await apiCallWithRetry("/search", "POST", {
1046
- query: args.query,
1047
- top_k: args.top_k || 3,
1048
- });
1049
- return { success: true, data: result.results };
1050
- }
1051
- catch (error) {
1052
- const message = formatApiError(error);
1053
- logger.error(`search_memory failed: ${message}`);
1054
- return { success: false, error: message };
1055
729
  }
1056
- }
1057
- async function storeEventWithFallback(args, context) {
1058
- if (!isEnabled) {
1059
- if (config?.fallbackToBuiltin && builtinMemory) {
1060
- logger.info("Using builtin memory (plugin disabled)");
1061
- try {
1062
- const id = await builtinMemory.store(args.summary, {
1063
- entities: args.entities,
1064
- outcome: args.outcome,
1065
- relations: args.relations
1066
- });
1067
- return { success: true, data: { event_id: id } };
730
+ if (cfg.readTuning) {
731
+ const numericReadTuningFields = [
732
+ ["readTuning.scoring.lexicalWeight", cfg.readTuning.scoring?.lexicalWeight, 0],
733
+ ["readTuning.scoring.bm25Scale", cfg.readTuning.scoring?.bm25Scale, 0],
734
+ ["readTuning.scoring.semanticWeight", cfg.readTuning.scoring?.semanticWeight, 0],
735
+ ["readTuning.scoring.recencyWeight", cfg.readTuning.scoring?.recencyWeight, 0],
736
+ ["readTuning.scoring.qualityWeight", cfg.readTuning.scoring?.qualityWeight, 0],
737
+ ["readTuning.scoring.typeMatchWeight", cfg.readTuning.scoring?.typeMatchWeight, 0],
738
+ ["readTuning.scoring.graphMatchWeight", cfg.readTuning.scoring?.graphMatchWeight, 0],
739
+ ["readTuning.rrf.k", cfg.readTuning.rrf?.k, 1],
740
+ ["readTuning.rrf.weight", cfg.readTuning.rrf?.weight, 0],
741
+ ["readTuning.autoContext.queryMaxChars", cfg.readTuning.autoContext?.queryMaxChars, 20],
742
+ ];
743
+ for (const [name, value, min] of numericReadTuningFields) {
744
+ if (typeof value === "number" && (!Number.isFinite(value) || value < min)) {
745
+ errors.push(`${name} must be >= ${min}.`);
1068
746
  }
1069
- catch (error) {
1070
- const message = error instanceof Error ? error.message : String(error);
1071
- return { success: false, error: `Builtin memory error: ${message}` };
747
+ }
748
+ if (Array.isArray(cfg.readTuning.recency?.buckets)) {
749
+ const buckets = cfg.readTuning.recency.buckets;
750
+ for (let i = 0; i < buckets.length; i += 1) {
751
+ const bucket = buckets[i];
752
+ if (!Number.isFinite(bucket.maxAgeHours) || bucket.maxAgeHours <= 0) {
753
+ errors.push(`readTuning.recency.buckets[${i}].maxAgeHours must be > 0.`);
754
+ }
755
+ if (!Number.isFinite(bucket.score) || bucket.score < 0) {
756
+ errors.push(`readTuning.recency.buckets[${i}].score must be >= 0.`);
757
+ }
758
+ if (!Number.isFinite(bucket.bonus) || bucket.bonus < 0) {
759
+ errors.push(`readTuning.recency.buckets[${i}].bonus must be >= 0.`);
760
+ }
1072
761
  }
1073
762
  }
1074
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1075
- }
1076
- try {
1077
- const result = await apiCallWithRetry("/event", "POST", {
1078
- summary: args.summary,
1079
- entities: args.entities,
1080
- outcome: args.outcome,
1081
- relations: args.relations,
1082
- });
1083
- return { success: true, data: result };
1084
- }
1085
- catch (error) {
1086
- const message = formatApiError(error);
1087
- logger.error(`store_event failed: ${message}`);
1088
- return { success: false, error: message };
1089
- }
1090
- }
1091
- async function queryGraph(args, _context) {
1092
- if (!args || !args.entity) {
1093
- logger.error(`query_graph called with invalid args: ${JSON.stringify(args)}`);
1094
- return { success: false, error: ERROR_CODES.INVALID_INPUT.message + " Missing 'entity' parameter.", errorCode: ERROR_CODES.INVALID_INPUT.code };
1095
- }
1096
- if (!isEnabled) {
1097
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1098
- }
1099
- try {
1100
- const result = await apiCallWithRetry("/graph/query", "POST", { entity: args.entity });
1101
- return { success: true, data: result.graph };
1102
- }
1103
- catch (error) {
1104
- const message = formatApiError(error);
1105
- logger.error(`query_graph failed: ${message}`);
1106
- return { success: false, error: message };
1107
- }
1108
- }
1109
- async function getHotContext(args, _context) {
1110
- if (!isEnabled) {
1111
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1112
- }
1113
- try {
1114
- const limit = typeof args.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 20;
1115
- const result = await apiCallWithRetry(`/hot-context?limit=${limit}`, "GET");
1116
- return { success: true, data: result.context };
1117
- }
1118
- catch (error) {
1119
- const message = formatApiError(error);
1120
- logger.error(`get_hot_context failed: ${message}`);
1121
- return { success: false, error: message };
1122
763
  }
764
+ return errors;
1123
765
  }
1124
- async function getAutoContext(args, context) {
1125
- if (!isEnabled) {
1126
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1127
- }
1128
- const now = Date.now();
1129
- const result = {};
1130
- const sessionId = resolveSessionId(context);
1131
- clearStaleAutoSearchCache(now);
1132
- const sessionCache = autoSearchCacheBySession.get(sessionId);
1133
- if (sessionCache) {
1134
- result.auto_search = {
1135
- query: sessionCache.query,
1136
- results: sessionCache.results,
1137
- age_seconds: Math.floor((now - sessionCache.timestamp) / 1000),
1138
- };
1139
- }
1140
- if (args.include_hot !== false) {
766
+ function checkOpenClawVersion() {
767
+ return new Promise((resolve) => {
1141
768
  try {
1142
- const hotResult = await apiCallWithRetry("/hot-context", "GET");
1143
- result.hot_context = hotResult.context;
1144
- }
1145
- catch (error) {
1146
- logger.debug(`Failed to get hot context: ${formatApiError(error)}`);
1147
- }
1148
- }
1149
- if (!result.auto_search && !result.hot_context) {
1150
- return {
1151
- success: true,
1152
- data: {
1153
- message: "No session-scoped auto-search results cached and hot context unavailable",
1154
- suggestion: "Send a user message in this session or call get_hot_context."
769
+ const apiObj = api;
770
+ const candidates = [
771
+ { source: "api.openclawVersion", value: apiObj?.openclawVersion },
772
+ { source: "api.gatewayVersion", value: apiObj?.gatewayVersion },
773
+ { source: "api.coreVersion", value: apiObj?.coreVersion },
774
+ { source: "api.openclaw.version", value: apiObj?.openclaw?.version },
775
+ ].filter((item) => typeof item.value === "string" && item.value.trim().length > 0);
776
+ const selected = candidates.find((item) => {
777
+ const value = item.value;
778
+ const major = Number(value.replace(/[^0-9.]/g, "").split(".")[0] || "0");
779
+ // OpenClaw release versions are calendar-like (e.g. 2026.x.x),
780
+ // while plugin/package versions like 0.x should be ignored.
781
+ return Number.isFinite(major) && major >= 2000;
782
+ });
783
+ const version = selected?.value || "";
784
+ if (!version) {
785
+ logger.debug("Could not determine OpenClaw gateway/core version from API fields; skip compatibility check");
786
+ resolve();
787
+ return;
1155
788
  }
1156
- };
1157
- }
1158
- return { success: true, data: result };
1159
- }
1160
- async function reflectMemory(_args, _context) {
1161
- if (!isEnabled) {
1162
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1163
- }
1164
- try {
1165
- await apiCallWithRetry("/reflect", "POST", undefined, {
1166
- timeout: 120000,
1167
- maxRetries: 2,
1168
- });
1169
- return { success: true };
1170
- }
1171
- catch (error) {
1172
- const message = formatApiError(error);
1173
- logger.error(`reflect_memory failed: ${message}`);
1174
- return { success: false, error: message };
1175
- }
1176
- }
1177
- async function syncMemory(_args, _context) {
1178
- if (!isEnabled) {
1179
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1180
- }
1181
- try {
1182
- await apiCallWithRetry("/sync", "POST", undefined, {
1183
- timeout: 300000,
1184
- maxRetries: 2,
1185
- });
1186
- return { success: true };
1187
- }
1188
- catch (error) {
1189
- const message = formatApiError(error);
1190
- logger.error(`sync_memory failed: ${message}`);
1191
- return { success: false, error: message };
1192
- }
1193
- }
1194
- async function promoteMemory(_args, _context) {
1195
- if (!isEnabled) {
1196
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1197
- }
1198
- try {
1199
- await apiCallWithRetry("/promote", "POST", undefined, {
1200
- timeout: 120000,
1201
- maxRetries: 2,
1202
- });
1203
- return { success: true };
1204
- }
1205
- catch (error) {
1206
- const message = formatApiError(error);
1207
- logger.error(`promote_memory failed: ${message}`);
1208
- return { success: false, error: message };
1209
- }
1210
- }
1211
- async function deleteMemory(args, _context) {
1212
- if (!isEnabled) {
1213
- if (config?.fallbackToBuiltin && builtinMemory) {
1214
- try {
1215
- const success = await builtinMemory.delete(args.memory_id);
1216
- return { success };
789
+ const parseVersion = (v) => {
790
+ const parts = v.replace(/[^0-9.]/g, "").split(".").map(Number);
791
+ return parts.length >= 3 ? parts : [...parts, ...Array(3 - parts.length).fill(0)];
792
+ };
793
+ const current = parseVersion(version);
794
+ const min = parseVersion(MIN_OPENCLAW_GATEWAY_VERSION);
795
+ const max = parseVersion(MAX_OPENCLAW_GATEWAY_VERSION);
796
+ const currentNum = current[0] * 10000 + current[1] * 100 + current[2];
797
+ const minNum = min[0] * 10000 + min[1] * 100 + min[2];
798
+ const maxNum = max[0] * 10000 + max[1] * 100 + max[2];
799
+ if (currentNum < minNum) {
800
+ logger.warn(`OpenClaw gateway/core version ${version} (from ${selected?.source || "unknown"}) is below minimum ${MIN_OPENCLAW_GATEWAY_VERSION}. Some features may not work.`);
1217
801
  }
1218
- catch (error) {
1219
- const message = error instanceof Error ? error.message : String(error);
1220
- return { success: false, error: `Builtin memory error: ${message}` };
802
+ else if (currentNum >= maxNum) {
803
+ logger.warn(`OpenClaw gateway/core version ${version} (from ${selected?.source || "unknown"}) may not be fully compatible. Maximum tested version is ${MAX_OPENCLAW_GATEWAY_VERSION}.`);
1221
804
  }
1222
805
  }
1223
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1224
- }
1225
- try {
1226
- await apiCallWithRetry(`/memory/${args.memory_id}`, "DELETE");
1227
- return { success: true };
1228
- }
1229
- catch (error) {
1230
- const message = formatApiError(error);
1231
- logger.error(`delete_memory failed: ${message}`);
1232
- return { success: false, error: message };
1233
- }
1234
- }
1235
- async function updateMemory(args, _context) {
1236
- if (!isEnabled) {
1237
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1238
- }
1239
- try {
1240
- await apiCallWithRetry(`/memory/${args.memory_id}`, "PATCH", {
1241
- text: args.text,
1242
- type: args.type,
1243
- weight: args.weight,
1244
- });
1245
- return { success: true };
1246
- }
1247
- catch (error) {
1248
- const message = formatApiError(error);
1249
- logger.error(`update_memory failed: ${message}`);
1250
- return { success: false, error: message };
1251
- }
1252
- }
1253
- async function cleanupMemories(args, _context) {
1254
- if (!isEnabled) {
1255
- return { success: false, error: ERROR_CODES.PLUGIN_DISABLED.message, errorCode: ERROR_CODES.PLUGIN_DISABLED.code };
1256
- }
1257
- try {
1258
- const result = await apiCallWithRetry("/cleanup", "POST", {
1259
- days_old: args.days_old || 90,
1260
- memory_type: args.memory_type,
1261
- });
1262
- return { success: true, data: { deletedCount: result.deleted_count } };
1263
- }
1264
- catch (error) {
1265
- const message = formatApiError(error);
1266
- logger.error(`cleanup_memories failed: ${message}`);
1267
- return { success: false, error: message };
1268
- }
806
+ catch (e) {
807
+ logger.warn(`Version check failed: ${e instanceof Error ? e.message : String(e)}`);
808
+ }
809
+ resolve();
810
+ });
1269
811
  }
1270
- async function runDiagnostics(_args, _context) {
1271
- if (!isEnabled) {
1272
- return {
1273
- success: true,
1274
- data: {
1275
- status: "disabled",
1276
- message: "Cortex Memory plugin is disabled",
1277
- suggestion: "Enable the plugin using 'openclaw plugins enable cortex-memory'"
1278
- }
1279
- };
1280
- }
1281
- try {
1282
- const result = await apiCallWithRetry("/doctor", "GET");
1283
- return { success: true, data: result };
812
+ function findOpenClawConfig() {
813
+ const explicitPath = (0, runtime_env_1.getEnvValue)("OPENCLAW_CONFIG_PATH");
814
+ if (explicitPath && fs.existsSync(explicitPath)) {
815
+ return explicitPath;
816
+ }
817
+ const stateDir = (0, runtime_env_1.getEnvValue)("OPENCLAW_STATE_DIR");
818
+ if (stateDir) {
819
+ const stateConfig = path.join(stateDir, "openclaw.json");
820
+ if (fs.existsSync(stateConfig)) {
821
+ return stateConfig;
822
+ }
1284
823
  }
1285
- catch (error) {
1286
- const message = formatApiError(error);
1287
- logger.error(`diagnostics failed: ${message}`);
1288
- return { success: false, error: message };
824
+ const basePath = (0, runtime_env_1.getEnvValue)("OPENCLAW_BASE_PATH");
825
+ if (basePath) {
826
+ const baseConfig = path.join(basePath, "openclaw.json");
827
+ if (fs.existsSync(baseConfig)) {
828
+ return baseConfig;
829
+ }
1289
830
  }
1290
- }
1291
- async function getPluginStatus(_args, _context) {
1292
- return {
1293
- success: true,
1294
- data: {
1295
- enabled: isEnabled,
1296
- service_running: pythonProcess !== null,
1297
- fallback_enabled: config?.fallbackToBuiltin ?? true,
1298
- builtin_memory_available: builtinMemory !== null,
1299
- engine_mode: config?.engineMode ?? "python",
831
+ const home = (0, runtime_env_1.getHomeDir)();
832
+ const candidates = [
833
+ path.join(home, ".openclaw", "openclaw.json"),
834
+ path.join(home, ".openclaw", "config.json"),
835
+ ];
836
+ for (const c of candidates) {
837
+ if (fs.existsSync(c)) {
838
+ return c;
1300
839
  }
1301
- };
840
+ }
841
+ return null;
1302
842
  }
1303
- async function onMessagePythonHandler(payload, context) {
1304
- if (!isEnabled)
1305
- return;
1306
- const normalized = normalizeIncomingMessage(payload);
1307
- if (!normalized)
1308
- return;
1309
- const { text, role, source } = normalized;
1310
- const sessionId = resolveSessionId(context, payload);
843
+ function loadPluginEnabledState() {
1311
844
  try {
1312
- const writeResult = await apiCallWithRetry("/write", "POST", {
1313
- text,
1314
- source,
1315
- role,
1316
- session_id: sessionId
1317
- });
1318
- if (writeResult.status === "ok") {
1319
- logger.info(`Stored ${role} message for session ${sessionId}`);
845
+ if (!configPath)
846
+ return true;
847
+ const raw = fs.readFileSync(configPath, "utf-8");
848
+ const cfg = JSON.parse(raw);
849
+ const entry = cfg?.plugins?.entries?.[PLUGIN_ID];
850
+ if (entry && typeof entry.enabled === "boolean") {
851
+ return entry.enabled;
1320
852
  }
1321
- else {
1322
- logger.debug(`Write skipped for session ${sessionId}: ${writeResult.reason || writeResult.status || "unknown"}`);
853
+ const allow = cfg?.plugins?.allow;
854
+ if (Array.isArray(allow)) {
855
+ return allow.includes(PLUGIN_ID);
1323
856
  }
857
+ return true;
1324
858
  }
1325
- catch (error) {
1326
- logger.warn(`Failed to store message: ${formatApiError(error)}`);
859
+ catch {
860
+ return true;
861
+ }
862
+ }
863
+ function startConfigWatcher() {
864
+ if (configWatchInterval) {
865
+ clearInterval(configWatchInterval);
1327
866
  }
1328
- if (role === "user" && text.length > 5) {
867
+ configWatchInterval = setInterval(() => {
1329
868
  try {
1330
- const searchResult = await apiCallWithRetry("/search", "POST", {
1331
- query: text,
1332
- top_k: 3,
1333
- session_id: sessionId,
1334
- });
1335
- if (searchResult.results && searchResult.results.length > 0) {
1336
- setSessionAutoSearchCache(sessionId, text, searchResult.results);
1337
- logger.info(`Auto-search cached ${searchResult.results.length} results for context`);
1338
- }
1339
- else if (searchResult.skipped) {
1340
- logger.debug(`Auto-search skipped for session ${sessionId}: ${searchResult.reason || "query filtered"}`);
869
+ if (!configPath || !fs.existsSync(configPath))
870
+ return;
871
+ const currentEnabled = loadPluginEnabledState();
872
+ if (currentEnabled !== isEnabled) {
873
+ logger.info(`Plugin enabled state changed from ${isEnabled} to ${currentEnabled}`);
874
+ if (currentEnabled) {
875
+ enable().catch(e => logger.error(`Failed to enable: ${e}`));
876
+ }
877
+ else {
878
+ disable().catch(e => logger.error(`Failed to disable: ${e}`));
879
+ }
1341
880
  }
1342
881
  }
1343
- catch (error) {
1344
- logger.debug(`Auto-search skipped: ${formatApiError(error)}`);
882
+ catch (e) {
883
+ logger.debug(`Config watch error: ${e}`);
1345
884
  }
1346
- }
885
+ }, 5000);
1347
886
  }
1348
- async function onSessionEndPythonHandler(payload, context) {
1349
- if (!isEnabled)
1350
- return;
1351
- const sessionId = resolveSessionId(context, payload);
1352
- try {
1353
- const endResult = await apiCallWithRetry("/session-end", "POST", {
1354
- session_id: sessionId,
1355
- sync_records: config?.autoSync ?? true,
1356
- });
1357
- logger.info(`Session ${sessionId} ended, generated ${endResult.events_generated} events`);
1358
- }
1359
- catch (error) {
1360
- logger.warn(`Failed to end session: ${formatApiError(error)}`);
887
+ function stopConfigWatcher() {
888
+ if (configWatchInterval) {
889
+ clearInterval(configWatchInterval);
890
+ configWatchInterval = null;
1361
891
  }
1362
892
  }
1363
- async function onTimerPythonHandler(payload, _context) {
1364
- if (!isEnabled)
893
+ function startAutoReflectScheduler() {
894
+ if (!config?.autoReflect) {
1365
895
  return;
1366
- const data = payload;
1367
- const action = data.action;
1368
- try {
1369
- if (action === "sync") {
1370
- await apiCallWithRetry("/sync", "POST", undefined, {
1371
- timeout: 300000,
1372
- maxRetries: 2,
1373
- });
1374
- logger.info("Scheduled sync complete");
1375
- }
1376
- else if (action === "reflect" || (config?.autoReflect && !action)) {
1377
- await apiCallWithRetry("/reflect", "POST", undefined, {
1378
- timeout: 120000,
1379
- maxRetries: 2,
1380
- });
1381
- logger.info("Scheduled reflection complete");
1382
- }
1383
- else if (action === "promote") {
1384
- await apiCallWithRetry("/promote", "POST", undefined, {
1385
- timeout: 120000,
1386
- maxRetries: 2,
1387
- });
1388
- logger.info("Scheduled promotion complete");
1389
- }
1390
896
  }
1391
- catch (error) {
1392
- logger.warn(`Timer action failed: ${formatApiError(error)}`);
897
+ const intervalMinutes = Math.max(5, config.autoReflectIntervalMinutes ?? 30);
898
+ if (autoReflectInterval) {
899
+ clearInterval(autoReflectInterval);
900
+ }
901
+ lastAutoReflectArchiveMarker = getArchiveMarker();
902
+ lastAutoReflectRunAt = Date.now();
903
+ autoReflectInterval = setInterval(async () => {
904
+ if (!isEnabled)
905
+ return;
906
+ const currentMarker = getArchiveMarker();
907
+ if (currentMarker !== lastAutoReflectArchiveMarker) {
908
+ lastAutoReflectArchiveMarker = currentMarker;
909
+ try {
910
+ const result = await resolveEngine().reflectMemory({}, { agentId: "scheduler", workspaceId: "default" });
911
+ if (result.success) {
912
+ logger.info("Auto-reflect completed successfully");
913
+ }
914
+ else {
915
+ logger.warn(`Auto-reflect failed: ${result.error}`);
916
+ }
917
+ }
918
+ catch (e) {
919
+ logger.error(`Auto-reflect error: ${e instanceof Error ? e.message : String(e)}`);
920
+ }
921
+ }
922
+ }, intervalMinutes * 60 * 1000);
923
+ logger.info(`Auto-reflect scheduler started (interval: ${intervalMinutes} minutes)`);
924
+ }
925
+ function stopAutoReflectScheduler() {
926
+ if (autoReflectInterval) {
927
+ clearInterval(autoReflectInterval);
928
+ autoReflectInterval = null;
1393
929
  }
1394
930
  }
931
+ function logLifecycle(event, data) {
932
+ logger.info(`[Lifecycle] ${event}${data ? `: ${JSON.stringify(sanitizeForLogging(data))}` : ""}`);
933
+ }
1395
934
  async function onMessageHandler(payload, context) {
1396
- if (isInternalSession(context.sessionId)) {
935
+ const sessionId = resolveSessionId(context, payload);
936
+ if (isInternalSession(sessionId)) {
1397
937
  return;
1398
938
  }
1399
939
  await runWithTimeout(resolveEngine().onMessage(payload, context), HOOK_GUARD_TIMEOUT_MS, "onMessage hook");
1400
940
  }
1401
941
  async function onSessionEndHandler(payload, context) {
1402
- if (isInternalSession(context.sessionId)) {
942
+ const sessionId = resolveSessionId(context, payload);
943
+ if (isInternalSession(sessionId)) {
1403
944
  return;
1404
945
  }
1405
946
  await runWithTimeout(resolveEngine().onSessionEnd(payload, context), HOOK_GUARD_TIMEOUT_MS, "onSessionEnd hook");
@@ -1410,6 +951,8 @@ async function onTimerHandler(payload, context) {
1410
951
  function registerTools() {
1411
952
  if (!api)
1412
953
  return;
954
+ const apiObj = api;
955
+ logger.info(`registerTools API capability: registerTool=${typeof apiObj.registerTool === "function"}, registerTools=${typeof apiObj.registerTools === "function"}, tools.register=${typeof apiObj.tools?.register === "function"}`);
1413
956
  const tools = [
1414
957
  {
1415
958
  name: "search_memory",
@@ -1424,7 +967,10 @@ function registerTools() {
1424
967
  additionalProperties: false,
1425
968
  },
1426
969
  execute: async (params) => {
970
+ logger.info(`search_memory execute called with params: ${JSON.stringify(params)}`);
971
+ logger.info(`params.args: ${JSON.stringify(params.args)}`);
1427
972
  const args = params.args || params;
973
+ logger.info(`args after extraction: ${JSON.stringify(args)}`);
1428
974
  return resolveEngine().searchMemory(args, params.context);
1429
975
  },
1430
976
  },
@@ -1438,13 +984,47 @@ function registerTools() {
1438
984
  entities: {
1439
985
  type: "array",
1440
986
  description: "Involved entities",
1441
- items: { type: "string" }
987
+ items: {
988
+ oneOf: [
989
+ { type: "string" },
990
+ {
991
+ type: "object",
992
+ properties: {
993
+ id: { type: "string" },
994
+ name: { type: "string" },
995
+ type: { type: "string" },
996
+ },
997
+ additionalProperties: false,
998
+ },
999
+ ],
1000
+ },
1001
+ },
1002
+ entity_types: {
1003
+ type: "object",
1004
+ description: "Entity type map, key is entity name and value is type",
1005
+ additionalProperties: { type: "string" },
1442
1006
  },
1443
1007
  outcome: { type: "string", description: "Event outcome" },
1444
1008
  relations: {
1445
1009
  type: "array",
1446
1010
  description: "Entity relationships",
1447
- items: { type: "string" }
1011
+ items: {
1012
+ oneOf: [
1013
+ { type: "string" },
1014
+ {
1015
+ type: "object",
1016
+ properties: {
1017
+ source: { type: "string" },
1018
+ target: { type: "string" },
1019
+ type: { type: "string" },
1020
+ evidence_span: { type: "string" },
1021
+ confidence: { type: "number" },
1022
+ },
1023
+ required: ["source", "target"],
1024
+ additionalProperties: false,
1025
+ },
1026
+ ],
1027
+ }
1448
1028
  },
1449
1029
  },
1450
1030
  required: ["summary"],
@@ -1461,7 +1041,15 @@ function registerTools() {
1461
1041
  parameters: {
1462
1042
  type: "object",
1463
1043
  properties: {
1464
- entity: { type: "string", description: "Entity name" }
1044
+ entity: { type: "string", description: "Entity name" },
1045
+ rel: { type: "string", description: "Optional relation type filter" },
1046
+ dir: {
1047
+ type: "string",
1048
+ description: "Relation direction filter",
1049
+ enum: ["incoming", "outgoing", "both"],
1050
+ },
1051
+ path_to: { type: "string", description: "Find path from entity to this target entity" },
1052
+ max_depth: { type: "integer", description: "Path query max depth (2~4)" },
1465
1053
  },
1466
1054
  required: ["entity"],
1467
1055
  additionalProperties: false,
@@ -1531,6 +1119,26 @@ function registerTools() {
1531
1119
  return resolveEngine().syncMemory(args, params.context);
1532
1120
  },
1533
1121
  },
1122
+ {
1123
+ name: "backfill_embeddings",
1124
+ description: "Backfill missing embeddings for active/archive records",
1125
+ parameters: {
1126
+ type: "object",
1127
+ properties: {
1128
+ layer: { type: "string", enum: ["active", "archive", "all"], description: "Target layer to backfill" },
1129
+ batch_size: { type: "integer", description: "Batch size per processing window" },
1130
+ max_retries: { type: "integer", description: "Max retry count for failed records" },
1131
+ retry_failed_only: { type: "boolean", description: "Only retry failed records" },
1132
+ rebuild_mode: { type: "string", enum: ["incremental", "vector_only", "full"], description: "Rebuild mode" },
1133
+ },
1134
+ required: [],
1135
+ additionalProperties: false,
1136
+ },
1137
+ execute: async (params) => {
1138
+ const args = params.args || params;
1139
+ return resolveEngine().backfillEmbeddings(args, params.context);
1140
+ },
1141
+ },
1534
1142
  {
1535
1143
  name: "delete_memory",
1536
1144
  description: "Delete a memory by ID",
@@ -1558,43 +1166,220 @@ function registerTools() {
1558
1166
  },
1559
1167
  execute: async (params) => {
1560
1168
  const args = params.args || params;
1561
- return resolveEngine().runDiagnostics(args, params.context);
1169
+ const result = await resolveEngine().runDiagnostics(args, params.context);
1170
+ return withToolVisibilityDiagnostics(result, params.context);
1562
1171
  },
1563
1172
  },
1564
1173
  ];
1174
+ let successCount = 0;
1565
1175
  for (const tool of tools) {
1566
- registerToolCompat(tool);
1567
- registeredTools.push(tool.name);
1176
+ try {
1177
+ registerToolCompat(tool);
1178
+ registeredTools.push(tool.name);
1179
+ successCount += 1;
1180
+ }
1181
+ catch (error) {
1182
+ logger.error(`Failed to register tool ${tool.name}: ${error instanceof Error ? error.message : String(error)}`);
1183
+ }
1184
+ }
1185
+ logger.info(`registerTools completed: ${successCount}/${tools.length} tools registered`);
1186
+ }
1187
+ function sanitizeToolParametersSchemaValue(schema) {
1188
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
1189
+ return {};
1190
+ }
1191
+ const source = schema;
1192
+ const target = {};
1193
+ if (typeof source.type === "string") {
1194
+ target.type = source.type;
1195
+ }
1196
+ if (typeof source.description === "string" && source.description.trim()) {
1197
+ target.description = source.description;
1198
+ }
1199
+ if (Array.isArray(source.enum)) {
1200
+ const values = source.enum.filter(item => typeof item === "string" || typeof item === "number" || typeof item === "boolean" || item === null);
1201
+ if (values.length > 0) {
1202
+ target.enum = values;
1203
+ }
1204
+ }
1205
+ if (source.properties && typeof source.properties === "object" && !Array.isArray(source.properties)) {
1206
+ const sanitizedProperties = {};
1207
+ for (const [key, value] of Object.entries(source.properties)) {
1208
+ sanitizedProperties[key] = sanitizeToolParametersSchemaValue(value);
1209
+ }
1210
+ target.properties = sanitizedProperties;
1211
+ }
1212
+ if (Array.isArray(source.required)) {
1213
+ const required = source.required.filter(item => typeof item === "string");
1214
+ if (required.length > 0) {
1215
+ target.required = required;
1216
+ }
1217
+ }
1218
+ if (source.items && typeof source.items === "object" && !Array.isArray(source.items)) {
1219
+ target.items = sanitizeToolParametersSchemaValue(source.items);
1220
+ }
1221
+ if (typeof source.additionalProperties === "boolean") {
1222
+ target.additionalProperties = source.additionalProperties;
1223
+ }
1224
+ else if (source.additionalProperties && typeof source.additionalProperties === "object") {
1225
+ target.additionalProperties = true;
1568
1226
  }
1227
+ return target;
1228
+ }
1229
+ function sanitizeToolParametersSchema(schema) {
1230
+ const sanitized = sanitizeToolParametersSchemaValue(schema);
1231
+ if (sanitized.type !== "object") {
1232
+ sanitized.type = "object";
1233
+ }
1234
+ if (!sanitized.properties || typeof sanitized.properties !== "object" || Array.isArray(sanitized.properties)) {
1235
+ sanitized.properties = {};
1236
+ }
1237
+ if (!Array.isArray(sanitized.required)) {
1238
+ sanitized.required = [];
1239
+ }
1240
+ if (typeof sanitized.additionalProperties !== "boolean") {
1241
+ sanitized.additionalProperties = false;
1242
+ }
1243
+ return sanitized;
1569
1244
  }
1570
1245
  function registerToolCompat(tool) {
1571
1246
  if (!api)
1572
1247
  return;
1573
- const execute = async (params) => tool.execute({
1574
- args: params?.args || {},
1575
- context: params.context,
1576
- });
1577
- const handler = async (...params) => {
1248
+ const normalizeContext = (value) => {
1249
+ const contextObj = asRecord(value) || {};
1250
+ return {
1251
+ agentId: firstString([contextObj.agentId, contextObj.agent_id]) || "unknown-agent",
1252
+ workspaceId: firstString([contextObj.workspaceId, contextObj.workspace_id]) || "default",
1253
+ sessionId: firstString([contextObj.sessionId, contextObj.session_id]) || undefined,
1254
+ };
1255
+ };
1256
+ const normalizeInvocation = (...params) => {
1257
+ logger.info(`normalizeInvocation called with params: ${JSON.stringify(params)}`);
1258
+ if (params.length === 1) {
1259
+ const first = params[0];
1260
+ const firstObj = asRecord(first);
1261
+ if (firstObj && ("context" in firstObj || "args" in firstObj)) {
1262
+ const explicitArgs = asRecord(firstObj.args);
1263
+ if (explicitArgs) {
1264
+ logger.info(`normalizeInvocation: single param with explicit args: ${JSON.stringify(explicitArgs)}`);
1265
+ return {
1266
+ args: explicitArgs,
1267
+ context: normalizeContext(firstObj.context),
1268
+ };
1269
+ }
1270
+ const directArgs = { ...firstObj };
1271
+ delete directArgs.context;
1272
+ delete directArgs.args;
1273
+ logger.info(`normalizeInvocation: single param with direct args: ${JSON.stringify(directArgs)}`);
1274
+ return {
1275
+ args: directArgs,
1276
+ context: normalizeContext(firstObj.context),
1277
+ };
1278
+ }
1279
+ if (firstObj) {
1280
+ logger.info(`normalizeInvocation: single param as args: ${JSON.stringify(firstObj)}`);
1281
+ return {
1282
+ args: firstObj,
1283
+ context: normalizeContext(undefined),
1284
+ };
1285
+ }
1286
+ }
1578
1287
  const first = params[0];
1579
1288
  const second = params[1];
1580
- if (first && typeof first === "object" && "context" in first) {
1581
- return execute({
1582
- args: first.args || {},
1583
- context: first.context,
1289
+ const third = params[2];
1290
+ const firstObj = asRecord(first);
1291
+ const secondObj = asRecord(second);
1292
+ if (typeof first === "string" && secondObj) {
1293
+ logger.info(`normalizeInvocation: first is string (tool call ID), second is args: ${JSON.stringify(secondObj)}`);
1294
+ return {
1295
+ args: secondObj,
1296
+ context: normalizeContext(third),
1297
+ };
1298
+ }
1299
+ if (firstObj && ("context" in firstObj || "args" in firstObj)) {
1300
+ const explicitArgs = asRecord(firstObj.args);
1301
+ if (explicitArgs) {
1302
+ logger.info(`normalizeInvocation: first has explicit args: ${JSON.stringify(explicitArgs)}`);
1303
+ return {
1304
+ args: explicitArgs,
1305
+ context: normalizeContext(firstObj.context),
1306
+ };
1307
+ }
1308
+ const directArgs = { ...firstObj };
1309
+ delete directArgs.context;
1310
+ delete directArgs.args;
1311
+ logger.info(`normalizeInvocation: first has direct args: ${JSON.stringify(directArgs)}`);
1312
+ return {
1313
+ args: directArgs,
1314
+ context: normalizeContext(firstObj.context),
1315
+ };
1316
+ }
1317
+ if (firstObj && Object.keys(firstObj).length > 0) {
1318
+ logger.info(`normalizeInvocation: first is args, second is context: ${JSON.stringify(firstObj)}`);
1319
+ return {
1320
+ args: firstObj,
1321
+ context: normalizeContext(second),
1322
+ };
1323
+ }
1324
+ logger.info(`normalizeInvocation: fallback to firstObj as args: ${JSON.stringify(firstObj)}`);
1325
+ return {
1326
+ args: firstObj || {},
1327
+ context: normalizeContext(second),
1328
+ };
1329
+ };
1330
+ const invoke = async (...params) => {
1331
+ const traceId = createToolTraceId(tool.name);
1332
+ const startedAt = Date.now();
1333
+ logger.info(`[ToolTrace] start traceId=${traceId} tool=${tool.name} paramCount=${params.length}`);
1334
+ const normalized = normalizeInvocation(...params);
1335
+ logger.debug(`[ToolTrace] normalized traceId=${traceId} tool=${tool.name} args=${formatUnknownForLog(sanitizeForLogging(normalized.args))} context=${formatUnknownForLog(sanitizeForLogging(normalized.context))}`);
1336
+ try {
1337
+ const result = await tool.execute({
1338
+ args: normalized.args,
1339
+ context: normalized.context,
1584
1340
  });
1341
+ const durationMs = Date.now() - startedAt;
1342
+ logger.info(`[ToolTrace] success traceId=${traceId} tool=${tool.name} durationMs=${durationMs} resultSuccess=${result.success}`);
1343
+ if (!result.success) {
1344
+ logger.error(`[ToolTrace] tool_failure traceId=${traceId} tool=${tool.name} error=${truncateForLog(result.error || "unknown_error")} errorCode=${result.errorCode || "none"} args=${formatUnknownForLog(sanitizeForLogging(normalized.args))}`);
1345
+ }
1346
+ return toAgentToolResult(result, traceId);
1347
+ }
1348
+ catch (error) {
1349
+ const durationMs = Date.now() - startedAt;
1350
+ const message = toErrorMessage(error);
1351
+ logger.error(`[ToolTrace] exception traceId=${traceId} tool=${tool.name} durationMs=${durationMs} message=${truncateForLog(message)} args=${formatUnknownForLog(sanitizeForLogging(normalized.args))} context=${formatUnknownForLog(sanitizeForLogging(normalized.context))}`);
1352
+ if (error instanceof Error && error.stack) {
1353
+ logger.error(`[ToolTrace] stack traceId=${traceId} tool=${tool.name} ${truncateForLog(error.stack, 4000)}`);
1354
+ }
1355
+ return toAgentToolResult({
1356
+ success: false,
1357
+ error: `Tool execution failed [traceId=${traceId}]: ${message}`,
1358
+ errorCode: "TOOL_EXECUTION_EXCEPTION",
1359
+ }, traceId);
1585
1360
  }
1586
- return execute({
1587
- args: first || {},
1588
- context: (second || {}),
1589
- });
1590
1361
  };
1591
- api.registerTool({
1362
+ const payload = {
1592
1363
  name: tool.name,
1593
1364
  description: tool.description,
1594
- parameters: tool.parameters,
1595
- execute,
1596
- handler,
1597
- });
1365
+ parameters: sanitizeToolParametersSchema(tool.parameters),
1366
+ execute: invoke,
1367
+ handler: invoke,
1368
+ };
1369
+ const apiObj = api;
1370
+ if (typeof apiObj.registerTool === "function") {
1371
+ apiObj.registerTool(payload);
1372
+ return;
1373
+ }
1374
+ if (typeof apiObj.registerTools === "function") {
1375
+ apiObj.registerTools([payload]);
1376
+ return;
1377
+ }
1378
+ if (typeof apiObj.tools?.register === "function") {
1379
+ apiObj.tools.register(payload);
1380
+ return;
1381
+ }
1382
+ throw new Error("No supported tool registration API found");
1598
1383
  }
1599
1384
  function unregisterTools() {
1600
1385
  if (!api || !api.unregisterTool)
@@ -1609,6 +1394,108 @@ function unregisterTools() {
1609
1394
  }
1610
1395
  registeredTools = [];
1611
1396
  }
1397
+ function registerFallbackTools() {
1398
+ if (!api || !builtinMemory)
1399
+ return;
1400
+ for (const name of ["search_memory", "store_event", "cortex_memory_status"]) {
1401
+ try {
1402
+ if (api.unregisterTool) {
1403
+ api.unregisterTool(name);
1404
+ logger.info(`Unregistered existing tool ${name} before registering fallback`);
1405
+ }
1406
+ }
1407
+ catch (e) {
1408
+ // ignore
1409
+ }
1410
+ }
1411
+ registerToolCompat({
1412
+ name: "search_memory",
1413
+ description: "Search memory (using builtin system - Cortex Memory disabled)",
1414
+ parameters: {
1415
+ type: "object",
1416
+ properties: {
1417
+ query: { type: "string", description: "Search query" },
1418
+ top_k: { type: "integer", description: "Number of results" },
1419
+ },
1420
+ required: ["query"],
1421
+ additionalProperties: false,
1422
+ },
1423
+ execute: async (params) => {
1424
+ const args = (params.args || params);
1425
+ const query = args.query || "";
1426
+ const topK = args.top_k || 5;
1427
+ try {
1428
+ const results = await builtinMemory.search(query, topK);
1429
+ return { success: true, data: results };
1430
+ }
1431
+ catch (error) {
1432
+ const message = error instanceof Error ? error.message : String(error);
1433
+ return { success: false, error: `Builtin memory error: ${message}` };
1434
+ }
1435
+ },
1436
+ });
1437
+ registeredFallbackTools.push("search_memory");
1438
+ registerToolCompat({
1439
+ name: "store_event",
1440
+ description: "Store event (using builtin system - Cortex Memory disabled)",
1441
+ parameters: {
1442
+ type: "object",
1443
+ properties: {
1444
+ summary: { type: "string", description: "Event summary" },
1445
+ },
1446
+ required: ["summary"],
1447
+ additionalProperties: false,
1448
+ },
1449
+ execute: async (params) => {
1450
+ const args = (params.args || params);
1451
+ const summary = args.summary || "";
1452
+ try {
1453
+ const id = await builtinMemory.store(summary);
1454
+ return { success: true, data: { id } };
1455
+ }
1456
+ catch (error) {
1457
+ const message = error instanceof Error ? error.message : String(error);
1458
+ return { success: false, error: `Builtin memory error: ${message}` };
1459
+ }
1460
+ },
1461
+ });
1462
+ registeredFallbackTools.push("store_event");
1463
+ registerToolCompat({
1464
+ name: "cortex_memory_status",
1465
+ description: "Get the current status of the Cortex Memory plugin",
1466
+ parameters: {
1467
+ type: "object",
1468
+ properties: {},
1469
+ required: [],
1470
+ additionalProperties: false,
1471
+ },
1472
+ execute: async (_params) => {
1473
+ return {
1474
+ success: true,
1475
+ data: {
1476
+ enabled: isEnabled,
1477
+ fallback_enabled: config?.fallbackToBuiltin ?? true,
1478
+ builtin_memory_available: builtinMemory !== null,
1479
+ }
1480
+ };
1481
+ },
1482
+ });
1483
+ registeredFallbackTools.push("cortex_memory_status");
1484
+ logger.info(`Registered ${registeredFallbackTools.length} fallback tools`);
1485
+ }
1486
+ function unregisterFallbackTools() {
1487
+ if (!api || !api.unregisterTool)
1488
+ return;
1489
+ for (const name of registeredFallbackTools) {
1490
+ try {
1491
+ api.unregisterTool(name);
1492
+ }
1493
+ catch (e) {
1494
+ logger.warn(`Failed to unregister fallback tool ${name}: ${e instanceof Error ? e.message : String(e)}`);
1495
+ }
1496
+ }
1497
+ registeredFallbackTools = [];
1498
+ }
1612
1499
  function registerHooks() {
1613
1500
  if (!api)
1614
1501
  return;
@@ -1669,27 +1556,15 @@ function setupProcessHandlers() {
1669
1556
  isShuttingDown = true;
1670
1557
  logger.info(`Received ${signal}, shutting down...`);
1671
1558
  stopConfigWatcher();
1672
- if (!shouldUsePythonRuntime()) {
1673
- process.exit(0);
1674
- return;
1675
- }
1676
- shutdownPythonApi().then(() => {
1677
- killPythonProcess();
1678
- process.exit(0);
1679
- }).catch(() => {
1680
- killPythonProcess();
1681
- process.exit(0);
1682
- });
1559
+ process.exit(0);
1683
1560
  };
1684
1561
  process.on("exit", () => {
1685
- killPythonProcess();
1686
1562
  stopConfigWatcher();
1687
1563
  });
1688
1564
  process.on("SIGINT", () => gracefulShutdown("SIGINT"));
1689
1565
  process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
1690
1566
  process.on("uncaughtException", (err) => {
1691
1567
  logger.error("Uncaught exception:", err.message);
1692
- killPythonProcess();
1693
1568
  stopConfigWatcher();
1694
1569
  process.exit(1);
1695
1570
  });
@@ -1700,21 +1575,20 @@ async function enable() {
1700
1575
  return;
1701
1576
  }
1702
1577
  logger.info("Enabling Cortex Memory plugin...");
1578
+ logLifecycle("enable_start");
1703
1579
  try {
1704
1580
  unregisterFallbackTools();
1705
- if (shouldUsePythonRuntime()) {
1706
- await startPythonService();
1707
- await waitForService();
1708
- }
1709
1581
  isEnabled = true;
1710
1582
  registerTools();
1711
1583
  registerHooks();
1712
1584
  startAutoReflectScheduler();
1713
1585
  logger.info("Cortex Memory plugin enabled successfully");
1586
+ logLifecycle("enable_success", { registeredTools: registeredTools.length, registeredHooks: registeredHooks.length });
1714
1587
  }
1715
1588
  catch (error) {
1716
1589
  const message = error instanceof Error ? error.message : String(error);
1717
1590
  logger.error(`Failed to enable Cortex Memory plugin: ${message}`);
1591
+ logLifecycle("enable_failed", { error: message });
1718
1592
  throw error;
1719
1593
  }
1720
1594
  }
@@ -1724,113 +1598,48 @@ async function disable() {
1724
1598
  return;
1725
1599
  }
1726
1600
  logger.info("Disabling Cortex Memory plugin...");
1601
+ logLifecycle("disable_start");
1727
1602
  unregisterHooks();
1728
1603
  unregisterTools();
1729
- unregisterFallbackTools();
1730
1604
  stopAutoReflectScheduler();
1731
- if (shouldUsePythonRuntime()) {
1732
- await stopPythonServiceAsync();
1733
- }
1734
1605
  isEnabled = false;
1735
1606
  memoryEngine = null;
1736
1607
  if (config?.fallbackToBuiltin && builtinMemory) {
1737
1608
  logger.info("Falling back to OpenClaw builtin memory system");
1738
1609
  registerFallbackTools();
1610
+ logLifecycle("fallback_enabled", { fallbackTools: registeredFallbackTools.length });
1739
1611
  }
1740
1612
  logger.info("Cortex Memory plugin disabled successfully");
1741
- }
1742
- function registerFallbackTools() {
1743
- if (!api || !builtinMemory)
1744
- return;
1745
- registerToolCompat({
1746
- name: "search_memory",
1747
- description: "Search memory (using builtin system - Cortex Memory disabled)",
1748
- parameters: {
1749
- type: "object",
1750
- properties: {
1751
- query: { type: "string", description: "Search query" },
1752
- top_k: { type: "integer", description: "Number of results" },
1753
- },
1754
- required: ["query"],
1755
- additionalProperties: false,
1756
- },
1757
- execute: async ({ args, context }) => searchMemoryWithFallback((args || {}), context),
1758
- });
1759
- registeredFallbackTools.push("search_memory");
1760
- registerToolCompat({
1761
- name: "store_event",
1762
- description: "Store event (using builtin system - Cortex Memory disabled)",
1763
- parameters: {
1764
- type: "object",
1765
- properties: {
1766
- summary: { type: "string", description: "Event summary" },
1767
- },
1768
- required: ["summary"],
1769
- additionalProperties: false,
1770
- },
1771
- execute: async ({ args, context }) => storeEventWithFallback((args || {}), context),
1772
- });
1773
- registeredFallbackTools.push("store_event");
1774
- registerToolCompat({
1775
- name: "cortex_memory_status",
1776
- description: "Get the current status of the Cortex Memory plugin",
1777
- parameters: {
1778
- type: "object",
1779
- properties: {},
1780
- required: [],
1781
- additionalProperties: false,
1782
- },
1783
- execute: async ({ args, context }) => getPluginStatus(args || {}, context),
1784
- });
1785
- registeredFallbackTools.push("cortex_memory_status");
1786
- }
1787
- function unregisterFallbackTools() {
1788
- if (!api || !api.unregisterTool)
1789
- return;
1790
- for (const name of registeredFallbackTools) {
1791
- try {
1792
- api.unregisterTool(name);
1793
- }
1794
- catch (e) {
1795
- logger.warn(`Failed to unregister fallback tool ${name}: ${e instanceof Error ? e.message : String(e)}`);
1796
- }
1797
- }
1798
- registeredFallbackTools = [];
1613
+ logLifecycle("disable_success", { fallbackEnabled: registeredFallbackTools.length > 0 });
1799
1614
  }
1800
1615
  function getStatus() {
1801
1616
  return {
1802
- enabled: isEnabled,
1803
- serviceRunning: pythonProcess !== null
1617
+ enabled: isEnabled
1804
1618
  };
1805
1619
  }
1806
1620
  async function unregister() {
1807
1621
  logger.info("Unregistering Cortex Memory plugin...");
1622
+ logLifecycle("unregister_start");
1808
1623
  stopConfigWatcher();
1809
1624
  stopAutoReflectScheduler();
1810
1625
  unregisterHooks();
1811
1626
  unregisterTools();
1812
1627
  unregisterFallbackTools();
1813
- if (shouldUsePythonRuntime()) {
1814
- await stopPythonServiceAsync();
1815
- }
1816
- else {
1817
- killPythonProcess();
1818
- }
1819
1628
  isEnabled = false;
1820
1629
  isInitializing = false;
1821
1630
  isRegistered = false;
1822
1631
  api = null;
1823
1632
  config = null;
1824
1633
  autoSearchCacheBySession.clear();
1825
- builtinMemory = null;
1826
1634
  memoryEngine = null;
1635
+ builtinMemory = null;
1827
1636
  registeredTools = [];
1828
1637
  registeredHooks = [];
1829
1638
  registeredFallbackTools = [];
1830
1639
  registeredHookHandlers.clear();
1831
- stopAutoReflectScheduler();
1832
1640
  configPath = null;
1833
1641
  logger.info("Cortex Memory plugin unregistered successfully");
1642
+ logLifecycle("unregister_success");
1834
1643
  }
1835
1644
  function register(pluginApi, userConfig) {
1836
1645
  if (isInitializing || isRegistered) {
@@ -1838,7 +1647,7 @@ function register(pluginApi, userConfig) {
1838
1647
  }
1839
1648
  isInitializing = true;
1840
1649
  api = pluginApi;
1841
- logger = api.getLogger?.() || createConsoleLogger();
1650
+ logger = api.logger || api.getLogger?.() || createConsoleLogger();
1842
1651
  const apiPluginConfig = api.pluginConfig || {};
1843
1652
  const openclawConfig = api.config || {};
1844
1653
  const pluginEntry = openclawConfig?.plugins?.entries?.[PLUGIN_ID];
@@ -1866,10 +1675,72 @@ function register(pluginApi, userConfig) {
1866
1675
  dbPath: effectiveConfig.dbPath,
1867
1676
  autoSync: effectiveConfig.autoSync ?? defaultConfig.autoSync,
1868
1677
  autoReflect: effectiveConfig.autoReflect ?? defaultConfig.autoReflect,
1678
+ autoReflectIntervalMinutes: effectiveConfig.autoReflectIntervalMinutes ?? defaultConfig.autoReflectIntervalMinutes,
1679
+ graphQualityMode: effectiveConfig.graphQualityMode ?? defaultConfig.graphQualityMode,
1680
+ readFusion: {
1681
+ enabled: effectiveConfig.readFusion?.enabled ?? defaultConfig.readFusion?.enabled,
1682
+ maxCandidates: effectiveConfig.readFusion?.maxCandidates ?? defaultConfig.readFusion?.maxCandidates,
1683
+ authoritative: effectiveConfig.readFusion?.authoritative ?? defaultConfig.readFusion?.authoritative,
1684
+ channelWeights: effectiveConfig.readFusion?.channelWeights ?? defaultConfig.readFusion?.channelWeights,
1685
+ channelTopK: effectiveConfig.readFusion?.channelTopK ?? defaultConfig.readFusion?.channelTopK,
1686
+ minLexicalHits: effectiveConfig.readFusion?.minLexicalHits ?? defaultConfig.readFusion?.minLexicalHits,
1687
+ minSemanticHits: effectiveConfig.readFusion?.minSemanticHits ?? defaultConfig.readFusion?.minSemanticHits,
1688
+ lengthNorm: {
1689
+ enabled: effectiveConfig.readFusion?.lengthNorm?.enabled ?? defaultConfig.readFusion?.lengthNorm?.enabled,
1690
+ pivotChars: effectiveConfig.readFusion?.lengthNorm?.pivotChars ?? defaultConfig.readFusion?.lengthNorm?.pivotChars,
1691
+ strength: effectiveConfig.readFusion?.lengthNorm?.strength ?? defaultConfig.readFusion?.lengthNorm?.strength,
1692
+ minFactor: effectiveConfig.readFusion?.lengthNorm?.minFactor ?? defaultConfig.readFusion?.lengthNorm?.minFactor,
1693
+ },
1694
+ },
1695
+ vectorChunking: {
1696
+ chunkSize: effectiveConfig.vectorChunking?.chunkSize ?? defaultConfig.vectorChunking?.chunkSize,
1697
+ chunkOverlap: effectiveConfig.vectorChunking?.chunkOverlap ?? defaultConfig.vectorChunking?.chunkOverlap,
1698
+ evidenceMaxChunks: effectiveConfig.vectorChunking?.evidenceMaxChunks ?? defaultConfig.vectorChunking?.evidenceMaxChunks,
1699
+ },
1700
+ writePolicy: {
1701
+ archiveMinConfidence: effectiveConfig.writePolicy?.archiveMinConfidence ?? defaultConfig.writePolicy?.archiveMinConfidence,
1702
+ archiveMinQualityScore: effectiveConfig.writePolicy?.archiveMinQualityScore ?? defaultConfig.writePolicy?.archiveMinQualityScore,
1703
+ activeMinQualityScore: effectiveConfig.writePolicy?.activeMinQualityScore ?? defaultConfig.writePolicy?.activeMinQualityScore,
1704
+ activeDedupTailLines: effectiveConfig.writePolicy?.activeDedupTailLines ?? defaultConfig.writePolicy?.activeDedupTailLines,
1705
+ activeTextMaxChars: effectiveConfig.writePolicy?.activeTextMaxChars ?? defaultConfig.writePolicy?.activeTextMaxChars,
1706
+ archiveSourceTextMaxChars: effectiveConfig.writePolicy?.archiveSourceTextMaxChars ?? defaultConfig.writePolicy?.archiveSourceTextMaxChars,
1707
+ },
1708
+ memoryDecay: {
1709
+ enabled: effectiveConfig.memoryDecay?.enabled ?? defaultConfig.memoryDecay?.enabled,
1710
+ minFloor: effectiveConfig.memoryDecay?.minFloor ?? defaultConfig.memoryDecay?.minFloor,
1711
+ defaultHalfLifeDays: effectiveConfig.memoryDecay?.defaultHalfLifeDays ?? defaultConfig.memoryDecay?.defaultHalfLifeDays,
1712
+ halfLifeByEventType: effectiveConfig.memoryDecay?.halfLifeByEventType ?? defaultConfig.memoryDecay?.halfLifeByEventType,
1713
+ antiDecay: {
1714
+ enabled: effectiveConfig.memoryDecay?.antiDecay?.enabled ?? defaultConfig.memoryDecay?.antiDecay?.enabled,
1715
+ maxBoost: effectiveConfig.memoryDecay?.antiDecay?.maxBoost ?? defaultConfig.memoryDecay?.antiDecay?.maxBoost,
1716
+ hitWeight: effectiveConfig.memoryDecay?.antiDecay?.hitWeight ?? defaultConfig.memoryDecay?.antiDecay?.hitWeight,
1717
+ recentWindowDays: effectiveConfig.memoryDecay?.antiDecay?.recentWindowDays ?? defaultConfig.memoryDecay?.antiDecay?.recentWindowDays,
1718
+ },
1719
+ },
1720
+ readTuning: {
1721
+ scoring: {
1722
+ lexicalWeight: effectiveConfig.readTuning?.scoring?.lexicalWeight ?? defaultConfig.readTuning?.scoring?.lexicalWeight,
1723
+ bm25Scale: effectiveConfig.readTuning?.scoring?.bm25Scale ?? defaultConfig.readTuning?.scoring?.bm25Scale,
1724
+ semanticWeight: effectiveConfig.readTuning?.scoring?.semanticWeight ?? defaultConfig.readTuning?.scoring?.semanticWeight,
1725
+ recencyWeight: effectiveConfig.readTuning?.scoring?.recencyWeight ?? defaultConfig.readTuning?.scoring?.recencyWeight,
1726
+ qualityWeight: effectiveConfig.readTuning?.scoring?.qualityWeight ?? defaultConfig.readTuning?.scoring?.qualityWeight,
1727
+ typeMatchWeight: effectiveConfig.readTuning?.scoring?.typeMatchWeight ?? defaultConfig.readTuning?.scoring?.typeMatchWeight,
1728
+ graphMatchWeight: effectiveConfig.readTuning?.scoring?.graphMatchWeight ?? defaultConfig.readTuning?.scoring?.graphMatchWeight,
1729
+ },
1730
+ rrf: {
1731
+ k: effectiveConfig.readTuning?.rrf?.k ?? defaultConfig.readTuning?.rrf?.k,
1732
+ weight: effectiveConfig.readTuning?.rrf?.weight ?? defaultConfig.readTuning?.rrf?.weight,
1733
+ },
1734
+ recency: {
1735
+ buckets: effectiveConfig.readTuning?.recency?.buckets ?? defaultConfig.readTuning?.recency?.buckets,
1736
+ },
1737
+ autoContext: {
1738
+ queryMaxChars: effectiveConfig.readTuning?.autoContext?.queryMaxChars ?? defaultConfig.readTuning?.autoContext?.queryMaxChars,
1739
+ lightweightSearch: effectiveConfig.readTuning?.autoContext?.lightweightSearch ?? defaultConfig.readTuning?.autoContext?.lightweightSearch,
1740
+ },
1741
+ },
1869
1742
  enabled: effectiveConfig.enabled ?? defaultConfig.enabled,
1870
- fallbackToBuiltin: effectiveConfig.fallbackToBuiltin ?? defaultConfig.fallbackToBuiltin,
1871
- apiUrl: effectiveConfig.apiUrl ?? "http://127.0.0.1:8765",
1872
- engineMode: "ts",
1743
+ fallbackToBuiltin: effectiveConfig.fallbackToBuiltin ?? true,
1873
1744
  };
1874
1745
  memoryEngine = null;
1875
1746
  if (api.getBuiltinMemory) {
@@ -1888,8 +1759,12 @@ function register(pluginApi, userConfig) {
1888
1759
  reranker: { model: config.reranker.model },
1889
1760
  enabled: config.enabled,
1890
1761
  fallbackToBuiltin: config.fallbackToBuiltin,
1891
- engineMode: config.engineMode,
1892
1762
  });
1763
+ logger.info(`Runtime config snapshot: ${JSON.stringify(safeConfig)}`);
1764
+ const configErrors = validateConfig(config);
1765
+ if (configErrors.length > 0) {
1766
+ logger.warn(`Cortex Memory config validation warnings: ${configErrors.join(" | ")}`);
1767
+ }
1893
1768
  checkOpenClawVersion().catch(e => logger.warn(`Version check failed: ${e}`));
1894
1769
  configPath = findOpenClawConfig();
1895
1770
  if (configPath) {
@@ -1902,33 +1777,13 @@ function register(pluginApi, userConfig) {
1902
1777
  isInitializing = false;
1903
1778
  isRegistered = true;
1904
1779
  logger.info("Cortex Memory plugin registered successfully");
1905
- logger.info(`Cortex Memory engine mode: ${resolveEngine().mode}`);
1780
+ logLifecycle("register_success", {
1781
+ enabled: isEnabled,
1782
+ });
1906
1783
  if (isEnabled) {
1907
1784
  registerTools();
1908
1785
  registerHooks();
1909
1786
  startAutoReflectScheduler();
1910
- initializeAsync().catch(error => {
1911
- const message = error instanceof Error ? error.message : String(error);
1912
- logger.error(`Failed to initialize Cortex Memory: ${message}`);
1913
- if (config?.fallbackToBuiltin && builtinMemory) {
1914
- unregisterHooks();
1915
- unregisterTools();
1916
- logger.info("Falling back to builtin memory");
1917
- isEnabled = false;
1918
- registerFallbackTools();
1919
- }
1920
- });
1921
- }
1922
- else if (config?.fallbackToBuiltin && builtinMemory) {
1923
- registerFallbackTools();
1924
- }
1925
- }
1926
- async function initializeAsync() {
1927
- if (!shouldUsePythonRuntime()) {
1928
- return;
1929
1787
  }
1930
- await startPythonService();
1931
- await waitForService();
1932
- logger.info("Cortex Memory Python service started successfully");
1933
1788
  }
1934
1789
  //# sourceMappingURL=index.js.map