@t0ken.ai/memoryx-openclaw-plugin 1.1.14 → 1.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +27 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +415 -221
- 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,22 @@ 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
|
-
|
|
84
|
+
forget(memoryId: string): Promise<boolean>;
|
|
85
|
+
list(limit?: number): Promise<any[]>;
|
|
86
|
+
getStatus(): Promise<{
|
|
95
87
|
initialized: boolean;
|
|
96
88
|
hasApiKey: boolean;
|
|
97
|
-
|
|
89
|
+
conversationStatus: {
|
|
98
90
|
messageCount: number;
|
|
99
91
|
conversationId: string;
|
|
92
|
+
rounds: number;
|
|
100
93
|
};
|
|
101
|
-
}
|
|
94
|
+
}>;
|
|
102
95
|
}
|
|
103
96
|
declare const _default: {
|
|
104
97
|
id: string;
|
|
@@ -108,5 +101,5 @@ declare const _default: {
|
|
|
108
101
|
register(api: any, pluginConfig?: PluginConfig): void;
|
|
109
102
|
};
|
|
110
103
|
export default _default;
|
|
111
|
-
export { MemoryXPlugin,
|
|
104
|
+
export { MemoryXPlugin, ConversationManager };
|
|
112
105
|
//# 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,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA6B1C,IAAI,CAAC,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAuCxC,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,wBAyQE;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
|
|
68
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
-
conversation_id TEXT NOT NULL,
|
|
70
|
-
role TEXT NOT NULL,
|
|
71
|
-
content TEXT NOT NULL,
|
|
72
|
-
timestamp INTEGER NOT NULL,
|
|
73
|
-
retry_count INTEGER DEFAULT 0,
|
|
74
|
-
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
CREATE INDEX IF NOT EXISTS idx_pending_conversation ON pending_messages(conversation_id);
|
|
78
|
-
CREATE INDEX IF NOT EXISTS idx_pending_created ON pending_messages(created_at);
|
|
79
|
-
|
|
80
|
-
CREATE TABLE IF NOT EXISTS temp_messages (
|
|
84
|
+
CREATE TABLE IF NOT EXISTS send_queue (
|
|
81
85
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
86
|
conversation_id TEXT NOT NULL,
|
|
87
|
+
conversation_created_at INTEGER NOT NULL,
|
|
83
88
|
role TEXT NOT NULL,
|
|
84
89
|
content TEXT NOT NULL,
|
|
85
90
|
timestamp INTEGER NOT NULL,
|
|
86
|
-
|
|
91
|
+
retry_count INTEGER DEFAULT 0
|
|
87
92
|
);
|
|
88
93
|
|
|
89
|
-
CREATE INDEX IF NOT EXISTS
|
|
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) {
|
|
124
|
+
static async addConversationToQueue(conversationId, conversationCreatedAt, messages) {
|
|
127
125
|
const db = await getDb();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const db = await getDb();
|
|
136
|
-
db.prepare("DELETE FROM pending_messages WHERE id = ?").run(id);
|
|
126
|
+
const stmt = db.prepare(`
|
|
127
|
+
INSERT INTO send_queue (conversation_id, conversation_created_at, role, content, timestamp)
|
|
128
|
+
VALUES (?, ?, ?, ?, ?)
|
|
129
|
+
`);
|
|
130
|
+
for (const msg of messages) {
|
|
131
|
+
stmt.run(conversationId, conversationCreatedAt, msg.role, msg.content, msg.timestamp);
|
|
132
|
+
}
|
|
137
133
|
}
|
|
138
|
-
static async
|
|
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,95 @@ class MemoryXPlugin {
|
|
|
512
468
|
return {
|
|
513
469
|
memories: (data.data || []).map((m) => ({
|
|
514
470
|
id: m.id,
|
|
515
|
-
content: m.content,
|
|
471
|
+
content: m.memory || m.content,
|
|
516
472
|
category: m.category || "other",
|
|
517
473
|
score: m.score || 0.5
|
|
518
474
|
})),
|
|
475
|
+
relatedMemories: (data.related_memories || []).map((m) => ({
|
|
476
|
+
id: m.id,
|
|
477
|
+
content: m.memory || m.content,
|
|
478
|
+
category: m.category || "other",
|
|
479
|
+
score: m.score || 0
|
|
480
|
+
})),
|
|
519
481
|
isLimited: false,
|
|
520
482
|
remainingQuota: data.remaining_quota ?? -1
|
|
521
483
|
};
|
|
522
484
|
}
|
|
523
485
|
catch (e) {
|
|
524
486
|
log(`Recall failed: ${e}`);
|
|
525
|
-
return { memories: [], isLimited: false, remainingQuota: 0 };
|
|
487
|
+
return { memories: [], relatedMemories: [], isLimited: false, remainingQuota: 0 };
|
|
526
488
|
}
|
|
527
489
|
}
|
|
528
490
|
async endConversation() {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
491
|
+
this.conversationManager.startNewConversation();
|
|
492
|
+
log("Conversation ended, starting new conversation");
|
|
493
|
+
}
|
|
494
|
+
async forget(memoryId) {
|
|
495
|
+
this.init();
|
|
496
|
+
if (!this.config.apiKey) {
|
|
497
|
+
log("Forget failed: no API key");
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const response = await fetch(`${this.apiBase}/v1/memories/${memoryId}`, {
|
|
502
|
+
method: "DELETE",
|
|
503
|
+
headers: {
|
|
504
|
+
"X-API-Key": this.config.apiKey
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
if (response.ok) {
|
|
508
|
+
log(`Forgot memory ${memoryId}`);
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
log(`Forget failed: ${response.status}`);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
log(`Forget failed: ${e}`);
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
532
518
|
}
|
|
533
|
-
|
|
519
|
+
async list(limit = 10) {
|
|
520
|
+
this.init();
|
|
521
|
+
if (!this.config.apiKey) {
|
|
522
|
+
log("List failed: no API key");
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const response = await fetch(`${this.apiBase}/v1/memories/search`, {
|
|
527
|
+
method: "POST",
|
|
528
|
+
headers: {
|
|
529
|
+
"Content-Type": "application/json",
|
|
530
|
+
"X-API-Key": this.config.apiKey
|
|
531
|
+
},
|
|
532
|
+
body: JSON.stringify({
|
|
533
|
+
query: "*",
|
|
534
|
+
project_id: this.config.projectId,
|
|
535
|
+
limit
|
|
536
|
+
})
|
|
537
|
+
});
|
|
538
|
+
if (!response.ok) {
|
|
539
|
+
log(`List failed: ${response.status}`);
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
const data = await response.json();
|
|
543
|
+
return (data.data || []).map((m) => ({
|
|
544
|
+
id: m.id,
|
|
545
|
+
content: m.memory || m.content,
|
|
546
|
+
category: m.category || "other"
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
catch (e) {
|
|
550
|
+
log(`List failed: ${e}`);
|
|
551
|
+
return [];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async getStatus() {
|
|
555
|
+
const status = await this.conversationManager.getStatus();
|
|
534
556
|
return {
|
|
535
557
|
initialized: this.config.initialized,
|
|
536
558
|
hasApiKey: !!this.config.apiKey,
|
|
537
|
-
|
|
559
|
+
conversationStatus: status
|
|
538
560
|
};
|
|
539
561
|
}
|
|
540
562
|
}
|
|
@@ -542,7 +564,7 @@ let plugin;
|
|
|
542
564
|
export default {
|
|
543
565
|
id: "memoryx-openclaw-plugin",
|
|
544
566
|
name: "MemoryX Realtime Plugin",
|
|
545
|
-
version: "1.
|
|
567
|
+
version: "1.1.16",
|
|
546
568
|
description: "Real-time memory capture and recall for OpenClaw",
|
|
547
569
|
register(api, pluginConfig) {
|
|
548
570
|
api.logger.info("[MemoryX] Plugin registering...");
|
|
@@ -550,6 +572,171 @@ export default {
|
|
|
550
572
|
api.logger.info(`[MemoryX] API Base: \`${pluginConfig.apiBaseUrl}\``);
|
|
551
573
|
}
|
|
552
574
|
plugin = new MemoryXPlugin(pluginConfig);
|
|
575
|
+
api.registerTool({
|
|
576
|
+
name: "memoryx_recall",
|
|
577
|
+
label: "MemoryX Recall",
|
|
578
|
+
description: "Search through long-term memories. Use when you need context about user preferences, past decisions, or previously discussed topics.",
|
|
579
|
+
parameters: {
|
|
580
|
+
type: "object",
|
|
581
|
+
properties: {
|
|
582
|
+
query: {
|
|
583
|
+
type: "string",
|
|
584
|
+
description: "Search query to find relevant memories"
|
|
585
|
+
},
|
|
586
|
+
limit: {
|
|
587
|
+
type: "number",
|
|
588
|
+
description: "Maximum number of results to return (default: 5)"
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
required: ["query"]
|
|
592
|
+
},
|
|
593
|
+
async execute(_toolCallId, params) {
|
|
594
|
+
const { query, limit = 5 } = params;
|
|
595
|
+
if (!plugin) {
|
|
596
|
+
return {
|
|
597
|
+
content: [{ type: "text", text: "MemoryX plugin not initialized." }],
|
|
598
|
+
details: { error: "not_initialized" }
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
const result = await plugin.recall(query, limit);
|
|
603
|
+
if (result.isLimited) {
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: result.upgradeHint || "Quota exceeded" }],
|
|
606
|
+
details: { error: "quota_exceeded", hint: result.upgradeHint }
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
if (result.memories.length === 0 && result.relatedMemories.length === 0) {
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: "text", text: "No relevant memories found." }],
|
|
612
|
+
details: { count: 0 }
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const lines = [];
|
|
616
|
+
const total = result.memories.length + result.relatedMemories.length;
|
|
617
|
+
if (result.memories.length > 0) {
|
|
618
|
+
lines.push(`Found ${result.memories.length} direct memories:`);
|
|
619
|
+
result.memories.forEach((m, i) => {
|
|
620
|
+
lines.push(`${i + 1}. [${m.category}] ${m.content} (${Math.round(m.score * 100)}%)`);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
if (result.relatedMemories.length > 0) {
|
|
624
|
+
if (lines.length > 0)
|
|
625
|
+
lines.push("");
|
|
626
|
+
lines.push(`Found ${result.relatedMemories.length} related memories:`);
|
|
627
|
+
result.relatedMemories.forEach((m, i) => {
|
|
628
|
+
lines.push(`${i + 1}. [${m.category}] ${m.content}`);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
633
|
+
details: {
|
|
634
|
+
count: total,
|
|
635
|
+
direct_count: result.memories.length,
|
|
636
|
+
related_count: result.relatedMemories.length,
|
|
637
|
+
remaining_quota: result.remainingQuota
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
return {
|
|
643
|
+
content: [{ type: "text", text: `Memory search failed: ${error.message}` }],
|
|
644
|
+
details: { error: error.message }
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}, { name: "memoryx_recall" });
|
|
649
|
+
api.registerTool({
|
|
650
|
+
name: "memoryx_forget",
|
|
651
|
+
label: "MemoryX Forget",
|
|
652
|
+
description: "Delete specific memories. Use when user explicitly asks to forget or remove something from memory.",
|
|
653
|
+
parameters: {
|
|
654
|
+
type: "object",
|
|
655
|
+
properties: {
|
|
656
|
+
memory_id: {
|
|
657
|
+
type: "string",
|
|
658
|
+
description: "The ID of the memory to delete"
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
required: ["memory_id"]
|
|
662
|
+
},
|
|
663
|
+
async execute(_toolCallId, params) {
|
|
664
|
+
const { memory_id } = params;
|
|
665
|
+
if (!plugin) {
|
|
666
|
+
return {
|
|
667
|
+
content: [{ type: "text", text: "MemoryX plugin not initialized." }],
|
|
668
|
+
details: { error: "not_initialized" }
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const success = await plugin.forget(memory_id);
|
|
673
|
+
if (success) {
|
|
674
|
+
return {
|
|
675
|
+
content: [{ type: "text", text: `Memory ${memory_id} has been forgotten.` }],
|
|
676
|
+
details: { action: "deleted", id: memory_id }
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
return {
|
|
681
|
+
content: [{ type: "text", text: `Memory ${memory_id} not found or could not be deleted.` }],
|
|
682
|
+
details: { action: "failed", id: memory_id }
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
return {
|
|
688
|
+
content: [{ type: "text", text: `Failed to forget memory: ${error.message}` }],
|
|
689
|
+
details: { error: error.message }
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}, { name: "memoryx_forget" });
|
|
694
|
+
api.registerTool({
|
|
695
|
+
name: "memoryx_list",
|
|
696
|
+
label: "MemoryX List",
|
|
697
|
+
description: "List all stored memories. Use when user asks what you remember about them.",
|
|
698
|
+
parameters: {
|
|
699
|
+
type: "object",
|
|
700
|
+
properties: {
|
|
701
|
+
limit: {
|
|
702
|
+
type: "number",
|
|
703
|
+
description: "Maximum number of memories to list (default: 10)"
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
async execute(_toolCallId, params) {
|
|
708
|
+
const { limit = 10 } = params;
|
|
709
|
+
if (!plugin) {
|
|
710
|
+
return {
|
|
711
|
+
content: [{ type: "text", text: "MemoryX plugin not initialized." }],
|
|
712
|
+
details: { error: "not_initialized" }
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
const memories = await plugin.list(limit);
|
|
717
|
+
if (memories.length === 0) {
|
|
718
|
+
return {
|
|
719
|
+
content: [{ type: "text", text: "No memories stored yet." }],
|
|
720
|
+
details: { count: 0 }
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const lines = [`Here are the ${memories.length} most recent memories:`];
|
|
724
|
+
memories.forEach((m, i) => {
|
|
725
|
+
lines.push(`${i + 1}. [${m.category || 'general'}] ${m.content || m.memory}`);
|
|
726
|
+
});
|
|
727
|
+
return {
|
|
728
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
729
|
+
details: { count: memories.length }
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
return {
|
|
734
|
+
content: [{ type: "text", text: `Failed to list memories: ${error.message}` }],
|
|
735
|
+
details: { error: error.message }
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}, { name: "memoryx_list" });
|
|
553
740
|
api.on("message_received", async (event, ctx) => {
|
|
554
741
|
const { content } = event;
|
|
555
742
|
if (content && plugin) {
|
|
@@ -571,17 +758,24 @@ export default {
|
|
|
571
758
|
if (result.isLimited) {
|
|
572
759
|
api.logger.warn(`[MemoryX] ${result.upgradeHint}`);
|
|
573
760
|
return {
|
|
574
|
-
prependContext: `[
|
|
761
|
+
prependContext: `[System] ${result.upgradeHint}\n`
|
|
575
762
|
};
|
|
576
763
|
}
|
|
577
|
-
if (result.memories.length === 0)
|
|
764
|
+
if (result.memories.length === 0 && result.relatedMemories.length === 0)
|
|
578
765
|
return;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
766
|
+
let context = "MemoryX by t0ken.ai found the following memories:\n";
|
|
767
|
+
if (result.memories.length > 0) {
|
|
768
|
+
context += "\n[Direct Memories]\n";
|
|
769
|
+
context += result.memories.map(m => `- ${m.content}`).join("\n");
|
|
770
|
+
}
|
|
771
|
+
if (result.relatedMemories.length > 0) {
|
|
772
|
+
context += "\n\n[Related Memories]\n";
|
|
773
|
+
context += result.relatedMemories.map(m => `- ${m.content}`).join("\n");
|
|
774
|
+
}
|
|
775
|
+
context += "\n";
|
|
776
|
+
api.logger.info(`[MemoryX] Recalled ${result.memories.length} direct + ${result.relatedMemories.length} related memories`);
|
|
583
777
|
return {
|
|
584
|
-
prependContext:
|
|
778
|
+
prependContext: context
|
|
585
779
|
};
|
|
586
780
|
}
|
|
587
781
|
catch (error) {
|
|
@@ -596,4 +790,4 @@ export default {
|
|
|
596
790
|
api.logger.info("[MemoryX] Plugin registered successfully");
|
|
597
791
|
}
|
|
598
792
|
};
|
|
599
|
-
export { MemoryXPlugin,
|
|
793
|
+
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.16",
|
|
4
4
|
"description": "MemoryX real-time memory capture and recall plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "MemoryX Team",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"typescript": "^5.0.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"better-sqlite3": "^11.0.0"
|
|
39
|
+
"better-sqlite3": "^11.0.0",
|
|
40
|
+
"js-tiktoken": "^1.0.21"
|
|
40
41
|
}
|
|
41
42
|
}
|