@t0ken.ai/memoryx-openclaw-plugin 1.1.9 → 1.1.11
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 +8 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +245 -94
- package/package.json +5 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MemoryX Realtime Plugin for OpenClaw
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - ConversationBuffer with token counting
|
|
6
|
-
* - Batch upload to /conversations/flush
|
|
7
|
-
* - Auto-register and quota handling
|
|
8
|
-
* - Sensitive data filtered on server
|
|
9
|
-
* - Configurable API base URL
|
|
10
|
-
* - Precise token counting with tiktoken
|
|
11
|
-
*/
|
|
12
1
|
interface PluginConfig {
|
|
13
2
|
apiBaseUrl?: string;
|
|
14
3
|
}
|
|
@@ -40,6 +29,7 @@ declare class ConversationBuffer {
|
|
|
40
29
|
private readonly TIMEOUT_MS;
|
|
41
30
|
constructor();
|
|
42
31
|
private generateId;
|
|
32
|
+
getConversationId(): string;
|
|
43
33
|
addMessage(role: string, content: string): boolean;
|
|
44
34
|
shouldFlush(): boolean;
|
|
45
35
|
flush(): {
|
|
@@ -59,15 +49,22 @@ declare class MemoryXPlugin {
|
|
|
59
49
|
private config;
|
|
60
50
|
private buffer;
|
|
61
51
|
private flushTimer;
|
|
52
|
+
private pendingRetryTimer;
|
|
62
53
|
private readonly FLUSH_CHECK_INTERVAL;
|
|
54
|
+
private readonly PENDING_RETRY_INTERVAL;
|
|
55
|
+
private readonly MAX_RETRY_COUNT;
|
|
63
56
|
private pluginConfig;
|
|
57
|
+
private initialized;
|
|
64
58
|
constructor(pluginConfig?: PluginConfig);
|
|
59
|
+
init(): void;
|
|
65
60
|
private get apiBase();
|
|
66
61
|
private loadConfig;
|
|
67
62
|
private saveConfig;
|
|
68
63
|
private autoRegister;
|
|
69
64
|
private getMachineFingerprint;
|
|
70
65
|
private startFlushTimer;
|
|
66
|
+
private startPendingRetryTimer;
|
|
67
|
+
private retryPendingMessages;
|
|
71
68
|
private flushConversation;
|
|
72
69
|
onMessage(role: string, content: string): Promise<boolean>;
|
|
73
70
|
recall(query: string, limit?: number): Promise<RecallResult>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkCA,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;IAgC1B,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"}
|
package/dist/index.js
CHANGED
|
@@ -1,79 +1,158 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MemoryX Realtime Plugin for OpenClaw
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - ConversationBuffer with token counting
|
|
6
|
-
* - Batch upload to /conversations/flush
|
|
7
|
-
* - Auto-register and quota handling
|
|
8
|
-
* - Sensitive data filtered on server
|
|
9
|
-
* - Configurable API base URL
|
|
10
|
-
* - Precise token counting with tiktoken
|
|
11
|
-
*/
|
|
12
1
|
import * as fs from "fs";
|
|
13
2
|
import * as path from "path";
|
|
14
3
|
import * as os from "os";
|
|
15
4
|
import * as crypto from "crypto";
|
|
16
5
|
const DEFAULT_API_BASE = "https://t0ken.ai/api";
|
|
17
|
-
|
|
6
|
+
const PLUGIN_DIR = path.join(os.homedir(), ".openclaw", "extensions", "memoryx-openclaw-plugin");
|
|
18
7
|
let logStream = null;
|
|
19
|
-
let
|
|
20
|
-
function
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const dir = path.dirname(logPath);
|
|
25
|
-
if (!fs.existsSync(dir)) {
|
|
26
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
return logPath;
|
|
29
|
-
}
|
|
30
|
-
function getConfigPath() {
|
|
31
|
-
if (configPath)
|
|
32
|
-
return configPath;
|
|
33
|
-
configPath = path.join(os.homedir(), ".openclaw", "extensions", "memoryx-openclaw-plugin", "config.json");
|
|
34
|
-
const dir = path.dirname(configPath);
|
|
35
|
-
if (!fs.existsSync(dir)) {
|
|
36
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
-
}
|
|
38
|
-
return configPath;
|
|
8
|
+
let logStreamReady = false;
|
|
9
|
+
function ensureDir() {
|
|
10
|
+
if (!fs.existsSync(PLUGIN_DIR)) {
|
|
11
|
+
fs.mkdirSync(PLUGIN_DIR, { recursive: true });
|
|
12
|
+
}
|
|
39
13
|
}
|
|
40
14
|
function log(message) {
|
|
41
|
-
const timestamp = new Date().toISOString();
|
|
42
|
-
const line = `[${timestamp}] ${message}\n`;
|
|
43
|
-
try {
|
|
44
|
-
if (!logStream) {
|
|
45
|
-
logStream = fs.createWriteStream(getLogPath(), { flags: "a" });
|
|
46
|
-
}
|
|
47
|
-
logStream.write(line);
|
|
48
|
-
}
|
|
49
|
-
catch (e) {
|
|
50
|
-
console.error("[MemoryX] Log write failed:", e);
|
|
51
|
-
}
|
|
52
15
|
console.log(`[MemoryX] ${message}`);
|
|
16
|
+
setImmediate(() => {
|
|
17
|
+
try {
|
|
18
|
+
if (!logStreamReady) {
|
|
19
|
+
ensureDir();
|
|
20
|
+
logStream = fs.createWriteStream(path.join(PLUGIN_DIR, "plugin.log"), { flags: "a" });
|
|
21
|
+
logStreamReady = true;
|
|
22
|
+
}
|
|
23
|
+
logStream?.write(`[${new Date().toISOString()}] ${message}\n`);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
// ignore
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
let dbPromise = null;
|
|
31
|
+
async function getDb() {
|
|
32
|
+
if (dbPromise)
|
|
33
|
+
return dbPromise;
|
|
34
|
+
dbPromise = (async () => {
|
|
35
|
+
ensureDir();
|
|
36
|
+
const Database = (await import("better-sqlite3")).default;
|
|
37
|
+
const dbPath = path.join(PLUGIN_DIR, "memoryx.db");
|
|
38
|
+
const db = new Database(dbPath);
|
|
39
|
+
db.exec(`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS config (
|
|
41
|
+
key TEXT PRIMARY KEY,
|
|
42
|
+
value TEXT
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS pending_messages (
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
conversation_id TEXT NOT NULL,
|
|
48
|
+
role TEXT NOT NULL,
|
|
49
|
+
content TEXT NOT NULL,
|
|
50
|
+
timestamp INTEGER NOT NULL,
|
|
51
|
+
retry_count INTEGER DEFAULT 0,
|
|
52
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_pending_conversation ON pending_messages(conversation_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_pending_created ON pending_messages(created_at);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS temp_messages (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
conversation_id TEXT NOT NULL,
|
|
61
|
+
role TEXT NOT NULL,
|
|
62
|
+
content TEXT NOT NULL,
|
|
63
|
+
timestamp INTEGER NOT NULL,
|
|
64
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_temp_conversation ON temp_messages(conversation_id);
|
|
68
|
+
`);
|
|
69
|
+
return db;
|
|
70
|
+
})();
|
|
71
|
+
return dbPromise;
|
|
53
72
|
}
|
|
54
|
-
class
|
|
55
|
-
static
|
|
73
|
+
class SQLiteStorage {
|
|
74
|
+
static async loadConfig() {
|
|
56
75
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return JSON.parse(
|
|
76
|
+
const db = await getDb();
|
|
77
|
+
const row = db.prepare("SELECT value FROM config WHERE key = 'config'").get();
|
|
78
|
+
if (row) {
|
|
79
|
+
return JSON.parse(row.value);
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
82
|
catch (e) {
|
|
64
|
-
|
|
83
|
+
log(`Failed to load config: ${e}`);
|
|
65
84
|
}
|
|
66
85
|
return null;
|
|
67
86
|
}
|
|
68
|
-
static
|
|
87
|
+
static async saveConfig(config) {
|
|
69
88
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
89
|
+
const db = await getDb();
|
|
90
|
+
db.prepare("INSERT OR REPLACE INTO config (key, value) VALUES ('config', ?)").run(JSON.stringify(config));
|
|
72
91
|
}
|
|
73
92
|
catch (e) {
|
|
74
|
-
|
|
93
|
+
log(`Failed to save config: ${e}`);
|
|
75
94
|
}
|
|
76
95
|
}
|
|
96
|
+
static async addPendingMessage(conversationId, role, content) {
|
|
97
|
+
const db = await getDb();
|
|
98
|
+
const result = db.prepare(`
|
|
99
|
+
INSERT INTO pending_messages (conversation_id, role, content, timestamp)
|
|
100
|
+
VALUES (?, ?, ?, ?)
|
|
101
|
+
`).run(conversationId, role, content, Date.now());
|
|
102
|
+
return result.lastInsertRowid;
|
|
103
|
+
}
|
|
104
|
+
static async getPendingMessages(limit = 100) {
|
|
105
|
+
const db = await getDb();
|
|
106
|
+
return db.prepare(`
|
|
107
|
+
SELECT * FROM pending_messages
|
|
108
|
+
ORDER BY created_at ASC
|
|
109
|
+
LIMIT ?
|
|
110
|
+
`).all(limit);
|
|
111
|
+
}
|
|
112
|
+
static async deletePendingMessage(id) {
|
|
113
|
+
const db = await getDb();
|
|
114
|
+
db.prepare("DELETE FROM pending_messages WHERE id = ?").run(id);
|
|
115
|
+
}
|
|
116
|
+
static async incrementRetryCount(id) {
|
|
117
|
+
const db = await getDb();
|
|
118
|
+
db.prepare("UPDATE pending_messages SET retry_count = retry_count + 1 WHERE id = ?").run(id);
|
|
119
|
+
}
|
|
120
|
+
static async clearOldPendingMessages(maxAge = 7 * 24 * 60 * 60) {
|
|
121
|
+
const db = await getDb();
|
|
122
|
+
const cutoff = Math.floor(Date.now() / 1000) - maxAge;
|
|
123
|
+
db.prepare("DELETE FROM pending_messages WHERE created_at < ?").run(cutoff);
|
|
124
|
+
}
|
|
125
|
+
static async addTempMessage(conversationId, role, content) {
|
|
126
|
+
const db = await getDb();
|
|
127
|
+
const result = db.prepare(`
|
|
128
|
+
INSERT INTO temp_messages (conversation_id, role, content, timestamp)
|
|
129
|
+
VALUES (?, ?, ?, ?)
|
|
130
|
+
`).run(conversationId, role, content, Date.now());
|
|
131
|
+
return result.lastInsertRowid;
|
|
132
|
+
}
|
|
133
|
+
static async getTempMessages(conversationId) {
|
|
134
|
+
const db = await getDb();
|
|
135
|
+
const rows = db.prepare(`
|
|
136
|
+
SELECT role, content, timestamp FROM temp_messages
|
|
137
|
+
WHERE conversation_id = ?
|
|
138
|
+
ORDER BY timestamp ASC
|
|
139
|
+
`).all(conversationId);
|
|
140
|
+
return rows.map((r) => ({
|
|
141
|
+
role: r.role,
|
|
142
|
+
content: r.content,
|
|
143
|
+
tokens: 0,
|
|
144
|
+
timestamp: r.timestamp
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
static async clearTempMessages(conversationId) {
|
|
148
|
+
const db = await getDb();
|
|
149
|
+
db.prepare("DELETE FROM temp_messages WHERE conversation_id = ?").run(conversationId);
|
|
150
|
+
}
|
|
151
|
+
static async clearOldTempMessages(maxAge = 24 * 60 * 60) {
|
|
152
|
+
const db = await getDb();
|
|
153
|
+
const cutoff = Math.floor(Date.now() / 1000) - maxAge;
|
|
154
|
+
db.prepare("DELETE FROM temp_messages WHERE created_at < ?").run(cutoff);
|
|
155
|
+
}
|
|
77
156
|
}
|
|
78
157
|
class ConversationBuffer {
|
|
79
158
|
messages = [];
|
|
@@ -90,6 +169,9 @@ class ConversationBuffer {
|
|
|
90
169
|
generateId() {
|
|
91
170
|
return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
92
171
|
}
|
|
172
|
+
getConversationId() {
|
|
173
|
+
return this.conversationId;
|
|
174
|
+
}
|
|
93
175
|
addMessage(role, content) {
|
|
94
176
|
if (!content || content.length < 2) {
|
|
95
177
|
return false;
|
|
@@ -157,39 +239,40 @@ class MemoryXPlugin {
|
|
|
157
239
|
};
|
|
158
240
|
buffer = new ConversationBuffer();
|
|
159
241
|
flushTimer = null;
|
|
242
|
+
pendingRetryTimer = null;
|
|
160
243
|
FLUSH_CHECK_INTERVAL = 30000;
|
|
244
|
+
PENDING_RETRY_INTERVAL = 60000;
|
|
245
|
+
MAX_RETRY_COUNT = 5;
|
|
161
246
|
pluginConfig = null;
|
|
247
|
+
initialized = false;
|
|
162
248
|
constructor(pluginConfig) {
|
|
163
|
-
log("Constructor started");
|
|
164
249
|
this.pluginConfig = pluginConfig || null;
|
|
165
250
|
if (pluginConfig?.apiBaseUrl) {
|
|
166
251
|
this.config.apiBaseUrl = pluginConfig.apiBaseUrl;
|
|
167
|
-
log(`API Base URL set to: ${pluginConfig.apiBaseUrl}`);
|
|
168
252
|
}
|
|
169
253
|
this.config.initialized = true;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
log(`Async init error: ${e}`);
|
|
254
|
+
}
|
|
255
|
+
init() {
|
|
256
|
+
if (this.initialized)
|
|
257
|
+
return;
|
|
258
|
+
this.initialized = true;
|
|
259
|
+
log("Async init started");
|
|
260
|
+
this.loadConfig().then(() => {
|
|
261
|
+
log(`Config loaded, apiKey: ${this.config.apiKey ? 'present' : 'missing'}`);
|
|
262
|
+
this.startFlushTimer();
|
|
263
|
+
this.startPendingRetryTimer();
|
|
264
|
+
this.retryPendingMessages();
|
|
265
|
+
if (!this.config.apiKey) {
|
|
266
|
+
log("Starting auto-register");
|
|
267
|
+
this.autoRegister().catch(e => log(`Auto-register failed: ${e}`));
|
|
185
268
|
}
|
|
186
|
-
});
|
|
269
|
+
}).catch(e => log(`Init failed: ${e}`));
|
|
187
270
|
}
|
|
188
271
|
get apiBase() {
|
|
189
272
|
return this.config.apiBaseUrl || DEFAULT_API_BASE;
|
|
190
273
|
}
|
|
191
|
-
loadConfig() {
|
|
192
|
-
const stored =
|
|
274
|
+
async loadConfig() {
|
|
275
|
+
const stored = await SQLiteStorage.loadConfig();
|
|
193
276
|
if (stored) {
|
|
194
277
|
this.config = {
|
|
195
278
|
...this.config,
|
|
@@ -198,8 +281,8 @@ class MemoryXPlugin {
|
|
|
198
281
|
};
|
|
199
282
|
}
|
|
200
283
|
}
|
|
201
|
-
saveConfig() {
|
|
202
|
-
|
|
284
|
+
async saveConfig() {
|
|
285
|
+
await SQLiteStorage.saveConfig(this.config);
|
|
203
286
|
}
|
|
204
287
|
async autoRegister() {
|
|
205
288
|
try {
|
|
@@ -222,11 +305,11 @@ class MemoryXPlugin {
|
|
|
222
305
|
this.config.apiKey = data.api_key;
|
|
223
306
|
this.config.projectId = String(data.project_id);
|
|
224
307
|
this.config.userId = data.agent_id;
|
|
225
|
-
this.saveConfig();
|
|
226
|
-
|
|
308
|
+
await this.saveConfig();
|
|
309
|
+
log("Auto-registered successfully");
|
|
227
310
|
}
|
|
228
311
|
catch (e) {
|
|
229
|
-
|
|
312
|
+
log(`Auto-register failed: ${e}`);
|
|
230
313
|
}
|
|
231
314
|
}
|
|
232
315
|
getMachineFingerprint() {
|
|
@@ -247,8 +330,70 @@ class MemoryXPlugin {
|
|
|
247
330
|
}
|
|
248
331
|
}, this.FLUSH_CHECK_INTERVAL);
|
|
249
332
|
}
|
|
333
|
+
startPendingRetryTimer() {
|
|
334
|
+
this.pendingRetryTimer = setInterval(() => {
|
|
335
|
+
this.retryPendingMessages();
|
|
336
|
+
}, this.PENDING_RETRY_INTERVAL);
|
|
337
|
+
}
|
|
338
|
+
async retryPendingMessages() {
|
|
339
|
+
if (!this.config.apiKey)
|
|
340
|
+
return;
|
|
341
|
+
try {
|
|
342
|
+
const pending = await SQLiteStorage.getPendingMessages(50);
|
|
343
|
+
if (pending.length === 0)
|
|
344
|
+
return;
|
|
345
|
+
log(`Retrying ${pending.length} pending messages`);
|
|
346
|
+
for (const msg of pending) {
|
|
347
|
+
if (msg.retry_count >= this.MAX_RETRY_COUNT) {
|
|
348
|
+
log(`Deleting message ${msg.id}: max retries exceeded`);
|
|
349
|
+
await SQLiteStorage.deletePendingMessage(msg.id);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const response = await fetch(`${this.apiBase}/v1/conversations/flush`, {
|
|
354
|
+
method: "POST",
|
|
355
|
+
headers: {
|
|
356
|
+
"Content-Type": "application/json",
|
|
357
|
+
"X-API-Key": this.config.apiKey
|
|
358
|
+
},
|
|
359
|
+
body: JSON.stringify({
|
|
360
|
+
conversation_id: msg.conversation_id,
|
|
361
|
+
messages: [{
|
|
362
|
+
role: msg.role,
|
|
363
|
+
content: msg.content,
|
|
364
|
+
timestamp: msg.timestamp,
|
|
365
|
+
tokens: 0
|
|
366
|
+
}]
|
|
367
|
+
})
|
|
368
|
+
});
|
|
369
|
+
if (response.ok) {
|
|
370
|
+
await SQLiteStorage.deletePendingMessage(msg.id);
|
|
371
|
+
log(`Sent pending message ${msg.id}`);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
await SQLiteStorage.incrementRetryCount(msg.id);
|
|
375
|
+
log(`Failed to send pending message ${msg.id}: ${response.status}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
await SQLiteStorage.incrementRetryCount(msg.id);
|
|
380
|
+
log(`Error sending pending message ${msg.id}: ${e}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch (e) {
|
|
385
|
+
log(`Retry pending messages failed: ${e}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
250
388
|
async flushConversation() {
|
|
251
389
|
if (!this.config.apiKey) {
|
|
390
|
+
const data = this.buffer.forceFlush();
|
|
391
|
+
if (data) {
|
|
392
|
+
for (const msg of data.messages) {
|
|
393
|
+
await SQLiteStorage.addPendingMessage(data.conversation_id, msg.role, msg.content);
|
|
394
|
+
}
|
|
395
|
+
log(`Cached ${data.messages.length} messages (no API key)`);
|
|
396
|
+
}
|
|
252
397
|
return;
|
|
253
398
|
}
|
|
254
399
|
const data = this.buffer.forceFlush();
|
|
@@ -267,22 +412,31 @@ class MemoryXPlugin {
|
|
|
267
412
|
if (!response.ok) {
|
|
268
413
|
const errorData = await response.json().catch(() => ({}));
|
|
269
414
|
if (response.status === 402) {
|
|
270
|
-
|
|
415
|
+
log(`Quota exceeded: ${errorData.detail}`);
|
|
271
416
|
}
|
|
272
417
|
else {
|
|
273
|
-
|
|
418
|
+
log(`Flush failed: ${JSON.stringify(errorData)}`);
|
|
419
|
+
}
|
|
420
|
+
for (const msg of data.messages) {
|
|
421
|
+
await SQLiteStorage.addPendingMessage(data.conversation_id, msg.role, msg.content);
|
|
274
422
|
}
|
|
423
|
+
log(`Cached ${data.messages.length} messages (API error)`);
|
|
275
424
|
}
|
|
276
425
|
else {
|
|
277
426
|
const result = await response.json();
|
|
278
|
-
|
|
427
|
+
log(`Flushed ${data.messages.length} messages, extracted ${result.extracted_count} memories`);
|
|
279
428
|
}
|
|
280
429
|
}
|
|
281
430
|
catch (e) {
|
|
282
|
-
|
|
431
|
+
log(`Flush error: ${e}`);
|
|
432
|
+
for (const msg of data.messages) {
|
|
433
|
+
await SQLiteStorage.addPendingMessage(data.conversation_id, msg.role, msg.content);
|
|
434
|
+
}
|
|
435
|
+
log(`Cached ${data.messages.length} messages (network error)`);
|
|
283
436
|
}
|
|
284
437
|
}
|
|
285
438
|
async onMessage(role, content) {
|
|
439
|
+
this.init();
|
|
286
440
|
if (!content || content.length < 2) {
|
|
287
441
|
return false;
|
|
288
442
|
}
|
|
@@ -295,6 +449,7 @@ class MemoryXPlugin {
|
|
|
295
449
|
return false;
|
|
296
450
|
}
|
|
297
451
|
}
|
|
452
|
+
await SQLiteStorage.addTempMessage(this.buffer.getConversationId(), role, content);
|
|
298
453
|
const shouldFlush = this.buffer.addMessage(role, content);
|
|
299
454
|
if (shouldFlush) {
|
|
300
455
|
await this.flushConversation();
|
|
@@ -302,6 +457,7 @@ class MemoryXPlugin {
|
|
|
302
457
|
return true;
|
|
303
458
|
}
|
|
304
459
|
async recall(query, limit = 5) {
|
|
460
|
+
this.init();
|
|
305
461
|
if (!this.config.apiKey || !query || query.length < 2) {
|
|
306
462
|
return { memories: [], isLimited: false, remainingQuota: 0 };
|
|
307
463
|
}
|
|
@@ -343,13 +499,14 @@ class MemoryXPlugin {
|
|
|
343
499
|
};
|
|
344
500
|
}
|
|
345
501
|
catch (e) {
|
|
346
|
-
|
|
502
|
+
log(`Recall failed: ${e}`);
|
|
347
503
|
return { memories: [], isLimited: false, remainingQuota: 0 };
|
|
348
504
|
}
|
|
349
505
|
}
|
|
350
506
|
async endConversation() {
|
|
351
507
|
await this.flushConversation();
|
|
352
|
-
|
|
508
|
+
await SQLiteStorage.clearTempMessages(this.buffer.getConversationId());
|
|
509
|
+
log("Conversation ended, buffer flushed");
|
|
353
510
|
}
|
|
354
511
|
getStatus() {
|
|
355
512
|
return {
|
|
@@ -362,23 +519,17 @@ class MemoryXPlugin {
|
|
|
362
519
|
let plugin;
|
|
363
520
|
export default {
|
|
364
521
|
id: "memoryx-openclaw-plugin",
|
|
365
|
-
name: "MemoryX
|
|
366
|
-
version: "1.
|
|
522
|
+
name: "MemoryX Realtime Plugin",
|
|
523
|
+
version: "1.2.0",
|
|
367
524
|
description: "Real-time memory capture and recall for OpenClaw",
|
|
368
525
|
register(api, pluginConfig) {
|
|
369
|
-
log("=== REGISTER CALLED ===");
|
|
370
526
|
api.logger.info("[MemoryX] Plugin registering...");
|
|
371
527
|
if (pluginConfig?.apiBaseUrl) {
|
|
372
528
|
api.logger.info(`[MemoryX] API Base: \`${pluginConfig.apiBaseUrl}\``);
|
|
373
529
|
}
|
|
374
|
-
api.logger.info(`[MemoryX] Config: ${getConfigPath()}`);
|
|
375
|
-
log(`Config path: ${getConfigPath()}`);
|
|
376
|
-
log(`Log file: ${getLogPath()}`);
|
|
377
|
-
log("Creating plugin instance");
|
|
378
530
|
plugin = new MemoryXPlugin(pluginConfig);
|
|
379
|
-
log("Plugin instance created");
|
|
380
531
|
api.on("message_received", async (event, ctx) => {
|
|
381
|
-
const { content
|
|
532
|
+
const { content } = event;
|
|
382
533
|
if (content && plugin) {
|
|
383
534
|
await plugin.onMessage("user", content);
|
|
384
535
|
}
|
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.11",
|
|
4
4
|
"description": "MemoryX real-time memory capture and recall plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "MemoryX Team",
|
|
@@ -31,8 +31,11 @@
|
|
|
31
31
|
]
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
34
35
|
"@types/node": "^20.0.0",
|
|
35
36
|
"typescript": "^5.0.0"
|
|
36
37
|
},
|
|
37
|
-
"dependencies": {
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"better-sqlite3": "^11.0.0"
|
|
40
|
+
}
|
|
38
41
|
}
|