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