@wrongstack/vector-memory 0.308.0 → 0.308.2
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 +63 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +926 -67
- package/dist/sage-event-mirror.d.ts +47 -0
- package/dist/sage-fusion.d.ts +87 -0
- package/dist/sage-port-wrapper.d.ts +53 -0
- package/dist/sage-sync-source.d.ts +39 -0
- package/dist/sage-sync.d.ts +55 -0
- package/dist/schema.d.ts +29 -5
- package/dist/search-race.d.ts +75 -0
- package/dist/store.d.ts +78 -1
- package/dist/types.d.ts +15 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -133,7 +133,7 @@ var TransformersEmbeddingProvider = class {
|
|
|
133
133
|
};
|
|
134
134
|
|
|
135
135
|
// src/schema.ts
|
|
136
|
-
var VECTOR_SCHEMA_VERSION =
|
|
136
|
+
var VECTOR_SCHEMA_VERSION = 2;
|
|
137
137
|
var VECTOR_PROVIDER_KEY = "active_provider_id";
|
|
138
138
|
var VECTOR_DIMENSIONS_KEY = "active_provider_dimensions";
|
|
139
139
|
function initVectorSchema(db) {
|
|
@@ -164,7 +164,7 @@ function initVectorSchema(db) {
|
|
|
164
164
|
`);
|
|
165
165
|
db.exec("CREATE INDEX IF NOT EXISTS idx_entries_scope ON entries(scope)");
|
|
166
166
|
db.exec("CREATE INDEX IF NOT EXISTS idx_entries_kind ON entries(kind)");
|
|
167
|
-
db.exec("CREATE INDEX IF NOT EXISTS idx_entries_hash ON entries(content_hash)");
|
|
167
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_entries_hash ON entries(content_hash)");
|
|
168
168
|
db.exec(
|
|
169
169
|
"CREATE INDEX IF NOT EXISTS idx_entries_updated ON entries(updated_at DESC)"
|
|
170
170
|
);
|
|
@@ -180,6 +180,20 @@ function initVectorSchema(db) {
|
|
|
180
180
|
);
|
|
181
181
|
`);
|
|
182
182
|
db.exec("CREATE INDEX IF NOT EXISTS idx_vectors_provider ON vectors(provider_id)");
|
|
183
|
+
db.exec(`
|
|
184
|
+
CREATE TABLE IF NOT EXISTS embedding_cache (
|
|
185
|
+
content_hash TEXT NOT NULL,
|
|
186
|
+
provider_id TEXT NOT NULL,
|
|
187
|
+
dimensions INTEGER NOT NULL,
|
|
188
|
+
vector BLOB NOT NULL,
|
|
189
|
+
text TEXT NOT NULL,
|
|
190
|
+
created_at TEXT NOT NULL,
|
|
191
|
+
last_used_at TEXT NOT NULL,
|
|
192
|
+
use_count INTEGER NOT NULL DEFAULT 0,
|
|
193
|
+
PRIMARY KEY (content_hash, provider_id, dimensions)
|
|
194
|
+
);
|
|
195
|
+
`);
|
|
196
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_cache_last_used ON embedding_cache(last_used_at)");
|
|
183
197
|
db.exec(`
|
|
184
198
|
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(
|
|
185
199
|
id UNINDEXED, text, tags, content='entries', content_rowid='rowid'
|
|
@@ -206,6 +220,41 @@ function initVectorSchema(db) {
|
|
|
206
220
|
END;
|
|
207
221
|
`);
|
|
208
222
|
}
|
|
223
|
+
function upsertEmbeddingCache(db, row) {
|
|
224
|
+
db.prepare(
|
|
225
|
+
`INSERT INTO embedding_cache
|
|
226
|
+
(content_hash, provider_id, dimensions, vector, text, created_at, last_used_at, use_count)
|
|
227
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 1)
|
|
228
|
+
ON CONFLICT(content_hash, provider_id, dimensions) DO UPDATE SET
|
|
229
|
+
last_used_at = excluded.last_used_at,
|
|
230
|
+
use_count = embedding_cache.use_count + 1,
|
|
231
|
+
vector = excluded.vector`
|
|
232
|
+
).run(
|
|
233
|
+
row.contentHash,
|
|
234
|
+
row.providerId,
|
|
235
|
+
row.dimensions,
|
|
236
|
+
row.vector,
|
|
237
|
+
row.text,
|
|
238
|
+
row.now,
|
|
239
|
+
row.now
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
function lookupEmbeddingCache(db, contentHash, providerId, dimensions, now) {
|
|
243
|
+
const row = db.prepare(
|
|
244
|
+
`SELECT vector FROM embedding_cache
|
|
245
|
+
WHERE content_hash = ? AND provider_id = ? AND dimensions = ?
|
|
246
|
+
LIMIT 1`
|
|
247
|
+
).get(contentHash, providerId, dimensions);
|
|
248
|
+
if (!row) return void 0;
|
|
249
|
+
try {
|
|
250
|
+
db.prepare(
|
|
251
|
+
`UPDATE embedding_cache SET last_used_at = ?, use_count = use_count + 1
|
|
252
|
+
WHERE content_hash = ? AND provider_id = ? AND dimensions = ?`
|
|
253
|
+
).run(now, contentHash, providerId, dimensions);
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
return decodeVector(row.vector);
|
|
257
|
+
}
|
|
209
258
|
function encodeVector(vec) {
|
|
210
259
|
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
211
260
|
}
|
|
@@ -224,11 +273,15 @@ import * as fs from "node:fs";
|
|
|
224
273
|
import * as path from "node:path";
|
|
225
274
|
import { DatabaseSync } from "node:sqlite";
|
|
226
275
|
import { HashingEmbeddingProvider, cosineSimilarity } from "@wrongstack/sage";
|
|
276
|
+
import { withFileLock } from "@wrongstack/core/utils";
|
|
227
277
|
var DEFAULT_DIRECTORY = ".wrongstack/vector-memory";
|
|
228
278
|
var DEFAULT_FILENAME = "vector-memory.db";
|
|
279
|
+
var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
|
|
280
|
+
var CACHE_EVICT_BATCH = 256;
|
|
229
281
|
var VectorMemoryStore = class _VectorMemoryStore {
|
|
230
282
|
db;
|
|
231
283
|
dbPath;
|
|
284
|
+
rootDir;
|
|
232
285
|
provider;
|
|
233
286
|
closed = false;
|
|
234
287
|
constructor(opts) {
|
|
@@ -246,11 +299,33 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
246
299
|
throw new Error("Vector memory directory must stay inside the project root.");
|
|
247
300
|
}
|
|
248
301
|
fs.mkdirSync(rootDir, { recursive: true });
|
|
302
|
+
this.rootDir = rootDir;
|
|
249
303
|
this.dbPath = path.join(rootDir, filename);
|
|
250
304
|
this.db = new DatabaseSync(this.dbPath);
|
|
251
305
|
initVectorSchema(this.db);
|
|
252
306
|
this.recordActiveProvider();
|
|
253
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Absolute path of the store's data directory (the resolved
|
|
310
|
+
* `opts.directory`, default `.wrongstack/vector-memory`). Hosts use this
|
|
311
|
+
* to place sidecar state (e.g. the first-boot SAGE sync marker) next to
|
|
312
|
+
* the db instead of re-deriving the path and drifting from it.
|
|
313
|
+
*/
|
|
314
|
+
get directory() {
|
|
315
|
+
return this.rootDir;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Absolute path of the SQLite database file. Hosts use this to take a
|
|
319
|
+
* file-level lock that covers all mutating operations (see
|
|
320
|
+
* `withFileLock(this.dbPath + '.lock', …)`).
|
|
321
|
+
*/
|
|
322
|
+
get databasePath() {
|
|
323
|
+
return this.dbPath;
|
|
324
|
+
}
|
|
325
|
+
/** The lockfile path used to serialize mutating operations. */
|
|
326
|
+
get lockPath() {
|
|
327
|
+
return `${this.dbPath}.lock`;
|
|
328
|
+
}
|
|
254
329
|
get activeProviderId() {
|
|
255
330
|
const row = this.db.prepare("SELECT value FROM schema_meta WHERE key = ?").get(VECTOR_PROVIDER_KEY);
|
|
256
331
|
return row?.value ?? this.provider.id;
|
|
@@ -275,28 +350,112 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
275
350
|
static contentHash(text) {
|
|
276
351
|
return createHash("sha256").update(text.normalize("NFKC").trim()).digest("hex");
|
|
277
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Look up a vector for `text` in the provider-level embedding cache.
|
|
355
|
+
* Cache hit returns the cached vector (no ONNX pass). Miss returns
|
|
356
|
+
* `undefined`.
|
|
357
|
+
*/
|
|
358
|
+
cachedVector(text, now) {
|
|
359
|
+
return lookupEmbeddingCache(
|
|
360
|
+
this.db,
|
|
361
|
+
_VectorMemoryStore.contentHash(text),
|
|
362
|
+
this.provider.id,
|
|
363
|
+
this.provider.dimensions,
|
|
364
|
+
now
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
/** Persist `vec` for `text` to the embedding cache. */
|
|
368
|
+
cacheVector(text, vec, now) {
|
|
369
|
+
upsertEmbeddingCache(this.db, {
|
|
370
|
+
contentHash: _VectorMemoryStore.contentHash(text),
|
|
371
|
+
providerId: this.provider.id,
|
|
372
|
+
dimensions: vec.length,
|
|
373
|
+
vector: encodeVector(vec),
|
|
374
|
+
text,
|
|
375
|
+
now
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Embed `text`, hitting the provider-level cache first. Cache miss falls
|
|
380
|
+
* through to the configured provider and writes the result back. Returns
|
|
381
|
+
* `undefined` when the provider fails — the caller can persist the entry
|
|
382
|
+
* without a vector (fail-open).
|
|
383
|
+
*/
|
|
384
|
+
async embedWithCache(text) {
|
|
385
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
386
|
+
const cached = this.cachedVector(text, now);
|
|
387
|
+
if (cached) return cached;
|
|
388
|
+
try {
|
|
389
|
+
const result = await this.provider.embed([text]);
|
|
390
|
+
const vec = result[0];
|
|
391
|
+
if (vec) this.cacheVector(text, vec, now);
|
|
392
|
+
return vec;
|
|
393
|
+
} catch {
|
|
394
|
+
return void 0;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Look up an existing entry by `content_hash`. Returns `undefined` when
|
|
399
|
+
* the entry is not present. Used by `remember()` to make writes idempotent
|
|
400
|
+
* and by `syncFromSage()` to skip already-indexed SAGE memories.
|
|
401
|
+
*/
|
|
402
|
+
findByContentHash(contentHash) {
|
|
403
|
+
this.assertOpen();
|
|
404
|
+
const row = this.db.prepare("SELECT * FROM entries WHERE content_hash = ? LIMIT 1").get(contentHash);
|
|
405
|
+
if (!row) return void 0;
|
|
406
|
+
const vectorRow = this.db.prepare("SELECT * FROM vectors WHERE entry_id = ?").get(row.id);
|
|
407
|
+
return this.rowToEntry(row, vectorRow);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Look up the entry mirroring a given SAGE memory id (i.e. the entry
|
|
411
|
+
* whose `metadata.sageId` equals `sageId`). Returns `undefined` when
|
|
412
|
+
* no such entry exists. Used by the event-driven mirror to delete
|
|
413
|
+
* vector entries on SAGE delete events (the emitter knows the SAGE
|
|
414
|
+
* id, not the vector entry id).
|
|
415
|
+
*
|
|
416
|
+
* Index lookup is `json_extract(metadata, '$.sageId')` — the metadata
|
|
417
|
+
* column is the JSON blob `syncFromSage` writes, so this avoids a
|
|
418
|
+
* full table scan.
|
|
419
|
+
*/
|
|
420
|
+
findBySageId(sageId) {
|
|
421
|
+
this.assertOpen();
|
|
422
|
+
const row = this.db.prepare(
|
|
423
|
+
`SELECT * FROM entries
|
|
424
|
+
WHERE json_extract(metadata, '$.sageId') = ?
|
|
425
|
+
ORDER BY updated_at DESC
|
|
426
|
+
LIMIT 1`
|
|
427
|
+
).get(sageId);
|
|
428
|
+
if (!row) return void 0;
|
|
429
|
+
const vectorRow = this.db.prepare("SELECT * FROM vectors WHERE entry_id = ?").get(row.id);
|
|
430
|
+
return this.rowToEntry(row, vectorRow);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Persist a new entry. Idempotent: if an entry with the same
|
|
434
|
+
* `content_hash` already exists, that entry is returned unchanged
|
|
435
|
+
* instead of inserting a duplicate. Mutating ops are wrapped in
|
|
436
|
+
* `withFileLock` so two processes cannot race the dedup check.
|
|
437
|
+
*/
|
|
278
438
|
async remember(input) {
|
|
279
439
|
this.assertOpen();
|
|
280
440
|
if (!input.text || input.text.trim().length === 0) {
|
|
281
441
|
throw new Error("VectorMemoryStore.remember: text must be non-empty");
|
|
282
442
|
}
|
|
443
|
+
return withFileLock(this.lockPath, () => this.rememberUnlocked(input), {
|
|
444
|
+
timeoutMs: DEFAULT_LOCK_TIMEOUT_MS
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async rememberUnlocked(input) {
|
|
283
448
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
284
|
-
const id = randomUUID();
|
|
285
449
|
const contentHash = _VectorMemoryStore.contentHash(input.text);
|
|
450
|
+
const existing = this.findByContentHash(contentHash);
|
|
451
|
+
if (existing) return existing;
|
|
286
452
|
const metadata = input.metadata ?? {};
|
|
287
453
|
const tags = input.tags ?? [];
|
|
288
454
|
const scope = input.scope ?? "project";
|
|
289
455
|
const kind = input.kind ?? "note";
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const result2 = await this.provider.embed([input.text]);
|
|
294
|
-
vector = result2[0];
|
|
295
|
-
providerId = this.provider.id;
|
|
296
|
-
} catch {
|
|
297
|
-
providerId = void 0;
|
|
298
|
-
vector = void 0;
|
|
299
|
-
}
|
|
456
|
+
const id = randomUUID();
|
|
457
|
+
const vector = await this.embedWithCache(input.text);
|
|
458
|
+
const providerId = vector ? this.provider.id : void 0;
|
|
300
459
|
this.db.exec("BEGIN");
|
|
301
460
|
try {
|
|
302
461
|
this.db.prepare(
|
|
@@ -354,31 +513,32 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
354
513
|
const vectorRow = this.db.prepare("SELECT * FROM vectors WHERE entry_id = ?").get(id);
|
|
355
514
|
return this.rowToEntry(row, vectorRow);
|
|
356
515
|
}
|
|
357
|
-
|
|
516
|
+
/** Hard-delete an entry by id. Wrapped in `withFileLock` for cross-process safety. */
|
|
517
|
+
async forget(id) {
|
|
358
518
|
this.assertOpen();
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
519
|
+
return withFileLock(
|
|
520
|
+
this.lockPath,
|
|
521
|
+
async () => {
|
|
522
|
+
this.db.exec("BEGIN");
|
|
523
|
+
try {
|
|
524
|
+
const info = this.db.prepare("DELETE FROM entries WHERE id = ?").run(id);
|
|
525
|
+
this.db.exec("COMMIT");
|
|
526
|
+
return info.changes > 0;
|
|
527
|
+
} catch (e) {
|
|
528
|
+
this.db.exec("ROLLBACK");
|
|
529
|
+
throw e;
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
{ timeoutMs: DEFAULT_LOCK_TIMEOUT_MS }
|
|
533
|
+
);
|
|
368
534
|
}
|
|
369
535
|
async search(query, opts = {}) {
|
|
370
536
|
this.assertOpen();
|
|
371
537
|
const limit = opts.limit ?? 10;
|
|
372
538
|
const threshold = opts.threshold ?? 0;
|
|
539
|
+
const includeVectors = opts.includeVectors === true;
|
|
373
540
|
if (typeof query !== "string" || query.trim().length === 0) return [];
|
|
374
|
-
|
|
375
|
-
try {
|
|
376
|
-
const result = await this.provider.embed([query]);
|
|
377
|
-
if (!result[0]) return [];
|
|
378
|
-
queryVec = result[0];
|
|
379
|
-
} catch {
|
|
380
|
-
return [];
|
|
381
|
-
}
|
|
541
|
+
const queryVec = await this.embedWithCache(query);
|
|
382
542
|
if (!queryVec || queryVec.length === 0) return [];
|
|
383
543
|
const providerId = this.provider.id;
|
|
384
544
|
const dimensions = this.provider.dimensions;
|
|
@@ -408,7 +568,9 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
408
568
|
const score = Math.max(0, Math.min(1, raw));
|
|
409
569
|
if (score < threshold) continue;
|
|
410
570
|
const entry = this.rowToEntry(row);
|
|
411
|
-
|
|
571
|
+
const hit = { entry, score, providerId };
|
|
572
|
+
if (includeVectors) hit.vector = vec;
|
|
573
|
+
scored.push(hit);
|
|
412
574
|
}
|
|
413
575
|
scored.sort((a, b) => b.score - a.score);
|
|
414
576
|
return scored.slice(0, limit);
|
|
@@ -433,37 +595,45 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
433
595
|
}
|
|
434
596
|
async reindexAll() {
|
|
435
597
|
this.assertOpen();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
598
|
+
return withFileLock(
|
|
599
|
+
this.lockPath,
|
|
600
|
+
async () => {
|
|
601
|
+
const rows = this.db.prepare("SELECT id, text FROM entries").all();
|
|
602
|
+
let processed = 0;
|
|
603
|
+
let errors = 0;
|
|
604
|
+
for (const row of rows) {
|
|
605
|
+
try {
|
|
606
|
+
const result = await this.provider.embed([row.text]);
|
|
607
|
+
const v = result[0];
|
|
608
|
+
if (!v) {
|
|
609
|
+
errors++;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
613
|
+
this.db.prepare(
|
|
614
|
+
`INSERT INTO vectors (entry_id, provider_id, dimensions, vector, created_at)
|
|
615
|
+
VALUES (?, ?, ?, ?, ?)
|
|
616
|
+
ON CONFLICT(entry_id, provider_id) DO UPDATE SET
|
|
617
|
+
vector = excluded.vector,
|
|
618
|
+
dimensions = excluded.dimensions,
|
|
619
|
+
created_at = excluded.created_at`
|
|
620
|
+
).run(
|
|
621
|
+
row.id,
|
|
622
|
+
this.provider.id,
|
|
623
|
+
v.length,
|
|
624
|
+
encodeVector(v),
|
|
625
|
+
now
|
|
626
|
+
);
|
|
627
|
+
this.cacheVector(row.text, v, now);
|
|
628
|
+
processed++;
|
|
629
|
+
} catch {
|
|
630
|
+
errors++;
|
|
631
|
+
}
|
|
446
632
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
vector = excluded.vector,
|
|
452
|
-
dimensions = excluded.dimensions,
|
|
453
|
-
created_at = excluded.created_at`
|
|
454
|
-
).run(
|
|
455
|
-
row.id,
|
|
456
|
-
this.provider.id,
|
|
457
|
-
v.length,
|
|
458
|
-
encodeVector(v),
|
|
459
|
-
(/* @__PURE__ */ new Date()).toISOString()
|
|
460
|
-
);
|
|
461
|
-
processed++;
|
|
462
|
-
} catch {
|
|
463
|
-
errors++;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return { processed, errors };
|
|
633
|
+
return { processed, errors };
|
|
634
|
+
},
|
|
635
|
+
{ timeoutMs: 6e4 }
|
|
636
|
+
);
|
|
467
637
|
}
|
|
468
638
|
stats() {
|
|
469
639
|
this.assertOpen();
|
|
@@ -479,6 +649,55 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
479
649
|
dimensions: this.provider.dimensions
|
|
480
650
|
};
|
|
481
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Embedding-cache diagnostics — entries, hit/miss counters, oldest entry.
|
|
654
|
+
* Useful for the WebUI's vector-memory panel and for diagnosing the
|
|
655
|
+
* "why is search slow?" question.
|
|
656
|
+
*/
|
|
657
|
+
cacheStats() {
|
|
658
|
+
this.assertOpen();
|
|
659
|
+
const entries = this.db.prepare("SELECT COUNT(*) AS n FROM embedding_cache").get().n;
|
|
660
|
+
const providers = this.db.prepare("SELECT COUNT(DISTINCT provider_id) AS n FROM embedding_cache").get().n;
|
|
661
|
+
const totalUseCount = this.db.prepare("SELECT COALESCE(SUM(use_count), 0) AS n FROM embedding_cache").get().n;
|
|
662
|
+
const oldest = this.db.prepare("SELECT MIN(last_used_at) AS t FROM embedding_cache").get();
|
|
663
|
+
return {
|
|
664
|
+
entries,
|
|
665
|
+
providers,
|
|
666
|
+
totalUseCount,
|
|
667
|
+
oldestLastUsedAt: oldest?.t ?? null
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* LRU-evict the embedding cache down to `keepMostRecent` rows. The
|
|
672
|
+
* `embedding_cache` table is independent of `entries`, so a sweep here
|
|
673
|
+
* only removes cached vectors — never stored entries. Called by hosts
|
|
674
|
+
* that want to bound cache growth on long-lived processes.
|
|
675
|
+
*/
|
|
676
|
+
async evictCache(keepMostRecent) {
|
|
677
|
+
this.assertOpen();
|
|
678
|
+
if (keepMostRecent < 0) {
|
|
679
|
+
throw new Error("evictCache: keepMostRecent must be >= 0");
|
|
680
|
+
}
|
|
681
|
+
return withFileLock(
|
|
682
|
+
this.lockPath,
|
|
683
|
+
async () => {
|
|
684
|
+
const total = this.db.prepare("SELECT COUNT(*) AS n FROM embedding_cache").get().n;
|
|
685
|
+
if (total <= keepMostRecent) return { removed: 0 };
|
|
686
|
+
const toRemove = total - keepMostRecent;
|
|
687
|
+
const stmt = this.db.prepare(
|
|
688
|
+
`DELETE FROM embedding_cache
|
|
689
|
+
WHERE content_hash IN (
|
|
690
|
+
SELECT content_hash FROM embedding_cache
|
|
691
|
+
ORDER BY last_used_at ASC
|
|
692
|
+
LIMIT ?
|
|
693
|
+
)`
|
|
694
|
+
);
|
|
695
|
+
const info = stmt.run(Math.min(toRemove, CACHE_EVICT_BATCH));
|
|
696
|
+
return { removed: Number(info.changes) };
|
|
697
|
+
},
|
|
698
|
+
{ timeoutMs: DEFAULT_LOCK_TIMEOUT_MS }
|
|
699
|
+
);
|
|
700
|
+
}
|
|
482
701
|
close() {
|
|
483
702
|
if (this.closed) return;
|
|
484
703
|
this.closed = true;
|
|
@@ -510,19 +729,20 @@ var VectorMemoryStore = class _VectorMemoryStore {
|
|
|
510
729
|
}
|
|
511
730
|
async syncFromSage(sage) {
|
|
512
731
|
this.assertOpen();
|
|
513
|
-
const memories = await sage.listActiveMemories({ limit:
|
|
732
|
+
const memories = await sage.listActiveMemories({ limit: Number.POSITIVE_INFINITY });
|
|
514
733
|
let indexed = 0;
|
|
515
734
|
let skipped = 0;
|
|
516
735
|
let failed = 0;
|
|
517
736
|
const errors = [];
|
|
518
737
|
for (const memory of memories) {
|
|
519
738
|
try {
|
|
520
|
-
const
|
|
739
|
+
const hash = _VectorMemoryStore.contentHash(memory.text);
|
|
740
|
+
const existing = this.findByContentHash(hash);
|
|
521
741
|
if (existing) {
|
|
522
742
|
skipped++;
|
|
523
743
|
continue;
|
|
524
744
|
}
|
|
525
|
-
await this.
|
|
745
|
+
await this.rememberUnlocked({
|
|
526
746
|
text: memory.text,
|
|
527
747
|
summary: memory.summary ?? void 0,
|
|
528
748
|
metadata: { source: "sage", sageId: memory.id, ...memory.metadata ?? {} },
|
|
@@ -554,6 +774,227 @@ function errMsg(err) {
|
|
|
554
774
|
return err instanceof Error ? err.message : String(err);
|
|
555
775
|
}
|
|
556
776
|
|
|
777
|
+
// src/sage-sync-source.ts
|
|
778
|
+
var DEFAULT_PAGE_SIZE = 500;
|
|
779
|
+
var HARD_FLOOR_PAGE = 1;
|
|
780
|
+
var HARD_CEILING_PAGE = 500;
|
|
781
|
+
var HARD_CEILING_TOTAL = 1e6;
|
|
782
|
+
function createSageSurfaceSyncSource(sage, opts = {}) {
|
|
783
|
+
const pageSize = clamp(opts.pageSize ?? DEFAULT_PAGE_SIZE, HARD_FLOOR_PAGE, HARD_CEILING_PAGE);
|
|
784
|
+
const maxTotal = opts.maxTotal === void 0 ? Number.POSITIVE_INFINITY : clamp(opts.maxTotal, 1, HARD_CEILING_TOTAL);
|
|
785
|
+
return {
|
|
786
|
+
async listActiveMemories({ limit }) {
|
|
787
|
+
const requested = limit === void 0 ? maxTotal : clamp(limit, 1, maxTotal === Number.POSITIVE_INFINITY ? limit : maxTotal);
|
|
788
|
+
const memories = [];
|
|
789
|
+
let cursor;
|
|
790
|
+
let noProgressPages = 0;
|
|
791
|
+
while (memories.length < requested) {
|
|
792
|
+
const remaining = requested - memories.length;
|
|
793
|
+
const page = await sage.listSagePage({
|
|
794
|
+
statuses: ["active"],
|
|
795
|
+
limit: Math.min(pageSize, Math.max(1, remaining)),
|
|
796
|
+
...cursor ? { cursor } : {}
|
|
797
|
+
});
|
|
798
|
+
const rows = page.memories ?? [];
|
|
799
|
+
for (const m of rows) {
|
|
800
|
+
memories.push({
|
|
801
|
+
id: m.id,
|
|
802
|
+
text: m.text,
|
|
803
|
+
...m.summary ? { summary: m.summary } : {},
|
|
804
|
+
...m.tags && m.tags.length > 0 ? { tags: m.tags } : {},
|
|
805
|
+
metadata: {
|
|
806
|
+
sageKind: m.kind,
|
|
807
|
+
sageScope: m.scope,
|
|
808
|
+
importance: m.importance,
|
|
809
|
+
confidence: m.confidence
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (!page.nextCursor || rows.length === 0) break;
|
|
814
|
+
if (rows.length === 0) {
|
|
815
|
+
noProgressPages++;
|
|
816
|
+
if (noProgressPages > 3) break;
|
|
817
|
+
} else {
|
|
818
|
+
noProgressPages = 0;
|
|
819
|
+
}
|
|
820
|
+
cursor = page.nextCursor;
|
|
821
|
+
}
|
|
822
|
+
return memories.slice(0, requested);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function clamp(value, min, max) {
|
|
827
|
+
return Math.max(min, Math.min(max, value));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/sage-sync.ts
|
|
831
|
+
import { getSageSurface } from "@wrongstack/sage";
|
|
832
|
+
import * as fs2 from "node:fs";
|
|
833
|
+
import * as path2 from "node:path";
|
|
834
|
+
var SAGE_SYNC_MARKER_FILENAME = "sage-sync.complete.json";
|
|
835
|
+
var RUNNING_STALE_MS = 10 * 60 * 1e3;
|
|
836
|
+
async function startFirstBootSageSync(opts) {
|
|
837
|
+
const { store, memoryStore } = opts;
|
|
838
|
+
const log = opts.logger;
|
|
839
|
+
try {
|
|
840
|
+
if (opts.force) {
|
|
841
|
+
try {
|
|
842
|
+
fs2.unlinkSync(markerPath(store));
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const decision = decideWhetherToSync(
|
|
847
|
+
store,
|
|
848
|
+
opts.staleAfterMs ?? RUNNING_STALE_MS,
|
|
849
|
+
void 0,
|
|
850
|
+
opts.pidAlive
|
|
851
|
+
);
|
|
852
|
+
if (!decision.run) {
|
|
853
|
+
log?.debug?.(`vector-memory sage sync skipped: ${decision.reason}`);
|
|
854
|
+
return { synced: false, reason: decision.reason };
|
|
855
|
+
}
|
|
856
|
+
const provider = storeProvider(store);
|
|
857
|
+
if (provider && typeof provider.isAvailable === "function" && !await provider.isAvailable()) {
|
|
858
|
+
log?.debug?.(
|
|
859
|
+
"vector-memory sage sync deferred: embedding provider unavailable (optional dependency not installed?)"
|
|
860
|
+
);
|
|
861
|
+
return { synced: false, reason: "provider-unavailable" };
|
|
862
|
+
}
|
|
863
|
+
if (provider && typeof provider.embed === "function") {
|
|
864
|
+
try {
|
|
865
|
+
const probe = await provider.embed(["wrongstack vector memory warmup probe"]);
|
|
866
|
+
if (!probe[0] || probe[0].length === 0) throw new Error("empty embedding");
|
|
867
|
+
} catch {
|
|
868
|
+
log?.debug?.(
|
|
869
|
+
"vector-memory sage sync deferred: embedding probe failed (model not cached / backend error)"
|
|
870
|
+
);
|
|
871
|
+
return { synced: false, reason: "provider-unavailable" };
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const surface = getSageSurface(memoryStore);
|
|
875
|
+
if (!surface) {
|
|
876
|
+
log?.debug?.("vector-memory sage sync skipped: memory store exposes no SAGE surface");
|
|
877
|
+
return { synced: false, reason: "no-sage-surface" };
|
|
878
|
+
}
|
|
879
|
+
writeMarker(store, {
|
|
880
|
+
phase: "running",
|
|
881
|
+
pid: process.pid,
|
|
882
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
883
|
+
});
|
|
884
|
+
const report = await store.syncFromSage(createSageSurfaceSyncSource(surface));
|
|
885
|
+
if (report.failed > 0) {
|
|
886
|
+
log?.warn?.(
|
|
887
|
+
`vector-memory sage sync finished with ${report.failed} failure(s) \u2014 will retry on next boot`
|
|
888
|
+
);
|
|
889
|
+
return {
|
|
890
|
+
synced: false,
|
|
891
|
+
reason: "partial-failure",
|
|
892
|
+
marker: { phase: "running", ...counts(report) }
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
let stats = store.stats();
|
|
896
|
+
if (stats.vectors !== stats.entries) {
|
|
897
|
+
log?.warn?.(
|
|
898
|
+
`vector-memory sage sync healed ${stats.entries - stats.vectors} vector-less entr(ies) via reindexAll()`
|
|
899
|
+
);
|
|
900
|
+
await store.reindexAll();
|
|
901
|
+
stats = store.stats();
|
|
902
|
+
}
|
|
903
|
+
if (stats.vectors !== stats.entries) {
|
|
904
|
+
log?.warn?.(
|
|
905
|
+
`vector-memory sage sync incomplete: ${stats.entries - stats.vectors} entr(ies) still vector-less \u2014 will retry on next boot`
|
|
906
|
+
);
|
|
907
|
+
return {
|
|
908
|
+
synced: false,
|
|
909
|
+
reason: "vector-incomplete",
|
|
910
|
+
marker: { phase: "running", ...counts(report) }
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
const marker = {
|
|
914
|
+
phase: "complete",
|
|
915
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
916
|
+
...provider ? { providerId: provider.id } : {},
|
|
917
|
+
...counts(report)
|
|
918
|
+
};
|
|
919
|
+
writeMarker(store, marker);
|
|
920
|
+
log?.info?.(
|
|
921
|
+
`vector-memory sage sync complete: ${report.indexed} indexed, ${report.skipped} skipped (already present)`
|
|
922
|
+
);
|
|
923
|
+
return { synced: true, reason: "synced", marker };
|
|
924
|
+
} catch (error) {
|
|
925
|
+
log?.warn?.(
|
|
926
|
+
`vector-memory sage sync failed: ${error instanceof Error ? error.message : String(error)} \u2014 will retry on next boot`
|
|
927
|
+
);
|
|
928
|
+
return { synced: false, reason: "error" };
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function decideWhetherToSync(store, staleAfterMs, now = /* @__PURE__ */ new Date(), pidAlive = defaultPidAlive) {
|
|
932
|
+
const existing = readMarker(store);
|
|
933
|
+
if (!existing) return { run: true, reason: "no-marker" };
|
|
934
|
+
if (existing.phase === "complete") return { run: false, reason: "already-complete" };
|
|
935
|
+
if (existing.pid === process.pid) {
|
|
936
|
+
return { run: true, reason: "running-own-pid" };
|
|
937
|
+
}
|
|
938
|
+
const startedAt = existing.startedAt ? Date.parse(existing.startedAt) : Number.NaN;
|
|
939
|
+
const ageMs = Number.isNaN(startedAt) ? Number.POSITIVE_INFINITY : now.getTime() - startedAt;
|
|
940
|
+
const stale = ageMs > staleAfterMs;
|
|
941
|
+
if (existing.pid === void 0) {
|
|
942
|
+
return Number.isNaN(startedAt) || stale ? { run: true, reason: Number.isNaN(startedAt) ? "running-marker-undated" : "running-marker-stale" } : { run: false, reason: "running-unknown-pid" };
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
if (pidAlive(existing.pid)) {
|
|
946
|
+
return {
|
|
947
|
+
run: false,
|
|
948
|
+
reason: `running-pid-${existing.pid}${stale ? "-stale-but-alive" : ""}`
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
return { run: true, reason: `running-pid-${existing.pid}-dead` };
|
|
952
|
+
} catch {
|
|
953
|
+
return { run: false, reason: `running-pid-${existing.pid}-probe-failed` };
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function defaultPidAlive(pid) {
|
|
957
|
+
try {
|
|
958
|
+
process.kill(pid, 0);
|
|
959
|
+
return true;
|
|
960
|
+
} catch (error) {
|
|
961
|
+
return error.code === "EPERM";
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function readMarker(store) {
|
|
965
|
+
try {
|
|
966
|
+
const raw = fs2.readFileSync(markerPath(store), "utf8");
|
|
967
|
+
const parsed = JSON.parse(raw);
|
|
968
|
+
return parsed && (parsed.phase === "running" || parsed.phase === "complete") ? parsed : void 0;
|
|
969
|
+
} catch {
|
|
970
|
+
return void 0;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function writeMarker(store, marker) {
|
|
974
|
+
try {
|
|
975
|
+
fs2.writeFileSync(markerPath(store), `${JSON.stringify(marker, null, 2)}
|
|
976
|
+
`, "utf8");
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function markerPath(store) {
|
|
981
|
+
const dir = store.directory;
|
|
982
|
+
if (typeof dir !== "string") {
|
|
983
|
+
throw new Error("VectorMemoryStore.directory is required to place the sage sync marker");
|
|
984
|
+
}
|
|
985
|
+
return path2.join(dir, SAGE_SYNC_MARKER_FILENAME);
|
|
986
|
+
}
|
|
987
|
+
function storeProvider(store) {
|
|
988
|
+
const provider = store.provider;
|
|
989
|
+
if (provider && typeof provider.id === "string") {
|
|
990
|
+
return provider;
|
|
991
|
+
}
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
function counts(report) {
|
|
995
|
+
return { scanned: report.scanned, indexed: report.indexed, skipped: report.skipped, failed: report.failed };
|
|
996
|
+
}
|
|
997
|
+
|
|
557
998
|
// src/tools.ts
|
|
558
999
|
function createVectorMemoryTools(store) {
|
|
559
1000
|
return [
|
|
@@ -706,13 +1147,420 @@ function vectorMemoryForgetTool(store) {
|
|
|
706
1147
|
required: ["id"],
|
|
707
1148
|
additionalProperties: false
|
|
708
1149
|
},
|
|
709
|
-
execute: async (input) => ({ removed: store.forget(input.id) })
|
|
1150
|
+
execute: async (input) => ({ removed: await store.forget(input.id) })
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/sage-fusion.ts
|
|
1155
|
+
var DEFAULT_RRF_K = 60;
|
|
1156
|
+
var DEFAULT_VECTOR_WEIGHT = 0.3;
|
|
1157
|
+
function lexicalRankScore(index, total) {
|
|
1158
|
+
if (total <= 1) return 1;
|
|
1159
|
+
return 1 - index / Math.max(1, total - 1);
|
|
1160
|
+
}
|
|
1161
|
+
async function fuseWithVectorMemory(query, lexical, options = {}) {
|
|
1162
|
+
const weight = clamp01(options.vectorWeight ?? DEFAULT_VECTOR_WEIGHT);
|
|
1163
|
+
const k = options.rrfK ?? DEFAULT_RRF_K;
|
|
1164
|
+
const limit = options.limit ?? 25;
|
|
1165
|
+
const vectorOnlyThreshold = options.vectorOnlyThreshold ?? 0;
|
|
1166
|
+
let vectorHits = [];
|
|
1167
|
+
if (options.vectorHits) {
|
|
1168
|
+
vectorHits = [...options.vectorHits];
|
|
1169
|
+
} else if (options.store) {
|
|
1170
|
+
try {
|
|
1171
|
+
vectorHits = await options.store.search(query, {
|
|
1172
|
+
...options.threshold !== void 0 ? { threshold: options.threshold } : {},
|
|
1173
|
+
limit: Math.max(limit * 2, 50)
|
|
1174
|
+
});
|
|
1175
|
+
} catch {
|
|
1176
|
+
vectorHits = [];
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const sageById = /* @__PURE__ */ new Map();
|
|
1180
|
+
for (const memory of lexical) sageById.set(memory.id, memory);
|
|
1181
|
+
for (const hit of vectorHits) {
|
|
1182
|
+
const sageId = hit.entry.metadata?.["sageId"];
|
|
1183
|
+
if (typeof sageId === "string" && !sageById.has(sageId)) {
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const lexicalRanked = lexical.map((memory, index) => ({
|
|
1188
|
+
memory,
|
|
1189
|
+
rankScore: lexicalRankScore(index, lexical.length),
|
|
1190
|
+
lexicalScore: lexicalRankScore(index, lexical.length)
|
|
1191
|
+
}));
|
|
1192
|
+
const vectorRanked = [];
|
|
1193
|
+
for (let i = 0; i < vectorHits.length; i++) {
|
|
1194
|
+
const hit = vectorHits[i];
|
|
1195
|
+
const sageId = hit.entry.metadata?.["sageId"];
|
|
1196
|
+
if (typeof sageId !== "string") continue;
|
|
1197
|
+
const memory = sageById.get(sageId);
|
|
1198
|
+
if (!memory) continue;
|
|
1199
|
+
vectorRanked.push({ memory, vectorScore: hit.score });
|
|
1200
|
+
}
|
|
1201
|
+
const fused = /* @__PURE__ */ new Map();
|
|
1202
|
+
for (let i = 0; i < lexicalRanked.length; i++) {
|
|
1203
|
+
const c = lexicalRanked[i];
|
|
1204
|
+
const rrf = weight * 0 + (1 - weight) * (1 / (k + i + 1));
|
|
1205
|
+
fused.set(c.memory.id, {
|
|
1206
|
+
memory: c.memory,
|
|
1207
|
+
vectorScore: null,
|
|
1208
|
+
lexicalScore: c.lexicalScore,
|
|
1209
|
+
finalScore: rrf,
|
|
1210
|
+
source: "lexical"
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
for (let i = 0; i < vectorRanked.length; i++) {
|
|
1214
|
+
const v = vectorRanked[i];
|
|
1215
|
+
const rrf = weight * (1 / (k + i + 1));
|
|
1216
|
+
const existing = fused.get(v.memory.id);
|
|
1217
|
+
if (existing) {
|
|
1218
|
+
existing.finalScore += rrf;
|
|
1219
|
+
existing.vectorScore = v.vectorScore;
|
|
1220
|
+
existing.source = "both";
|
|
1221
|
+
} else if (v.vectorScore >= vectorOnlyThreshold) {
|
|
1222
|
+
fused.set(v.memory.id, {
|
|
1223
|
+
memory: v.memory,
|
|
1224
|
+
vectorScore: v.vectorScore,
|
|
1225
|
+
lexicalScore: null,
|
|
1226
|
+
finalScore: rrf,
|
|
1227
|
+
source: "vector"
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const out = Array.from(fused.values());
|
|
1232
|
+
out.sort((a, b) => b.finalScore - a.finalScore);
|
|
1233
|
+
return out.slice(0, limit);
|
|
1234
|
+
}
|
|
1235
|
+
function asVectorRecallProvider(store) {
|
|
1236
|
+
return {
|
|
1237
|
+
async search(query, opts) {
|
|
1238
|
+
const hits = await store.search(query, {
|
|
1239
|
+
limit: opts.limit,
|
|
1240
|
+
...opts.threshold !== void 0 ? { threshold: opts.threshold } : {}
|
|
1241
|
+
});
|
|
1242
|
+
return hits.map((h) => ({
|
|
1243
|
+
id: h.entry.id,
|
|
1244
|
+
score: h.score,
|
|
1245
|
+
text: h.entry.text,
|
|
1246
|
+
...h.entry.summary ? { summary: h.entry.summary } : {},
|
|
1247
|
+
tags: h.entry.tags,
|
|
1248
|
+
...h.entry.metadata ? { metadata: h.entry.metadata } : {}
|
|
1249
|
+
}));
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function clamp01(value) {
|
|
1254
|
+
if (!Number.isFinite(value)) return DEFAULT_VECTOR_WEIGHT;
|
|
1255
|
+
if (value < 0) return 0;
|
|
1256
|
+
if (value > 1) return 1;
|
|
1257
|
+
return value;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/sage-port-wrapper.ts
|
|
1261
|
+
import {
|
|
1262
|
+
SAGE_RETRIEVAL_CAPABILITY,
|
|
1263
|
+
SAGE_SURFACE_CAPABILITY
|
|
1264
|
+
} from "@wrongstack/sage";
|
|
1265
|
+
function mergeVectorRecall(options, recall, weight, threshold) {
|
|
1266
|
+
if (options && typeof options === "object" && "vectorRecall" in options && options["vectorRecall"]) {
|
|
1267
|
+
return options;
|
|
1268
|
+
}
|
|
1269
|
+
return {
|
|
1270
|
+
...options ?? {},
|
|
1271
|
+
vectorRecall: recall,
|
|
1272
|
+
...weight !== void 0 ? { vectorRecallWeight: weight } : {},
|
|
1273
|
+
...threshold !== void 0 ? { vectorRecallMinScore: threshold } : {}
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function asVectorRecallProviderAdapter(store) {
|
|
1277
|
+
return {
|
|
1278
|
+
async search(query, opts) {
|
|
1279
|
+
const hits = await store.search(query, {
|
|
1280
|
+
limit: opts.limit,
|
|
1281
|
+
...opts.threshold !== void 0 ? { threshold: opts.threshold } : {}
|
|
1282
|
+
});
|
|
1283
|
+
return hits.map((h) => ({
|
|
1284
|
+
id: h.entry.id,
|
|
1285
|
+
score: h.score,
|
|
1286
|
+
text: h.entry.text,
|
|
1287
|
+
...h.entry.summary ? { summary: h.entry.summary } : {},
|
|
1288
|
+
tags: h.entry.tags,
|
|
1289
|
+
...h.entry.metadata ? { metadata: h.entry.metadata } : {}
|
|
1290
|
+
}));
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
function wrapMemoryPortWithVectorRecall(port, options) {
|
|
1295
|
+
const recall = options.vectorRecall ?? asVectorRecallProviderAdapter(options.store);
|
|
1296
|
+
const weight = options.weight;
|
|
1297
|
+
const threshold = options.threshold;
|
|
1298
|
+
const wrapSearch = (original) => {
|
|
1299
|
+
return ((query, searchOpts) => original(
|
|
1300
|
+
query,
|
|
1301
|
+
mergeVectorRecall(
|
|
1302
|
+
searchOpts,
|
|
1303
|
+
recall,
|
|
1304
|
+
weight,
|
|
1305
|
+
threshold
|
|
1306
|
+
)
|
|
1307
|
+
));
|
|
1308
|
+
};
|
|
1309
|
+
const wrapped = Object.create(
|
|
1310
|
+
Object.getPrototypeOf(port),
|
|
1311
|
+
Object.getOwnPropertyDescriptors(port)
|
|
1312
|
+
);
|
|
1313
|
+
wrapped.getCapability = (capability) => {
|
|
1314
|
+
if (capability.id === SAGE_RETRIEVAL_CAPABILITY.id) {
|
|
1315
|
+
const original = port.getCapability(capability);
|
|
1316
|
+
if (!original) return void 0;
|
|
1317
|
+
return {
|
|
1318
|
+
...original,
|
|
1319
|
+
searchSage: wrapSearch(original.searchSage),
|
|
1320
|
+
// The rich-breakdown variant uses the same options-merge
|
|
1321
|
+
// helper — pass the vector recall through so consumers that
|
|
1322
|
+
// want the per-channel score breakdown get the same fusion
|
|
1323
|
+
// behaviour as `searchSage`.
|
|
1324
|
+
...original.searchSageWithBreakdown ? {
|
|
1325
|
+
searchSageWithBreakdown: wrapSearch(
|
|
1326
|
+
original.searchSageWithBreakdown
|
|
1327
|
+
)
|
|
1328
|
+
} : {}
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
if (capability.id === SAGE_SURFACE_CAPABILITY.id) {
|
|
1332
|
+
const original = port.getCapability(capability);
|
|
1333
|
+
if (!original) return void 0;
|
|
1334
|
+
return {
|
|
1335
|
+
...original,
|
|
1336
|
+
searchSage: wrapSearch(original.searchSage),
|
|
1337
|
+
...original.searchSageWithBreakdown ? {
|
|
1338
|
+
searchSageWithBreakdown: wrapSearch(
|
|
1339
|
+
original.searchSageWithBreakdown
|
|
1340
|
+
)
|
|
1341
|
+
} : {}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
return port.getCapability(capability);
|
|
1345
|
+
};
|
|
1346
|
+
return wrapped;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/sage-event-mirror.ts
|
|
1350
|
+
import { getSageSurface as getSageSurface2 } from "@wrongstack/sage";
|
|
1351
|
+
function subscribeVectorMemoryToSage(opts) {
|
|
1352
|
+
const { store, memoryStore } = opts;
|
|
1353
|
+
const log = opts.logger;
|
|
1354
|
+
if (opts.enabled === false) {
|
|
1355
|
+
return { dispose: () => void 0 };
|
|
1356
|
+
}
|
|
1357
|
+
const surface = getSageSurface2(memoryStore);
|
|
1358
|
+
if (!surface) {
|
|
1359
|
+
log?.debug?.("vector-memory mirror disabled: memory store exposes no SAGE surface");
|
|
1360
|
+
return { dispose: () => void 0 };
|
|
1361
|
+
}
|
|
1362
|
+
const events = memoryStore.events;
|
|
1363
|
+
if (!events) {
|
|
1364
|
+
log?.debug?.("vector-memory mirror disabled: memory store has no event bus");
|
|
1365
|
+
return { dispose: () => void 0 };
|
|
1366
|
+
}
|
|
1367
|
+
const fetch = async (memoryId) => {
|
|
1368
|
+
try {
|
|
1369
|
+
return await surface.getSage(memoryId);
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
log?.warn?.(
|
|
1372
|
+
`vector-memory mirror fetch failed for ${memoryId}: ${errMsg2(err)}`
|
|
1373
|
+
);
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
const mirror = async (memoryId) => {
|
|
1378
|
+
const memory = await fetch(memoryId);
|
|
1379
|
+
if (!memory) return;
|
|
1380
|
+
if (memory.scope === "session") return;
|
|
1381
|
+
try {
|
|
1382
|
+
const existing = store.findBySageId(memoryId);
|
|
1383
|
+
if (existing) await store.forget(existing.id);
|
|
1384
|
+
await store.remember({
|
|
1385
|
+
text: memory.text,
|
|
1386
|
+
...memory.summary ? { summary: memory.summary } : {},
|
|
1387
|
+
tags: memory.tags ?? [],
|
|
1388
|
+
scope: "project",
|
|
1389
|
+
kind: "note",
|
|
1390
|
+
metadata: {
|
|
1391
|
+
source: "sage",
|
|
1392
|
+
sageId: memory.id,
|
|
1393
|
+
sageKind: memory.kind,
|
|
1394
|
+
sageScope: memory.scope,
|
|
1395
|
+
importance: memory.importance,
|
|
1396
|
+
confidence: memory.confidence
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
log?.warn?.(
|
|
1401
|
+
`vector-memory mirror remember failed for ${memoryId}: ${errMsg2(err)}`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
const forgetMirror = async (memoryId) => {
|
|
1406
|
+
try {
|
|
1407
|
+
const existing = store.findBySageId(memoryId);
|
|
1408
|
+
if (existing) await store.forget(existing.id);
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
log?.warn?.(
|
|
1411
|
+
`vector-memory mirror forget failed for ${memoryId}: ${errMsg2(err)}`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
const offAccepted = events.onPattern("memory.accepted", (_event, payload) => {
|
|
1416
|
+
const memoryId = payload?.memoryId;
|
|
1417
|
+
if (typeof memoryId !== "string") return;
|
|
1418
|
+
void mirror(memoryId);
|
|
1419
|
+
});
|
|
1420
|
+
const offRecovered = events.onPattern("memory.recovered", (_event, payload) => {
|
|
1421
|
+
const memoryId = payload?.memoryId;
|
|
1422
|
+
if (typeof memoryId !== "string") return;
|
|
1423
|
+
void mirror(memoryId);
|
|
1424
|
+
});
|
|
1425
|
+
const offUpdated = events.onPattern("memory.updated", (_event, payload) => {
|
|
1426
|
+
const memoryId = payload?.memoryId;
|
|
1427
|
+
if (typeof memoryId !== "string") return;
|
|
1428
|
+
const status = payload?.status;
|
|
1429
|
+
if (status === "deleted") return;
|
|
1430
|
+
void mirror(memoryId);
|
|
1431
|
+
});
|
|
1432
|
+
const offDeleted = events.onPattern("memory.deleted", (_event, payload) => {
|
|
1433
|
+
const memoryId = payload?.memoryId;
|
|
1434
|
+
if (typeof memoryId !== "string") return;
|
|
1435
|
+
void forgetMirror(memoryId);
|
|
1436
|
+
});
|
|
1437
|
+
return {
|
|
1438
|
+
dispose: () => {
|
|
1439
|
+
offAccepted();
|
|
1440
|
+
offRecovered();
|
|
1441
|
+
offUpdated();
|
|
1442
|
+
offDeleted();
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
async function forgetStaleSageMirrors(store, memoryStore, logger) {
|
|
1447
|
+
const surface = getSageSurface2(memoryStore);
|
|
1448
|
+
if (!surface) return { scanned: 0, removed: 0 };
|
|
1449
|
+
let scanned = 0;
|
|
1450
|
+
let removed = 0;
|
|
1451
|
+
for (let offset = 0; ; offset += 1e3) {
|
|
1452
|
+
const page = store.list({ limit: 1e3 });
|
|
1453
|
+
if (page.length === 0) break;
|
|
1454
|
+
for (const entry of page) {
|
|
1455
|
+
scanned++;
|
|
1456
|
+
const sageId = entry.metadata?.sageId;
|
|
1457
|
+
if (typeof sageId !== "string") continue;
|
|
1458
|
+
try {
|
|
1459
|
+
const memory = await surface.getSage(sageId);
|
|
1460
|
+
if (memory !== null && memory.status !== "deleted") continue;
|
|
1461
|
+
await store.forget(entry.id);
|
|
1462
|
+
removed++;
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
logger?.warn?.(
|
|
1465
|
+
`vector-memory stale-mirror sweep failed for ${sageId}: ${errMsg2(err)}`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (page.length < 1e3) break;
|
|
1470
|
+
}
|
|
1471
|
+
return { scanned, removed };
|
|
1472
|
+
}
|
|
1473
|
+
function errMsg2(err) {
|
|
1474
|
+
return err instanceof Error ? err.message : String(err);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/search-race.ts
|
|
1478
|
+
function previewText(text, maxLen) {
|
|
1479
|
+
if (text.length <= maxLen) return text;
|
|
1480
|
+
return text.slice(0, maxLen - 1) + "\u2026";
|
|
1481
|
+
}
|
|
1482
|
+
async function runSearchRace(query, lexical, vectorStore, options = {}) {
|
|
1483
|
+
const limit = options.limit ?? 20;
|
|
1484
|
+
const threshold = options.threshold ?? 0;
|
|
1485
|
+
let vectorHits = [];
|
|
1486
|
+
try {
|
|
1487
|
+
vectorHits = await vectorStore.search(query, {
|
|
1488
|
+
limit,
|
|
1489
|
+
...threshold > 0 ? { threshold } : {}
|
|
1490
|
+
});
|
|
1491
|
+
} catch {
|
|
1492
|
+
vectorHits = [];
|
|
1493
|
+
}
|
|
1494
|
+
const lexicalOnly = [];
|
|
1495
|
+
const vectorOnly = [];
|
|
1496
|
+
const overlap = [];
|
|
1497
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1498
|
+
const lexicalCapped = lexical.slice(0, limit);
|
|
1499
|
+
for (let i = 0; i < lexicalCapped.length; i++) {
|
|
1500
|
+
const mem = lexicalCapped[i];
|
|
1501
|
+
const score = lexicalCapped.length <= 1 ? 1 : 1 - i / Math.max(1, lexicalCapped.length - 1);
|
|
1502
|
+
const id = mem.id;
|
|
1503
|
+
seenIds.add(id);
|
|
1504
|
+
overlap.push({
|
|
1505
|
+
id,
|
|
1506
|
+
lexicalScore: score,
|
|
1507
|
+
vectorScore: 0,
|
|
1508
|
+
// patched below
|
|
1509
|
+
preview: previewText(mem.text, 140)
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
for (const hit of vectorHits) {
|
|
1513
|
+
const sageId = hit.entry.metadata?.["sageId"];
|
|
1514
|
+
if (typeof sageId !== "string") continue;
|
|
1515
|
+
const existing = overlap.find((row) => row.id === sageId);
|
|
1516
|
+
if (existing) {
|
|
1517
|
+
existing.vectorScore = hit.score;
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (seenIds.has(sageId)) continue;
|
|
1521
|
+
seenIds.add(sageId);
|
|
1522
|
+
vectorOnly.push({
|
|
1523
|
+
id: sageId,
|
|
1524
|
+
lexicalScore: null,
|
|
1525
|
+
vectorScore: hit.score,
|
|
1526
|
+
preview: previewText(hit.entry.text, 140)
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
for (let i = overlap.length - 1; i >= 0; i--) {
|
|
1530
|
+
const row = overlap[i];
|
|
1531
|
+
if (row.vectorScore === 0) {
|
|
1532
|
+
lexicalOnly.push({
|
|
1533
|
+
id: row.id,
|
|
1534
|
+
lexicalScore: row.lexicalScore,
|
|
1535
|
+
vectorScore: null,
|
|
1536
|
+
preview: row.preview
|
|
1537
|
+
});
|
|
1538
|
+
overlap.splice(i, 1);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
const lexicalCount = lexicalOnly.length + overlap.length;
|
|
1542
|
+
const vectorCount = vectorOnly.length + overlap.length;
|
|
1543
|
+
const denom = Math.max(lexicalCount, vectorCount, 1);
|
|
1544
|
+
return {
|
|
1545
|
+
query,
|
|
1546
|
+
lexicalOnly,
|
|
1547
|
+
vectorOnly,
|
|
1548
|
+
overlap,
|
|
1549
|
+
metrics: {
|
|
1550
|
+
lexicalCount,
|
|
1551
|
+
vectorCount,
|
|
1552
|
+
overlapCount: overlap.length,
|
|
1553
|
+
lexicalOnlyRatio: lexicalCount === 0 ? 0 : lexicalOnly.length / lexicalCount,
|
|
1554
|
+
vectorOnlyRatio: vectorCount === 0 ? 0 : vectorOnly.length / vectorCount,
|
|
1555
|
+
agreementRatio: overlap.length / denom
|
|
1556
|
+
}
|
|
710
1557
|
};
|
|
711
1558
|
}
|
|
712
1559
|
export {
|
|
713
1560
|
DEFAULT_VECTOR_DIMENSIONS,
|
|
714
1561
|
DEFAULT_VECTOR_DTYPE,
|
|
715
1562
|
DEFAULT_VECTOR_MODEL_ID,
|
|
1563
|
+
SAGE_SYNC_MARKER_FILENAME,
|
|
716
1564
|
TransformersEmbeddingProvider,
|
|
717
1565
|
VECTOR_DIMENSIONS_KEY,
|
|
718
1566
|
VECTOR_PROVIDER_KEY,
|
|
@@ -720,10 +1568,21 @@ export {
|
|
|
720
1568
|
VectorMemoryError,
|
|
721
1569
|
VectorMemoryProviderUnavailableError,
|
|
722
1570
|
VectorMemoryStore,
|
|
1571
|
+
asVectorRecallProvider,
|
|
1572
|
+
createSageSurfaceSyncSource,
|
|
723
1573
|
createVectorMemoryTools,
|
|
1574
|
+
decideWhetherToSync,
|
|
724
1575
|
decodeVector,
|
|
725
1576
|
encodeVector,
|
|
726
1577
|
fallbackHashingProvider,
|
|
727
|
-
|
|
1578
|
+
forgetStaleSageMirrors,
|
|
1579
|
+
fuseWithVectorMemory,
|
|
1580
|
+
initVectorSchema,
|
|
1581
|
+
lookupEmbeddingCache,
|
|
1582
|
+
runSearchRace,
|
|
1583
|
+
startFirstBootSageSync,
|
|
1584
|
+
subscribeVectorMemoryToSage,
|
|
1585
|
+
upsertEmbeddingCache,
|
|
1586
|
+
wrapMemoryPortWithVectorRecall
|
|
728
1587
|
};
|
|
729
1588
|
//# sourceMappingURL=index.js.map
|