openclaw-cortex-memory 0.1.0-Alpha.8 → 0.1.0

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