@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 +25 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +194 -230
- 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}`));
|
|
@@ -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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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.
|
|
356
|
-
this.
|
|
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
|
-
|
|
361
|
-
this.
|
|
362
|
-
this.
|
|
363
|
-
}, this.
|
|
351
|
+
startSendQueueTimer() {
|
|
352
|
+
this.sendQueueTimer = setInterval(() => {
|
|
353
|
+
this.processSendQueue();
|
|
354
|
+
}, this.SEND_QUEUE_INTERVAL);
|
|
364
355
|
}
|
|
365
|
-
async
|
|
356
|
+
async processSendQueue() {
|
|
357
|
+
if (isSending)
|
|
358
|
+
return;
|
|
366
359
|
if (!this.config.apiKey)
|
|
367
360
|
return;
|
|
361
|
+
isSending = true;
|
|
368
362
|
try {
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
log(`
|
|
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(`
|
|
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
|
|
480
|
-
const shouldFlush = this.buffer.addMessage(role, content);
|
|
430
|
+
const shouldFlush = await this.conversationManager.addMessage(role, content);
|
|
481
431
|
if (shouldFlush) {
|
|
482
|
-
|
|
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
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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: `[
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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:
|
|
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,
|
|
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
|
}
|