@evermind-ai/openclaw-plugin 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -328
- package/README.zh.md +98 -328
- package/SKILL.md +333 -0
- package/bin/install.js +354 -0
- package/index.js +152 -61
- package/openclaw.plugin.json +8 -8
- package/package.json +7 -2
- package/src/assembler.js +6 -6
- package/src/config.js +2 -2
- package/src/formatter.js +1 -1
- package/src/http-client.js +2 -1
- package/src/memory-api.js +18 -22
- package/src/subagent.js +3 -3
- package/src/compaction.js +0 -85
- package/src/context-engine.js +0 -283
- package/src/lifecycle.js +0 -65
package/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Registers
|
|
2
|
+
* EverOS OpenClaw Plugin
|
|
3
|
+
* Registers the EverOS backend as the context engine for memory management
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createRequire } from "node:module";
|
|
6
7
|
import { resolveConfig } from "./src/config.js";
|
|
7
8
|
import { searchMemories, saveMemories } from "./src/memory-api.js";
|
|
8
9
|
import { buildMemoryPrompt, parseSearchResponse } from "./src/formatter.js";
|
|
9
10
|
import { collectMessages, toText, isSessionResetPrompt } from "./src/message-utils.js";
|
|
11
|
+
import { ContextAssembler } from "./src/assembler.js";
|
|
12
|
+
import { SubagentTracker } from "./src/subagent.js";
|
|
10
13
|
|
|
11
|
-
const
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const pluginMeta = require("./openclaw.plugin.json");
|
|
16
|
+
const PLUGIN_ID = pluginMeta.id;
|
|
17
|
+
const PLUGIN_VERSION = pluginMeta.version;
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
20
|
* Convert OpenClaw AgentMessage to EverMemOS message format
|
|
@@ -131,7 +137,7 @@ function safePreview(content, maxLength = 200) {
|
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
/**
|
|
134
|
-
* Create
|
|
140
|
+
* Create EverOS ContextEngine instance
|
|
135
141
|
* @param {Object} pluginConfig - Plugin configuration
|
|
136
142
|
* @param {Object} logger - Logger instance
|
|
137
143
|
* @returns {Object} - ContextEngine implementation
|
|
@@ -139,36 +145,49 @@ function safePreview(content, maxLength = 200) {
|
|
|
139
145
|
function createContextEngineInstance(pluginConfig, logger) {
|
|
140
146
|
const cfg = resolveConfig(pluginConfig);
|
|
141
147
|
const log = logger || { info: (...a) => console.log(...a), warn: (...a) => console.warn(...a) };
|
|
148
|
+
const assembler = new ContextAssembler(cfg, log);
|
|
149
|
+
const subagentTracker = new SubagentTracker(cfg, log);
|
|
142
150
|
|
|
143
|
-
log.info(`[
|
|
151
|
+
log.info(`[everos] ContextEngine config: baseUrl=${cfg.serverUrl}, userId=${cfg.userId}`);
|
|
144
152
|
|
|
145
153
|
// Session state - shared across all sessions for this engine instance
|
|
146
154
|
const sessionState = new Map();
|
|
155
|
+
const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
156
|
+
|
|
157
|
+
function pruneStaleSessionState() {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
for (const [key, state] of sessionState) {
|
|
160
|
+
if (now - (state.lastActiveTime || 0) > SESSION_TTL_MS) {
|
|
161
|
+
sessionState.delete(key);
|
|
162
|
+
log.info(`[everos] pruned stale session state: ${key}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
147
166
|
|
|
148
167
|
return {
|
|
149
168
|
info: {
|
|
150
169
|
id: PLUGIN_ID,
|
|
151
|
-
name:
|
|
152
|
-
version:
|
|
170
|
+
name: pluginMeta.name,
|
|
171
|
+
version: PLUGIN_VERSION,
|
|
153
172
|
ownsCompaction: false,
|
|
154
173
|
},
|
|
155
174
|
|
|
156
175
|
async bootstrap({ sessionId, sessionKey }) {
|
|
157
|
-
log.info(`[
|
|
176
|
+
log.info(`[everos] bootstrap: session=${sessionId}, key=${sessionKey}`);
|
|
158
177
|
|
|
159
|
-
// Verify
|
|
178
|
+
// Verify EverOS backend health
|
|
160
179
|
try {
|
|
161
180
|
const response = await fetch(`${cfg.serverUrl}/health`, {
|
|
162
181
|
signal: AbortSignal.timeout(5000),
|
|
163
182
|
});
|
|
164
183
|
if (response.ok) {
|
|
165
184
|
const result = await response.json();
|
|
166
|
-
log.info(`[
|
|
185
|
+
log.info(`[everos] bootstrap: backend healthy, status=${result.status}`);
|
|
167
186
|
} else {
|
|
168
|
-
log.warn(`[
|
|
187
|
+
log.warn(`[everos] bootstrap: backend unhealthy, status=${response.status}`);
|
|
169
188
|
}
|
|
170
189
|
} catch (err) {
|
|
171
|
-
log.warn(`[
|
|
190
|
+
log.warn(`[everos] bootstrap: health check failed: ${err.message}`);
|
|
172
191
|
}
|
|
173
192
|
|
|
174
193
|
// Initialize or get session state
|
|
@@ -176,83 +195,81 @@ function createContextEngineInstance(pluginConfig, logger) {
|
|
|
176
195
|
sessionState.set(sessionKey, {
|
|
177
196
|
turnCount: 0,
|
|
178
197
|
lastAssembleTime: 0,
|
|
198
|
+
lastActiveTime: Date.now(),
|
|
179
199
|
pendingFlush: false,
|
|
180
|
-
pendingMessages: [],
|
|
181
200
|
});
|
|
182
|
-
log.info(`[
|
|
201
|
+
log.info(`[everos] bootstrap: initialized state for ${sessionKey}`);
|
|
183
202
|
} else {
|
|
184
|
-
log.info(`[
|
|
203
|
+
log.info(`[everos] bootstrap: reusing existing state for ${sessionKey}, turn=${sessionState.get(sessionKey).turnCount}`);
|
|
185
204
|
}
|
|
186
205
|
|
|
187
206
|
return { bootstrapped: true };
|
|
188
207
|
},
|
|
189
208
|
|
|
190
209
|
async ingest({ sessionId, sessionKey, message }) {
|
|
191
|
-
log.info(`[
|
|
192
|
-
|
|
193
|
-
const state = sessionState.get(sessionKey);
|
|
194
|
-
if (!state) {
|
|
195
|
-
log.warn(`[evermemos] ingest: no state for session=${sessionKey}`);
|
|
196
|
-
return { ingested: false };
|
|
197
|
-
}
|
|
210
|
+
log.info(`[everos] ingest: session=${sessionKey}, role=${message?.role}, isHeartbeat=${message?.isHeartbeat}`);
|
|
198
211
|
|
|
199
212
|
// Don't ingest heartbeats
|
|
200
213
|
if (message.isHeartbeat) {
|
|
201
214
|
return { ingested: false };
|
|
202
215
|
}
|
|
203
216
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
if (!sessionState.has(sessionKey)) {
|
|
218
|
+
log.warn(`[everos] ingest: no state for session=${sessionKey}`);
|
|
219
|
+
return { ingested: false };
|
|
220
|
+
}
|
|
207
221
|
|
|
222
|
+
// Messages are not buffered here; actual saving happens in afterTurn()
|
|
223
|
+
// where the full message list is available from the OpenClaw runtime.
|
|
208
224
|
return { ingested: true };
|
|
209
225
|
},
|
|
210
226
|
|
|
211
227
|
async ingestBatch({ sessionId, sessionKey, messages, isHeartbeat }) {
|
|
212
|
-
log.info(`[
|
|
228
|
+
log.info(`[everos] ingestBatch: session=${sessionKey}, count=${messages?.length}, isHeartbeat=${isHeartbeat}`);
|
|
213
229
|
|
|
214
230
|
if (isHeartbeat) {
|
|
215
231
|
return { ingestedCount: 0 };
|
|
216
232
|
}
|
|
217
233
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
log.warn(`[evermemos] ingestBatch: no state for session=${sessionKey}`);
|
|
234
|
+
if (!sessionState.has(sessionKey)) {
|
|
235
|
+
log.warn(`[everos] ingestBatch: no state for session=${sessionKey}`);
|
|
221
236
|
return { ingestedCount: 0 };
|
|
222
237
|
}
|
|
223
238
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return { ingestedCount: messages.length };
|
|
239
|
+
// Messages are not buffered here; actual saving happens in afterTurn()
|
|
240
|
+
// where the full message list is available from the OpenClaw runtime.
|
|
241
|
+
return { ingestedCount: 0 };
|
|
229
242
|
},
|
|
230
243
|
|
|
231
244
|
async afterTurn({ sessionId, sessionKey, messages, prePromptMessageCount }) {
|
|
232
245
|
const state = sessionState.get(sessionKey);
|
|
233
246
|
if (!state) {
|
|
234
|
-
log.warn(`[
|
|
247
|
+
log.warn(`[everos] afterTurn: no state for session=${sessionKey}`);
|
|
235
248
|
return;
|
|
236
249
|
}
|
|
237
250
|
|
|
238
251
|
state.turnCount++;
|
|
252
|
+
state.lastActiveTime = Date.now();
|
|
239
253
|
|
|
240
|
-
//
|
|
254
|
+
// When OpenClaw provides prePromptMessageCount, save only the new tail.
|
|
255
|
+
// Otherwise, fall back to the last conversational turn to avoid re-saving the full session.
|
|
241
256
|
const newMessages = prePromptMessageCount !== undefined
|
|
242
257
|
? messages.slice(prePromptMessageCount)
|
|
243
|
-
: messages;
|
|
258
|
+
: collectMessages(messages);
|
|
244
259
|
|
|
245
|
-
log.info(`[
|
|
260
|
+
log.info(`[everos] afterTurn: session=${sessionKey}, turn=${state.turnCount}, totalMessages=${messages.length}, newMessages=${newMessages.length}`);
|
|
246
261
|
|
|
247
262
|
if (newMessages.length === 0) {
|
|
248
|
-
log.info(`[
|
|
263
|
+
log.info(`[everos] afterTurn: session=${sessionKey}, turn=${state.turnCount}, no new messages to save`);
|
|
249
264
|
return;
|
|
250
265
|
}
|
|
251
266
|
|
|
252
267
|
try {
|
|
253
|
-
const evermemosMessages =
|
|
268
|
+
const evermemosMessages = prePromptMessageCount !== undefined
|
|
269
|
+
? newMessages.map(convertMessage).filter((m) => m.content)
|
|
270
|
+
: newMessages.filter((m) => m.content);
|
|
254
271
|
if (evermemosMessages.length === 0) {
|
|
255
|
-
log.info(`[
|
|
272
|
+
log.info(`[everos] afterTurn: session=${sessionKey}, turn=${state.turnCount}, no valid messages to save`);
|
|
256
273
|
return;
|
|
257
274
|
}
|
|
258
275
|
await saveMemories(cfg, {
|
|
@@ -260,35 +277,39 @@ function createContextEngineInstance(pluginConfig, logger) {
|
|
|
260
277
|
groupId: cfg.groupId,
|
|
261
278
|
messages: evermemosMessages,
|
|
262
279
|
flush: state.pendingFlush || false,
|
|
263
|
-
});
|
|
264
|
-
log.info(`[
|
|
280
|
+
}, log);
|
|
281
|
+
log.info(`[everos] afterTurn: session=${sessionKey}, turn=${state.turnCount}, saved ${evermemosMessages.length} messages`);
|
|
265
282
|
|
|
266
283
|
if (state.pendingFlush) {
|
|
267
284
|
state.pendingFlush = false;
|
|
268
|
-
log.info(`[
|
|
285
|
+
log.info(`[everos] afterTurn: flush flag consumed`);
|
|
269
286
|
}
|
|
270
287
|
} catch (err) {
|
|
271
|
-
log.warn(`[
|
|
288
|
+
log.warn(`[everos] afterTurn: save failed: ${err.message}`);
|
|
272
289
|
}
|
|
273
290
|
},
|
|
274
291
|
|
|
275
292
|
async assemble({ sessionId, sessionKey, messages, tokenBudget }) {
|
|
293
|
+
// Periodically prune stale sessions to prevent memory leaks
|
|
294
|
+
pruneStaleSessionState();
|
|
295
|
+
|
|
276
296
|
// Initialize state if not exists (assemble can be called before bootstrap)
|
|
277
297
|
if (!sessionState.has(sessionKey)) {
|
|
278
298
|
sessionState.set(sessionKey, {
|
|
279
299
|
turnCount: 0,
|
|
280
300
|
lastAssembleTime: 0,
|
|
301
|
+
lastActiveTime: Date.now(),
|
|
281
302
|
pendingFlush: false,
|
|
282
|
-
pendingMessages: [],
|
|
283
303
|
});
|
|
284
|
-
log.info(`[
|
|
304
|
+
log.info(`[everos] assemble: initialized state for ${sessionKey}`);
|
|
285
305
|
}
|
|
286
306
|
|
|
287
307
|
const state = sessionState.get(sessionKey);
|
|
308
|
+
state.lastActiveTime = Date.now();
|
|
288
309
|
|
|
289
310
|
// Get the last user message as query
|
|
290
311
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
291
|
-
const query = lastUserMsg ? toText(lastUserMsg) : "";
|
|
312
|
+
const query = lastUserMsg ? toText(lastUserMsg.content) : "";
|
|
292
313
|
|
|
293
314
|
if (!query || query.length < 3) {
|
|
294
315
|
return { messages, estimatedTokens: 0 };
|
|
@@ -296,7 +317,7 @@ function createContextEngineInstance(pluginConfig, logger) {
|
|
|
296
317
|
|
|
297
318
|
// Detect session reset - flush memory but keep current messages
|
|
298
319
|
if (isSessionResetPrompt(query)) {
|
|
299
|
-
log.info(`[
|
|
320
|
+
log.info(`[everos] assemble: session reset detected, keeping current messages`);
|
|
300
321
|
state.pendingFlush = true;
|
|
301
322
|
// Return original messages without memory injection
|
|
302
323
|
// The reset intent is captured in the query itself
|
|
@@ -313,14 +334,14 @@ function createContextEngineInstance(pluginConfig, logger) {
|
|
|
313
334
|
const params = {
|
|
314
335
|
query,
|
|
315
336
|
user_id: cfg.userId,
|
|
316
|
-
|
|
337
|
+
group_id: cfg.groupId || undefined,
|
|
317
338
|
memory_types: cfg.memoryTypes,
|
|
318
339
|
retrieve_method: cfg.retrieveMethod,
|
|
319
|
-
top_k,
|
|
340
|
+
top_k: topK,
|
|
320
341
|
};
|
|
321
342
|
|
|
322
|
-
const result = await searchMemories(cfg, params);
|
|
323
|
-
const parsed = parseSearchResponse(result);
|
|
343
|
+
const result = await searchMemories(cfg, params, log);
|
|
344
|
+
const parsed = parseSearchResponse(result) || { episodic: [], traits: [], case: null, skill: null };
|
|
324
345
|
|
|
325
346
|
const memoryCount =
|
|
326
347
|
(parsed.episodic?.length || 0) +
|
|
@@ -340,26 +361,90 @@ function createContextEngineInstance(pluginConfig, logger) {
|
|
|
340
361
|
_memory: true,
|
|
341
362
|
};
|
|
342
363
|
|
|
343
|
-
log.info(`[
|
|
364
|
+
log.info(`[everos] assemble: session=${sessionKey}, retrieved ${memoryCount} memories`);
|
|
344
365
|
|
|
345
366
|
// Return memory message + existing messages
|
|
346
367
|
return {
|
|
347
368
|
messages: [memoryMessage, ...messages],
|
|
348
|
-
estimatedTokens: Math.floor(
|
|
369
|
+
estimatedTokens: Math.floor(memoryText.length / 4),
|
|
349
370
|
};
|
|
350
371
|
} catch (err) {
|
|
351
|
-
log.warn(`[
|
|
372
|
+
log.warn(`[everos] assemble: ${err.message}`);
|
|
352
373
|
return { messages, estimatedTokens: 0 };
|
|
353
374
|
}
|
|
354
375
|
},
|
|
355
376
|
|
|
377
|
+
async prepareSubagentSpawn({ sessionKey, subagentId, prompt, subagentType }) {
|
|
378
|
+
if (!sessionState.has(sessionKey)) {
|
|
379
|
+
sessionState.set(sessionKey, {
|
|
380
|
+
turnCount: 0,
|
|
381
|
+
lastAssembleTime: 0,
|
|
382
|
+
lastActiveTime: Date.now(),
|
|
383
|
+
pendingFlush: false,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const state = sessionState.get(sessionKey);
|
|
388
|
+
subagentTracker.register(subagentId, {
|
|
389
|
+
subagentType,
|
|
390
|
+
parentTurnCount: state.turnCount,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const query = toText(prompt);
|
|
395
|
+
const prependContext = query ? await assembler.assembleForSubagent(query) : "";
|
|
396
|
+
return {
|
|
397
|
+
prependContext,
|
|
398
|
+
metadata: {
|
|
399
|
+
subagentId,
|
|
400
|
+
parentTurnCount: state.turnCount,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
} catch (err) {
|
|
404
|
+
log.warn(`[everos] prepareSubagentSpawn: ${err.message}`);
|
|
405
|
+
return {
|
|
406
|
+
prependContext: "",
|
|
407
|
+
metadata: {
|
|
408
|
+
subagentId,
|
|
409
|
+
parentTurnCount: state.turnCount,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
async onSubagentEnded({ subagentId, messages, success }) {
|
|
416
|
+
subagentTracker.unregister(subagentId);
|
|
417
|
+
|
|
418
|
+
if (!success || !messages?.length) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const collected = collectMessages(messages);
|
|
424
|
+
if (!collected.length) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await saveMemories(cfg, {
|
|
429
|
+
userId: cfg.userId,
|
|
430
|
+
groupId: cfg.groupId,
|
|
431
|
+
messages: collected,
|
|
432
|
+
flush: false,
|
|
433
|
+
}, log);
|
|
434
|
+
|
|
435
|
+
log.info(`[everos] onSubagentEnded: saved ${collected.length} messages from subagent ${subagentId}`);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
log.warn(`[everos] onSubagentEnded: ${err.message}`);
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
|
|
356
441
|
async compact({ sessionId, sessionKey, tokenBudget, currentTokenCount }) {
|
|
357
442
|
const state = sessionState.get(sessionKey);
|
|
358
443
|
if (!state) {
|
|
359
444
|
return { ok: true, compacted: false, reason: "no session state" };
|
|
360
445
|
}
|
|
361
446
|
|
|
362
|
-
log.info(`[
|
|
447
|
+
log.info(`[everos] compact: session=${sessionKey}, tokens=${currentTokenCount}, budget=${tokenBudget}`);
|
|
363
448
|
|
|
364
449
|
// Simple compaction strategy: if over 80% of budget, recommend compaction
|
|
365
450
|
const threshold = tokenBudget ? tokenBudget * 0.8 : 8000;
|
|
@@ -374,9 +459,15 @@ function createContextEngineInstance(pluginConfig, logger) {
|
|
|
374
459
|
return { ok: true, compacted: false, reason: "within threshold" };
|
|
375
460
|
},
|
|
376
461
|
|
|
377
|
-
async dispose() {
|
|
378
|
-
|
|
379
|
-
|
|
462
|
+
async dispose({ sessionKey } = {}) {
|
|
463
|
+
if (sessionKey && sessionState.has(sessionKey)) {
|
|
464
|
+
sessionState.delete(sessionKey);
|
|
465
|
+
log.info(`[everos] dispose: cleared state for ${sessionKey}`);
|
|
466
|
+
} else if (!sessionKey) {
|
|
467
|
+
// No sessionKey = global shutdown; clear all sessions to avoid leaks
|
|
468
|
+
sessionState.clear();
|
|
469
|
+
log.info("[everos] dispose: cleared all session states");
|
|
470
|
+
}
|
|
380
471
|
},
|
|
381
472
|
};
|
|
382
473
|
}
|
|
@@ -388,7 +479,7 @@ function createContextEngineInstance(pluginConfig, logger) {
|
|
|
388
479
|
export default function register(api) {
|
|
389
480
|
const log = api.logger || { info: (...a) => console.log(...a), warn: (...a) => console.warn(...a) };
|
|
390
481
|
|
|
391
|
-
log.info(`[
|
|
482
|
+
log.info(`[everos] Registering EverOS OpenClaw Plugin`);
|
|
392
483
|
|
|
393
484
|
// Register the ContextEngine factory
|
|
394
485
|
api.registerContextEngine(PLUGIN_ID, (pluginConfig) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "@evermind-ai/openclaw-plugin",
|
|
3
|
-
"name": "
|
|
4
|
-
"description": "Full-lifecycle memory management
|
|
5
|
-
"version": "1.
|
|
3
|
+
"name": "EverOS OpenClaw Plugin",
|
|
4
|
+
"description": "Full-lifecycle memory management for OpenClaw through the EverOS backend, powered by EverMemOS",
|
|
5
|
+
"version": "1.3.0",
|
|
6
6
|
"kind": "context-engine",
|
|
7
7
|
"contextEngine": true,
|
|
8
8
|
"main": "./index.js",
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"properties": {
|
|
13
13
|
"baseUrl": {
|
|
14
14
|
"type": "string",
|
|
15
|
-
"description": "
|
|
15
|
+
"description": "EverOS backend base URL",
|
|
16
16
|
"default": "http://localhost:1995"
|
|
17
17
|
},
|
|
18
18
|
"userId": {
|
|
19
19
|
"type": "string",
|
|
20
20
|
"description": "Identity used for memory ownership and as message sender",
|
|
21
|
-
"default": "
|
|
21
|
+
"default": "everos-user"
|
|
22
22
|
},
|
|
23
23
|
"groupId": {
|
|
24
24
|
"type": "string",
|
|
25
25
|
"description": "Group id for shared memory",
|
|
26
|
-
"default": "
|
|
26
|
+
"default": "everos-group"
|
|
27
27
|
},
|
|
28
28
|
"topK": {
|
|
29
29
|
"type": "integer",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"memoryTypes": {
|
|
34
34
|
"type": "array",
|
|
35
|
-
"description": "
|
|
35
|
+
"description": "EverOS memory types to search",
|
|
36
36
|
"items": {
|
|
37
37
|
"type": "string",
|
|
38
38
|
"enum": ["episodic_memory", "profile", "agent_skill", "agent_case"]
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"retrieveMethod": {
|
|
43
43
|
"type": "string",
|
|
44
|
-
"description": "Retrieval strategy used by
|
|
44
|
+
"description": "Retrieval strategy used by the EverOS backend",
|
|
45
45
|
"enum": ["keyword", "vector", "hybrid", "rrf", "agentic"],
|
|
46
46
|
"default": "hybrid"
|
|
47
47
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evermind-ai/openclaw-plugin",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "EverOS OpenClaw Plugin for OpenClaw 3.8+",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"everos-install": "./bin/install.js"
|
|
9
|
+
},
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./index.js"
|
|
9
12
|
},
|
|
@@ -12,6 +15,8 @@
|
|
|
12
15
|
"openclaw.plugin.json",
|
|
13
16
|
"README.md",
|
|
14
17
|
"README.zh.md",
|
|
18
|
+
"SKILL.md",
|
|
19
|
+
"bin/",
|
|
15
20
|
"src/"
|
|
16
21
|
],
|
|
17
22
|
"keywords": [
|
package/src/assembler.js
CHANGED
|
@@ -42,16 +42,16 @@ export class ContextAssembler {
|
|
|
42
42
|
const params = {
|
|
43
43
|
query,
|
|
44
44
|
user_id: this.cfg.userId,
|
|
45
|
-
|
|
45
|
+
group_id: this.cfg.groupId || undefined,
|
|
46
46
|
memory_types: this.cfg.memoryTypes,
|
|
47
47
|
retrieve_method: this.cfg.retrieveMethod,
|
|
48
48
|
top_k: topK,
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
/** @type {any} */
|
|
52
|
-
const result = await searchMemories(this.cfg, params);
|
|
52
|
+
const result = await searchMemories(this.cfg, params, this.log);
|
|
53
53
|
/** @type {ParsedMemoryResponse} */
|
|
54
|
-
const parsed = parseSearchResponse(result);
|
|
54
|
+
const parsed = parseSearchResponse(result) || { episodic: [], traits: [], case: null, skill: null };
|
|
55
55
|
|
|
56
56
|
// Count total memories
|
|
57
57
|
const memoryCount =
|
|
@@ -79,16 +79,16 @@ export class ContextAssembler {
|
|
|
79
79
|
const params = {
|
|
80
80
|
query,
|
|
81
81
|
user_id: this.cfg.userId,
|
|
82
|
-
|
|
82
|
+
group_id: this.cfg.groupId || undefined,
|
|
83
83
|
memory_types: this.cfg.memoryTypes,
|
|
84
84
|
retrieve_method: this.cfg.retrieveMethod,
|
|
85
85
|
top_k: topK,
|
|
86
86
|
};
|
|
87
87
|
|
|
88
88
|
/** @type {any} */
|
|
89
|
-
const result = await searchMemories(this.cfg, params);
|
|
89
|
+
const result = await searchMemories(this.cfg, params, this.log);
|
|
90
90
|
/** @type {ParsedMemoryResponse} */
|
|
91
|
-
const parsed = parseSearchResponse(result);
|
|
91
|
+
const parsed = parseSearchResponse(result) || { episodic: [], traits: [], case: null, skill: null };
|
|
92
92
|
|
|
93
93
|
// Use no code block for subagents (cleaner format)
|
|
94
94
|
return buildMemoryPrompt(parsed, { wrapInCodeBlock: false });
|
package/src/config.js
CHANGED
|
@@ -5,8 +5,8 @@ export const TIMEOUT_MS = 60000;
|
|
|
5
5
|
export function resolveConfig(pc = {}) {
|
|
6
6
|
return {
|
|
7
7
|
serverUrl: (pc.baseUrl || DEFAULT_URL).replace(/\/*$/, ""),
|
|
8
|
-
userId: pc.userId || "
|
|
9
|
-
groupId: pc.groupId || "
|
|
8
|
+
userId: pc.userId || "everos-user",
|
|
9
|
+
groupId: pc.groupId || "everos-group",
|
|
10
10
|
topK: pc.topK ?? 5,
|
|
11
11
|
memoryTypes: pc.memoryTypes ?? ["episodic_memory", "profile", "agent_skill", "agent_case"],
|
|
12
12
|
retrieveMethod: pc.retrieveMethod ?? "hybrid",
|
package/src/formatter.js
CHANGED
package/src/http-client.js
CHANGED
|
@@ -39,7 +39,8 @@ export async function request(cfg, method, path, params) {
|
|
|
39
39
|
|
|
40
40
|
try {
|
|
41
41
|
return await send(url, method, headers, body, ms);
|
|
42
|
-
} catch {
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.warn(`[everos] first request failed, retrying: ${err.message}`);
|
|
43
44
|
await sleep(150);
|
|
44
45
|
return send(url, method, headers, body, ms);
|
|
45
46
|
}
|
package/src/memory-api.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { request } from "./http-client.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const noop = { info() {}, warn() {} };
|
|
4
|
+
|
|
5
|
+
export async function searchMemories(cfg, params, log = noop) {
|
|
4
6
|
const { memory_types, ...baseParams } = params;
|
|
5
7
|
|
|
6
8
|
const episodicTypes = (memory_types ?? []).filter((t) => t === "episodic_memory" || t === "profile");
|
|
@@ -13,9 +15,9 @@ export async function searchMemories(cfg, params) {
|
|
|
13
15
|
const results = await Promise.all(
|
|
14
16
|
searches.map(async ({ label, types }) => {
|
|
15
17
|
const p = { ...baseParams, memory_types: types };
|
|
16
|
-
|
|
18
|
+
log.info(`[everos] GET /api/v1/memories/search ${label}`, JSON.stringify(p));
|
|
17
19
|
const r = await request(cfg, "GET", "/api/v1/memories/search", p);
|
|
18
|
-
|
|
20
|
+
log.info(`[everos] GET response ${label}`, JSON.stringify(r));
|
|
19
21
|
return r;
|
|
20
22
|
}),
|
|
21
23
|
);
|
|
@@ -35,15 +37,16 @@ export async function searchMemories(cfg, params) {
|
|
|
35
37
|
return merged;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
export async function saveMemories(cfg, { userId, groupId, messages = [], flush = false }) {
|
|
40
|
+
export async function saveMemories(cfg, { userId, groupId, messages = [], flush = false }, log = noop) {
|
|
39
41
|
if (!messages.length) return;
|
|
40
42
|
const stamp = Date.now();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
|
|
44
|
+
const payloads = messages.map((msg, i) => {
|
|
45
|
+
const { role = "user", content = "", tool_calls, tool_call_id } = msg;
|
|
43
46
|
const sender = role === "assistant" ? role : (role === "tool" ? "tool" : userId);
|
|
44
47
|
const isLast = i === messages.length - 1;
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
return {
|
|
47
50
|
message_id: `em_${stamp}_${i}`,
|
|
48
51
|
create_time: new Date().toISOString(),
|
|
49
52
|
role,
|
|
@@ -58,20 +61,13 @@ export async function saveMemories(cfg, { userId, groupId, messages = [], flush
|
|
|
58
61
|
...(tool_call_id && { tool_call_id }),
|
|
59
62
|
...(flush && isLast && { flush: true }),
|
|
60
63
|
};
|
|
61
|
-
|
|
62
|
-
const result = await request(cfg, "POST", "/api/v1/memories", payload);
|
|
63
|
-
console.log("[memory-api] POST response", JSON.stringify(result));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
64
|
+
});
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const result = await request(cfg, "DELETE", "/api/v1/memories", payload);
|
|
75
|
-
console.log("[memory-api] DELETE response", JSON.stringify(result));
|
|
76
|
-
return result;
|
|
66
|
+
await Promise.all(
|
|
67
|
+
payloads.map(async (payload) => {
|
|
68
|
+
log.info(`[everos] POST /api/v1/memories`, JSON.stringify(payload));
|
|
69
|
+
const result = await request(cfg, "POST", "/api/v1/memories", payload);
|
|
70
|
+
log.info(`[everos] POST response`, JSON.stringify(result));
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
77
73
|
}
|
package/src/subagent.js
CHANGED
|
@@ -38,7 +38,7 @@ export class SubagentTracker {
|
|
|
38
38
|
startTime: Date.now(),
|
|
39
39
|
...metadata,
|
|
40
40
|
});
|
|
41
|
-
this.log(`[
|
|
41
|
+
this.log.info(`[everos] subagent tracker: registered ${subagentId}`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -49,7 +49,7 @@ export class SubagentTracker {
|
|
|
49
49
|
unregister(subagentId) {
|
|
50
50
|
const removed = this.activeSubagents.delete(subagentId);
|
|
51
51
|
if (removed) {
|
|
52
|
-
this.log(`[
|
|
52
|
+
this.log.info(`[everos] subagent tracker: unregistered ${subagentId}`);
|
|
53
53
|
}
|
|
54
54
|
return removed;
|
|
55
55
|
}
|
|
@@ -108,7 +108,7 @@ export class SubagentTracker {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
if (stale.length > 0) {
|
|
111
|
-
this.log(`[
|
|
111
|
+
this.log.info(`[everos] subagent tracker: cleaned up ${stale.length} stale subagents`);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
return stale;
|