openclaw-cortex-memory 0.1.0-Alpha.9 → 0.1.1

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