@t0ken.ai/memoryx-openclaw-plugin 1.1.14 → 1.1.16

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/dist/index.d.ts CHANGED
@@ -23,12 +23,6 @@
23
23
  interface PluginConfig {
24
24
  apiBaseUrl?: string;
25
25
  }
26
- interface Message {
27
- role: string;
28
- content: string;
29
- tokens: number;
30
- timestamp: number;
31
- }
32
26
  interface RecallResult {
33
27
  memories: Array<{
34
28
  id: string;
@@ -36,44 +30,41 @@ interface RecallResult {
36
30
  category: string;
37
31
  score: number;
38
32
  }>;
33
+ relatedMemories: Array<{
34
+ id: string;
35
+ content: string;
36
+ category: string;
37
+ score: number;
38
+ }>;
39
39
  isLimited: boolean;
40
40
  remainingQuota: number;
41
41
  upgradeHint?: string;
42
42
  }
43
- declare class ConversationBuffer {
44
- private messages;
45
- private roundCount;
46
- private lastRole;
47
- private conversationId;
48
- private startedAt;
43
+ declare class ConversationManager {
44
+ private currentConversationId;
45
+ private conversationCreatedAt;
49
46
  private lastActivityAt;
50
47
  private readonly ROUND_THRESHOLD;
51
48
  private readonly TIMEOUT_MS;
52
49
  constructor();
53
50
  private generateId;
54
51
  getConversationId(): string;
55
- addMessage(role: string, content: string): boolean;
56
- shouldFlush(): boolean;
57
- flush(): {
58
- conversation_id: string;
59
- messages: Message[];
60
- };
61
- forceFlush(): {
62
- conversation_id: string;
63
- messages: Message[];
64
- } | null;
65
- getStatus(): {
52
+ addMessage(role: string, content: string): Promise<boolean>;
53
+ shouldFlush(): Promise<boolean>;
54
+ startNewConversation(): void;
55
+ getStatus(): Promise<{
66
56
  messageCount: number;
67
57
  conversationId: string;
68
- };
58
+ rounds: number;
59
+ }>;
69
60
  }
70
61
  declare class MemoryXPlugin {
71
62
  private config;
72
- private buffer;
63
+ private conversationManager;
73
64
  private flushTimer;
74
- private pendingRetryTimer;
65
+ private sendQueueTimer;
75
66
  private readonly FLUSH_CHECK_INTERVAL;
76
- private readonly PENDING_RETRY_INTERVAL;
67
+ private readonly SEND_QUEUE_INTERVAL;
77
68
  private readonly MAX_RETRY_COUNT;
78
69
  private pluginConfig;
79
70
  private initialized;
@@ -85,20 +76,22 @@ declare class MemoryXPlugin {
85
76
  private autoRegister;
86
77
  private getMachineFingerprint;
87
78
  private startFlushTimer;
88
- private startPendingRetryTimer;
89
- private retryPendingMessages;
90
- private flushConversation;
79
+ private startSendQueueTimer;
80
+ private processSendQueue;
91
81
  onMessage(role: string, content: string): Promise<boolean>;
92
82
  recall(query: string, limit?: number): Promise<RecallResult>;
93
83
  endConversation(): Promise<void>;
94
- getStatus(): {
84
+ forget(memoryId: string): Promise<boolean>;
85
+ list(limit?: number): Promise<any[]>;
86
+ getStatus(): Promise<{
95
87
  initialized: boolean;
96
88
  hasApiKey: boolean;
97
- bufferStatus: {
89
+ conversationStatus: {
98
90
  messageCount: number;
99
91
  conversationId: string;
92
+ rounds: number;
100
93
  };
101
- };
94
+ }>;
102
95
  }
103
96
  declare const _default: {
104
97
  id: string;
@@ -108,5 +101,5 @@ declare const _default: {
108
101
  register(api: any, pluginConfig?: PluginConfig): void;
109
102
  };
110
103
  export default _default;
111
- export { MemoryXPlugin, ConversationBuffer };
104
+ export { MemoryXPlugin, ConversationManager };
112
105
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAoCH,UAAU,YAAY;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAUD,UAAU,OAAO;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,YAAY;IAClB,QAAQ,EAAE,KAAK,CAAC;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IACH,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAyJD,cAAM,kBAAkB;IACpB,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,cAAc,CAAsB;IAE5C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkB;;IAM7C,OAAO,CAAC,UAAU;IAIlB,iBAAiB,IAAI,MAAM;IAI3B,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;IAuBlD,WAAW,IAAI,OAAO;IAiBtB,KAAK,IAAI;QAAE,eAAe,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE;IAgBzD,UAAU,IAAI;QAAE,eAAe,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,GAAG,IAAI;IAOrE,SAAS,IAAI;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE;CAMhE;AAED,cAAM,aAAa;IACf,OAAO,CAAC,MAAM,CAMZ;IAEF,OAAO,CAAC,MAAM,CAAgD;IAC9D,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAK;IACrC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,WAAW,CAAkB;gBAEzB,YAAY,CAAC,EAAE,YAAY;IAQvC,IAAI,IAAI,IAAI;IAkBZ,OAAO,KAAK,OAAO,GAElB;YAEa,UAAU;YAWV,UAAU;YAIV,YAAY;IA+B1B,OAAO,CAAC,qBAAqB;IAa7B,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,sBAAsB;YAMhB,oBAAoB;YAmDpB,iBAAiB;IAsDlB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA6B1D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,OAAO,CAAC,YAAY,CAAC;IAsD/D,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtC,SAAS,IAAI;QAChB,WAAW,EAAE,OAAO,CAAC;QACrB,SAAS,EAAE,OAAO,CAAC;QACnB,YAAY,EAAE;YAAE,YAAY,EAAE,MAAM,CAAC;YAAC,cAAc,EAAE,MAAM,CAAA;SAAE,CAAA;KACjE;CAOJ;;;;;;kBAUiB,GAAG,iBAAiB,YAAY,GAAG,IAAI;;AANzD,wBAmEE;AAEF,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -24,10 +24,26 @@ import * as fs from "fs";
24
24
  import * as path from "path";
25
25
  import * as os from "os";
26
26
  import * as crypto from "crypto";
27
+ import { getEncoding } from "js-tiktoken";
27
28
  const DEFAULT_API_BASE = "https://t0ken.ai/api";
28
29
  const PLUGIN_DIR = path.join(os.homedir(), ".openclaw", "extensions", "memoryx-openclaw-plugin");
29
30
  let logStream = null;
30
31
  let logStreamReady = false;
32
+ let tokenizer = null;
33
+ function getTokenizer() {
34
+ if (!tokenizer) {
35
+ tokenizer = getEncoding("cl100k_base");
36
+ }
37
+ return tokenizer;
38
+ }
39
+ function countTokens(text) {
40
+ try {
41
+ return getTokenizer().encode(text).length;
42
+ }
43
+ catch {
44
+ return Math.ceil(text.length / 4);
45
+ }
46
+ }
31
47
  function ensureDir() {
32
48
  if (!fs.existsSync(PLUGIN_DIR)) {
33
49
  fs.mkdirSync(PLUGIN_DIR, { recursive: true });
@@ -50,6 +66,7 @@ function log(message) {
50
66
  });
51
67
  }
52
68
  let dbPromise = null;
69
+ let isSending = false;
53
70
  async function getDb() {
54
71
  if (dbPromise)
55
72
  return dbPromise;
@@ -64,29 +81,18 @@ async function getDb() {
64
81
  value TEXT
65
82
  );
66
83
 
67
- CREATE TABLE IF NOT EXISTS pending_messages (
68
- id INTEGER PRIMARY KEY AUTOINCREMENT,
69
- conversation_id TEXT NOT NULL,
70
- role TEXT NOT NULL,
71
- content TEXT NOT NULL,
72
- timestamp INTEGER NOT NULL,
73
- retry_count INTEGER DEFAULT 0,
74
- created_at INTEGER DEFAULT (strftime('%s', 'now'))
75
- );
76
-
77
- CREATE INDEX IF NOT EXISTS idx_pending_conversation ON pending_messages(conversation_id);
78
- CREATE INDEX IF NOT EXISTS idx_pending_created ON pending_messages(created_at);
79
-
80
- CREATE TABLE IF NOT EXISTS temp_messages (
84
+ CREATE TABLE IF NOT EXISTS send_queue (
81
85
  id INTEGER PRIMARY KEY AUTOINCREMENT,
82
86
  conversation_id TEXT NOT NULL,
87
+ conversation_created_at INTEGER NOT NULL,
83
88
  role TEXT NOT NULL,
84
89
  content TEXT NOT NULL,
85
90
  timestamp INTEGER NOT NULL,
86
- created_at INTEGER DEFAULT (strftime('%s', 'now'))
91
+ retry_count INTEGER DEFAULT 0
87
92
  );
88
93
 
89
- CREATE INDEX IF NOT EXISTS idx_temp_conversation ON temp_messages(conversation_id);
94
+ CREATE INDEX IF NOT EXISTS idx_send_queue_created ON send_queue(conversation_created_at);
95
+ CREATE INDEX IF NOT EXISTS idx_send_queue_conversation ON send_queue(conversation_id);
90
96
  `);
91
97
  return db;
92
98
  })();
@@ -115,108 +121,109 @@ class SQLiteStorage {
115
121
  log(`Failed to save config: ${e}`);
116
122
  }
117
123
  }
118
- static async addPendingMessage(conversationId, role, content) {
119
- const db = await getDb();
120
- const result = db.prepare(`
121
- INSERT INTO pending_messages (conversation_id, role, content, timestamp)
122
- VALUES (?, ?, ?, ?)
123
- `).run(conversationId, role, content, Date.now());
124
- return result.lastInsertRowid;
125
- }
126
- static async getPendingMessages(limit = 100) {
124
+ static async addConversationToQueue(conversationId, conversationCreatedAt, messages) {
127
125
  const db = await getDb();
128
- return db.prepare(`
129
- SELECT * FROM pending_messages
130
- ORDER BY created_at ASC
131
- LIMIT ?
132
- `).all(limit);
133
- }
134
- static async deletePendingMessage(id) {
135
- const db = await getDb();
136
- db.prepare("DELETE FROM pending_messages WHERE id = ?").run(id);
126
+ const stmt = db.prepare(`
127
+ INSERT INTO send_queue (conversation_id, conversation_created_at, role, content, timestamp)
128
+ VALUES (?, ?, ?, ?, ?)
129
+ `);
130
+ for (const msg of messages) {
131
+ stmt.run(conversationId, conversationCreatedAt, msg.role, msg.content, msg.timestamp);
132
+ }
137
133
  }
138
- static async incrementRetryCount(id) {
134
+ static async getNextConversation() {
139
135
  const db = await getDb();
140
- db.prepare("UPDATE pending_messages SET retry_count = retry_count + 1 WHERE id = ?").run(id);
136
+ const row = db.prepare(`
137
+ SELECT conversation_id, MIN(conversation_created_at) as conversation_created_at
138
+ FROM send_queue
139
+ GROUP BY conversation_id
140
+ ORDER BY MIN(conversation_created_at) ASC
141
+ LIMIT 1
142
+ `).get();
143
+ if (!row)
144
+ return null;
145
+ const messages = db.prepare(`
146
+ SELECT id, conversation_id, conversation_created_at, role, content, timestamp, retry_count
147
+ FROM send_queue
148
+ WHERE conversation_id = ?
149
+ ORDER BY id ASC
150
+ `).all(row.conversation_id);
151
+ return {
152
+ conversationId: row.conversation_id,
153
+ conversationCreatedAt: row.conversation_created_at,
154
+ messages
155
+ };
141
156
  }
142
- static async clearOldPendingMessages(maxAge = 7 * 24 * 60 * 60) {
157
+ static async deleteConversation(conversationId) {
143
158
  const db = await getDb();
144
- const cutoff = Math.floor(Date.now() / 1000) - maxAge;
145
- db.prepare("DELETE FROM pending_messages WHERE created_at < ?").run(cutoff);
159
+ db.prepare("DELETE FROM send_queue WHERE conversation_id = ?").run(conversationId);
146
160
  }
147
- static async addTempMessage(conversationId, role, content) {
161
+ static async incrementRetryByConversation(conversationId) {
148
162
  const db = await getDb();
149
- const result = db.prepare(`
150
- INSERT INTO temp_messages (conversation_id, role, content, timestamp)
151
- VALUES (?, ?, ?, ?)
152
- `).run(conversationId, role, content, Date.now());
153
- return result.lastInsertRowid;
163
+ db.prepare("UPDATE send_queue SET retry_count = retry_count + 1 WHERE conversation_id = ?").run(conversationId);
154
164
  }
155
- static async getTempMessages(conversationId) {
165
+ static async getQueueStats(conversationId) {
156
166
  const db = await getDb();
157
167
  const rows = db.prepare(`
158
- SELECT role, content, timestamp FROM temp_messages
168
+ SELECT role FROM send_queue
159
169
  WHERE conversation_id = ?
160
- ORDER BY timestamp ASC
170
+ ORDER BY id ASC
161
171
  `).all(conversationId);
162
- return rows.map((r) => ({
163
- role: r.role,
164
- content: r.content,
165
- tokens: 0,
166
- timestamp: r.timestamp
167
- }));
168
- }
169
- static async clearTempMessages(conversationId) {
170
- const db = await getDb();
171
- db.prepare("DELETE FROM temp_messages WHERE conversation_id = ?").run(conversationId);
172
+ let rounds = 0;
173
+ let lastRole = "";
174
+ for (const row of rows) {
175
+ if (row.role === "assistant" && lastRole === "user") {
176
+ rounds++;
177
+ }
178
+ lastRole = row.role;
179
+ }
180
+ return { count: rows.length, rounds };
172
181
  }
173
- static async clearOldTempMessages(maxAge = 24 * 60 * 60) {
182
+ static async clearOldConversations(maxAge = 7 * 24 * 60 * 60) {
174
183
  const db = await getDb();
175
184
  const cutoff = Math.floor(Date.now() / 1000) - maxAge;
176
- db.prepare("DELETE FROM temp_messages WHERE created_at < ?").run(cutoff);
185
+ db.prepare("DELETE FROM send_queue WHERE conversation_created_at < ?").run(cutoff);
186
+ }
187
+ static async getQueueLength() {
188
+ const db = await getDb();
189
+ const row = db.prepare("SELECT COUNT(DISTINCT conversation_id) as count FROM send_queue").get();
190
+ return row?.count || 0;
177
191
  }
178
192
  }
179
- class ConversationBuffer {
180
- messages = [];
181
- roundCount = 0;
182
- lastRole = "";
183
- conversationId = "";
184
- startedAt = Date.now();
193
+ class ConversationManager {
194
+ currentConversationId = "";
195
+ conversationCreatedAt = Math.floor(Date.now() / 1000);
185
196
  lastActivityAt = Date.now();
186
197
  ROUND_THRESHOLD = 2;
187
198
  TIMEOUT_MS = 30 * 60 * 1000;
188
199
  constructor() {
189
- this.conversationId = this.generateId();
200
+ this.currentConversationId = this.generateId();
190
201
  }
191
202
  generateId() {
192
203
  return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
193
204
  }
194
205
  getConversationId() {
195
- return this.conversationId;
206
+ return this.currentConversationId;
196
207
  }
197
- addMessage(role, content) {
208
+ async addMessage(role, content) {
198
209
  if (!content || content.length < 2) {
199
210
  return false;
200
211
  }
201
- const message = {
202
- role,
203
- content,
204
- tokens: 0,
205
- timestamp: Date.now()
206
- };
207
- this.messages.push(message);
212
+ const db = await getDb();
213
+ db.prepare(`
214
+ INSERT INTO send_queue (conversation_id, conversation_created_at, role, content, timestamp)
215
+ VALUES (?, ?, ?, ?, ?)
216
+ `).run(this.currentConversationId, this.conversationCreatedAt, role, content, Date.now());
208
217
  this.lastActivityAt = Date.now();
209
- if (role === "assistant" && this.lastRole === "user") {
210
- this.roundCount++;
211
- }
212
- this.lastRole = role;
213
- return this.roundCount >= this.ROUND_THRESHOLD;
218
+ const stats = await SQLiteStorage.getQueueStats(this.currentConversationId);
219
+ return stats.rounds >= this.ROUND_THRESHOLD;
214
220
  }
215
- shouldFlush() {
216
- if (this.messages.length === 0) {
221
+ async shouldFlush() {
222
+ const stats = await SQLiteStorage.getQueueStats(this.currentConversationId);
223
+ if (stats.count === 0) {
217
224
  return false;
218
225
  }
219
- if (this.roundCount >= this.ROUND_THRESHOLD) {
226
+ if (stats.rounds >= this.ROUND_THRESHOLD) {
220
227
  return true;
221
228
  }
222
229
  const elapsed = Date.now() - this.lastActivityAt;
@@ -225,29 +232,17 @@ class ConversationBuffer {
225
232
  }
226
233
  return false;
227
234
  }
228
- flush() {
229
- const data = {
230
- conversation_id: this.conversationId,
231
- messages: [...this.messages]
232
- };
233
- this.messages = [];
234
- this.roundCount = 0;
235
- this.lastRole = "";
236
- this.conversationId = this.generateId();
237
- this.startedAt = Date.now();
235
+ startNewConversation() {
236
+ this.currentConversationId = this.generateId();
237
+ this.conversationCreatedAt = Math.floor(Date.now() / 1000);
238
238
  this.lastActivityAt = Date.now();
239
- return data;
240
- }
241
- forceFlush() {
242
- if (this.messages.length === 0) {
243
- return null;
244
- }
245
- return this.flush();
246
239
  }
247
- getStatus() {
240
+ async getStatus() {
241
+ const stats = await SQLiteStorage.getQueueStats(this.currentConversationId);
248
242
  return {
249
- messageCount: this.messages.length,
250
- conversationId: this.conversationId
243
+ messageCount: stats.count,
244
+ conversationId: this.currentConversationId,
245
+ rounds: stats.rounds
251
246
  };
252
247
  }
253
248
  }
@@ -259,11 +254,11 @@ class MemoryXPlugin {
259
254
  initialized: false,
260
255
  apiBaseUrl: DEFAULT_API_BASE
261
256
  };
262
- buffer = new ConversationBuffer();
257
+ conversationManager = new ConversationManager();
263
258
  flushTimer = null;
264
- pendingRetryTimer = null;
259
+ sendQueueTimer = null;
265
260
  FLUSH_CHECK_INTERVAL = 30000;
266
- PENDING_RETRY_INTERVAL = 60000;
261
+ SEND_QUEUE_INTERVAL = 5000;
267
262
  MAX_RETRY_COUNT = 5;
268
263
  pluginConfig = null;
269
264
  initialized = false;
@@ -282,8 +277,9 @@ class MemoryXPlugin {
282
277
  this.loadConfig().then(() => {
283
278
  log(`Config loaded, apiKey: ${this.config.apiKey ? 'present' : 'missing'}`);
284
279
  this.startFlushTimer();
285
- this.startPendingRetryTimer();
286
- this.retryPendingMessages();
280
+ this.startSendQueueTimer();
281
+ SQLiteStorage.clearOldConversations().catch(() => { });
282
+ this.processSendQueue();
287
283
  if (!this.config.apiKey) {
288
284
  log("Starting auto-register");
289
285
  this.autoRegister().catch(e => log(`Auto-register failed: ${e}`));
@@ -346,116 +342,76 @@ class MemoryXPlugin {
346
342
  return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 32);
347
343
  }
348
344
  startFlushTimer() {
349
- this.flushTimer = setInterval(() => {
350
- if (this.buffer.shouldFlush()) {
351
- this.flushConversation();
345
+ this.flushTimer = setInterval(async () => {
346
+ if (await this.conversationManager.shouldFlush()) {
347
+ this.conversationManager.startNewConversation();
352
348
  }
353
349
  }, this.FLUSH_CHECK_INTERVAL);
354
350
  }
355
- startPendingRetryTimer() {
356
- this.pendingRetryTimer = setInterval(() => {
357
- this.retryPendingMessages();
358
- }, this.PENDING_RETRY_INTERVAL);
351
+ startSendQueueTimer() {
352
+ this.sendQueueTimer = setInterval(() => {
353
+ this.processSendQueue();
354
+ }, this.SEND_QUEUE_INTERVAL);
359
355
  }
360
- async retryPendingMessages() {
356
+ async processSendQueue() {
357
+ if (isSending)
358
+ return;
361
359
  if (!this.config.apiKey)
362
360
  return;
361
+ isSending = true;
363
362
  try {
364
- const pending = await SQLiteStorage.getPendingMessages(50);
365
- if (pending.length === 0)
363
+ await SQLiteStorage.clearOldConversations();
364
+ const conversation = await SQLiteStorage.getNextConversation();
365
+ if (!conversation) {
366
+ isSending = false;
366
367
  return;
367
- log(`Retrying ${pending.length} pending messages`);
368
- for (const msg of pending) {
369
- if (msg.retry_count >= this.MAX_RETRY_COUNT) {
370
- log(`Deleting message ${msg.id}: max retries exceeded`);
371
- await SQLiteStorage.deletePendingMessage(msg.id);
372
- continue;
373
- }
374
- try {
375
- const response = await fetch(`${this.apiBase}/v1/conversations/flush`, {
376
- method: "POST",
377
- headers: {
378
- "Content-Type": "application/json",
379
- "X-API-Key": this.config.apiKey
380
- },
381
- body: JSON.stringify({
382
- conversation_id: msg.conversation_id,
383
- messages: [{
384
- role: msg.role,
385
- content: msg.content,
386
- timestamp: msg.timestamp,
387
- tokens: 0
388
- }]
389
- })
390
- });
391
- if (response.ok) {
392
- await SQLiteStorage.deletePendingMessage(msg.id);
393
- log(`Sent pending message ${msg.id}`);
394
- }
395
- else {
396
- await SQLiteStorage.incrementRetryCount(msg.id);
397
- log(`Failed to send pending message ${msg.id}: ${response.status}`);
398
- }
399
- }
400
- catch (e) {
401
- await SQLiteStorage.incrementRetryCount(msg.id);
402
- log(`Error sending pending message ${msg.id}: ${e}`);
403
- }
404
368
  }
405
- }
406
- catch (e) {
407
- log(`Retry pending messages failed: ${e}`);
408
- }
409
- }
410
- async flushConversation() {
411
- if (!this.config.apiKey) {
412
- const data = this.buffer.forceFlush();
413
- if (data) {
414
- for (const msg of data.messages) {
415
- await SQLiteStorage.addPendingMessage(data.conversation_id, msg.role, msg.content);
416
- }
417
- log(`Cached ${data.messages.length} messages (no API key)`);
369
+ const { conversationId, messages } = conversation;
370
+ const maxRetry = Math.max(...messages.map(m => m.retry_count));
371
+ if (maxRetry >= this.MAX_RETRY_COUNT) {
372
+ log(`Deleting conversation ${conversationId}: max retries exceeded`);
373
+ await SQLiteStorage.deleteConversation(conversationId);
374
+ isSending = false;
375
+ return;
418
376
  }
419
- return;
420
- }
421
- const data = this.buffer.forceFlush();
422
- if (!data || data.messages.length === 0) {
423
- return;
424
- }
425
- try {
426
- const response = await fetch(`${this.apiBase}/v1/conversations/flush`, {
427
- method: "POST",
428
- headers: {
429
- "Content-Type": "application/json",
430
- "X-API-Key": this.config.apiKey
431
- },
432
- body: JSON.stringify(data)
433
- });
434
- if (!response.ok) {
435
- const errorData = await response.json().catch(() => ({}));
436
- if (response.status === 402) {
437
- log(`Quota exceeded: ${errorData.detail}`);
377
+ log(`Sending conversation ${conversationId} (${messages.length} messages)`);
378
+ try {
379
+ const response = await fetch(`${this.apiBase}/v1/conversations/flush`, {
380
+ method: "POST",
381
+ headers: {
382
+ "Content-Type": "application/json",
383
+ "X-API-Key": this.config.apiKey
384
+ },
385
+ body: JSON.stringify({
386
+ conversation_id: conversationId,
387
+ messages: messages.map(m => ({
388
+ role: m.role,
389
+ content: m.content,
390
+ timestamp: m.timestamp,
391
+ tokens: countTokens(m.content)
392
+ }))
393
+ })
394
+ });
395
+ if (response.ok) {
396
+ await SQLiteStorage.deleteConversation(conversationId);
397
+ const result = await response.json();
398
+ log(`Sent conversation ${conversationId}, extracted ${result.extracted_count || 0} memories`);
438
399
  }
439
400
  else {
440
- log(`Flush failed: ${JSON.stringify(errorData)}`);
441
- }
442
- for (const msg of data.messages) {
443
- await SQLiteStorage.addPendingMessage(data.conversation_id, msg.role, msg.content);
401
+ await SQLiteStorage.incrementRetryByConversation(conversationId);
402
+ const errorData = await response.json().catch(() => ({}));
403
+ log(`Failed to send conversation ${conversationId}: ${response.status} ${JSON.stringify(errorData)}`);
444
404
  }
445
- log(`Cached ${data.messages.length} messages (API error)`);
446
405
  }
447
- else {
448
- const result = await response.json();
449
- log(`Flushed ${data.messages.length} messages, extracted ${result.extracted_count} memories`);
406
+ catch (e) {
407
+ await SQLiteStorage.incrementRetryByConversation(conversationId);
408
+ log(`Error sending conversation ${conversationId}: ${e}`);
450
409
  }
451
410
  }
452
411
  catch (e) {
453
- log(`Flush error: ${e}`);
454
- for (const msg of data.messages) {
455
- await SQLiteStorage.addPendingMessage(data.conversation_id, msg.role, msg.content);
456
- }
457
- log(`Cached ${data.messages.length} messages (network error)`);
412
+ log(`Process send queue failed: ${e}`);
458
413
  }
414
+ isSending = false;
459
415
  }
460
416
  async onMessage(role, content) {
461
417
  this.init();
@@ -471,17 +427,16 @@ class MemoryXPlugin {
471
427
  return false;
472
428
  }
473
429
  }
474
- await SQLiteStorage.addTempMessage(this.buffer.getConversationId(), role, content);
475
- const shouldFlush = this.buffer.addMessage(role, content);
430
+ const shouldFlush = await this.conversationManager.addMessage(role, content);
476
431
  if (shouldFlush) {
477
- await this.flushConversation();
432
+ this.conversationManager.startNewConversation();
478
433
  }
479
434
  return true;
480
435
  }
481
436
  async recall(query, limit = 5) {
482
437
  this.init();
483
438
  if (!this.config.apiKey || !query || query.length < 2) {
484
- return { memories: [], isLimited: false, remainingQuota: 0 };
439
+ return { memories: [], relatedMemories: [], isLimited: false, remainingQuota: 0 };
485
440
  }
486
441
  try {
487
442
  const response = await fetch(`${this.apiBase}/v1/memories/search`, {
@@ -501,6 +456,7 @@ class MemoryXPlugin {
501
456
  if (response.status === 402 || response.status === 429) {
502
457
  return {
503
458
  memories: [],
459
+ relatedMemories: [],
504
460
  isLimited: true,
505
461
  remainingQuota: 0,
506
462
  upgradeHint: errorData.detail || "云端查询配额已用尽,请升级到付费版"
@@ -512,29 +468,95 @@ class MemoryXPlugin {
512
468
  return {
513
469
  memories: (data.data || []).map((m) => ({
514
470
  id: m.id,
515
- content: m.content,
471
+ content: m.memory || m.content,
516
472
  category: m.category || "other",
517
473
  score: m.score || 0.5
518
474
  })),
475
+ relatedMemories: (data.related_memories || []).map((m) => ({
476
+ id: m.id,
477
+ content: m.memory || m.content,
478
+ category: m.category || "other",
479
+ score: m.score || 0
480
+ })),
519
481
  isLimited: false,
520
482
  remainingQuota: data.remaining_quota ?? -1
521
483
  };
522
484
  }
523
485
  catch (e) {
524
486
  log(`Recall failed: ${e}`);
525
- return { memories: [], isLimited: false, remainingQuota: 0 };
487
+ return { memories: [], relatedMemories: [], isLimited: false, remainingQuota: 0 };
526
488
  }
527
489
  }
528
490
  async endConversation() {
529
- await this.flushConversation();
530
- await SQLiteStorage.clearTempMessages(this.buffer.getConversationId());
531
- log("Conversation ended, buffer flushed");
491
+ this.conversationManager.startNewConversation();
492
+ log("Conversation ended, starting new conversation");
493
+ }
494
+ async forget(memoryId) {
495
+ this.init();
496
+ if (!this.config.apiKey) {
497
+ log("Forget failed: no API key");
498
+ return false;
499
+ }
500
+ try {
501
+ const response = await fetch(`${this.apiBase}/v1/memories/${memoryId}`, {
502
+ method: "DELETE",
503
+ headers: {
504
+ "X-API-Key": this.config.apiKey
505
+ }
506
+ });
507
+ if (response.ok) {
508
+ log(`Forgot memory ${memoryId}`);
509
+ return true;
510
+ }
511
+ log(`Forget failed: ${response.status}`);
512
+ return false;
513
+ }
514
+ catch (e) {
515
+ log(`Forget failed: ${e}`);
516
+ return false;
517
+ }
532
518
  }
533
- getStatus() {
519
+ async list(limit = 10) {
520
+ this.init();
521
+ if (!this.config.apiKey) {
522
+ log("List failed: no API key");
523
+ return [];
524
+ }
525
+ try {
526
+ const response = await fetch(`${this.apiBase}/v1/memories/search`, {
527
+ method: "POST",
528
+ headers: {
529
+ "Content-Type": "application/json",
530
+ "X-API-Key": this.config.apiKey
531
+ },
532
+ body: JSON.stringify({
533
+ query: "*",
534
+ project_id: this.config.projectId,
535
+ limit
536
+ })
537
+ });
538
+ if (!response.ok) {
539
+ log(`List failed: ${response.status}`);
540
+ return [];
541
+ }
542
+ const data = await response.json();
543
+ return (data.data || []).map((m) => ({
544
+ id: m.id,
545
+ content: m.memory || m.content,
546
+ category: m.category || "other"
547
+ }));
548
+ }
549
+ catch (e) {
550
+ log(`List failed: ${e}`);
551
+ return [];
552
+ }
553
+ }
554
+ async getStatus() {
555
+ const status = await this.conversationManager.getStatus();
534
556
  return {
535
557
  initialized: this.config.initialized,
536
558
  hasApiKey: !!this.config.apiKey,
537
- bufferStatus: this.buffer.getStatus()
559
+ conversationStatus: status
538
560
  };
539
561
  }
540
562
  }
@@ -542,7 +564,7 @@ let plugin;
542
564
  export default {
543
565
  id: "memoryx-openclaw-plugin",
544
566
  name: "MemoryX Realtime Plugin",
545
- version: "1.2.0",
567
+ version: "1.1.16",
546
568
  description: "Real-time memory capture and recall for OpenClaw",
547
569
  register(api, pluginConfig) {
548
570
  api.logger.info("[MemoryX] Plugin registering...");
@@ -550,6 +572,171 @@ export default {
550
572
  api.logger.info(`[MemoryX] API Base: \`${pluginConfig.apiBaseUrl}\``);
551
573
  }
552
574
  plugin = new MemoryXPlugin(pluginConfig);
575
+ api.registerTool({
576
+ name: "memoryx_recall",
577
+ label: "MemoryX Recall",
578
+ description: "Search through long-term memories. Use when you need context about user preferences, past decisions, or previously discussed topics.",
579
+ parameters: {
580
+ type: "object",
581
+ properties: {
582
+ query: {
583
+ type: "string",
584
+ description: "Search query to find relevant memories"
585
+ },
586
+ limit: {
587
+ type: "number",
588
+ description: "Maximum number of results to return (default: 5)"
589
+ }
590
+ },
591
+ required: ["query"]
592
+ },
593
+ async execute(_toolCallId, params) {
594
+ const { query, limit = 5 } = params;
595
+ if (!plugin) {
596
+ return {
597
+ content: [{ type: "text", text: "MemoryX plugin not initialized." }],
598
+ details: { error: "not_initialized" }
599
+ };
600
+ }
601
+ try {
602
+ const result = await plugin.recall(query, limit);
603
+ if (result.isLimited) {
604
+ return {
605
+ content: [{ type: "text", text: result.upgradeHint || "Quota exceeded" }],
606
+ details: { error: "quota_exceeded", hint: result.upgradeHint }
607
+ };
608
+ }
609
+ if (result.memories.length === 0 && result.relatedMemories.length === 0) {
610
+ return {
611
+ content: [{ type: "text", text: "No relevant memories found." }],
612
+ details: { count: 0 }
613
+ };
614
+ }
615
+ const lines = [];
616
+ const total = result.memories.length + result.relatedMemories.length;
617
+ if (result.memories.length > 0) {
618
+ lines.push(`Found ${result.memories.length} direct memories:`);
619
+ result.memories.forEach((m, i) => {
620
+ lines.push(`${i + 1}. [${m.category}] ${m.content} (${Math.round(m.score * 100)}%)`);
621
+ });
622
+ }
623
+ if (result.relatedMemories.length > 0) {
624
+ if (lines.length > 0)
625
+ lines.push("");
626
+ lines.push(`Found ${result.relatedMemories.length} related memories:`);
627
+ result.relatedMemories.forEach((m, i) => {
628
+ lines.push(`${i + 1}. [${m.category}] ${m.content}`);
629
+ });
630
+ }
631
+ return {
632
+ content: [{ type: "text", text: lines.join("\n") }],
633
+ details: {
634
+ count: total,
635
+ direct_count: result.memories.length,
636
+ related_count: result.relatedMemories.length,
637
+ remaining_quota: result.remainingQuota
638
+ }
639
+ };
640
+ }
641
+ catch (error) {
642
+ return {
643
+ content: [{ type: "text", text: `Memory search failed: ${error.message}` }],
644
+ details: { error: error.message }
645
+ };
646
+ }
647
+ }
648
+ }, { name: "memoryx_recall" });
649
+ api.registerTool({
650
+ name: "memoryx_forget",
651
+ label: "MemoryX Forget",
652
+ description: "Delete specific memories. Use when user explicitly asks to forget or remove something from memory.",
653
+ parameters: {
654
+ type: "object",
655
+ properties: {
656
+ memory_id: {
657
+ type: "string",
658
+ description: "The ID of the memory to delete"
659
+ }
660
+ },
661
+ required: ["memory_id"]
662
+ },
663
+ async execute(_toolCallId, params) {
664
+ const { memory_id } = params;
665
+ if (!plugin) {
666
+ return {
667
+ content: [{ type: "text", text: "MemoryX plugin not initialized." }],
668
+ details: { error: "not_initialized" }
669
+ };
670
+ }
671
+ try {
672
+ const success = await plugin.forget(memory_id);
673
+ if (success) {
674
+ return {
675
+ content: [{ type: "text", text: `Memory ${memory_id} has been forgotten.` }],
676
+ details: { action: "deleted", id: memory_id }
677
+ };
678
+ }
679
+ else {
680
+ return {
681
+ content: [{ type: "text", text: `Memory ${memory_id} not found or could not be deleted.` }],
682
+ details: { action: "failed", id: memory_id }
683
+ };
684
+ }
685
+ }
686
+ catch (error) {
687
+ return {
688
+ content: [{ type: "text", text: `Failed to forget memory: ${error.message}` }],
689
+ details: { error: error.message }
690
+ };
691
+ }
692
+ }
693
+ }, { name: "memoryx_forget" });
694
+ api.registerTool({
695
+ name: "memoryx_list",
696
+ label: "MemoryX List",
697
+ description: "List all stored memories. Use when user asks what you remember about them.",
698
+ parameters: {
699
+ type: "object",
700
+ properties: {
701
+ limit: {
702
+ type: "number",
703
+ description: "Maximum number of memories to list (default: 10)"
704
+ }
705
+ }
706
+ },
707
+ async execute(_toolCallId, params) {
708
+ const { limit = 10 } = params;
709
+ if (!plugin) {
710
+ return {
711
+ content: [{ type: "text", text: "MemoryX plugin not initialized." }],
712
+ details: { error: "not_initialized" }
713
+ };
714
+ }
715
+ try {
716
+ const memories = await plugin.list(limit);
717
+ if (memories.length === 0) {
718
+ return {
719
+ content: [{ type: "text", text: "No memories stored yet." }],
720
+ details: { count: 0 }
721
+ };
722
+ }
723
+ const lines = [`Here are the ${memories.length} most recent memories:`];
724
+ memories.forEach((m, i) => {
725
+ lines.push(`${i + 1}. [${m.category || 'general'}] ${m.content || m.memory}`);
726
+ });
727
+ return {
728
+ content: [{ type: "text", text: lines.join("\n") }],
729
+ details: { count: memories.length }
730
+ };
731
+ }
732
+ catch (error) {
733
+ return {
734
+ content: [{ type: "text", text: `Failed to list memories: ${error.message}` }],
735
+ details: { error: error.message }
736
+ };
737
+ }
738
+ }
739
+ }, { name: "memoryx_list" });
553
740
  api.on("message_received", async (event, ctx) => {
554
741
  const { content } = event;
555
742
  if (content && plugin) {
@@ -571,17 +758,24 @@ export default {
571
758
  if (result.isLimited) {
572
759
  api.logger.warn(`[MemoryX] ${result.upgradeHint}`);
573
760
  return {
574
- prependContext: `[系统提示] ${result.upgradeHint}\n`
761
+ prependContext: `[System] ${result.upgradeHint}\n`
575
762
  };
576
763
  }
577
- if (result.memories.length === 0)
764
+ if (result.memories.length === 0 && result.relatedMemories.length === 0)
578
765
  return;
579
- const memories = result.memories
580
- .map(m => `- [${m.category}] ${m.content}`)
581
- .join("\n");
582
- api.logger.info(`[MemoryX] Recalled ${result.memories.length} memories from cloud`);
766
+ let context = "MemoryX by t0ken.ai found the following memories:\n";
767
+ if (result.memories.length > 0) {
768
+ context += "\n[Direct Memories]\n";
769
+ context += result.memories.map(m => `- ${m.content}`).join("\n");
770
+ }
771
+ if (result.relatedMemories.length > 0) {
772
+ context += "\n\n[Related Memories]\n";
773
+ context += result.relatedMemories.map(m => `- ${m.content}`).join("\n");
774
+ }
775
+ context += "\n";
776
+ api.logger.info(`[MemoryX] Recalled ${result.memories.length} direct + ${result.relatedMemories.length} related memories`);
583
777
  return {
584
- prependContext: `[相关记忆]\n${memories}\n[End of memories]\n`
778
+ prependContext: context
585
779
  };
586
780
  }
587
781
  catch (error) {
@@ -596,4 +790,4 @@ export default {
596
790
  api.logger.info("[MemoryX] Plugin registered successfully");
597
791
  }
598
792
  };
599
- export { MemoryXPlugin, ConversationBuffer };
793
+ export { MemoryXPlugin, ConversationManager };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "memoryx-openclaw-plugin",
3
3
  "name": "MemoryX Real-time Plugin",
4
- "version": "1.1.1",
4
+ "version": "1.1.15",
5
5
  "description": "Real-time memory capture and recall for OpenClaw",
6
6
  "kind": "memory",
7
7
  "main": "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t0ken.ai/memoryx-openclaw-plugin",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "description": "MemoryX real-time memory capture and recall plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "author": "MemoryX Team",
@@ -36,6 +36,7 @@
36
36
  "typescript": "^5.0.0"
37
37
  },
38
38
  "dependencies": {
39
- "better-sqlite3": "^11.0.0"
39
+ "better-sqlite3": "^11.0.0",
40
+ "js-tiktoken": "^1.0.21"
40
41
  }
41
42
  }