openclaw-memory-alibaba-local 0.1.9-beta.7 → 1.0.0
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/capture-state.ts +1 -2
- package/categories.ts +18 -0
- package/config.ts +4 -2
- package/db.ts +139 -2
- package/index.ts +521 -218
- package/package.json +1 -1
- package/prompts.ts +166 -121
- package/web/env-paths.ts +36 -0
- package/web/memory-admin-ops.ts +14 -4
- package/web/memory-routes.ts +1 -28
- package/web/memory-ui.ts +19 -7
package/capture-state.ts
CHANGED
|
@@ -166,7 +166,6 @@ function getMessageRoleRaw(msg: unknown): string {
|
|
|
166
166
|
export function resolveRoleCountsForSession(
|
|
167
167
|
entry: CursorFileEntry | undefined,
|
|
168
168
|
messages: unknown[],
|
|
169
|
-
log: { info: (m: string) => void },
|
|
170
169
|
): { roleCounts: Record<string, number>; lastMessagesLength: number } {
|
|
171
170
|
if (isV2Entry(entry)) {
|
|
172
171
|
return {
|
|
@@ -176,7 +175,7 @@ export function resolveRoleCountsForSession(
|
|
|
176
175
|
}
|
|
177
176
|
if (isLegacyEntry(entry)) {
|
|
178
177
|
const end = Math.min(Math.max(0, entry.lastEndExclusive), messages.length);
|
|
179
|
-
log
|
|
178
|
+
console.log("[openclaw-memory-alibaba-local] migrated legacy full-context cursor to per-role counts");
|
|
180
179
|
return {
|
|
181
180
|
roleCounts: countRolesInMessagesPrefix(messages, end),
|
|
182
181
|
lastMessagesLength: end,
|
package/categories.ts
CHANGED
|
@@ -8,6 +8,9 @@ export const USER_MEMORY_FACT = "user_memory_fact" as const;
|
|
|
8
8
|
export const USER_MEMORY_PREFERENCE = "user_memory_preference" as const;
|
|
9
9
|
export const USER_MEMORY_DECISION = "user_memory_decision" as const;
|
|
10
10
|
|
|
11
|
+
/** World fact: event/entity memories from third-party conversations (LRU-managed). */
|
|
12
|
+
export const WORLD_FACT = "world_fact" as const;
|
|
13
|
+
|
|
11
14
|
/** All user memory category values (for recall: treat as one logical "user_memory" set) */
|
|
12
15
|
export const USER_MEMORY_CATEGORIES = [
|
|
13
16
|
USER_MEMORY_FACT,
|
|
@@ -15,11 +18,23 @@ export const USER_MEMORY_CATEGORIES = [
|
|
|
15
18
|
USER_MEMORY_DECISION,
|
|
16
19
|
] as const;
|
|
17
20
|
|
|
21
|
+
/** Categories to search during recall (user memories + world facts). */
|
|
22
|
+
export const RECALL_USER_CATEGORIES = [
|
|
23
|
+
USER_MEMORY_FACT,
|
|
24
|
+
USER_MEMORY_PREFERENCE,
|
|
25
|
+
USER_MEMORY_DECISION,
|
|
26
|
+
WORLD_FACT,
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
18
29
|
export type UserMemoryCategory =
|
|
19
30
|
| typeof USER_MEMORY_FACT
|
|
20
31
|
| typeof USER_MEMORY_PREFERENCE
|
|
21
32
|
| typeof USER_MEMORY_DECISION;
|
|
22
33
|
|
|
34
|
+
export function isWorldFact(cat: string): boolean {
|
|
35
|
+
return cat === WORLD_FACT;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
/** Full context (conversation audit). Legacy single category. */
|
|
24
39
|
export const FULL_CONTEXT_MEMORY = "full_context_memory" as const;
|
|
25
40
|
|
|
@@ -62,12 +77,14 @@ export type SelfImprovingCategory = (typeof SELF_IMPROVING_CATEGORIES)[number];
|
|
|
62
77
|
/** All allowed category values */
|
|
63
78
|
export type MemoryCategory =
|
|
64
79
|
| UserMemoryCategory
|
|
80
|
+
| typeof WORLD_FACT
|
|
65
81
|
| typeof FULL_CONTEXT_MEMORY
|
|
66
82
|
| FullContextSourceCategory
|
|
67
83
|
| SelfImprovingCategory;
|
|
68
84
|
|
|
69
85
|
export const ALL_CATEGORIES: readonly MemoryCategory[] = [
|
|
70
86
|
...USER_MEMORY_CATEGORIES,
|
|
87
|
+
WORLD_FACT,
|
|
71
88
|
FULL_CONTEXT_MEMORY,
|
|
72
89
|
...FULL_CONTEXT_SOURCE_CATEGORIES,
|
|
73
90
|
...SELF_IMPROVING_CATEGORIES,
|
|
@@ -103,6 +120,7 @@ export const MEMORY_CATEGORY_LABEL_ZH: Readonly<Record<MemoryCategory, string>>
|
|
|
103
120
|
[USER_MEMORY_FACT]: "用户事实",
|
|
104
121
|
[USER_MEMORY_PREFERENCE]: "用户偏好",
|
|
105
122
|
[USER_MEMORY_DECISION]: "用户决策",
|
|
123
|
+
[WORLD_FACT]: "世界事实",
|
|
106
124
|
[FULL_CONTEXT_MEMORY]: "全文记忆",
|
|
107
125
|
[FULL_CONTEXT_USER]: "全文 · 用户消息",
|
|
108
126
|
[FULL_CONTEXT_ASSISTANT]: "全文 · AI助手消息",
|
package/config.ts
CHANGED
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
ALL_CATEGORIES,
|
|
7
7
|
FULL_CONTEXT_SOURCE_CATEGORIES,
|
|
8
8
|
MEMORY_CATEGORY_LABEL_ZH,
|
|
9
|
+
RECALL_USER_CATEGORIES,
|
|
9
10
|
SELF_IMPROVING_CATEGORIES,
|
|
10
11
|
USER_MEMORY_CATEGORIES,
|
|
11
12
|
USER_MEMORY_FACT,
|
|
13
|
+
WORLD_FACT,
|
|
12
14
|
} from "./categories.js";
|
|
13
15
|
|
|
14
16
|
/** OpenAI-compatible HTTP embeddings; all fields required — invalid/empty values fail on first embed, not at startup. */
|
|
@@ -52,7 +54,7 @@ export type AdminPanelMemoryTypeOptionsResolved = {
|
|
|
52
54
|
|
|
53
55
|
const ADMIN_PANEL_TAB_KEYS = ["user", "self", "full"] as const;
|
|
54
56
|
|
|
55
|
-
const USER_MEMORY_CAT_SET = new Set<string>(USER_MEMORY_CATEGORIES);
|
|
57
|
+
const USER_MEMORY_CAT_SET = new Set<string>([...USER_MEMORY_CATEGORIES, WORLD_FACT]);
|
|
56
58
|
const SELF_MEMORY_CAT_SET = new Set<string>(SELF_IMPROVING_CATEGORIES);
|
|
57
59
|
const FULL_SOURCE_CAT_SET = new Set<string>(FULL_CONTEXT_SOURCE_CATEGORIES);
|
|
58
60
|
|
|
@@ -569,7 +571,7 @@ export const memoryConfigSchema = {
|
|
|
569
571
|
enableFullContextMemory,
|
|
570
572
|
enableSelfImprovingMemory,
|
|
571
573
|
memoryExtractionMethod,
|
|
572
|
-
autoRecall: cfg.autoRecall
|
|
574
|
+
autoRecall: cfg.autoRecall !== false,
|
|
573
575
|
autoCapture: cfg.autoCapture !== false,
|
|
574
576
|
captureMaxChars: captureMaxChars ?? DEFAULT_CAPTURE_MAX_CHARS,
|
|
575
577
|
enableMemoryDecay,
|
package/db.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SELF_IMPROVING_CATEGORIES,
|
|
8
8
|
USER_MEMORY_FACT,
|
|
9
9
|
USER_MEMORY_CATEGORIES,
|
|
10
|
+
WORLD_FACT,
|
|
10
11
|
} from "./categories.js";
|
|
11
12
|
|
|
12
13
|
export type MemoryEntry = {
|
|
@@ -235,7 +236,7 @@ export type MemoryDashboardAggregate = {
|
|
|
235
236
|
uniqueSessions: number;
|
|
236
237
|
};
|
|
237
238
|
|
|
238
|
-
const USER_CAT_SET = new Set<string>(USER_MEMORY_CATEGORIES);
|
|
239
|
+
const USER_CAT_SET = new Set<string>([...USER_MEMORY_CATEGORIES, WORLD_FACT]);
|
|
239
240
|
const SELF_CAT_SET = new Set<string>(SELF_IMPROVING_CATEGORIES);
|
|
240
241
|
const FULL_CAT_SET = new Set<string>([FULL_CONTEXT_MEMORY, ...FULL_CONTEXT_SOURCE_CATEGORIES]);
|
|
241
242
|
|
|
@@ -733,6 +734,7 @@ export class MemoryDB {
|
|
|
733
734
|
|
|
734
735
|
for (const raw of rows as Array<Record<string, unknown>>) {
|
|
735
736
|
const cat = String(raw.category ?? "");
|
|
737
|
+
const displayCat = cat === WORLD_FACT ? USER_MEMORY_FACT : cat;
|
|
736
738
|
const createdAt = Number(raw.createdAt ?? 0);
|
|
737
739
|
const importance = Number(raw.importance ?? 0);
|
|
738
740
|
const ag = String(raw.agentId ?? "").trim();
|
|
@@ -741,7 +743,7 @@ export class MemoryDB {
|
|
|
741
743
|
const k = memoryCategoryKind(cat);
|
|
742
744
|
byKind[k]++;
|
|
743
745
|
|
|
744
|
-
byCategory[
|
|
746
|
+
byCategory[displayCat] = (byCategory[displayCat] ?? 0) + 1;
|
|
745
747
|
|
|
746
748
|
if (ag) {
|
|
747
749
|
agentCounts.set(ag, (agentCounts.get(ag) ?? 0) + 1);
|
|
@@ -1318,4 +1320,139 @@ export class MemoryDB {
|
|
|
1318
1320
|
this.db = null;
|
|
1319
1321
|
this.initPromise = null;
|
|
1320
1322
|
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Return top N user memory entries sorted by importance DESC.
|
|
1326
|
+
* Used by session_start hook to inject key personal facts into a new session.
|
|
1327
|
+
*/
|
|
1328
|
+
async listTopByImportance(
|
|
1329
|
+
agentId: string,
|
|
1330
|
+
categories: MemoryCategory[],
|
|
1331
|
+
limit: number,
|
|
1332
|
+
): Promise<MemoryEntry[]> {
|
|
1333
|
+
if (categories.length === 0 || limit <= 0) return [];
|
|
1334
|
+
await this.ensureInitialized();
|
|
1335
|
+
await this.refreshToLatest();
|
|
1336
|
+
const a = sqlEscapeLiteral(agentId);
|
|
1337
|
+
const list = categories.map((c) => `'${sqlEscapeLiteral(String(c))}'`).join(", ");
|
|
1338
|
+
const where = `agentId = '${a}' AND isDeleted = 0 AND category IN (${list})`;
|
|
1339
|
+
const rows = await this.table!
|
|
1340
|
+
.query()
|
|
1341
|
+
.where(where)
|
|
1342
|
+
.select(["id", "agentId", "sessionId", "text", "importance", "category", "createdAt", "isDeleted", "batchId", "seqInBatch", "chunkIndex"])
|
|
1343
|
+
.toArray();
|
|
1344
|
+
const entries = (rows as Array<Record<string, unknown>>).map((r) => rowToEntry(r, agentId));
|
|
1345
|
+
entries.sort((a, b) => b.importance - a.importance);
|
|
1346
|
+
return entries.slice(0, limit);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* GC full_context_* rows for a given agent:
|
|
1351
|
+
* 1. Delete rows older than maxAgeMs.
|
|
1352
|
+
* 2. If remaining count > maxRows, delete oldest until count <= maxRows.
|
|
1353
|
+
*/
|
|
1354
|
+
async gcFullContext(
|
|
1355
|
+
agentId: string,
|
|
1356
|
+
maxRows: number,
|
|
1357
|
+
maxAgeMs: number,
|
|
1358
|
+
): Promise<void> {
|
|
1359
|
+
await this.ensureInitialized();
|
|
1360
|
+
const a = sqlEscapeLiteral(agentId);
|
|
1361
|
+
const cats = FULL_CONTEXT_SOURCE_CATEGORIES.map((c) => `'${sqlEscapeLiteral(String(c))}'`).join(", ");
|
|
1362
|
+
const base = `agentId = '${a}' AND category IN (${cats})`;
|
|
1363
|
+
|
|
1364
|
+
// 1. Delete rows older than maxAgeMs
|
|
1365
|
+
const cutoffMs = Date.now() - maxAgeMs;
|
|
1366
|
+
const agePred = `${base} AND createdAt < ${Math.floor(cutoffMs)}`;
|
|
1367
|
+
await this.table!.delete(agePred);
|
|
1368
|
+
|
|
1369
|
+
// 2. Count remaining; if over maxRows, trim oldest
|
|
1370
|
+
const remaining = await this.table!.countRows(base);
|
|
1371
|
+
if (remaining <= maxRows) {
|
|
1372
|
+
console.log(`[openclaw-memory-alibaba-local] gcFullContext agent=${agentId} remaining=${remaining} (within limit)`);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const excess = remaining - maxRows;
|
|
1376
|
+
// Find the createdAt of the oldest rows to delete
|
|
1377
|
+
const oldest: Array<Record<string, unknown>> = await this.table!.query()
|
|
1378
|
+
.where(base)
|
|
1379
|
+
.select(["createdAt"])
|
|
1380
|
+
.toArray();
|
|
1381
|
+
oldest.sort((a, b) => Number(a.createdAt) - Number(b.createdAt));
|
|
1382
|
+
if (oldest.length > maxRows) {
|
|
1383
|
+
const cutoff = Number(oldest[excess - 1]!.createdAt);
|
|
1384
|
+
await this.table!.delete(`${base} AND createdAt <= ${Math.floor(cutoff)}`);
|
|
1385
|
+
}
|
|
1386
|
+
console.log(`[openclaw-memory-alibaba-local] gcFullContext agent=${agentId} deleted ${excess} excess rows (was ${remaining}, cap ${maxRows})`);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Touch world_fact entries: update createdAt to current UTC time (LRU refresh).
|
|
1391
|
+
* Fire-and-forget: errors are logged but not thrown.
|
|
1392
|
+
*/
|
|
1393
|
+
async touchWorldFactCreatedAt(agentId: string, ids: string[]): Promise<void> {
|
|
1394
|
+
if (ids.length === 0) return;
|
|
1395
|
+
await this.ensureInitialized();
|
|
1396
|
+
const now = Date.now();
|
|
1397
|
+
const a = sqlEscapeLiteral(agentId);
|
|
1398
|
+
const wf = sqlEscapeLiteral(WORLD_FACT);
|
|
1399
|
+
for (const id of ids) {
|
|
1400
|
+
try {
|
|
1401
|
+
const eid = sqlEscapeLiteral(id);
|
|
1402
|
+
await (this.table! as any).update(
|
|
1403
|
+
{ createdAt: now },
|
|
1404
|
+
{ where: `id = '${eid}' AND agentId = '${a}' AND category = '${wf}'` },
|
|
1405
|
+
);
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
console.warn(`[openclaw-memory-alibaba-local] touchWorldFactCreatedAt failed id=${id}: ${err}`);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* GC world_fact rows for a given agent (LRU eviction):
|
|
1414
|
+
* Phase 1: If count > softMaxRows, delete rows where createdAt > maxAgeMs old AND importance < 0.7.
|
|
1415
|
+
* Phase 2: If count still > hardMaxRows, delete oldest rows regardless of importance.
|
|
1416
|
+
*/
|
|
1417
|
+
async gcWorldFact(
|
|
1418
|
+
agentId: string,
|
|
1419
|
+
softMaxRows: number,
|
|
1420
|
+
maxAgeMs: number,
|
|
1421
|
+
hardMaxRows: number,
|
|
1422
|
+
): Promise<void> {
|
|
1423
|
+
await this.ensureInitialized();
|
|
1424
|
+
const a = sqlEscapeLiteral(agentId);
|
|
1425
|
+
const wf = sqlEscapeLiteral(WORLD_FACT);
|
|
1426
|
+
const base = `agentId = '${a}' AND category = '${wf}'`;
|
|
1427
|
+
|
|
1428
|
+
let count = await this.table!.countRows(base);
|
|
1429
|
+
|
|
1430
|
+
// Phase 1: soft GC — delete old + low-importance entries when count > softMaxRows
|
|
1431
|
+
if (count > softMaxRows) {
|
|
1432
|
+
const cutoffMs = Date.now() - maxAgeMs;
|
|
1433
|
+
const softPred = `${base} AND createdAt < ${Math.floor(cutoffMs)} AND importance < 0.7`;
|
|
1434
|
+
await this.table!.delete(softPred);
|
|
1435
|
+
const afterSoft = await this.table!.countRows(base);
|
|
1436
|
+
const softDeleted = count - afterSoft;
|
|
1437
|
+
if (softDeleted > 0) {
|
|
1438
|
+
console.log(`[openclaw-memory-alibaba-local] gcWorldFact agent=${agentId} phase1: deleted ${softDeleted} old+low-importance rows (was ${count}, now ${afterSoft})`);
|
|
1439
|
+
}
|
|
1440
|
+
count = afterSoft;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Phase 2: hard cap — delete oldest regardless of importance
|
|
1444
|
+
if (count > hardMaxRows) {
|
|
1445
|
+
const excess = count - hardMaxRows;
|
|
1446
|
+
const oldest: Array<Record<string, unknown>> = await this.table!.query()
|
|
1447
|
+
.where(base)
|
|
1448
|
+
.select(["createdAt"])
|
|
1449
|
+
.toArray();
|
|
1450
|
+
oldest.sort((a, b) => Number(a.createdAt) - Number(b.createdAt));
|
|
1451
|
+
if (oldest.length > hardMaxRows) {
|
|
1452
|
+
const cutoff = Number(oldest[excess - 1]!.createdAt);
|
|
1453
|
+
await this.table!.delete(`${base} AND createdAt <= ${Math.floor(cutoff)}`);
|
|
1454
|
+
}
|
|
1455
|
+
console.log(`[openclaw-memory-alibaba-local] gcWorldFact agent=${agentId} phase2: deleted ${excess} excess rows (was ${count}, cap ${hardMaxRows})`);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1321
1458
|
}
|