@t0ken.ai/memoryx-openclaw-plugin 1.1.13 → 1.1.15

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,20 @@ 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
+ getStatus(): Promise<{
95
85
  initialized: boolean;
96
86
  hasApiKey: boolean;
97
- bufferStatus: {
87
+ conversationStatus: {
98
88
  messageCount: number;
99
89
  conversationId: string;
90
+ rounds: number;
100
91
  };
101
- };
92
+ }>;
102
93
  }
103
94
  declare const _default: {
104
95
  id: string;
@@ -108,5 +99,5 @@ declare const _default: {
108
99
  register(api: any, pluginConfig?: PluginConfig): void;
109
100
  };
110
101
  export default _default;
111
- export { MemoryXPlugin, ConversationBuffer };
102
+ export { MemoryXPlugin, ConversationManager };
112
103
  //# 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;IAmC1B,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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAqDH,UAAU,YAAY;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAiBD,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,eAAe,EAAE,KAAK,CAAC;QACnB,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;AA0JD,cAAM,mBAAmB;IACrB,OAAO,CAAC,qBAAqB,CAAc;IAC3C,OAAO,CAAC,qBAAqB,CAAyC;IACtE,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;IAIrB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB3D,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAkBrC,oBAAoB,IAAI,IAAI;IAMtB,SAAS,IAAI,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAQ/F;AAED,cAAM,aAAa;IACf,OAAO,CAAC,MAAM,CAMZ;IAEF,OAAO,CAAC,mBAAmB,CAAkD;IAC7E,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAQ;IAC5C,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;IAmBZ,OAAO,KAAK,OAAO,GAElB;YAEa,UAAU;YAWV,UAAU;YAIV,YAAY;IA+B1B,OAAO,CAAC,qBAAqB;IAa7B,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,mBAAmB;YAMb,gBAAgB;IAiEjB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA2B1D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,OAAO,CAAC,YAAY,CAAC;IA6D/D,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAKhC,SAAS,IAAI,OAAO,CAAC;QAC9B,WAAW,EAAE,OAAO,CAAC;QACrB,SAAS,EAAE,OAAO,CAAC;QACnB,kBAAkB,EAAE;YAAE,YAAY,EAAE,MAAM,CAAC;YAAC,cAAc,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;KACvF,CAAC;CAQL;;;;;;kBAUiB,GAAG,iBAAiB,YAAY,GAAG,IAAI;;AANzD,wBA6EE;AAEF,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAC"}
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 (
84
+ CREATE TABLE IF NOT EXISTS send_queue (
68
85
  id INTEGER PRIMARY KEY AUTOINCREMENT,
69
86
  conversation_id TEXT NOT NULL,
87
+ conversation_created_at INTEGER NOT NULL,
70
88
  role TEXT NOT NULL,
71
89
  content TEXT NOT NULL,
72
90
  timestamp INTEGER NOT NULL,
73
- retry_count INTEGER DEFAULT 0,
74
- created_at INTEGER DEFAULT (strftime('%s', 'now'))
91
+ retry_count INTEGER DEFAULT 0
75
92
  );
76
93
 
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 (
81
- id INTEGER PRIMARY KEY AUTOINCREMENT,
82
- conversation_id TEXT NOT NULL,
83
- role TEXT NOT NULL,
84
- content TEXT NOT NULL,
85
- timestamp INTEGER NOT NULL,
86
- created_at INTEGER DEFAULT (strftime('%s', 'now'))
87
- );
88
-
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) {
127
- 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) {
124
+ static async addConversationToQueue(conversationId, conversationCreatedAt, messages) {
135
125
  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}`));
@@ -324,16 +320,11 @@ class MemoryXPlugin {
324
320
  throw new Error(`Auto-register failed: ${response.status}`);
325
321
  }
326
322
  const data = await response.json();
327
- if (data.api_key) {
328
- this.config.apiKey = data.api_key;
329
- this.config.projectId = String(data.project_id);
330
- this.config.userId = data.agent_id;
331
- await this.saveConfig();
332
- log("Auto-registered successfully");
333
- }
334
- else {
335
- log("Machine already registered, using cached API key");
336
- }
323
+ this.config.apiKey = data.api_key;
324
+ this.config.projectId = String(data.project_id);
325
+ this.config.userId = data.agent_id;
326
+ await this.saveConfig();
327
+ log("Auto-registered successfully");
337
328
  }
338
329
  catch (e) {
339
330
  log(`Auto-register failed: ${e}`);
@@ -351,116 +342,76 @@ class MemoryXPlugin {
351
342
  return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 32);
352
343
  }
353
344
  startFlushTimer() {
354
- this.flushTimer = setInterval(() => {
355
- if (this.buffer.shouldFlush()) {
356
- this.flushConversation();
345
+ this.flushTimer = setInterval(async () => {
346
+ if (await this.conversationManager.shouldFlush()) {
347
+ this.conversationManager.startNewConversation();
357
348
  }
358
349
  }, this.FLUSH_CHECK_INTERVAL);
359
350
  }
360
- startPendingRetryTimer() {
361
- this.pendingRetryTimer = setInterval(() => {
362
- this.retryPendingMessages();
363
- }, this.PENDING_RETRY_INTERVAL);
351
+ startSendQueueTimer() {
352
+ this.sendQueueTimer = setInterval(() => {
353
+ this.processSendQueue();
354
+ }, this.SEND_QUEUE_INTERVAL);
364
355
  }
365
- async retryPendingMessages() {
356
+ async processSendQueue() {
357
+ if (isSending)
358
+ return;
366
359
  if (!this.config.apiKey)
367
360
  return;
361
+ isSending = true;
368
362
  try {
369
- const pending = await SQLiteStorage.getPendingMessages(50);
370
- if (pending.length === 0)
363
+ await SQLiteStorage.clearOldConversations();
364
+ const conversation = await SQLiteStorage.getNextConversation();
365
+ if (!conversation) {
366
+ isSending = false;
371
367
  return;
372
- log(`Retrying ${pending.length} pending messages`);
373
- for (const msg of pending) {
374
- if (msg.retry_count >= this.MAX_RETRY_COUNT) {
375
- log(`Deleting message ${msg.id}: max retries exceeded`);
376
- await SQLiteStorage.deletePendingMessage(msg.id);
377
- continue;
378
- }
379
- try {
380
- const response = await fetch(`${this.apiBase}/v1/conversations/flush`, {
381
- method: "POST",
382
- headers: {
383
- "Content-Type": "application/json",
384
- "X-API-Key": this.config.apiKey
385
- },
386
- body: JSON.stringify({
387
- conversation_id: msg.conversation_id,
388
- messages: [{
389
- role: msg.role,
390
- content: msg.content,
391
- timestamp: msg.timestamp,
392
- tokens: 0
393
- }]
394
- })
395
- });
396
- if (response.ok) {
397
- await SQLiteStorage.deletePendingMessage(msg.id);
398
- log(`Sent pending message ${msg.id}`);
399
- }
400
- else {
401
- await SQLiteStorage.incrementRetryCount(msg.id);
402
- log(`Failed to send pending message ${msg.id}: ${response.status}`);
403
- }
404
- }
405
- catch (e) {
406
- await SQLiteStorage.incrementRetryCount(msg.id);
407
- log(`Error sending pending message ${msg.id}: ${e}`);
408
- }
409
368
  }
410
- }
411
- catch (e) {
412
- log(`Retry pending messages failed: ${e}`);
413
- }
414
- }
415
- async flushConversation() {
416
- if (!this.config.apiKey) {
417
- const data = this.buffer.forceFlush();
418
- if (data) {
419
- for (const msg of data.messages) {
420
- await SQLiteStorage.addPendingMessage(data.conversation_id, msg.role, msg.content);
421
- }
422
- 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;
423
376
  }
424
- return;
425
- }
426
- const data = this.buffer.forceFlush();
427
- if (!data || data.messages.length === 0) {
428
- return;
429
- }
430
- try {
431
- const response = await fetch(`${this.apiBase}/v1/conversations/flush`, {
432
- method: "POST",
433
- headers: {
434
- "Content-Type": "application/json",
435
- "X-API-Key": this.config.apiKey
436
- },
437
- body: JSON.stringify(data)
438
- });
439
- if (!response.ok) {
440
- const errorData = await response.json().catch(() => ({}));
441
- if (response.status === 402) {
442
- 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`);
443
399
  }
444
400
  else {
445
- log(`Flush failed: ${JSON.stringify(errorData)}`);
446
- }
447
- for (const msg of data.messages) {
448
- 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)}`);
449
404
  }
450
- log(`Cached ${data.messages.length} messages (API error)`);
451
405
  }
452
- else {
453
- const result = await response.json();
454
- 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}`);
455
409
  }
456
410
  }
457
411
  catch (e) {
458
- log(`Flush error: ${e}`);
459
- for (const msg of data.messages) {
460
- await SQLiteStorage.addPendingMessage(data.conversation_id, msg.role, msg.content);
461
- }
462
- log(`Cached ${data.messages.length} messages (network error)`);
412
+ log(`Process send queue failed: ${e}`);
463
413
  }
414
+ isSending = false;
464
415
  }
465
416
  async onMessage(role, content) {
466
417
  this.init();
@@ -476,17 +427,16 @@ class MemoryXPlugin {
476
427
  return false;
477
428
  }
478
429
  }
479
- await SQLiteStorage.addTempMessage(this.buffer.getConversationId(), role, content);
480
- const shouldFlush = this.buffer.addMessage(role, content);
430
+ const shouldFlush = await this.conversationManager.addMessage(role, content);
481
431
  if (shouldFlush) {
482
- await this.flushConversation();
432
+ this.conversationManager.startNewConversation();
483
433
  }
484
434
  return true;
485
435
  }
486
436
  async recall(query, limit = 5) {
487
437
  this.init();
488
438
  if (!this.config.apiKey || !query || query.length < 2) {
489
- return { memories: [], isLimited: false, remainingQuota: 0 };
439
+ return { memories: [], relatedMemories: [], isLimited: false, remainingQuota: 0 };
490
440
  }
491
441
  try {
492
442
  const response = await fetch(`${this.apiBase}/v1/memories/search`, {
@@ -506,6 +456,7 @@ class MemoryXPlugin {
506
456
  if (response.status === 402 || response.status === 429) {
507
457
  return {
508
458
  memories: [],
459
+ relatedMemories: [],
509
460
  isLimited: true,
510
461
  remainingQuota: 0,
511
462
  upgradeHint: errorData.detail || "云端查询配额已用尽,请升级到付费版"
@@ -517,29 +468,35 @@ class MemoryXPlugin {
517
468
  return {
518
469
  memories: (data.data || []).map((m) => ({
519
470
  id: m.id,
520
- content: m.content,
471
+ content: m.memory || m.content,
521
472
  category: m.category || "other",
522
473
  score: m.score || 0.5
523
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
+ })),
524
481
  isLimited: false,
525
482
  remainingQuota: data.remaining_quota ?? -1
526
483
  };
527
484
  }
528
485
  catch (e) {
529
486
  log(`Recall failed: ${e}`);
530
- return { memories: [], isLimited: false, remainingQuota: 0 };
487
+ return { memories: [], relatedMemories: [], isLimited: false, remainingQuota: 0 };
531
488
  }
532
489
  }
533
490
  async endConversation() {
534
- await this.flushConversation();
535
- await SQLiteStorage.clearTempMessages(this.buffer.getConversationId());
536
- log("Conversation ended, buffer flushed");
491
+ this.conversationManager.startNewConversation();
492
+ log("Conversation ended, starting new conversation");
537
493
  }
538
- getStatus() {
494
+ async getStatus() {
495
+ const status = await this.conversationManager.getStatus();
539
496
  return {
540
497
  initialized: this.config.initialized,
541
498
  hasApiKey: !!this.config.apiKey,
542
- bufferStatus: this.buffer.getStatus()
499
+ conversationStatus: status
543
500
  };
544
501
  }
545
502
  }
@@ -576,17 +533,24 @@ export default {
576
533
  if (result.isLimited) {
577
534
  api.logger.warn(`[MemoryX] ${result.upgradeHint}`);
578
535
  return {
579
- prependContext: `[系统提示] ${result.upgradeHint}\n`
536
+ prependContext: `[System] ${result.upgradeHint}\n`
580
537
  };
581
538
  }
582
- if (result.memories.length === 0)
539
+ if (result.memories.length === 0 && result.relatedMemories.length === 0)
583
540
  return;
584
- const memories = result.memories
585
- .map(m => `- [${m.category}] ${m.content}`)
586
- .join("\n");
587
- api.logger.info(`[MemoryX] Recalled ${result.memories.length} memories from cloud`);
541
+ let context = "MemoryX by t0ken.ai found the following memories:\n";
542
+ if (result.memories.length > 0) {
543
+ context += "\n[Direct Memories]\n";
544
+ context += result.memories.map(m => `- ${m.content}`).join("\n");
545
+ }
546
+ if (result.relatedMemories.length > 0) {
547
+ context += "\n\n[Related Memories]\n";
548
+ context += result.relatedMemories.map(m => `- ${m.content}`).join("\n");
549
+ }
550
+ context += "\n";
551
+ api.logger.info(`[MemoryX] Recalled ${result.memories.length} direct + ${result.relatedMemories.length} related memories`);
588
552
  return {
589
- prependContext: `[相关记忆]\n${memories}\n[End of memories]\n`
553
+ prependContext: context
590
554
  };
591
555
  }
592
556
  catch (error) {
@@ -601,4 +565,4 @@ export default {
601
565
  api.logger.info("[MemoryX] Plugin registered successfully");
602
566
  }
603
567
  };
604
- export { MemoryXPlugin, ConversationBuffer };
568
+ 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.13",
3
+ "version": "1.1.15",
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
  }