@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/src/compaction.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compaction Handler Module
|
|
3
|
-
* Handles session compaction evaluation and participation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @typedef {import("./types.js").EverMemOSConfig} EverMemOSConfig
|
|
8
|
-
* @typedef {import("./types.js").Logger} Logger
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Compaction evaluation parameters
|
|
13
|
-
* @typedef {Object} CompactionEvalParams
|
|
14
|
-
* @property {Array} messages
|
|
15
|
-
* @property {number} tokenCount
|
|
16
|
-
* @property {number} turnCount
|
|
17
|
-
* @property {number} lastAssembleTime
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Compaction decision result
|
|
22
|
-
* @typedef {Object} CompactionDecision
|
|
23
|
-
* @property {boolean} shouldCompact
|
|
24
|
-
* @property {string} reason
|
|
25
|
-
* @property {string} [memoryStrategy]
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Handles session compaction evaluation and participation
|
|
30
|
-
* Evaluates when to compact the conversation context
|
|
31
|
-
*/
|
|
32
|
-
export class CompactionHandler {
|
|
33
|
-
/**
|
|
34
|
-
* @param {EverMemOSConfig} cfg
|
|
35
|
-
* @param {Logger} logger
|
|
36
|
-
*/
|
|
37
|
-
constructor(cfg, logger) {
|
|
38
|
-
this.cfg = cfg;
|
|
39
|
-
this.log = logger;
|
|
40
|
-
|
|
41
|
-
// Configurable thresholds
|
|
42
|
-
this.compactThresholdTokens = 8000;
|
|
43
|
-
this.compactThresholdTurns = 10;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Evaluate whether session should be compacted
|
|
48
|
-
* @param {CompactionEvalParams} params
|
|
49
|
-
* @returns {Promise<CompactionDecision>}
|
|
50
|
-
*/
|
|
51
|
-
async evaluate({ messages, tokenCount, turnCount, lastAssembleTime }) {
|
|
52
|
-
// Compact if token count exceeds threshold
|
|
53
|
-
if (tokenCount > this.compactThresholdTokens) {
|
|
54
|
-
return {
|
|
55
|
-
shouldCompact: true,
|
|
56
|
-
reason: `token count (${tokenCount}) exceeds threshold (${this.compactThresholdTokens})`,
|
|
57
|
-
memoryStrategy: "consolidate_to_long_term",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Compact if turn count exceeds threshold
|
|
62
|
-
if (turnCount > this.compactThresholdTurns) {
|
|
63
|
-
return {
|
|
64
|
-
shouldCompact: true,
|
|
65
|
-
reason: `turn count (${turnCount}) exceeds threshold (${this.compactThresholdTurns})`,
|
|
66
|
-
memoryStrategy: "consolidate_to_long_term",
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Compact if no recent memory assembly (stale session)
|
|
71
|
-
const timeSinceAssemble = Date.now() - lastAssembleTime;
|
|
72
|
-
if (lastAssembleTime > 0 && timeSinceAssemble > 30 * 60 * 1000) { // 30 minutes
|
|
73
|
-
return {
|
|
74
|
-
shouldCompact: true,
|
|
75
|
-
reason: `session inactive for ${Math.round(timeSinceAssemble / 60000)} minutes`,
|
|
76
|
-
memoryStrategy: "consolidate_to_long_term",
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
shouldCompact: false,
|
|
82
|
-
reason: "token and turn counts within acceptable range",
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
package/src/context-engine.js
DELETED
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EverMemOS ContextEngine Implementation for OpenClaw 3.8+
|
|
3
|
-
* Implements full-lifecycle memory management: bootstrap, assemble, afterTurn, compact, subagent
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { resolveConfig } from "./config.js";
|
|
7
|
-
import { toText, isSessionResetPrompt, collectMessages } from "./message-utils.js";
|
|
8
|
-
import { ContextAssembler } from "./assembler.js";
|
|
9
|
-
import { LifecycleManager } from "./lifecycle.js";
|
|
10
|
-
import { CompactionHandler } from "./compaction.js";
|
|
11
|
-
import { SubagentTracker } from "./subagent.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {import("./types.js").EverMemOSConfig} EverMemOSConfig
|
|
15
|
-
* @typedef {import("./types.js").Logger} Logger
|
|
16
|
-
* @typedef {import("./types.js").BootstrapContext} BootstrapContext
|
|
17
|
-
* @typedef {import("./types.js").AssembleContext} AssembleContext
|
|
18
|
-
* @typedef {import("./types.js").AssembleResult} AssembleResult
|
|
19
|
-
* @typedef {import("./types.js").AfterTurnContext} AfterTurnContext
|
|
20
|
-
* @typedef {import("./types.js").CompactContext} CompactContext
|
|
21
|
-
* @typedef {import("./types.js").CompactResult} CompactResult
|
|
22
|
-
* @typedef {import("./types.js").PrepareSubagentContext} PrepareSubagentContext
|
|
23
|
-
* @typedef {import("./types.js").PrepareSubagentResult} PrepareSubagentResult
|
|
24
|
-
* @typedef {import("./types.js").SubagentEndedContext} SubagentEndedContext
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* EverMemOS ContextEngine implementation for OpenClaw 3.8+
|
|
29
|
-
* Implements full-lifecycle memory management: bootstrap, assemble, afterTurn, compact, subagent
|
|
30
|
-
*/
|
|
31
|
-
export class EverMemOSContextEngine {
|
|
32
|
-
/**
|
|
33
|
-
* @param {EverMemOSConfig} config
|
|
34
|
-
* @param {Logger} logger
|
|
35
|
-
*/
|
|
36
|
-
constructor(config, logger = console) {
|
|
37
|
-
this.cfg = resolveConfig(config);
|
|
38
|
-
this.log = logger;
|
|
39
|
-
|
|
40
|
-
// Session state tracking
|
|
41
|
-
this.turnCount = 0;
|
|
42
|
-
this.lastAssembleTime = 0;
|
|
43
|
-
this.pendingFlush = false;
|
|
44
|
-
|
|
45
|
-
// Submodules
|
|
46
|
-
this.assembler = new ContextAssembler(this.cfg, this.log);
|
|
47
|
-
this.lifecycle = new LifecycleManager(this.cfg, this.log);
|
|
48
|
-
this.compaction = new CompactionHandler(this.cfg, this.log);
|
|
49
|
-
this.subagentTracker = new SubagentTracker(this.cfg, this.log);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Bootstrap: Called once when plugin loads. Initialize connection and verify backend health.
|
|
54
|
-
* @param {BootstrapContext} ctx
|
|
55
|
-
* @returns {Promise<void>}
|
|
56
|
-
*/
|
|
57
|
-
async bootstrap(ctx) {
|
|
58
|
-
this.log("[evermemos] bootstrap: initializing EverMemOS connection");
|
|
59
|
-
try {
|
|
60
|
-
const result = await this._healthCheck();
|
|
61
|
-
if (result?.status === "ok") {
|
|
62
|
-
this.log("[evermemos] bootstrap: backend healthy");
|
|
63
|
-
} else {
|
|
64
|
-
this.log.warn("[evermemos] bootstrap: backend unhealthy, will degrade gracefully");
|
|
65
|
-
}
|
|
66
|
-
} catch (err) {
|
|
67
|
-
this.log.warn(`[evermemos] bootstrap: health check failed: ${err.message}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Assemble: Build context from memories before agent processes the prompt.
|
|
73
|
-
* This replaces the old before_agent_start hook with query-aware assembly.
|
|
74
|
-
* @param {AssembleContext} ctx
|
|
75
|
-
* @returns {Promise<AssembleResult>}
|
|
76
|
-
*/
|
|
77
|
-
async assemble(ctx) {
|
|
78
|
-
const { prompt, messages } = ctx;
|
|
79
|
-
const query = toText(prompt);
|
|
80
|
-
|
|
81
|
-
// Detect session reset (/new, /reset)
|
|
82
|
-
if (isSessionResetPrompt(query)) {
|
|
83
|
-
this.log("[evermemos] assemble: session reset detected, skipping search");
|
|
84
|
-
this.pendingFlush = true;
|
|
85
|
-
return { context: "", metadata: { skipped: "session_reset" } };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!query || query.length < 3) {
|
|
89
|
-
return { context: "", metadata: { skipped: "empty_query" } };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
this.lastAssembleTime = Date.now();
|
|
94
|
-
const result = await this.assembler.assemble(query, messages, this.turnCount);
|
|
95
|
-
this.log(`[evermemos] assemble: retrieved ${result.memoryCount} memories`);
|
|
96
|
-
return {
|
|
97
|
-
context: result.context,
|
|
98
|
-
metadata: {
|
|
99
|
-
memoryCount: result.memoryCount,
|
|
100
|
-
retrieveMethod: this.cfg.retrieveMethod,
|
|
101
|
-
turnCount: this.turnCount,
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
} catch (err) {
|
|
105
|
-
this.log.warn(`[evermemos] assemble: ${err.message}`);
|
|
106
|
-
return { context: "", metadata: { error: err.message } };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* AfterTurn: Called after each agent turn. Extract memories promptly.
|
|
112
|
-
* This enables timely extraction instead of waiting for agent_end.
|
|
113
|
-
* @param {AfterTurnContext} ctx
|
|
114
|
-
* @returns {Promise<void>}
|
|
115
|
-
*/
|
|
116
|
-
async afterTurn(ctx) {
|
|
117
|
-
const { messages, success } = ctx;
|
|
118
|
-
this.turnCount++;
|
|
119
|
-
|
|
120
|
-
if (!success || !messages?.length) {
|
|
121
|
-
this.log(`[evermemos] afterTurn: turn ${this.turnCount} skipped (no success/messages)`);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const collected = collectMessages(messages);
|
|
127
|
-
if (!collected.length) {
|
|
128
|
-
this.log(`[evermemos] afterTurn: turn ${this.turnCount} - no messages to save`);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
await this.lifecycle.ingestTurn({
|
|
133
|
-
userId: this.cfg.userId,
|
|
134
|
-
groupId: this.cfg.groupId,
|
|
135
|
-
messages: collected,
|
|
136
|
-
turnCount: this.turnCount,
|
|
137
|
-
flush: this.pendingFlush,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
this.log(`[evermemos] afterTurn: turn ${this.turnCount} - saved ${collected.length} messages`);
|
|
141
|
-
|
|
142
|
-
// Consume flush flag after successful save
|
|
143
|
-
if (this.pendingFlush) {
|
|
144
|
-
this.pendingFlush = false;
|
|
145
|
-
this.log("[evermemos] afterTurn: flush flag consumed");
|
|
146
|
-
}
|
|
147
|
-
} catch (err) {
|
|
148
|
-
this.log.warn(`[evermemos] afterTurn: ${err.message}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Compact: Participate in session compaction decision and memory consolidation.
|
|
154
|
-
* @param {CompactContext} ctx
|
|
155
|
-
* @returns {Promise<CompactResult>}
|
|
156
|
-
*/
|
|
157
|
-
async compact(ctx) {
|
|
158
|
-
const { messages, tokenCount } = ctx;
|
|
159
|
-
|
|
160
|
-
this.log(`[evermemos] compact: evaluating compaction (tokens: ${tokenCount}, turns: ${this.turnCount})`);
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const decision = await this.compaction.evaluate({
|
|
164
|
-
messages,
|
|
165
|
-
tokenCount,
|
|
166
|
-
turnCount: this.turnCount,
|
|
167
|
-
lastAssembleTime: this.lastAssembleTime,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
if (decision.shouldCompact) {
|
|
171
|
-
this.log(`[evermemos] compact: compaction recommended - ${decision.reason}`);
|
|
172
|
-
return {
|
|
173
|
-
shouldCompact: true,
|
|
174
|
-
reason: decision.reason,
|
|
175
|
-
metadata: {
|
|
176
|
-
turnCount: this.turnCount,
|
|
177
|
-
memoryStrategy: decision.memoryStrategy,
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
this.log("[evermemos] compact: no compaction needed");
|
|
183
|
-
return { shouldCompact: false, reason: decision.reason };
|
|
184
|
-
} catch (err) {
|
|
185
|
-
this.log.warn(`[evermemos] compact: ${err.message}`);
|
|
186
|
-
return { shouldCompact: false, reason: "error evaluating compaction" };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* PrepareSubagentSpawn: Called before spawning a subagent.
|
|
192
|
-
* @param {PrepareSubagentContext} ctx
|
|
193
|
-
* @returns {Promise<PrepareSubagentResult>}
|
|
194
|
-
*/
|
|
195
|
-
async prepareSubagentSpawn(ctx) {
|
|
196
|
-
const { subagentId, parentMessages } = ctx;
|
|
197
|
-
|
|
198
|
-
this.log(`[evermemos] prepareSubagentSpawn: tracking subagent ${subagentId}`);
|
|
199
|
-
|
|
200
|
-
// Register the subagent
|
|
201
|
-
this.subagentTracker.register(subagentId, {
|
|
202
|
-
subagentType: ctx.subagentType,
|
|
203
|
-
parentTurnCount: this.turnCount,
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
// Collect relevant memories for the subagent
|
|
208
|
-
const query = toText(ctx.prompt);
|
|
209
|
-
const context = query
|
|
210
|
-
? await this.assembler.assembleForSubagent(query)
|
|
211
|
-
: "";
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
prependContext: context || "",
|
|
215
|
-
metadata: {
|
|
216
|
-
subagentId,
|
|
217
|
-
parentTurnCount: this.turnCount,
|
|
218
|
-
},
|
|
219
|
-
};
|
|
220
|
-
} catch (err) {
|
|
221
|
-
this.log.warn(`[evermemos] prepareSubagentSpawn: ${err.message}`);
|
|
222
|
-
return { prependContext: "", metadata: {} };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* OnSubagentEnded: Called after a subagent completes.
|
|
228
|
-
* @param {SubagentEndedContext} ctx
|
|
229
|
-
* @returns {Promise<void>}
|
|
230
|
-
*/
|
|
231
|
-
async onSubagentEnded(ctx) {
|
|
232
|
-
const { subagentId, messages, success } = ctx;
|
|
233
|
-
|
|
234
|
-
this.log(`[evermemos] onSubagentEnded: subagent ${subagentId} ended, success=${success}`);
|
|
235
|
-
|
|
236
|
-
// Unregister the subagent
|
|
237
|
-
this.subagentTracker.unregister(subagentId);
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
if (success && messages?.length) {
|
|
241
|
-
const collected = collectMessages(messages);
|
|
242
|
-
if (collected.length) {
|
|
243
|
-
await this.lifecycle.ingestTurn({
|
|
244
|
-
userId: this.cfg.userId,
|
|
245
|
-
groupId: this.cfg.groupId,
|
|
246
|
-
messages: collected,
|
|
247
|
-
turnCount: this.turnCount,
|
|
248
|
-
flush: false,
|
|
249
|
-
});
|
|
250
|
-
this.log(`[evermemos] onSubagentEnded: saved ${collected.length} messages from subagent`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
} catch (err) {
|
|
254
|
-
this.log.warn(`[evermemos] onSubagentEnded: ${err.message}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Internal: Verify backend health
|
|
260
|
-
* @private
|
|
261
|
-
* @returns {Promise<{status: string}|null>}
|
|
262
|
-
*/
|
|
263
|
-
async _healthCheck() {
|
|
264
|
-
const { serverUrl } = this.cfg;
|
|
265
|
-
try {
|
|
266
|
-
const controller = new AbortController();
|
|
267
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
268
|
-
|
|
269
|
-
const response = await fetch(`${serverUrl}/api/v0/health`, {
|
|
270
|
-
method: "GET",
|
|
271
|
-
signal: controller.signal,
|
|
272
|
-
});
|
|
273
|
-
clearTimeout(timeout);
|
|
274
|
-
|
|
275
|
-
if (response.ok) {
|
|
276
|
-
return await response.json();
|
|
277
|
-
}
|
|
278
|
-
return { status: "error", httpStatus: response.status };
|
|
279
|
-
} catch {
|
|
280
|
-
return { status: "error" };
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
package/src/lifecycle.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lifecycle Manager Module
|
|
3
|
-
* Handles turn-level memory ingestion
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { saveMemories } from "./memory-api.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {import("./types.js").EverMemOSConfig} EverMemOSConfig
|
|
10
|
-
* @typedef {import("./types.js").Logger} Logger
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Turn ingestion parameters
|
|
15
|
-
* @typedef {Object} IngestTurnParams
|
|
16
|
-
* @property {string} userId
|
|
17
|
-
* @property {string} groupId
|
|
18
|
-
* @property {Array} messages
|
|
19
|
-
* @property {number} turnCount
|
|
20
|
-
* @property {boolean} flush
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Handles turn-level memory ingestion
|
|
25
|
-
* Extracts and saves memories after each conversation turn
|
|
26
|
-
*/
|
|
27
|
-
export class LifecycleManager {
|
|
28
|
-
/**
|
|
29
|
-
* @param {EverMemOSConfig} cfg
|
|
30
|
-
* @param {Logger} logger
|
|
31
|
-
*/
|
|
32
|
-
constructor(cfg, logger) {
|
|
33
|
-
this.cfg = cfg;
|
|
34
|
-
this.log = logger;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Ingest messages from a single turn
|
|
39
|
-
* @param {IngestTurnParams} params
|
|
40
|
-
* @returns {Promise<void>}
|
|
41
|
-
*/
|
|
42
|
-
async ingestTurn({ userId, groupId, messages, turnCount, flush }) {
|
|
43
|
-
await saveMemories(this.cfg, {
|
|
44
|
-
userId,
|
|
45
|
-
groupId,
|
|
46
|
-
messages,
|
|
47
|
-
flush,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Flush pending memories to storage
|
|
53
|
-
* Called when session ends or reset is detected
|
|
54
|
-
* @returns {Promise<void>}
|
|
55
|
-
*/
|
|
56
|
-
async flush() {
|
|
57
|
-
// Force save with flush=true
|
|
58
|
-
await saveMemories(this.cfg, {
|
|
59
|
-
userId: this.cfg.userId,
|
|
60
|
-
groupId: this.cfg.groupId,
|
|
61
|
-
messages: [],
|
|
62
|
-
flush: true,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|