@t0ken.ai/memoryx-openclaw-plugin 1.1.14 → 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 +25 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -220
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
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
|
|
44
|
-
private
|
|
45
|
-
private
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
63
|
+
private conversationManager;
|
|
73
64
|
private flushTimer;
|
|
74
|
-
private
|
|
65
|
+
private sendQueueTimer;
|
|
75
66
|
private readonly FLUSH_CHECK_INTERVAL;
|
|
76
|
-
private readonly
|
|
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
|
|
89
|
-
private
|
|
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
|
-
|
|
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,
|
|
102
|
+
export { MemoryXPlugin, ConversationManager };
|
|
112
103
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;
|
|
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
|
|
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
|
|
78
|
-
CREATE INDEX IF NOT EXISTS
|
|
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
|
|
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
|
-
|
|
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
|
|
134
|
+
static async getNextConversation() {
|
|
139
135
|
const db = await getDb();
|
|
140
|
-
|
|
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
|
|
157
|
+
static async deleteConversation(conversationId) {
|
|
143
158
|
const db = await getDb();
|
|
144
|
-
|
|
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
|
|
161
|
+
static async incrementRetryByConversation(conversationId) {
|
|
148
162
|
const db = await getDb();
|
|
149
|
-
|
|
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
|
|
165
|
+
static async getQueueStats(conversationId) {
|
|
156
166
|
const db = await getDb();
|
|
157
167
|
const rows = db.prepare(`
|
|
158
|
-
SELECT role
|
|
168
|
+
SELECT role FROM send_queue
|
|
159
169
|
WHERE conversation_id = ?
|
|
160
|
-
ORDER BY
|
|
170
|
+
ORDER BY id ASC
|
|
161
171
|
`).all(conversationId);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
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.
|
|
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
|
|
202
|
-
|
|
203
|
-
content,
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
221
|
+
async shouldFlush() {
|
|
222
|
+
const stats = await SQLiteStorage.getQueueStats(this.currentConversationId);
|
|
223
|
+
if (stats.count === 0) {
|
|
217
224
|
return false;
|
|
218
225
|
}
|
|
219
|
-
if (
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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:
|
|
250
|
-
conversationId: this.
|
|
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
|
-
|
|
257
|
+
conversationManager = new ConversationManager();
|
|
263
258
|
flushTimer = null;
|
|
264
|
-
|
|
259
|
+
sendQueueTimer = null;
|
|
265
260
|
FLUSH_CHECK_INTERVAL = 30000;
|
|
266
|
-
|
|
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.
|
|
286
|
-
|
|
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.
|
|
351
|
-
this.
|
|
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
|
-
|
|
356
|
-
this.
|
|
357
|
-
this.
|
|
358
|
-
}, this.
|
|
351
|
+
startSendQueueTimer() {
|
|
352
|
+
this.sendQueueTimer = setInterval(() => {
|
|
353
|
+
this.processSendQueue();
|
|
354
|
+
}, this.SEND_QUEUE_INTERVAL);
|
|
359
355
|
}
|
|
360
|
-
async
|
|
356
|
+
async processSendQueue() {
|
|
357
|
+
if (isSending)
|
|
358
|
+
return;
|
|
361
359
|
if (!this.config.apiKey)
|
|
362
360
|
return;
|
|
361
|
+
isSending = true;
|
|
363
362
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
log(`
|
|
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(`
|
|
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
|
|
475
|
-
const shouldFlush = this.buffer.addMessage(role, content);
|
|
430
|
+
const shouldFlush = await this.conversationManager.addMessage(role, content);
|
|
476
431
|
if (shouldFlush) {
|
|
477
|
-
|
|
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,35 @@ 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
|
-
|
|
530
|
-
|
|
531
|
-
log("Conversation ended, buffer flushed");
|
|
491
|
+
this.conversationManager.startNewConversation();
|
|
492
|
+
log("Conversation ended, starting new conversation");
|
|
532
493
|
}
|
|
533
|
-
getStatus() {
|
|
494
|
+
async getStatus() {
|
|
495
|
+
const status = await this.conversationManager.getStatus();
|
|
534
496
|
return {
|
|
535
497
|
initialized: this.config.initialized,
|
|
536
498
|
hasApiKey: !!this.config.apiKey,
|
|
537
|
-
|
|
499
|
+
conversationStatus: status
|
|
538
500
|
};
|
|
539
501
|
}
|
|
540
502
|
}
|
|
@@ -571,17 +533,24 @@ export default {
|
|
|
571
533
|
if (result.isLimited) {
|
|
572
534
|
api.logger.warn(`[MemoryX] ${result.upgradeHint}`);
|
|
573
535
|
return {
|
|
574
|
-
prependContext: `[
|
|
536
|
+
prependContext: `[System] ${result.upgradeHint}\n`
|
|
575
537
|
};
|
|
576
538
|
}
|
|
577
|
-
if (result.memories.length === 0)
|
|
539
|
+
if (result.memories.length === 0 && result.relatedMemories.length === 0)
|
|
578
540
|
return;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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`);
|
|
583
552
|
return {
|
|
584
|
-
prependContext:
|
|
553
|
+
prependContext: context
|
|
585
554
|
};
|
|
586
555
|
}
|
|
587
556
|
catch (error) {
|
|
@@ -596,4 +565,4 @@ export default {
|
|
|
596
565
|
api.logger.info("[MemoryX] Plugin registered successfully");
|
|
597
566
|
}
|
|
598
567
|
};
|
|
599
|
-
export { MemoryXPlugin,
|
|
568
|
+
export { MemoryXPlugin, ConversationManager };
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t0ken.ai/memoryx-openclaw-plugin",
|
|
3
|
-
"version": "1.1.
|
|
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
|
}
|