memory-lancedb-pro 1.0.25 → 1.0.26
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/CHANGELOG.md +15 -0
- package/README.md +14 -1
- package/README_CN.md +14 -1
- package/index.ts +243 -85
- package/openclaw.plugin.json +15 -1
- package/package.json +1 -1
- package/src/access-tracker.ts +330 -0
- package/src/retriever.ts +183 -68
- package/src/store.ts +207 -67
- package/src/tools.ts +339 -87
- package/test/access-tracker.test.mjs +770 -0
- package/test/cli-smoke.mjs +22 -0
package/src/store.ts
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import type * as LanceDB from "@lancedb/lancedb";
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
accessSync,
|
|
10
|
+
constants,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
lstatSync,
|
|
14
|
+
} from "node:fs";
|
|
8
15
|
import { dirname } from "node:path";
|
|
9
16
|
|
|
10
17
|
// ============================================================================
|
|
@@ -36,16 +43,22 @@ export interface StoreConfig {
|
|
|
36
43
|
// LanceDB Dynamic Import
|
|
37
44
|
// ============================================================================
|
|
38
45
|
|
|
39
|
-
let lancedbImportPromise: Promise<typeof import("@lancedb/lancedb")> | null =
|
|
46
|
+
let lancedbImportPromise: Promise<typeof import("@lancedb/lancedb")> | null =
|
|
47
|
+
null;
|
|
40
48
|
|
|
41
|
-
export const loadLanceDB = async (): Promise<
|
|
49
|
+
export const loadLanceDB = async (): Promise<
|
|
50
|
+
typeof import("@lancedb/lancedb")
|
|
51
|
+
> => {
|
|
42
52
|
if (!lancedbImportPromise) {
|
|
43
53
|
lancedbImportPromise = import("@lancedb/lancedb");
|
|
44
54
|
}
|
|
45
55
|
try {
|
|
46
56
|
return await lancedbImportPromise;
|
|
47
57
|
} catch (err) {
|
|
48
|
-
throw new Error(
|
|
58
|
+
throw new Error(
|
|
59
|
+
`memory-lancedb-pro: failed to load LanceDB. ${String(err)}`,
|
|
60
|
+
{ cause: err },
|
|
61
|
+
);
|
|
49
62
|
}
|
|
50
63
|
};
|
|
51
64
|
|
|
@@ -83,8 +96,8 @@ export function validateStoragePath(dbPath: string): string {
|
|
|
83
96
|
} catch (err: any) {
|
|
84
97
|
throw new Error(
|
|
85
98
|
`dbPath "${dbPath}" is a symlink whose target does not exist.\n` +
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
` Fix: Create the target directory, or update the symlink to point to a valid path.\n` +
|
|
100
|
+
` Details: ${err.code || ""} ${err.message}`,
|
|
88
101
|
);
|
|
89
102
|
}
|
|
90
103
|
}
|
|
@@ -92,7 +105,10 @@ export function validateStoragePath(dbPath: string): string {
|
|
|
92
105
|
// Missing path is OK (it will be created below)
|
|
93
106
|
if (err?.code === "ENOENT") {
|
|
94
107
|
// no-op
|
|
95
|
-
} else if (
|
|
108
|
+
} else if (
|
|
109
|
+
typeof err?.message === "string" &&
|
|
110
|
+
err.message.includes("symlink whose target does not exist")
|
|
111
|
+
) {
|
|
96
112
|
throw err;
|
|
97
113
|
} else {
|
|
98
114
|
// Other lstat failures — continue with original path
|
|
@@ -106,9 +122,9 @@ export function validateStoragePath(dbPath: string): string {
|
|
|
106
122
|
} catch (err: any) {
|
|
107
123
|
throw new Error(
|
|
108
124
|
`Failed to create dbPath directory "${resolvedPath}".\n` +
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
` Fix: Ensure the parent directory "${dirname(resolvedPath)}" exists and is writable,\n` +
|
|
126
|
+
` or create it manually: mkdir -p "${resolvedPath}"\n` +
|
|
127
|
+
` Details: ${err.code || ""} ${err.message}`,
|
|
112
128
|
);
|
|
113
129
|
}
|
|
114
130
|
}
|
|
@@ -119,9 +135,9 @@ export function validateStoragePath(dbPath: string): string {
|
|
|
119
135
|
} catch (err: any) {
|
|
120
136
|
throw new Error(
|
|
121
137
|
`dbPath directory "${resolvedPath}" is not writable.\n` +
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
` Fix: Check permissions with: ls -la "${dirname(resolvedPath)}"\n` +
|
|
139
|
+
` Or grant write access: chmod u+w "${resolvedPath}"\n` +
|
|
140
|
+
` Details: ${err.code || ""} ${err.message}`,
|
|
125
141
|
);
|
|
126
142
|
}
|
|
127
143
|
|
|
@@ -172,7 +188,7 @@ export class MemoryStore {
|
|
|
172
188
|
const message = err.message || String(err);
|
|
173
189
|
throw new Error(
|
|
174
190
|
`Failed to open LanceDB at "${this.config.dbPath}": ${code} ${message}\n` +
|
|
175
|
-
|
|
191
|
+
` Fix: Verify the path exists and is writable. Check parent directory permissions.`,
|
|
176
192
|
);
|
|
177
193
|
}
|
|
178
194
|
|
|
@@ -188,7 +204,9 @@ export class MemoryStore {
|
|
|
188
204
|
try {
|
|
189
205
|
const sample = await table.query().limit(1).toArray();
|
|
190
206
|
if (sample.length > 0 && !("scope" in sample[0])) {
|
|
191
|
-
console.warn(
|
|
207
|
+
console.warn(
|
|
208
|
+
"Adding scope column for backward compatibility with existing data",
|
|
209
|
+
);
|
|
192
210
|
}
|
|
193
211
|
} catch (err) {
|
|
194
212
|
console.warn("Could not check table schema:", err);
|
|
@@ -198,7 +216,9 @@ export class MemoryStore {
|
|
|
198
216
|
const schemaEntry: MemoryEntry = {
|
|
199
217
|
id: "__schema__",
|
|
200
218
|
text: "",
|
|
201
|
-
vector: Array.from({ length: this.config.vectorDim }).fill(
|
|
219
|
+
vector: Array.from({ length: this.config.vectorDim }).fill(
|
|
220
|
+
0,
|
|
221
|
+
) as number[],
|
|
202
222
|
category: "other",
|
|
203
223
|
scope: "global",
|
|
204
224
|
importance: 0,
|
|
@@ -228,7 +248,7 @@ export class MemoryStore {
|
|
|
228
248
|
const existingDim = sample[0].vector.length;
|
|
229
249
|
if (existingDim !== this.config.vectorDim) {
|
|
230
250
|
throw new Error(
|
|
231
|
-
`Vector dimension mismatch: table=${existingDim}, config=${this.config.vectorDim}. Create a new table/dbPath or set matching embedding.dimensions
|
|
251
|
+
`Vector dimension mismatch: table=${existingDim}, config=${this.config.vectorDim}. Create a new table/dbPath or set matching embedding.dimensions.`,
|
|
232
252
|
);
|
|
233
253
|
}
|
|
234
254
|
}
|
|
@@ -238,7 +258,10 @@ export class MemoryStore {
|
|
|
238
258
|
await this.createFtsIndex(table);
|
|
239
259
|
this.ftsIndexCreated = true;
|
|
240
260
|
} catch (err) {
|
|
241
|
-
console.warn(
|
|
261
|
+
console.warn(
|
|
262
|
+
"Failed to create FTS index, falling back to vector-only search:",
|
|
263
|
+
err,
|
|
264
|
+
);
|
|
242
265
|
this.ftsIndexCreated = false;
|
|
243
266
|
}
|
|
244
267
|
|
|
@@ -250,8 +273,8 @@ export class MemoryStore {
|
|
|
250
273
|
try {
|
|
251
274
|
// Check if FTS index already exists
|
|
252
275
|
const indices = await table.listIndices();
|
|
253
|
-
const hasFtsIndex = indices?.some(
|
|
254
|
-
idx.indexType === "FTS" || idx.columns?.includes("text")
|
|
276
|
+
const hasFtsIndex = indices?.some(
|
|
277
|
+
(idx: any) => idx.indexType === "FTS" || idx.columns?.includes("text"),
|
|
255
278
|
);
|
|
256
279
|
|
|
257
280
|
if (!hasFtsIndex) {
|
|
@@ -262,11 +285,15 @@ export class MemoryStore {
|
|
|
262
285
|
});
|
|
263
286
|
}
|
|
264
287
|
} catch (err) {
|
|
265
|
-
throw new Error(
|
|
288
|
+
throw new Error(
|
|
289
|
+
`FTS index creation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
290
|
+
);
|
|
266
291
|
}
|
|
267
292
|
}
|
|
268
293
|
|
|
269
|
-
async store(
|
|
294
|
+
async store(
|
|
295
|
+
entry: Omit<MemoryEntry, "id" | "timestamp">,
|
|
296
|
+
): Promise<MemoryEntry> {
|
|
270
297
|
await this.ensureInitialized();
|
|
271
298
|
|
|
272
299
|
const fullEntry: MemoryEntry = {
|
|
@@ -282,7 +309,7 @@ export class MemoryStore {
|
|
|
282
309
|
const code = err.code || "";
|
|
283
310
|
const message = err.message || String(err);
|
|
284
311
|
throw new Error(
|
|
285
|
-
`Failed to store memory in "${this.config.dbPath}": ${code} ${message}
|
|
312
|
+
`Failed to store memory in "${this.config.dbPath}": ${code} ${message}`,
|
|
286
313
|
);
|
|
287
314
|
}
|
|
288
315
|
return fullEntry;
|
|
@@ -303,7 +330,7 @@ export class MemoryStore {
|
|
|
303
330
|
const vector = entry.vector || [];
|
|
304
331
|
if (!Array.isArray(vector) || vector.length !== this.config.vectorDim) {
|
|
305
332
|
throw new Error(
|
|
306
|
-
`Vector dimension mismatch: expected ${this.config.vectorDim}, got ${Array.isArray(vector) ? vector.length :
|
|
333
|
+
`Vector dimension mismatch: expected ${this.config.vectorDim}, got ${Array.isArray(vector) ? vector.length : "non-array"}`,
|
|
307
334
|
);
|
|
308
335
|
}
|
|
309
336
|
|
|
@@ -311,7 +338,9 @@ export class MemoryStore {
|
|
|
311
338
|
...entry,
|
|
312
339
|
scope: entry.scope || "global",
|
|
313
340
|
importance: Number.isFinite(entry.importance) ? entry.importance : 0.7,
|
|
314
|
-
timestamp: Number.isFinite(entry.timestamp)
|
|
341
|
+
timestamp: Number.isFinite(entry.timestamp)
|
|
342
|
+
? entry.timestamp
|
|
343
|
+
: Date.now(),
|
|
315
344
|
metadata: entry.metadata || "{}",
|
|
316
345
|
};
|
|
317
346
|
|
|
@@ -322,11 +351,46 @@ export class MemoryStore {
|
|
|
322
351
|
async hasId(id: string): Promise<boolean> {
|
|
323
352
|
await this.ensureInitialized();
|
|
324
353
|
const safeId = escapeSqlLiteral(id);
|
|
325
|
-
const res = await this.table!.query()
|
|
354
|
+
const res = await this.table!.query()
|
|
355
|
+
.select(["id"])
|
|
356
|
+
.where(`id = '${safeId}'`)
|
|
357
|
+
.limit(1)
|
|
358
|
+
.toArray();
|
|
326
359
|
return res.length > 0;
|
|
327
360
|
}
|
|
328
361
|
|
|
329
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Read a single memory entry by exact ID without any mutation.
|
|
364
|
+
* Unlike update(id, {}), this performs a pure read (no delete+add cycle).
|
|
365
|
+
*/
|
|
366
|
+
async getById(id: string): Promise<MemoryEntry | null> {
|
|
367
|
+
await this.ensureInitialized();
|
|
368
|
+
const safeId = escapeSqlLiteral(id);
|
|
369
|
+
const rows = await this.table!.query()
|
|
370
|
+
.where(`id = '${safeId}'`)
|
|
371
|
+
.limit(1)
|
|
372
|
+
.toArray();
|
|
373
|
+
if (rows.length === 0) return null;
|
|
374
|
+
|
|
375
|
+
const row = rows[0];
|
|
376
|
+
return {
|
|
377
|
+
id: row.id as string,
|
|
378
|
+
text: row.text as string,
|
|
379
|
+
vector: Array.from(row.vector as Iterable<number>),
|
|
380
|
+
category: row.category as MemoryEntry["category"],
|
|
381
|
+
scope: (row.scope as string | undefined) ?? "global",
|
|
382
|
+
importance: Number(row.importance),
|
|
383
|
+
timestamp: Number(row.timestamp),
|
|
384
|
+
metadata: (row.metadata as string) || "{}",
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async vectorSearch(
|
|
389
|
+
vector: number[],
|
|
390
|
+
limit = 5,
|
|
391
|
+
minScore = 0.3,
|
|
392
|
+
scopeFilter?: string[],
|
|
393
|
+
): Promise<MemorySearchResult[]> {
|
|
330
394
|
await this.ensureInitialized();
|
|
331
395
|
|
|
332
396
|
const safeLimit = clampInt(limit, 1, 20);
|
|
@@ -337,7 +401,7 @@ export class MemoryStore {
|
|
|
337
401
|
// Apply scope filter if provided
|
|
338
402
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
339
403
|
const scopeConditions = scopeFilter
|
|
340
|
-
.map(scope => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
404
|
+
.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
341
405
|
.join(" OR ");
|
|
342
406
|
query = query.where(`(${scopeConditions}) OR scope IS NULL`); // NULL for backward compatibility
|
|
343
407
|
}
|
|
@@ -354,7 +418,11 @@ export class MemoryStore {
|
|
|
354
418
|
const rowScope = (row.scope as string | undefined) ?? "global";
|
|
355
419
|
|
|
356
420
|
// Double-check scope filter in application layer
|
|
357
|
-
if (
|
|
421
|
+
if (
|
|
422
|
+
scopeFilter &&
|
|
423
|
+
scopeFilter.length > 0 &&
|
|
424
|
+
!scopeFilter.includes(rowScope)
|
|
425
|
+
) {
|
|
358
426
|
continue;
|
|
359
427
|
}
|
|
360
428
|
|
|
@@ -378,7 +446,11 @@ export class MemoryStore {
|
|
|
378
446
|
return mapped;
|
|
379
447
|
}
|
|
380
448
|
|
|
381
|
-
async bm25Search(
|
|
449
|
+
async bm25Search(
|
|
450
|
+
query: string,
|
|
451
|
+
limit = 5,
|
|
452
|
+
scopeFilter?: string[],
|
|
453
|
+
): Promise<MemorySearchResult[]> {
|
|
382
454
|
await this.ensureInitialized();
|
|
383
455
|
|
|
384
456
|
if (!this.ftsIndexCreated) {
|
|
@@ -394,9 +466,11 @@ export class MemoryStore {
|
|
|
394
466
|
// Apply scope filter if provided
|
|
395
467
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
396
468
|
const scopeConditions = scopeFilter
|
|
397
|
-
.map(scope => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
469
|
+
.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
398
470
|
.join(" OR ");
|
|
399
|
-
searchQuery = searchQuery.where(
|
|
471
|
+
searchQuery = searchQuery.where(
|
|
472
|
+
`(${scopeConditions}) OR scope IS NULL`,
|
|
473
|
+
);
|
|
400
474
|
}
|
|
401
475
|
|
|
402
476
|
const results = await searchQuery.toArray();
|
|
@@ -406,14 +480,19 @@ export class MemoryStore {
|
|
|
406
480
|
const rowScope = (row.scope as string | undefined) ?? "global";
|
|
407
481
|
|
|
408
482
|
// Double-check scope filter in application layer
|
|
409
|
-
if (
|
|
483
|
+
if (
|
|
484
|
+
scopeFilter &&
|
|
485
|
+
scopeFilter.length > 0 &&
|
|
486
|
+
!scopeFilter.includes(rowScope)
|
|
487
|
+
) {
|
|
410
488
|
continue;
|
|
411
489
|
}
|
|
412
490
|
|
|
413
491
|
// LanceDB FTS _score is raw BM25 (unbounded). Normalize with sigmoid.
|
|
414
492
|
// LanceDB may return BigInt for numeric columns; coerce safely.
|
|
415
|
-
const rawScore =
|
|
416
|
-
const normalizedScore =
|
|
493
|
+
const rawScore = row._score != null ? Number(row._score) : 0;
|
|
494
|
+
const normalizedScore =
|
|
495
|
+
rawScore > 0 ? 1 / (1 + Math.exp(-rawScore / 5)) : 0.5;
|
|
417
496
|
|
|
418
497
|
mapped.push({
|
|
419
498
|
entry: {
|
|
@@ -441,7 +520,8 @@ export class MemoryStore {
|
|
|
441
520
|
await this.ensureInitialized();
|
|
442
521
|
|
|
443
522
|
// Support both full UUID and short prefix (8+ hex chars)
|
|
444
|
-
const uuidRegex =
|
|
523
|
+
const uuidRegex =
|
|
524
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
445
525
|
const prefixRegex = /^[0-9a-f]{8,}$/i;
|
|
446
526
|
const isFullId = uuidRegex.test(id);
|
|
447
527
|
const isPrefix = !isFullId && prefixRegex.test(id);
|
|
@@ -452,13 +532,21 @@ export class MemoryStore {
|
|
|
452
532
|
|
|
453
533
|
let candidates: any[];
|
|
454
534
|
if (isFullId) {
|
|
455
|
-
candidates = await this.table!.query()
|
|
535
|
+
candidates = await this.table!.query()
|
|
536
|
+
.where(`id = '${id}'`)
|
|
537
|
+
.limit(1)
|
|
538
|
+
.toArray();
|
|
456
539
|
} else {
|
|
457
540
|
// Prefix match: fetch candidates and filter in app layer
|
|
458
|
-
const all = await this.table!.query()
|
|
541
|
+
const all = await this.table!.query()
|
|
542
|
+
.select(["id", "scope"])
|
|
543
|
+
.limit(1000)
|
|
544
|
+
.toArray();
|
|
459
545
|
candidates = all.filter((r: any) => (r.id as string).startsWith(id));
|
|
460
546
|
if (candidates.length > 1) {
|
|
461
|
-
throw new Error(
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Ambiguous prefix "${id}" matches ${candidates.length} memories. Use a longer prefix or full ID.`,
|
|
549
|
+
);
|
|
462
550
|
}
|
|
463
551
|
}
|
|
464
552
|
if (candidates.length === 0) {
|
|
@@ -469,7 +557,11 @@ export class MemoryStore {
|
|
|
469
557
|
const rowScope = (candidates[0].scope as string | undefined) ?? "global";
|
|
470
558
|
|
|
471
559
|
// Check scope permissions
|
|
472
|
-
if (
|
|
560
|
+
if (
|
|
561
|
+
scopeFilter &&
|
|
562
|
+
scopeFilter.length > 0 &&
|
|
563
|
+
!scopeFilter.includes(rowScope)
|
|
564
|
+
) {
|
|
473
565
|
throw new Error(`Memory ${resolvedId} is outside accessible scopes`);
|
|
474
566
|
}
|
|
475
567
|
|
|
@@ -477,7 +569,12 @@ export class MemoryStore {
|
|
|
477
569
|
return true;
|
|
478
570
|
}
|
|
479
571
|
|
|
480
|
-
async list(
|
|
572
|
+
async list(
|
|
573
|
+
scopeFilter?: string[],
|
|
574
|
+
category?: string,
|
|
575
|
+
limit = 20,
|
|
576
|
+
offset = 0,
|
|
577
|
+
): Promise<MemoryEntry[]> {
|
|
481
578
|
await this.ensureInitialized();
|
|
482
579
|
|
|
483
580
|
let query = this.table!.query();
|
|
@@ -487,7 +584,7 @@ export class MemoryStore {
|
|
|
487
584
|
|
|
488
585
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
489
586
|
const scopeConditions = scopeFilter
|
|
490
|
-
.map(scope => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
587
|
+
.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
491
588
|
.join(" OR ");
|
|
492
589
|
conditions.push(`((${scopeConditions}) OR scope IS NULL)`);
|
|
493
590
|
}
|
|
@@ -502,20 +599,30 @@ export class MemoryStore {
|
|
|
502
599
|
|
|
503
600
|
// Fetch all matching rows (no pre-limit) so app-layer sort is correct across full dataset
|
|
504
601
|
const results = await query
|
|
505
|
-
.select([
|
|
602
|
+
.select([
|
|
603
|
+
"id",
|
|
604
|
+
"text",
|
|
605
|
+
"category",
|
|
606
|
+
"scope",
|
|
607
|
+
"importance",
|
|
608
|
+
"timestamp",
|
|
609
|
+
"metadata",
|
|
610
|
+
])
|
|
506
611
|
.toArray();
|
|
507
612
|
|
|
508
613
|
return results
|
|
509
|
-
.map(
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
614
|
+
.map(
|
|
615
|
+
(row): MemoryEntry => ({
|
|
616
|
+
id: row.id as string,
|
|
617
|
+
text: row.text as string,
|
|
618
|
+
vector: [], // Don't include vectors in list results for performance
|
|
619
|
+
category: row.category as MemoryEntry["category"],
|
|
620
|
+
scope: (row.scope as string | undefined) ?? "global",
|
|
621
|
+
importance: Number(row.importance),
|
|
622
|
+
timestamp: Number(row.timestamp),
|
|
623
|
+
metadata: (row.metadata as string) || "{}",
|
|
624
|
+
}),
|
|
625
|
+
)
|
|
519
626
|
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
|
|
520
627
|
.slice(offset, offset + limit);
|
|
521
628
|
}
|
|
@@ -523,7 +630,7 @@ export class MemoryStore {
|
|
|
523
630
|
async stats(scopeFilter?: string[]): Promise<{
|
|
524
631
|
totalCount: number;
|
|
525
632
|
scopeCounts: Record<string, number>;
|
|
526
|
-
categoryCounts: Record<string, number
|
|
633
|
+
categoryCounts: Record<string, number>;
|
|
527
634
|
}> {
|
|
528
635
|
await this.ensureInitialized();
|
|
529
636
|
|
|
@@ -531,7 +638,7 @@ export class MemoryStore {
|
|
|
531
638
|
|
|
532
639
|
if (scopeFilter && scopeFilter.length > 0) {
|
|
533
640
|
const scopeConditions = scopeFilter
|
|
534
|
-
.map(scope => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
641
|
+
.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
535
642
|
.join(" OR ");
|
|
536
643
|
query = query.where(`((${scopeConditions}) OR scope IS NULL)`);
|
|
537
644
|
}
|
|
@@ -558,13 +665,20 @@ export class MemoryStore {
|
|
|
558
665
|
|
|
559
666
|
async update(
|
|
560
667
|
id: string,
|
|
561
|
-
updates: {
|
|
562
|
-
|
|
668
|
+
updates: {
|
|
669
|
+
text?: string;
|
|
670
|
+
vector?: number[];
|
|
671
|
+
importance?: number;
|
|
672
|
+
category?: MemoryEntry["category"];
|
|
673
|
+
metadata?: string;
|
|
674
|
+
},
|
|
675
|
+
scopeFilter?: string[],
|
|
563
676
|
): Promise<MemoryEntry | null> {
|
|
564
677
|
await this.ensureInitialized();
|
|
565
678
|
|
|
566
679
|
// Support both full UUID and short prefix (8+ hex chars), same as delete()
|
|
567
|
-
const uuidRegex =
|
|
680
|
+
const uuidRegex =
|
|
681
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
568
682
|
const prefixRegex = /^[0-9a-f]{8,}$/i;
|
|
569
683
|
const isFullId = uuidRegex.test(id);
|
|
570
684
|
const isPrefix = !isFullId && prefixRegex.test(id);
|
|
@@ -576,13 +690,30 @@ export class MemoryStore {
|
|
|
576
690
|
let rows: any[];
|
|
577
691
|
if (isFullId) {
|
|
578
692
|
const safeId = escapeSqlLiteral(id);
|
|
579
|
-
rows = await this.table!.query()
|
|
693
|
+
rows = await this.table!.query()
|
|
694
|
+
.where(`id = '${safeId}'`)
|
|
695
|
+
.limit(1)
|
|
696
|
+
.toArray();
|
|
580
697
|
} else {
|
|
581
698
|
// Prefix match
|
|
582
|
-
const all = await this.table!.query()
|
|
699
|
+
const all = await this.table!.query()
|
|
700
|
+
.select([
|
|
701
|
+
"id",
|
|
702
|
+
"text",
|
|
703
|
+
"vector",
|
|
704
|
+
"category",
|
|
705
|
+
"scope",
|
|
706
|
+
"importance",
|
|
707
|
+
"timestamp",
|
|
708
|
+
"metadata",
|
|
709
|
+
])
|
|
710
|
+
.limit(1000)
|
|
711
|
+
.toArray();
|
|
583
712
|
rows = all.filter((r: any) => (r.id as string).startsWith(id));
|
|
584
713
|
if (rows.length > 1) {
|
|
585
|
-
throw new Error(
|
|
714
|
+
throw new Error(
|
|
715
|
+
`Ambiguous prefix "${id}" matches ${rows.length} memories. Use a longer prefix or full ID.`,
|
|
716
|
+
);
|
|
586
717
|
}
|
|
587
718
|
}
|
|
588
719
|
|
|
@@ -592,7 +723,11 @@ export class MemoryStore {
|
|
|
592
723
|
const rowScope = (row.scope as string | undefined) ?? "global";
|
|
593
724
|
|
|
594
725
|
// Check scope permissions
|
|
595
|
-
if (
|
|
726
|
+
if (
|
|
727
|
+
scopeFilter &&
|
|
728
|
+
scopeFilter.length > 0 &&
|
|
729
|
+
!scopeFilter.includes(rowScope)
|
|
730
|
+
) {
|
|
596
731
|
throw new Error(`Memory ${id} is outside accessible scopes`);
|
|
597
732
|
}
|
|
598
733
|
|
|
@@ -600,7 +735,7 @@ export class MemoryStore {
|
|
|
600
735
|
const updated: MemoryEntry = {
|
|
601
736
|
id: row.id as string,
|
|
602
737
|
text: updates.text ?? (row.text as string),
|
|
603
|
-
vector: updates.vector ??
|
|
738
|
+
vector: updates.vector ?? Array.from(row.vector as Iterable<number>),
|
|
604
739
|
category: updates.category ?? (row.category as MemoryEntry["category"]),
|
|
605
740
|
scope: rowScope,
|
|
606
741
|
importance: updates.importance ?? Number(row.importance),
|
|
@@ -616,14 +751,17 @@ export class MemoryStore {
|
|
|
616
751
|
return updated;
|
|
617
752
|
}
|
|
618
753
|
|
|
619
|
-
async bulkDelete(
|
|
754
|
+
async bulkDelete(
|
|
755
|
+
scopeFilter: string[],
|
|
756
|
+
beforeTimestamp?: number,
|
|
757
|
+
): Promise<number> {
|
|
620
758
|
await this.ensureInitialized();
|
|
621
759
|
|
|
622
760
|
const conditions: string[] = [];
|
|
623
761
|
|
|
624
762
|
if (scopeFilter.length > 0) {
|
|
625
763
|
const scopeConditions = scopeFilter
|
|
626
|
-
.map(scope => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
764
|
+
.map((scope) => `scope = '${escapeSqlLiteral(scope)}'`)
|
|
627
765
|
.join(" OR ");
|
|
628
766
|
conditions.push(`(${scopeConditions})`);
|
|
629
767
|
}
|
|
@@ -633,7 +771,9 @@ export class MemoryStore {
|
|
|
633
771
|
}
|
|
634
772
|
|
|
635
773
|
if (conditions.length === 0) {
|
|
636
|
-
throw new Error(
|
|
774
|
+
throw new Error(
|
|
775
|
+
"Bulk delete requires at least scope or timestamp filter for safety",
|
|
776
|
+
);
|
|
637
777
|
}
|
|
638
778
|
|
|
639
779
|
const whereClause = conditions.join(" AND ");
|
|
@@ -653,4 +793,4 @@ export class MemoryStore {
|
|
|
653
793
|
get hasFtsSupport(): boolean {
|
|
654
794
|
return this.ftsIndexCreated;
|
|
655
795
|
}
|
|
656
|
-
}
|
|
796
|
+
}
|