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 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.95,
89
- "similarityThresholdSelfImproving": 0.92,
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
- mysql: MysqlConnectionConfig;
28
- embedding: EmbeddingConfig;
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.95;
259
+ : 0.65;
227
260
  const similarityThresholdSelfImproving =
228
261
  typeof cfg.similarityThresholdSelfImproving === "number"
229
262
  ? cfg.similarityThresholdSelfImproving
230
- : 0.92;
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 { Pool } from "mysql2/promise";
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
- private async ensureInitialized(): Promise<void> {
43
- if (this.pool) {
44
- return;
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 conn.query(`
70
- CREATE TABLE IF NOT EXISTS \`${this.tableName}\` (
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.release();
55
+ await conn.end();
90
56
  }
91
57
  }
92
58
 
93
- private async ensureIsDeletedColumn(conn: mysql.PoolConnection): Promise<void> {
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: mysql.PoolConnection): Promise<void> {
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: mysql.PoolConnection): Promise<void> {
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
- await this.ensureInitialized();
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
- const id = randomUUID();
157
- const createdAt = Date.now();
158
- const vectorStr = JSON.stringify(entry.vector);
159
- const userId = entry.userId ?? null;
160
- const sessionId = entry.sessionId ?? null;
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
- if (!this.vectorIndexCreated) {
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
- return {
179
- id,
180
- agentId,
181
- text: textSafe,
182
- importance: entry.importance,
183
- category: entry.category,
184
- createdAt,
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
- await this.ensureInitialized();
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
- const textSafe = stripFourByteUtf8(entry.text);
206
- const vectorStr = JSON.stringify(entry.vector);
207
- const userId = entry.userId ?? null;
208
- const createdAt = Date.now();
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
- const [existingRows] = await this.pool!.query(
211
- `SELECT id FROM \`${this.tableName}\`
212
- WHERE agent_id = ? AND category = ? AND is_deleted = 0
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: "updated",
232
+ action: "created",
227
233
  entry: {
228
- id: existing.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
- await this.ensureInitialized();
279
-
280
- const vectorStr = JSON.stringify(vector);
281
- const hasCategoryFilter = Array.isArray(categories) && categories.length > 0;
282
- const placeholders = hasCategoryFilter ? categories!.map(() => "?").join(", ") : "";
283
- const whereClause = hasCategoryFilter
284
- ? `WHERE agent_id = ? AND (is_deleted = 0 OR is_deleted IS NULL) AND category IN (${placeholders})`
285
- : `WHERE agent_id = ? AND (is_deleted = 0 OR is_deleted IS NULL)`;
286
- const args: unknown[] = hasCategoryFilter
287
- ? [vectorStr, agentId, ...categories!, limit]
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
- const [rows] = await this.pool!.query(
291
- `SELECT id, text, importance, category, created_at, is_deleted,
292
- VEC_DISTANCE_COSINE(embedding, VEC_FROMTEXT(?)) AS distance
293
- FROM \`${this.tableName}\`
294
- ${whereClause}
295
- ORDER BY distance ASC
296
- LIMIT ?`,
297
- args,
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
- const results: MemorySearchResult[] = [];
301
- for (const row of rows as Array<Record<string, unknown>>) {
302
- const distance = Number(row.distance) || 0;
303
- const score = 1 - distance;
304
- if (score < minScore) {
305
- continue;
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
- const isDeleted = row.is_deleted;
308
- results.push({
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
- await this.ensureInitialized();
331
- const [result] = await this.pool!.query(
332
- `UPDATE \`${this.tableName}\` SET is_deleted = 1 WHERE id = ? AND agent_id = ?`,
333
- [id, agentId],
334
- );
335
- return ((result as mysql.ResultSetHeader).affectedRows ?? 0) > 0;
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
- await this.ensureInitialized();
343
-
344
- const [result] = await this.pool!.query(
345
- `DELETE FROM \`${this.tableName}\` WHERE id = ? AND agent_id = ?`,
346
- [id, agentId],
347
- );
348
- return ((result as mysql.ResultSetHeader).affectedRows ?? 0) > 0;
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
- if (this.pool) {
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 = 5;
55
- const RECALL_LIMIT_USER_BEFORE_START = 3;
56
- const RECALL_LIMIT_SELF = 2;
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 and take top N. */
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].sort((a, b) => b.score - a.score);
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
- ).slice(0, takeTotal);
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
- const { model, dimensions, apiKey, baseUrl } = cfg.embedding;
749
-
750
- const vectorDim = vectorDimsForModel(model, dimensions);
751
- const db = new MemoryDB(cfg.mysql, cfg.tableName, vectorDim);
752
- const embeddings = new Embeddings(apiKey, model, baseUrl, vectorDim);
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
- api.logger.info(
755
- `openclaw-memory-alibaba-mysql: registered (host: ${cfg.mysql.host}, table: ${cfg.tableName})`,
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
  });
@@ -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.95 },
50
- "similarityThresholdSelfImproving": { "type": "number", "default": 0.92 },
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.95",
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.92",
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.4",
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
  }