openclaw-memory-alibaba-mysql 0.2.4 → 0.2.6
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/README.md +2 -2
- package/config.ts +37 -4
- package/db.ts +155 -181
- package/index.ts +60 -18
- package/openclaw.plugin.json +4 -4
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -85,8 +85,8 @@ OpenClaw 记忆插件,使用阿里云 RDS MySQL 做向量存储。支持用户
|
|
|
85
85
|
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
86
86
|
},
|
|
87
87
|
"memory_duplication_conflict_process": true,
|
|
88
|
-
"similarityThresholdUserMemory": 0.
|
|
89
|
-
"similarityThresholdSelfImproving": 0.
|
|
88
|
+
"similarityThresholdUserMemory": 0.65,
|
|
89
|
+
"similarityThresholdSelfImproving": 0.62,
|
|
90
90
|
"enableFullContextMemory": true,
|
|
91
91
|
"enableSelfImprovingMemory": true,
|
|
92
92
|
"memoryExtractionMethod": "llm",
|
package/config.ts
CHANGED
|
@@ -24,8 +24,10 @@ export type LLMConfig = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export type MemoryConfig = {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/** Omitted when plugin is loaded without DB config (e.g. npm install); required at runtime for memory ops. */
|
|
28
|
+
mysql?: MysqlConnectionConfig;
|
|
29
|
+
/** Omitted when plugin is loaded without DB config; required at runtime for memory ops. */
|
|
30
|
+
embedding?: EmbeddingConfig;
|
|
29
31
|
/** When true, use LLM to decide insert vs update among top-10 similar memories; requires llm config. Default false. */
|
|
30
32
|
memory_duplication_conflict_process: boolean;
|
|
31
33
|
/** Required when memory_duplication_conflict_process is true. */
|
|
@@ -204,6 +206,37 @@ export const memoryConfigSchema = {
|
|
|
204
206
|
"memory config",
|
|
205
207
|
);
|
|
206
208
|
|
|
209
|
+
// --- When DB config is missing, return minimal config without throwing (e.g. npm install) ---
|
|
210
|
+
if (!cfg.mysql || typeof cfg.mysql !== "object" || Array.isArray(cfg.mysql) ||
|
|
211
|
+
!cfg.embedding || typeof cfg.embedding !== "object" || Array.isArray(cfg.embedding)) {
|
|
212
|
+
const tableName =
|
|
213
|
+
typeof cfg.tableName === "string" && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(cfg.tableName)
|
|
214
|
+
? cfg.tableName
|
|
215
|
+
: DEFAULT_TABLE_NAME;
|
|
216
|
+
const capChars =
|
|
217
|
+
typeof cfg.captureMaxChars === "number" && cfg.captureMaxChars >= 100 && cfg.captureMaxChars <= 100_000
|
|
218
|
+
? Math.floor(cfg.captureMaxChars)
|
|
219
|
+
: DEFAULT_CAPTURE_MAX_CHARS;
|
|
220
|
+
return {
|
|
221
|
+
mysql: undefined,
|
|
222
|
+
embedding: undefined,
|
|
223
|
+
memory_duplication_conflict_process: false,
|
|
224
|
+
llm: undefined,
|
|
225
|
+
similarityThresholdUserMemory: 0.65,
|
|
226
|
+
similarityThresholdSelfImproving: 0.62,
|
|
227
|
+
enableFullContextMemory: false,
|
|
228
|
+
enableSelfImprovingMemory: false,
|
|
229
|
+
memoryExtractionMethod: "llm",
|
|
230
|
+
autoRecall: cfg.autoRecall !== false,
|
|
231
|
+
autoCapture: cfg.autoCapture !== false,
|
|
232
|
+
captureMaxChars: capChars,
|
|
233
|
+
enableMemoryDecay: false,
|
|
234
|
+
memoryDecayHalfLifeDays: 30,
|
|
235
|
+
memoryDecayStrategy: "exponential",
|
|
236
|
+
tableName,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
207
240
|
// --- LLM requirement ---
|
|
208
241
|
const memory_duplication_conflict_process = cfg.memory_duplication_conflict_process === true;
|
|
209
242
|
const rawMethod =
|
|
@@ -223,11 +256,11 @@ export const memoryConfigSchema = {
|
|
|
223
256
|
const similarityThresholdUserMemory =
|
|
224
257
|
typeof cfg.similarityThresholdUserMemory === "number"
|
|
225
258
|
? cfg.similarityThresholdUserMemory
|
|
226
|
-
: 0.
|
|
259
|
+
: 0.65;
|
|
227
260
|
const similarityThresholdSelfImproving =
|
|
228
261
|
typeof cfg.similarityThresholdSelfImproving === "number"
|
|
229
262
|
? cfg.similarityThresholdSelfImproving
|
|
230
|
-
: 0.
|
|
263
|
+
: 0.62;
|
|
231
264
|
if (
|
|
232
265
|
similarityThresholdUserMemory < 0 ||
|
|
233
266
|
similarityThresholdUserMemory > 1 ||
|
package/db.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import mysql from "mysql2/promise";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Connection } from "mysql2/promise";
|
|
4
4
|
import type { MemoryCategory } from "./config.js";
|
|
5
5
|
import { USER_MEMORY_FACT } from "./categories.js";
|
|
6
6
|
import type { MysqlConnectionConfig } from "./config.js";
|
|
@@ -29,8 +29,6 @@ function stripFourByteUtf8(text: string): string {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export class MemoryDB {
|
|
32
|
-
private pool: Pool | null = null;
|
|
33
|
-
private initPromise: Promise<void> | null = null;
|
|
34
32
|
private vectorIndexCreated = false;
|
|
35
33
|
|
|
36
34
|
constructor(
|
|
@@ -39,58 +37,48 @@ export class MemoryDB {
|
|
|
39
37
|
private readonly vectorDim: number,
|
|
40
38
|
) {}
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
if (this.initPromise) {
|
|
47
|
-
return this.initPromise;
|
|
48
|
-
}
|
|
49
|
-
this.initPromise = this.doInitialize();
|
|
50
|
-
return this.initPromise;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private async doInitialize(): Promise<void> {
|
|
54
|
-
this.pool = mysql.createPool({
|
|
40
|
+
/** Short-lived connection: create, ensure table, run fn, close. */
|
|
41
|
+
private async withConnection<T>(fn: (conn: Connection) => Promise<T>): Promise<T> {
|
|
42
|
+
const conn = await mysql.createConnection({
|
|
55
43
|
host: this.mysqlConfig.host,
|
|
56
44
|
port: this.mysqlConfig.port,
|
|
57
45
|
user: this.mysqlConfig.user,
|
|
58
46
|
password: this.mysqlConfig.password,
|
|
59
47
|
database: this.mysqlConfig.database,
|
|
60
48
|
ssl: this.mysqlConfig.ssl ? {} : undefined,
|
|
61
|
-
connectionLimit: 5,
|
|
62
|
-
waitForConnections: true,
|
|
63
|
-
queueLimit: 0,
|
|
64
49
|
});
|
|
65
|
-
|
|
66
|
-
const conn = await this.pool.getConnection();
|
|
67
50
|
try {
|
|
68
51
|
await conn.query("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
72
|
-
agent_id VARCHAR(128) NOT NULL,
|
|
73
|
-
user_id VARCHAR(128) NULL DEFAULT NULL,
|
|
74
|
-
session_id VARCHAR(128) NULL DEFAULT NULL,
|
|
75
|
-
text LONGTEXT NOT NULL,
|
|
76
|
-
embedding VECTOR(${this.vectorDim}) NOT NULL,
|
|
77
|
-
importance FLOAT DEFAULT 0,
|
|
78
|
-
category VARCHAR(64) DEFAULT '${USER_MEMORY_FACT}',
|
|
79
|
-
created_at BIGINT NOT NULL,
|
|
80
|
-
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
81
|
-
INDEX idx_agent_id (agent_id),
|
|
82
|
-
INDEX idx_agent_session_category (agent_id, session_id, category)
|
|
83
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
84
|
-
`);
|
|
85
|
-
await this.ensureIsDeletedColumn(conn);
|
|
86
|
-
await this.ensureSessionCategoryIndex(conn);
|
|
87
|
-
await this.tryCreateVectorIndex(conn);
|
|
52
|
+
await this.ensureTable(conn);
|
|
53
|
+
return await fn(conn);
|
|
88
54
|
} finally {
|
|
89
|
-
conn.
|
|
55
|
+
await conn.end();
|
|
90
56
|
}
|
|
91
57
|
}
|
|
92
58
|
|
|
93
|
-
private async
|
|
59
|
+
private async ensureTable(conn: Connection): Promise<void> {
|
|
60
|
+
await conn.query(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS \`${this.tableName}\` (
|
|
62
|
+
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
63
|
+
agent_id VARCHAR(128) NOT NULL,
|
|
64
|
+
user_id VARCHAR(128) NULL DEFAULT NULL,
|
|
65
|
+
session_id VARCHAR(128) NULL DEFAULT NULL,
|
|
66
|
+
text LONGTEXT NOT NULL,
|
|
67
|
+
embedding VECTOR(${this.vectorDim}) NOT NULL,
|
|
68
|
+
importance FLOAT DEFAULT 0,
|
|
69
|
+
category VARCHAR(64) DEFAULT '${USER_MEMORY_FACT}',
|
|
70
|
+
created_at BIGINT NOT NULL,
|
|
71
|
+
is_deleted TINYINT NOT NULL DEFAULT 0,
|
|
72
|
+
INDEX idx_agent_id (agent_id),
|
|
73
|
+
INDEX idx_agent_session_category (agent_id, session_id, category)
|
|
74
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
75
|
+
`);
|
|
76
|
+
await this.ensureIsDeletedColumn(conn);
|
|
77
|
+
await this.ensureSessionCategoryIndex(conn);
|
|
78
|
+
await this.tryCreateVectorIndex(conn);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async ensureIsDeletedColumn(conn: Connection): Promise<void> {
|
|
94
82
|
const [rows] = await conn.query(
|
|
95
83
|
`SELECT COLUMN_NAME FROM information_schema.COLUMNS
|
|
96
84
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'is_deleted'`,
|
|
@@ -102,7 +90,7 @@ export class MemoryDB {
|
|
|
102
90
|
);
|
|
103
91
|
}
|
|
104
92
|
|
|
105
|
-
private async ensureSessionCategoryIndex(conn:
|
|
93
|
+
private async ensureSessionCategoryIndex(conn: Connection): Promise<void> {
|
|
106
94
|
const [rows] = await conn.query(
|
|
107
95
|
`SELECT COUNT(1) AS cnt FROM information_schema.statistics
|
|
108
96
|
WHERE table_schema = DATABASE() AND table_name = ? AND index_name = 'idx_agent_session_category'`,
|
|
@@ -114,7 +102,7 @@ export class MemoryDB {
|
|
|
114
102
|
);
|
|
115
103
|
}
|
|
116
104
|
|
|
117
|
-
private async tryCreateVectorIndex(conn:
|
|
105
|
+
private async tryCreateVectorIndex(conn: Connection): Promise<void> {
|
|
118
106
|
if (this.vectorIndexCreated) {
|
|
119
107
|
return;
|
|
120
108
|
}
|
|
@@ -151,38 +139,33 @@ export class MemoryDB {
|
|
|
151
139
|
sessionId?: string | null;
|
|
152
140
|
},
|
|
153
141
|
): Promise<MemoryEntry> {
|
|
154
|
-
|
|
142
|
+
return this.withConnection(async (conn) => {
|
|
143
|
+
const id = randomUUID();
|
|
144
|
+
const createdAt = Date.now();
|
|
145
|
+
const vectorStr = JSON.stringify(entry.vector);
|
|
146
|
+
const userId = entry.userId ?? null;
|
|
147
|
+
const sessionId = entry.sessionId ?? null;
|
|
148
|
+
const textSafe = stripFourByteUtf8(entry.text);
|
|
155
149
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const textSafe = stripFourByteUtf8(entry.text);
|
|
162
|
-
|
|
163
|
-
await this.pool!.query(
|
|
164
|
-
`INSERT INTO \`${this.tableName}\` (id, agent_id, user_id, session_id, text, embedding, importance, category, created_at, is_deleted)
|
|
165
|
-
VALUES (?, ?, ?, ?, ?, VEC_FROMTEXT(?), ?, ?, ?, 0)`,
|
|
166
|
-
[id, agentId, userId, sessionId, textSafe, vectorStr, entry.importance, entry.category, createdAt],
|
|
167
|
-
);
|
|
150
|
+
await conn.query(
|
|
151
|
+
`INSERT INTO \`${this.tableName}\` (id, agent_id, user_id, session_id, text, embedding, importance, category, created_at, is_deleted)
|
|
152
|
+
VALUES (?, ?, ?, ?, ?, VEC_FROMTEXT(?), ?, ?, ?, 0)`,
|
|
153
|
+
[id, agentId, userId, sessionId, textSafe, vectorStr, entry.importance, entry.category, createdAt],
|
|
154
|
+
);
|
|
168
155
|
|
|
169
|
-
|
|
170
|
-
const conn = await this.pool!.getConnection();
|
|
171
|
-
try {
|
|
156
|
+
if (!this.vectorIndexCreated) {
|
|
172
157
|
await this.tryCreateVectorIndex(conn);
|
|
173
|
-
} finally {
|
|
174
|
-
conn.release();
|
|
175
158
|
}
|
|
176
|
-
}
|
|
177
159
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
160
|
+
return {
|
|
161
|
+
id,
|
|
162
|
+
agentId,
|
|
163
|
+
text: textSafe,
|
|
164
|
+
importance: entry.importance,
|
|
165
|
+
category: entry.category,
|
|
166
|
+
createdAt,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
186
169
|
}
|
|
187
170
|
|
|
188
171
|
/**
|
|
@@ -200,32 +183,55 @@ export class MemoryDB {
|
|
|
200
183
|
userId?: string | null;
|
|
201
184
|
},
|
|
202
185
|
): Promise<{ action: "created" | "updated"; entry: MemoryEntry }> {
|
|
203
|
-
|
|
186
|
+
return this.withConnection(async (conn) => {
|
|
187
|
+
const textSafe = stripFourByteUtf8(entry.text);
|
|
188
|
+
const vectorStr = JSON.stringify(entry.vector);
|
|
189
|
+
const userId = entry.userId ?? null;
|
|
190
|
+
const createdAt = Date.now();
|
|
191
|
+
|
|
192
|
+
const [existingRows] = await conn.query(
|
|
193
|
+
`SELECT id FROM \`${this.tableName}\`
|
|
194
|
+
WHERE agent_id = ? AND category = ? AND is_deleted = 0
|
|
195
|
+
AND ((? IS NULL AND session_id IS NULL) OR (session_id = ?))
|
|
196
|
+
LIMIT 1`,
|
|
197
|
+
[agentId, entry.category, sessionId, sessionId],
|
|
198
|
+
);
|
|
199
|
+
const existing = (existingRows as Array<{ id: string }>)[0];
|
|
200
|
+
|
|
201
|
+
if (existing) {
|
|
202
|
+
await conn.query(
|
|
203
|
+
`UPDATE \`${this.tableName}\` SET text = ?, embedding = VEC_FROMTEXT(?), importance = ?, created_at = ?, user_id = ?
|
|
204
|
+
WHERE id = ? AND agent_id = ?`,
|
|
205
|
+
[textSafe, vectorStr, entry.importance, createdAt, userId, existing.id, agentId],
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
action: "updated",
|
|
209
|
+
entry: {
|
|
210
|
+
id: existing.id,
|
|
211
|
+
agentId,
|
|
212
|
+
text: textSafe,
|
|
213
|
+
importance: entry.importance,
|
|
214
|
+
category: entry.category,
|
|
215
|
+
createdAt,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
204
219
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
const id = randomUUID();
|
|
221
|
+
await conn.query(
|
|
222
|
+
`INSERT INTO \`${this.tableName}\` (id, agent_id, user_id, session_id, text, embedding, importance, category, created_at, is_deleted)
|
|
223
|
+
VALUES (?, ?, ?, ?, ?, VEC_FROMTEXT(?), ?, ?, ?, 0)`,
|
|
224
|
+
[id, agentId, userId, sessionId, textSafe, vectorStr, entry.importance, entry.category, createdAt],
|
|
225
|
+
);
|
|
209
226
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
AND ((? IS NULL AND session_id IS NULL) OR (session_id = ?))
|
|
214
|
-
LIMIT 1`,
|
|
215
|
-
[agentId, entry.category, sessionId, sessionId],
|
|
216
|
-
);
|
|
217
|
-
const existing = (existingRows as Array<{ id: string }>)[0];
|
|
227
|
+
if (!this.vectorIndexCreated) {
|
|
228
|
+
await this.tryCreateVectorIndex(conn);
|
|
229
|
+
}
|
|
218
230
|
|
|
219
|
-
if (existing) {
|
|
220
|
-
await this.pool!.query(
|
|
221
|
-
`UPDATE \`${this.tableName}\` SET text = ?, embedding = VEC_FROMTEXT(?), importance = ?, created_at = ?, user_id = ?
|
|
222
|
-
WHERE id = ? AND agent_id = ?`,
|
|
223
|
-
[textSafe, vectorStr, entry.importance, createdAt, userId, existing.id, agentId],
|
|
224
|
-
);
|
|
225
231
|
return {
|
|
226
|
-
action: "
|
|
232
|
+
action: "created",
|
|
227
233
|
entry: {
|
|
228
|
-
id
|
|
234
|
+
id,
|
|
229
235
|
agentId,
|
|
230
236
|
text: textSafe,
|
|
231
237
|
importance: entry.importance,
|
|
@@ -233,35 +239,7 @@ export class MemoryDB {
|
|
|
233
239
|
createdAt,
|
|
234
240
|
},
|
|
235
241
|
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const id = randomUUID();
|
|
239
|
-
await this.pool!.query(
|
|
240
|
-
`INSERT INTO \`${this.tableName}\` (id, agent_id, user_id, session_id, text, embedding, importance, category, created_at, is_deleted)
|
|
241
|
-
VALUES (?, ?, ?, ?, ?, VEC_FROMTEXT(?), ?, ?, ?, 0)`,
|
|
242
|
-
[id, agentId, userId, sessionId, textSafe, vectorStr, entry.importance, entry.category, createdAt],
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
if (!this.vectorIndexCreated) {
|
|
246
|
-
const conn = await this.pool!.getConnection();
|
|
247
|
-
try {
|
|
248
|
-
await this.tryCreateVectorIndex(conn);
|
|
249
|
-
} finally {
|
|
250
|
-
conn.release();
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
action: "created",
|
|
256
|
-
entry: {
|
|
257
|
-
id,
|
|
258
|
-
agentId,
|
|
259
|
-
text: textSafe,
|
|
260
|
-
importance: entry.importance,
|
|
261
|
-
category: entry.category,
|
|
262
|
-
createdAt,
|
|
263
|
-
},
|
|
264
|
-
};
|
|
242
|
+
});
|
|
265
243
|
}
|
|
266
244
|
|
|
267
245
|
/**
|
|
@@ -275,51 +253,50 @@ export class MemoryDB {
|
|
|
275
253
|
minScore = 0.5,
|
|
276
254
|
categories?: MemoryCategory[],
|
|
277
255
|
): Promise<MemorySearchResult[]> {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
: [vectorStr, agentId, limit];
|
|
256
|
+
return this.withConnection(async (conn) => {
|
|
257
|
+
const vectorStr = JSON.stringify(vector);
|
|
258
|
+
const hasCategoryFilter = Array.isArray(categories) && categories.length > 0;
|
|
259
|
+
const placeholders = hasCategoryFilter ? categories!.map(() => "?").join(", ") : "";
|
|
260
|
+
const whereClause = hasCategoryFilter
|
|
261
|
+
? `WHERE agent_id = ? AND (is_deleted = 0 OR is_deleted IS NULL) AND category IN (${placeholders})`
|
|
262
|
+
: `WHERE agent_id = ? AND (is_deleted = 0 OR is_deleted IS NULL)`;
|
|
263
|
+
const args: unknown[] = hasCategoryFilter
|
|
264
|
+
? [vectorStr, agentId, ...categories!, limit]
|
|
265
|
+
: [vectorStr, agentId, limit];
|
|
289
266
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
267
|
+
const [rows] = await conn.query(
|
|
268
|
+
`SELECT id, text, importance, category, created_at, is_deleted,
|
|
269
|
+
VEC_DISTANCE_COSINE(embedding, VEC_FROMTEXT(?)) AS distance
|
|
270
|
+
FROM \`${this.tableName}\`
|
|
271
|
+
${whereClause}
|
|
272
|
+
ORDER BY distance ASC
|
|
273
|
+
LIMIT ?`,
|
|
274
|
+
args,
|
|
275
|
+
);
|
|
299
276
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
277
|
+
const results: MemorySearchResult[] = [];
|
|
278
|
+
for (const row of rows as Array<Record<string, unknown>>) {
|
|
279
|
+
const distance = Number(row.distance) || 0;
|
|
280
|
+
const score = 1 - distance;
|
|
281
|
+
if (score < minScore) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const isDeleted = row.is_deleted;
|
|
285
|
+
results.push({
|
|
286
|
+
entry: {
|
|
287
|
+
id: row.id as string,
|
|
288
|
+
agentId,
|
|
289
|
+
text: row.text as string,
|
|
290
|
+
importance: Number(row.importance) || 0,
|
|
291
|
+
category: (row.category as MemoryCategory) || USER_MEMORY_FACT,
|
|
292
|
+
createdAt: Number(row.created_at) || 0,
|
|
293
|
+
isDeleted: isDeleted !== undefined && isDeleted !== null ? Number(isDeleted) : undefined,
|
|
294
|
+
},
|
|
295
|
+
score,
|
|
296
|
+
});
|
|
306
297
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
entry: {
|
|
310
|
-
id: row.id as string,
|
|
311
|
-
agentId,
|
|
312
|
-
text: row.text as string,
|
|
313
|
-
importance: Number(row.importance) || 0,
|
|
314
|
-
category: (row.category as MemoryCategory) || USER_MEMORY_FACT,
|
|
315
|
-
createdAt: Number(row.created_at) || 0,
|
|
316
|
-
isDeleted: isDeleted !== undefined && isDeleted !== null ? Number(isDeleted) : undefined,
|
|
317
|
-
},
|
|
318
|
-
score,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return results;
|
|
298
|
+
return results;
|
|
299
|
+
});
|
|
323
300
|
}
|
|
324
301
|
|
|
325
302
|
/** Soft-delete: set is_deleted = 1. Returns true if a row was updated. */
|
|
@@ -327,32 +304,29 @@ export class MemoryDB {
|
|
|
327
304
|
if (!UUID_RE.test(id)) {
|
|
328
305
|
throw new Error(`Invalid memory ID format: ${id}`);
|
|
329
306
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
307
|
+
return this.withConnection(async (conn) => {
|
|
308
|
+
const [result] = await conn.query(
|
|
309
|
+
`UPDATE \`${this.tableName}\` SET is_deleted = 1 WHERE id = ? AND agent_id = ?`,
|
|
310
|
+
[id, agentId],
|
|
311
|
+
);
|
|
312
|
+
return ((result as mysql.ResultSetHeader).affectedRows ?? 0) > 0;
|
|
313
|
+
});
|
|
336
314
|
}
|
|
337
315
|
|
|
338
316
|
async delete(agentId: string, id: string): Promise<boolean> {
|
|
339
317
|
if (!UUID_RE.test(id)) {
|
|
340
318
|
throw new Error(`Invalid memory ID format: ${id}`);
|
|
341
319
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
320
|
+
return this.withConnection(async (conn) => {
|
|
321
|
+
const [result] = await conn.query(
|
|
322
|
+
`DELETE FROM \`${this.tableName}\` WHERE id = ? AND agent_id = ?`,
|
|
323
|
+
[id, agentId],
|
|
324
|
+
);
|
|
325
|
+
return ((result as mysql.ResultSetHeader).affectedRows ?? 0) > 0;
|
|
326
|
+
});
|
|
349
327
|
}
|
|
350
328
|
|
|
351
329
|
async close(): Promise<void> {
|
|
352
|
-
|
|
353
|
-
this.pool.end();
|
|
354
|
-
this.pool = null;
|
|
355
|
-
this.initPromise = null;
|
|
356
|
-
}
|
|
330
|
+
this.vectorIndexCreated = false;
|
|
357
331
|
}
|
|
358
332
|
}
|
package/index.ts
CHANGED
|
@@ -51,9 +51,10 @@ import {
|
|
|
51
51
|
// Constants (recall limits, etc.)
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
|
|
54
|
-
const RECALL_LIMIT_USER_DEFAULT =
|
|
55
|
-
const RECALL_LIMIT_USER_BEFORE_START =
|
|
56
|
-
const RECALL_LIMIT_SELF =
|
|
54
|
+
const RECALL_LIMIT_USER_DEFAULT = 80;
|
|
55
|
+
const RECALL_LIMIT_USER_BEFORE_START = 80;
|
|
56
|
+
const RECALL_LIMIT_SELF = 30;
|
|
57
|
+
const RECALL_LIMIT_TOTAL = 100;
|
|
57
58
|
const RECALL_MIN_SCORE_STRICT = 0.7;
|
|
58
59
|
const RECALL_MIN_SCORE_RELAXED = 0.1;
|
|
59
60
|
const RECALL_MIN_SCORE_HOOK = 0.3;
|
|
@@ -181,7 +182,7 @@ function applyMemoryDecay(
|
|
|
181
182
|
return withDecay.sort((a, b) => b.score - a.score);
|
|
182
183
|
}
|
|
183
184
|
|
|
184
|
-
/** Run vector recall for user + optional self-improving memories; optionally apply time decay
|
|
185
|
+
/** Run vector recall for user + optional self-improving memories; optionally apply time decay, sort by importance, cap total. */
|
|
185
186
|
async function runRecall(
|
|
186
187
|
db: MemoryDB,
|
|
187
188
|
cfg: MemoryConfig,
|
|
@@ -190,7 +191,6 @@ async function runRecall(
|
|
|
190
191
|
options: { limitUser: number; limitSelf: number; minScore: number },
|
|
191
192
|
): Promise<MemorySearchResult[]> {
|
|
192
193
|
const { limitUser, limitSelf, minScore } = options;
|
|
193
|
-
const takeTotal = limitUser + limitSelf;
|
|
194
194
|
const fetchMultiplier = cfg.enableMemoryDecay ? DECAY_FETCH_MULTIPLIER : 1;
|
|
195
195
|
|
|
196
196
|
const resultsUser = await db.search(
|
|
@@ -211,17 +211,24 @@ async function runRecall(
|
|
|
211
211
|
)
|
|
212
212
|
: [];
|
|
213
213
|
|
|
214
|
-
let results = [...resultsUser, ...resultsSelf]
|
|
214
|
+
let results = [...resultsUser, ...resultsSelf];
|
|
215
215
|
if (cfg.enableMemoryDecay && results.length > 0) {
|
|
216
216
|
results = applyMemoryDecay(
|
|
217
217
|
results,
|
|
218
218
|
Date.now(),
|
|
219
219
|
cfg.memoryDecayStrategy,
|
|
220
220
|
cfg.memoryDecayHalfLifeDays,
|
|
221
|
-
)
|
|
222
|
-
} else if (results.length > takeTotal) {
|
|
223
|
-
results = results.slice(0, takeTotal);
|
|
221
|
+
);
|
|
224
222
|
}
|
|
223
|
+
// Sort by importance (desc) then score (desc), then cap total
|
|
224
|
+
results = results
|
|
225
|
+
.sort((a, b) => {
|
|
226
|
+
const impA = a.entry.importance ?? 0;
|
|
227
|
+
const impB = b.entry.importance ?? 0;
|
|
228
|
+
if (impB !== impA) return impB - impA;
|
|
229
|
+
return b.score - a.score;
|
|
230
|
+
})
|
|
231
|
+
.slice(0, RECALL_LIMIT_TOTAL);
|
|
225
232
|
return results;
|
|
226
233
|
}
|
|
227
234
|
|
|
@@ -745,15 +752,24 @@ const memoryPlugin = {
|
|
|
745
752
|
|
|
746
753
|
register(api: OpenClawPluginApi) {
|
|
747
754
|
const cfg = memoryConfigSchema.parse(api.pluginConfig);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
755
|
+
let db: MemoryDB | null = null;
|
|
756
|
+
let embeddings: Embeddings | null = null;
|
|
757
|
+
if (cfg.mysql && cfg.embedding) {
|
|
758
|
+
const { model, dimensions, apiKey, baseUrl } = cfg.embedding;
|
|
759
|
+
const vectorDim = vectorDimsForModel(model, dimensions);
|
|
760
|
+
db = new MemoryDB(cfg.mysql, cfg.tableName, vectorDim);
|
|
761
|
+
embeddings = new Embeddings(apiKey, model, baseUrl, vectorDim);
|
|
762
|
+
api.logger.info(
|
|
763
|
+
`openclaw-memory-alibaba-mysql: registered (host: ${cfg.mysql.host}, table: ${cfg.tableName})`,
|
|
764
|
+
);
|
|
765
|
+
} else {
|
|
766
|
+
api.logger.info(
|
|
767
|
+
"openclaw-memory-alibaba-mysql: registered without mysql/embedding config (memory ops no-op until configured)",
|
|
768
|
+
);
|
|
769
|
+
}
|
|
753
770
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
);
|
|
771
|
+
const getDbAndEmbeddings = (): { db: MemoryDB; embeddings: Embeddings } | null =>
|
|
772
|
+
db && embeddings ? { db, embeddings } : null;
|
|
757
773
|
|
|
758
774
|
// --- Tools: memory_recall, memory_store, memory_forget ---
|
|
759
775
|
|
|
@@ -768,6 +784,14 @@ const memoryPlugin = {
|
|
|
768
784
|
limit: Type.Optional(Type.Number({ description: "Max results (default: 5)" })),
|
|
769
785
|
}),
|
|
770
786
|
async execute(_toolCallId, params) {
|
|
787
|
+
const out = getDbAndEmbeddings();
|
|
788
|
+
if (!out) {
|
|
789
|
+
return {
|
|
790
|
+
content: [{ type: "text", text: "Memory plugin: configure mysql and embedding in plugin config to use memory." }],
|
|
791
|
+
details: { error: "not_configured" },
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
const { db, embeddings } = out;
|
|
771
795
|
const { query, limit = RECALL_LIMIT_USER_DEFAULT } = params as { query: string; limit?: number };
|
|
772
796
|
const agentId = ctx.agentId ?? "default";
|
|
773
797
|
const vector = await embeddings.embed(query);
|
|
@@ -844,6 +868,14 @@ const memoryPlugin = {
|
|
|
844
868
|
),
|
|
845
869
|
}),
|
|
846
870
|
async execute(_toolCallId, params) {
|
|
871
|
+
const out = getDbAndEmbeddings();
|
|
872
|
+
if (!out) {
|
|
873
|
+
return {
|
|
874
|
+
content: [{ type: "text", text: "Memory plugin: configure mysql and embedding in plugin config to use memory." }],
|
|
875
|
+
details: { error: "not_configured" },
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const { db, embeddings } = out;
|
|
847
879
|
const {
|
|
848
880
|
text,
|
|
849
881
|
importance = DEFAULT_IMPORTANCE,
|
|
@@ -893,6 +925,14 @@ const memoryPlugin = {
|
|
|
893
925
|
memoryId: Type.Optional(Type.String({ description: "Specific memory ID" })),
|
|
894
926
|
}),
|
|
895
927
|
async execute(_toolCallId, params) {
|
|
928
|
+
const out = getDbAndEmbeddings();
|
|
929
|
+
if (!out) {
|
|
930
|
+
return {
|
|
931
|
+
content: [{ type: "text", text: "Memory plugin: configure mysql and embedding in plugin config to use memory." }],
|
|
932
|
+
details: { error: "not_configured" },
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
const { db, embeddings } = out;
|
|
896
936
|
const { query, memoryId } = params as { query?: string; memoryId?: string };
|
|
897
937
|
const agentId = ctx.agentId ?? "default";
|
|
898
938
|
|
|
@@ -961,6 +1001,7 @@ const memoryPlugin = {
|
|
|
961
1001
|
|
|
962
1002
|
if (cfg.autoRecall) {
|
|
963
1003
|
api.on("before_agent_start", async (event, ctx) => {
|
|
1004
|
+
if (!db || !embeddings) return;
|
|
964
1005
|
if (!event.prompt || event.prompt.length < 5) return;
|
|
965
1006
|
|
|
966
1007
|
try {
|
|
@@ -993,6 +1034,7 @@ const memoryPlugin = {
|
|
|
993
1034
|
|
|
994
1035
|
if (cfg.autoCapture) {
|
|
995
1036
|
api.on("agent_end", async (event, ctx) => {
|
|
1037
|
+
if (!db || !embeddings) return;
|
|
996
1038
|
if (!event.success || !event.messages || event.messages.length === 0) return;
|
|
997
1039
|
|
|
998
1040
|
try {
|
|
@@ -1042,7 +1084,7 @@ const memoryPlugin = {
|
|
|
1042
1084
|
);
|
|
1043
1085
|
},
|
|
1044
1086
|
stop: async () => {
|
|
1045
|
-
await db.close();
|
|
1087
|
+
if (db) await db.close();
|
|
1046
1088
|
api.logger.info("openclaw-memory-alibaba-mysql: stopped");
|
|
1047
1089
|
},
|
|
1048
1090
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"baseUrl": { "type": "string", "default": "https://dashscope.aliyuncs.com/compatible-mode/v1" }
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
|
-
"similarityThresholdUserMemory": { "type": "number", "default": 0.
|
|
50
|
-
"similarityThresholdSelfImproving": { "type": "number", "default": 0.
|
|
49
|
+
"similarityThresholdUserMemory": { "type": "number", "default": 0.65 },
|
|
50
|
+
"similarityThresholdSelfImproving": { "type": "number", "default": 0.62 },
|
|
51
51
|
"enableFullContextMemory": {
|
|
52
52
|
"type": "boolean",
|
|
53
53
|
"default": false,
|
|
@@ -145,12 +145,12 @@
|
|
|
145
145
|
},
|
|
146
146
|
"similarityThresholdUserMemory": {
|
|
147
147
|
"label": "User Memory Similarity Threshold",
|
|
148
|
-
"placeholder": "0.
|
|
148
|
+
"placeholder": "0.65",
|
|
149
149
|
"help": "0–1, for user_memory_* dedup and recall"
|
|
150
150
|
},
|
|
151
151
|
"similarityThresholdSelfImproving": {
|
|
152
152
|
"label": "Self-Improving Similarity Threshold",
|
|
153
|
-
"placeholder": "0.
|
|
153
|
+
"placeholder": "0.62",
|
|
154
154
|
"help": "0–1, for self_improving_*"
|
|
155
155
|
},
|
|
156
156
|
"enableFullContextMemory": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-memory-alibaba-mysql",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "OpenClaw memory plugin using Alibaba Cloud RDS MySQL vector storage",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,6 +40,11 @@
|
|
|
40
40
|
]
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"tsx": "^4.21.0"
|
|
43
|
+
"tsx": "^4.21.0",
|
|
44
|
+
"vitest": "^2.1.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:watch": "vitest"
|
|
44
49
|
}
|
|
45
50
|
}
|