opencode-rag-plugin 1.19.4 → 1.19.8
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/dist/chunker/base.js +19 -5
- package/dist/chunker/factory.js +27 -9
- package/dist/chunker/grammar.d.ts +18 -1
- package/dist/chunker/grammar.js +48 -10
- package/dist/chunker/image.js +5 -0
- package/dist/chunker/pdf.js +30 -14
- package/dist/cli/commands/backend-detect.d.ts +61 -0
- package/dist/cli/commands/backend-detect.js +119 -0
- package/dist/cli/commands/index-command.js +11 -0
- package/dist/cli/commands/init-helpers.d.ts +4 -1
- package/dist/cli/commands/init-helpers.js +21 -4
- package/dist/cli/commands/init.js +61 -24
- package/dist/cli/commands/quirk.js +9 -3
- package/dist/cli/commands/setup.js +12 -3
- package/dist/cli/commands/status.js +14 -5
- package/dist/cli/commands/ui.js +23 -9
- package/dist/cli/commands/update.js +4 -5
- package/dist/cli/format.d.ts +5 -2
- package/dist/cli/format.js +14 -5
- package/dist/content/image.js +33 -11
- package/dist/content/reader.js +74 -13
- package/dist/core/bootstrap.js +10 -3
- package/dist/core/config.d.ts +27 -1
- package/dist/core/config.js +41 -2
- package/dist/core/desc-cache.d.ts +8 -2
- package/dist/core/desc-cache.js +10 -3
- package/dist/core/doc-progress.js +5 -2
- package/dist/core/interfaces.d.ts +32 -4
- package/dist/core/manifest.js +1 -1
- package/dist/core/provider-defaults.d.ts +2 -0
- package/dist/core/provider-defaults.js +19 -4
- package/dist/core/runtime-overrides.d.ts +0 -6
- package/dist/core/version-check.d.ts +5 -0
- package/dist/core/version-check.js +8 -2
- package/dist/describer/anthropic.d.ts +2 -2
- package/dist/describer/anthropic.js +19 -5
- package/dist/describer/describer.d.ts +20 -0
- package/dist/describer/describer.js +132 -16
- package/dist/describer/gemini.js +25 -10
- package/dist/describer/shared.d.ts +28 -0
- package/dist/describer/shared.js +60 -0
- package/dist/embedder/factory.d.ts +5 -3
- package/dist/embedder/factory.js +42 -9
- package/dist/embedder/health.js +19 -19
- package/dist/embedder/http.d.ts +14 -1
- package/dist/embedder/http.js +60 -6
- package/dist/embedder/ollama.d.ts +3 -1
- package/dist/embedder/ollama.js +12 -2
- package/dist/eval/session-logger.js +7 -0
- package/dist/eval/storage.js +8 -0
- package/dist/indexer/git-diff.d.ts +1 -1
- package/dist/indexer/git-diff.js +5 -1
- package/dist/indexer/pipeline.js +511 -346
- package/dist/indexer/stats.d.ts +2 -0
- package/dist/indexer/stats.js +1 -0
- package/dist/indexer/watch.js +8 -1
- package/dist/indexer/worker.js +21 -0
- package/dist/mcp/cli.js +4 -0
- package/dist/mcp/handlers.js +16 -5
- package/dist/mcp/server.js +3 -0
- package/dist/opencode/create-read-tool.js +14 -3
- package/dist/opencode/tool-args.js +23 -1
- package/dist/plugin.js +66 -152
- package/dist/quirks/auto-capture.js +5 -0
- package/dist/quirks/quirk-store.d.ts +1 -1
- package/dist/quirks/quirk-store.js +56 -17
- package/dist/retriever/context-optimizer.js +18 -4
- package/dist/retriever/keyword-index.d.ts +2 -0
- package/dist/retriever/keyword-index.js +38 -4
- package/dist/retriever/retriever.js +6 -1
- package/dist/tui.js +41 -4
- package/dist/vectorstore/lancedb.d.ts +104 -8
- package/dist/vectorstore/lancedb.js +345 -71
- package/dist/vectorstore/memory.d.ts +8 -3
- package/dist/vectorstore/memory.js +27 -4
- package/dist/watcher.d.ts +8 -0
- package/dist/watcher.js +237 -85
- package/dist/web/api.d.ts +5 -1
- package/dist/web/api.js +195 -69
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +66 -28
- package/dist/web/static.d.ts +5 -2
- package/dist/web/static.js +9 -5
- package/dist/web/ui/assets/index-BDPYdtA1.js +3 -0
- package/dist/web/ui/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/ui/assets/index-CJBvt6e0.js +0 -3
|
@@ -99,6 +99,14 @@ export class LanceDbStore {
|
|
|
99
99
|
table = null;
|
|
100
100
|
tableInit = null;
|
|
101
101
|
writeLock = Promise.resolve(void 0);
|
|
102
|
+
/**
|
|
103
|
+
* Memoized once-per-process index-metric repair. Stores built by versions
|
|
104
|
+
* before the cosine search switch carry an IVF index trained with the
|
|
105
|
+
* default L2 metric, which makes every cosine query log
|
|
106
|
+
* "Requested metric Cosine is incompatible with index metric L2" and fall
|
|
107
|
+
* back to brute-force. This repairs that stale index on first search.
|
|
108
|
+
*/
|
|
109
|
+
indexRepairPromise = null;
|
|
102
110
|
/**
|
|
103
111
|
* Execute an async function under an exclusive write lock.
|
|
104
112
|
*
|
|
@@ -294,32 +302,65 @@ export class LanceDbStore {
|
|
|
294
302
|
}
|
|
295
303
|
/**
|
|
296
304
|
* Store chunks in the LanceDB table. New rows are inserted first, then
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* insert and cleanup. Automatically attempts repair on
|
|
305
|
+
* old rows for the same file that are not part of this write are removed
|
|
306
|
+
* in a single delete per file. This ensures no data is lost if the process
|
|
307
|
+
* aborts between insert and cleanup. Automatically attempts repair on
|
|
308
|
+
* corruption errors.
|
|
309
|
+
*
|
|
310
|
+
* When `options.dedup` is `false` the cleanup step is skipped entirely —
|
|
311
|
+
* a pure append. Use this when writing into a store that provably has no
|
|
312
|
+
* prior rows for these files (e.g. a freshly-created rebuild store).
|
|
313
|
+
*
|
|
300
314
|
* @param chunks - The chunks to add.
|
|
315
|
+
* @param options - Optional write options (`dedup`, default `true`).
|
|
301
316
|
*/
|
|
302
|
-
async addChunks(chunks) {
|
|
317
|
+
async addChunks(chunks, options) {
|
|
303
318
|
if (chunks.length === 0)
|
|
304
319
|
return;
|
|
320
|
+
const dedup = options?.dedup ?? true;
|
|
305
321
|
await this.withWriteLock(async () => {
|
|
306
322
|
try {
|
|
307
|
-
await this.addChunksInternal(chunks);
|
|
323
|
+
await this.addChunksInternal(chunks, dedup);
|
|
308
324
|
}
|
|
309
325
|
catch (err) {
|
|
310
326
|
if (isCorruptionError(err) && await this.tryRepair()) {
|
|
311
|
-
await this.addChunksInternal(chunks);
|
|
327
|
+
await this.addChunksInternal(chunks, dedup);
|
|
312
328
|
return;
|
|
313
329
|
}
|
|
314
330
|
throw err;
|
|
315
331
|
}
|
|
316
332
|
});
|
|
317
333
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Store chunks for many files in a single transaction: one `table.add`
|
|
336
|
+
* across all items, then one `table.delete` per item that needs dedup.
|
|
337
|
+
* This collapses what used to be a per-file add + per-startLine deletes
|
|
338
|
+
* (K+2 LanceDB versions per file) into ~1 + M versions per batch.
|
|
339
|
+
*
|
|
340
|
+
* @param items - Per-file chunk payloads with their dedup flags.
|
|
341
|
+
*/
|
|
342
|
+
async addChunksBulk(items) {
|
|
343
|
+
const active = items.filter((item) => item.chunks.length > 0);
|
|
344
|
+
if (active.length === 0)
|
|
345
|
+
return;
|
|
346
|
+
await this.withWriteLock(async () => {
|
|
347
|
+
try {
|
|
348
|
+
await this.addChunksBulkInternal(active);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if (isCorruptionError(err) && await this.tryRepair()) {
|
|
352
|
+
await this.addChunksBulkInternal(active);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/** Map a chunk to its internal row shape, or null if it has no embedding. */
|
|
360
|
+
chunkToRow(c) {
|
|
361
|
+
if (!c.embedding || c.embedding.length === 0)
|
|
362
|
+
return null;
|
|
363
|
+
return {
|
|
323
364
|
id: c.id,
|
|
324
365
|
content: c.content,
|
|
325
366
|
description: c.description ?? "",
|
|
@@ -331,56 +372,75 @@ export class LanceDbStore {
|
|
|
331
372
|
kind: c.metadata.kind ?? "",
|
|
332
373
|
quirkType: c.metadata.quirkType ?? "",
|
|
333
374
|
tags: c.metadata.tags ? JSON.stringify(c.metadata.tags) : "",
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/** Group new rows by file path for dedup deletes. */
|
|
378
|
+
rowsByFilePath(rows) {
|
|
379
|
+
const byFile = new Map();
|
|
339
380
|
for (const row of rows) {
|
|
340
|
-
const
|
|
341
|
-
const ids = newIdsByLine.get(key);
|
|
381
|
+
const ids = byFile.get(row.filePath);
|
|
342
382
|
if (ids) {
|
|
343
|
-
ids.
|
|
383
|
+
ids.push(row.id);
|
|
344
384
|
}
|
|
345
385
|
else {
|
|
346
|
-
|
|
386
|
+
byFile.set(row.filePath, [row.id]);
|
|
347
387
|
}
|
|
348
388
|
}
|
|
389
|
+
return byFile;
|
|
390
|
+
}
|
|
391
|
+
async addChunksInternal(chunks, dedup = true) {
|
|
392
|
+
const table = await this.getTable();
|
|
393
|
+
const rows = chunks
|
|
394
|
+
.map((c) => this.chunkToRow(c))
|
|
395
|
+
.filter((r) => r !== null);
|
|
396
|
+
if (rows.length === 0)
|
|
397
|
+
return;
|
|
349
398
|
// INSERT FIRST: data is safely stored before any delete
|
|
350
399
|
await table.add(rows);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
400
|
+
if (!dedup)
|
|
401
|
+
return;
|
|
402
|
+
// THEN DEDUP: one delete per file removes prior-revision rows at the
|
|
403
|
+
// same (filePath, startLine) positions AND stale startLines, while the
|
|
404
|
+
// NOT IN clause preserves the newly inserted rows (so multiple new IDs
|
|
405
|
+
// sharing a startLine never delete each other). Insert-first ordering
|
|
406
|
+
// keeps an abort between insert and delete from losing data.
|
|
407
|
+
const byFile = this.rowsByFilePath(rows);
|
|
408
|
+
for (const [filePath, ids] of byFile) {
|
|
359
409
|
const escapedPath = filePath.replace(/'/g, "''");
|
|
360
|
-
const idList =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (
|
|
410
|
+
const idList = ids.map((id) => `'${id.replace(/'/g, "''")}'`).join(", ");
|
|
411
|
+
await table.delete(`filePath = '${escapedPath}' AND id NOT IN (${idList})`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async addChunksBulkInternal(items) {
|
|
415
|
+
const table = await this.getTable();
|
|
416
|
+
const allRows = [];
|
|
417
|
+
const dedupByFile = new Map();
|
|
418
|
+
for (const item of items) {
|
|
419
|
+
const rows = item.chunks
|
|
420
|
+
.map((c) => this.chunkToRow(c))
|
|
421
|
+
.filter((r) => r !== null);
|
|
422
|
+
if (rows.length === 0)
|
|
373
423
|
continue;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
424
|
+
allRows.push(...rows);
|
|
425
|
+
if (item.dedup) {
|
|
426
|
+
for (const [filePath, ids] of this.rowsByFilePath(rows)) {
|
|
427
|
+
const existing = dedupByFile.get(filePath);
|
|
428
|
+
if (existing) {
|
|
429
|
+
existing.push(...ids);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
dedupByFile.set(filePath, [...ids]);
|
|
433
|
+
}
|
|
380
434
|
}
|
|
381
435
|
}
|
|
436
|
+
}
|
|
437
|
+
if (allRows.length === 0)
|
|
438
|
+
return;
|
|
439
|
+
// INSERT FIRST (single add for the whole batch), then per-file dedup
|
|
440
|
+
await table.add(allRows);
|
|
441
|
+
for (const [filePath, ids] of dedupByFile) {
|
|
382
442
|
const escapedPath = filePath.replace(/'/g, "''");
|
|
383
|
-
const idList =
|
|
443
|
+
const idList = ids.map((id) => `'${id.replace(/'/g, "''")}'`).join(", ");
|
|
384
444
|
await table.delete(`filePath = '${escapedPath}' AND id NOT IN (${idList})`);
|
|
385
445
|
}
|
|
386
446
|
}
|
|
@@ -396,6 +456,12 @@ export class LanceDbStore {
|
|
|
396
456
|
}
|
|
397
457
|
async searchWithFilter(embedding, topK, filter) {
|
|
398
458
|
try {
|
|
459
|
+
// Guard against dimension mismatch BEFORE the native call — LanceDB
|
|
460
|
+
// throws a cryptic error that used to be swallowed into "no results".
|
|
461
|
+
if (embedding.length !== this.vectorDimension) {
|
|
462
|
+
console.warn(`[lancedb] searchWithFilter: query embedding dimension ${embedding.length} != store dimension ${this.vectorDimension} — returning empty`);
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
399
465
|
return await this.searchInternal(embedding, topK, filter);
|
|
400
466
|
}
|
|
401
467
|
catch (err) {
|
|
@@ -404,7 +470,12 @@ export class LanceDbStore {
|
|
|
404
470
|
if (repaired) {
|
|
405
471
|
return this.searchInternal(embedding, topK, filter);
|
|
406
472
|
}
|
|
473
|
+
console.warn(`[lancedb] searchWithFilter: repair failed: ${err.message}`);
|
|
474
|
+
return [];
|
|
407
475
|
}
|
|
476
|
+
// Never silently mask non-corruption failures as "no results" —
|
|
477
|
+
// that hides provider outages and store bugs from every caller.
|
|
478
|
+
console.warn(`[lancedb] searchWithFilter failed (returning []): ${err.message}`);
|
|
408
479
|
return [];
|
|
409
480
|
}
|
|
410
481
|
}
|
|
@@ -534,6 +605,104 @@ export class LanceDbStore {
|
|
|
534
605
|
}));
|
|
535
606
|
});
|
|
536
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Retrieve a paginated list of chunks without embeddings, with optional
|
|
610
|
+
* language and file-path filters pushed into the SQL WHERE clause.
|
|
611
|
+
*
|
|
612
|
+
* @param offset - Number of rows to skip (for pagination).
|
|
613
|
+
* @param limit - Maximum number of rows to return.
|
|
614
|
+
* @param lang - Optional exact language filter.
|
|
615
|
+
* @param filePrefix - Optional filePath prefix filter.
|
|
616
|
+
* @returns The page of chunks plus the total count of matching rows.
|
|
617
|
+
*/
|
|
618
|
+
async getChunksFiltered(offset, limit, lang, filePrefix) {
|
|
619
|
+
const conditions = [];
|
|
620
|
+
if (lang)
|
|
621
|
+
conditions.push(`language = '${lang.replace(/'/g, "''")}'`);
|
|
622
|
+
if (filePrefix)
|
|
623
|
+
conditions.push(`filePath LIKE '${filePrefix.replace(/'/g, "''")}%'`);
|
|
624
|
+
const where = conditions.length > 0 ? conditions.join(" AND ") : undefined;
|
|
625
|
+
return this.withCorruptionRecovery(async () => {
|
|
626
|
+
const table = await this.getTable();
|
|
627
|
+
let query = table.query().select(QUERY_COLUMNS);
|
|
628
|
+
let countQuery = table.query().select(["id"]);
|
|
629
|
+
if (where) {
|
|
630
|
+
query = query.where(where);
|
|
631
|
+
countQuery = countQuery.where(where);
|
|
632
|
+
}
|
|
633
|
+
const [rows, countRows] = await Promise.all([
|
|
634
|
+
query.offset(Math.max(0, offset)).limit(Math.max(1, Math.min(limit, 1000))).toArray(),
|
|
635
|
+
countQuery.toArray(),
|
|
636
|
+
]);
|
|
637
|
+
return {
|
|
638
|
+
chunks: rows.map((row) => ({
|
|
639
|
+
id: row.id,
|
|
640
|
+
filePath: row.filePath,
|
|
641
|
+
language: row.language,
|
|
642
|
+
startLine: row.startLine,
|
|
643
|
+
endLine: row.endLine,
|
|
644
|
+
content: row.content,
|
|
645
|
+
description: row.description ?? "",
|
|
646
|
+
kind: row.kind ?? "",
|
|
647
|
+
quirkType: row.quirkType ?? "",
|
|
648
|
+
tags: row.tags ?? "",
|
|
649
|
+
})),
|
|
650
|
+
total: countRows.length,
|
|
651
|
+
};
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
/** Look up a single chunk by its ID. Returns undefined when not found. */
|
|
655
|
+
async getChunkById(id) {
|
|
656
|
+
return this.withCorruptionRecovery(async () => {
|
|
657
|
+
const table = await this.getTable();
|
|
658
|
+
const rows = await table.query()
|
|
659
|
+
.select(QUERY_COLUMNS)
|
|
660
|
+
.where(`id = '${id.replace(/'/g, "''")}'`)
|
|
661
|
+
.limit(1)
|
|
662
|
+
.toArray();
|
|
663
|
+
const row = rows[0];
|
|
664
|
+
if (!row)
|
|
665
|
+
return undefined;
|
|
666
|
+
return {
|
|
667
|
+
id: row.id,
|
|
668
|
+
filePath: row.filePath,
|
|
669
|
+
language: row.language,
|
|
670
|
+
startLine: row.startLine,
|
|
671
|
+
endLine: row.endLine,
|
|
672
|
+
content: row.content,
|
|
673
|
+
description: row.description ?? "",
|
|
674
|
+
kind: row.kind ?? "",
|
|
675
|
+
quirkType: row.quirkType ?? "",
|
|
676
|
+
tags: row.tags ?? "",
|
|
677
|
+
};
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
/** Fetch chunks by their IDs (for the compare view). Bounded to 100 ids. */
|
|
681
|
+
async getChunksByIds(ids) {
|
|
682
|
+
const bounded = ids.slice(0, 100);
|
|
683
|
+
if (bounded.length === 0)
|
|
684
|
+
return [];
|
|
685
|
+
return this.withCorruptionRecovery(async () => {
|
|
686
|
+
const table = await this.getTable();
|
|
687
|
+
const idList = bounded.map((id) => `'${id.replace(/'/g, "''")}'`).join(", ");
|
|
688
|
+
const rows = await table.query()
|
|
689
|
+
.select(QUERY_COLUMNS)
|
|
690
|
+
.where(`id IN (${idList})`)
|
|
691
|
+
.toArray();
|
|
692
|
+
return rows.map((row) => ({
|
|
693
|
+
id: row.id,
|
|
694
|
+
filePath: row.filePath,
|
|
695
|
+
language: row.language,
|
|
696
|
+
startLine: row.startLine,
|
|
697
|
+
endLine: row.endLine,
|
|
698
|
+
content: row.content,
|
|
699
|
+
description: row.description ?? "",
|
|
700
|
+
kind: row.kind ?? "",
|
|
701
|
+
quirkType: row.quirkType ?? "",
|
|
702
|
+
tags: row.tags ?? "",
|
|
703
|
+
}));
|
|
704
|
+
});
|
|
705
|
+
}
|
|
537
706
|
/**
|
|
538
707
|
* Fetch chunks with their embedding vectors included (for embedding projection).
|
|
539
708
|
* @param limit - Maximum number of rows to return.
|
|
@@ -575,6 +744,10 @@ export class LanceDbStore {
|
|
|
575
744
|
const count = await table.countRows();
|
|
576
745
|
if (count === 0)
|
|
577
746
|
return [];
|
|
747
|
+
// One-time per-process repair: drop any stale L2 index so the cosine query
|
|
748
|
+
// below doesn't log "Requested metric Cosine is incompatible" and degrade
|
|
749
|
+
// to brute-force. Await it so the repair completes before this search.
|
|
750
|
+
await this.withWriteLock(() => this.ensureCosineIndex());
|
|
578
751
|
const whereClause = buildWhereClause(filter);
|
|
579
752
|
let results;
|
|
580
753
|
try {
|
|
@@ -614,8 +787,11 @@ export class LanceDbStore {
|
|
|
614
787
|
return 0;
|
|
615
788
|
const table = await this.getTable();
|
|
616
789
|
const COUNT_TIMEOUT_MS = 30_000;
|
|
790
|
+
// Attach a no-op catch to the race loser so a late rejection (after the
|
|
791
|
+
// timeout resolved `null`) cannot become an unhandled rejection.
|
|
792
|
+
const countPromise = table.countRows().catch(() => 0);
|
|
617
793
|
const result = await Promise.race([
|
|
618
|
-
|
|
794
|
+
countPromise,
|
|
619
795
|
new Promise((resolve) => setTimeout(() => resolve(null), COUNT_TIMEOUT_MS)),
|
|
620
796
|
]);
|
|
621
797
|
if (result === null) {
|
|
@@ -628,25 +804,104 @@ export class LanceDbStore {
|
|
|
628
804
|
return 0;
|
|
629
805
|
}
|
|
630
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* Ensure the ANN index on the `embedding` column uses the cosine metric,
|
|
809
|
+
* matching the `distanceType` requested by searchInternal.
|
|
810
|
+
*
|
|
811
|
+
* Older stores built the IVF index with the ivfFlat default (L2), so every
|
|
812
|
+
* cosine query hit "Requested metric Cosine is incompatible with index
|
|
813
|
+
* metric L2" and silently fell back to brute-force O(N) scans. This lazily
|
|
814
|
+
* replaces such an index with a cosine one — once per process.
|
|
815
|
+
*
|
|
816
|
+
* Callers must hold the write lock (searchInternal wraps the call in
|
|
817
|
+
* withWriteLock; optimize runs under it already).
|
|
818
|
+
*/
|
|
819
|
+
ensureCosineIndex() {
|
|
820
|
+
if (!this.indexRepairPromise) {
|
|
821
|
+
this.indexRepairPromise = this.repairIndexMetricOnce();
|
|
822
|
+
}
|
|
823
|
+
return this.indexRepairPromise;
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Perform a single index-metric repair pass. Skips stores that have no
|
|
827
|
+
* index and fewer than 1000 rows (brute-force is optimal there). Uses a
|
|
828
|
+
* single `createIndex` with `replace: true` — a dropIndex + createIndex
|
|
829
|
+
* sequence races in LanceDB ("Retryable commit conflict") and leaves the
|
|
830
|
+
* stale index in place. On failure the memo is cleared so the next
|
|
831
|
+
* search/optimize retries.
|
|
832
|
+
*/
|
|
833
|
+
async repairIndexMetricOnce() {
|
|
834
|
+
try {
|
|
835
|
+
const table = await this.getTable();
|
|
836
|
+
const count = await table.countRows().catch(() => 0);
|
|
837
|
+
const indices = await table.listIndices();
|
|
838
|
+
const vecIndex = indices.find((i) => i.columns.includes("embedding"));
|
|
839
|
+
const idxName = vecIndex?.name ?? "embedding_idx";
|
|
840
|
+
const stats = await table.indexStats(idxName);
|
|
841
|
+
if (stats && stats.distanceType === "cosine")
|
|
842
|
+
return; // already healthy
|
|
843
|
+
if (!stats && count < 1000)
|
|
844
|
+
return; // tiny store, no index — leave brute-force
|
|
845
|
+
const numPartitions = Math.max(16, Math.min(256, Math.floor(count / 256)));
|
|
846
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
847
|
+
try {
|
|
848
|
+
await table.createIndex("embedding", {
|
|
849
|
+
config: lancedb.Index.ivfFlat({ numPartitions, distanceType: "cosine" }),
|
|
850
|
+
replace: true,
|
|
851
|
+
name: idxName,
|
|
852
|
+
// Wait for the commit so the repair is deterministic and cannot
|
|
853
|
+
// race a background index-creation from another process.
|
|
854
|
+
waitTimeoutSeconds: 120,
|
|
855
|
+
});
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
catch (err) {
|
|
859
|
+
const retriable = err instanceof Error &&
|
|
860
|
+
(err.message.includes("Retryable commit conflict") || err.message.includes("Please retry"));
|
|
861
|
+
if (!retriable || attempt === 3)
|
|
862
|
+
throw err;
|
|
863
|
+
await new Promise((resolve) => setTimeout(resolve, 150 * attempt));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (err) {
|
|
868
|
+
this.indexRepairPromise = null; // allow retry on a later search/optimize
|
|
869
|
+
console.warn(`[lancedb] index metric repair failed (queries continue in brute-force): ` +
|
|
870
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
631
873
|
/**
|
|
632
874
|
* Compact fragments and prune old version manifests to prevent the
|
|
633
|
-
* version-manifest accumulation that causes countRows() to hang
|
|
634
|
-
*
|
|
875
|
+
* version-manifest accumulation that causes countRows() to hang and the
|
|
876
|
+
* store phase to slow down as the index grows. Should be called at the
|
|
877
|
+
* end of a successful index pass, and periodically during long passes.
|
|
878
|
+
*
|
|
879
|
+
* @param options - `aggressive: true` prunes every version but the current
|
|
880
|
+
* one with `deleteUnverified`. Only safe for a private store that no other
|
|
881
|
+
* process reads (e.g. a temporary rebuild store). For the shared store,
|
|
882
|
+
* versions newer than 1 hour are retained so in-flight queries (Web UI,
|
|
883
|
+
* background auto-index) can finish before their data files are reclaimed.
|
|
635
884
|
*/
|
|
636
|
-
async optimize() {
|
|
885
|
+
async optimize(options) {
|
|
637
886
|
await this.withWriteLock(async () => {
|
|
638
887
|
try {
|
|
639
888
|
const table = await this.getTable();
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
889
|
+
if (options?.aggressive) {
|
|
890
|
+
await table.optimize({ cleanupOlderThan: new Date(), deleteUnverified: true });
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
const threshold = new Date(Date.now() - 60 * 60 * 1000);
|
|
894
|
+
await table.optimize({ cleanupOlderThan: threshold, deleteUnverified: false });
|
|
895
|
+
}
|
|
896
|
+
// Build the ANN vector index — without it every vectorSearch() is a
|
|
897
|
+
// brute-force O(N) flat scan (slow at 50k+ chunks). Skips early when
|
|
898
|
+
// an index with the correct cosine metric already exists.
|
|
899
|
+
await this.ensureCosineIndex();
|
|
647
900
|
}
|
|
648
|
-
catch {
|
|
649
|
-
// Optimize is best-effort
|
|
901
|
+
catch (err) {
|
|
902
|
+
// Optimize is best-effort — must not break indexing, but surface the
|
|
903
|
+
// failure instead of swallowing it silently.
|
|
904
|
+
console.warn(`[lancedb] optimize failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
650
905
|
}
|
|
651
906
|
});
|
|
652
907
|
}
|
|
@@ -684,20 +939,39 @@ export class LanceDbStore {
|
|
|
684
939
|
* @param newPath - Optional new filesystem path for the LanceDB database.
|
|
685
940
|
*/
|
|
686
941
|
async reopen(newPath) {
|
|
687
|
-
await this.
|
|
688
|
-
await this.db?.close();
|
|
689
|
-
this.table = null;
|
|
690
|
-
this.db = null;
|
|
691
|
-
this.tableInit = null;
|
|
942
|
+
await this.close();
|
|
692
943
|
if (newPath)
|
|
693
944
|
this.dbPath = newPath;
|
|
694
945
|
}
|
|
695
|
-
/**
|
|
946
|
+
/**
|
|
947
|
+
* Close the database connection and release resources.
|
|
948
|
+
*
|
|
949
|
+
* The native LanceDB `close()` can hang indefinitely on Windows, so the
|
|
950
|
+
* whole close is raced against a timeout — callers (CLI commands, plugin
|
|
951
|
+
* reload, web server) must never block forever on shutdown.
|
|
952
|
+
*/
|
|
696
953
|
async close() {
|
|
697
|
-
await
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
954
|
+
await Promise.race([
|
|
955
|
+
(async () => {
|
|
956
|
+
try {
|
|
957
|
+
await this.table?.close();
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
// best-effort — the connection may already be gone
|
|
961
|
+
}
|
|
962
|
+
this.table = null;
|
|
963
|
+
try {
|
|
964
|
+
await this.db?.close();
|
|
965
|
+
}
|
|
966
|
+
catch {
|
|
967
|
+
// best-effort — the connection may already be gone
|
|
968
|
+
}
|
|
969
|
+
this.db = null;
|
|
970
|
+
})(),
|
|
971
|
+
new Promise((resolve) => {
|
|
972
|
+
setTimeout(resolve, 5000).unref();
|
|
973
|
+
}),
|
|
974
|
+
]);
|
|
701
975
|
}
|
|
702
976
|
/**
|
|
703
977
|
* Back up the chunks.lance directory before a destructive operation.
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Ephemeral in-memory vector store using cosine similarity search.
|
|
3
3
|
*/
|
|
4
|
-
import type { VectorStore, Chunk, ChunkSummary, FileSummary, SearchResult, MetadataFilter } from "../core/interfaces.js";
|
|
4
|
+
import type { VectorStore, Chunk, ChunkSummary, FileSummary, SearchResult, MetadataFilter, BulkChunkWrite } from "../core/interfaces.js";
|
|
5
5
|
/** Ephemeral in-memory vector store using cosine similarity search. */
|
|
6
6
|
export declare class InMemoryVectorStore implements VectorStore {
|
|
7
7
|
private chunks;
|
|
8
|
-
addChunks(chunks: Chunk[]
|
|
8
|
+
addChunks(chunks: Chunk[], options?: {
|
|
9
|
+
dedup?: boolean;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
addChunksBulk(items: BulkChunkWrite[]): Promise<void>;
|
|
9
12
|
search(embedding: number[], topK: number): Promise<SearchResult[]>;
|
|
10
13
|
searchWithFilter(embedding: number[], topK: number, filter?: MetadataFilter): Promise<SearchResult[]>;
|
|
11
14
|
count(): Promise<number>;
|
|
@@ -22,5 +25,7 @@ export declare class InMemoryVectorStore implements VectorStore {
|
|
|
22
25
|
/** Release any held resources. No-op for the in-memory store. */
|
|
23
26
|
close(): Promise<void>;
|
|
24
27
|
/** No-op for the in-memory store. */
|
|
25
|
-
optimize(
|
|
28
|
+
optimize(_options?: {
|
|
29
|
+
aggressive?: boolean;
|
|
30
|
+
}): Promise<void>;
|
|
26
31
|
}
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
/** Ephemeral in-memory vector store using cosine similarity search. */
|
|
2
2
|
export class InMemoryVectorStore {
|
|
3
3
|
chunks = [];
|
|
4
|
-
async addChunks(chunks) {
|
|
5
|
-
|
|
4
|
+
async addChunks(chunks, options) {
|
|
5
|
+
const valid = chunks.filter((c) => c.embedding && c.embedding.length > 0);
|
|
6
|
+
if (valid.length === 0)
|
|
7
|
+
return;
|
|
8
|
+
if (options?.dedup === false) {
|
|
9
|
+
this.chunks.push(...valid);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
// Mirror LanceDB dedup semantics: keep existing rows for touched files
|
|
13
|
+
// that are part of this write; drop prior-revision rows that aren't.
|
|
14
|
+
const newIds = new Set(valid.map((c) => c.id));
|
|
15
|
+
const newPaths = new Set(valid.map((c) => c.metadata.filePath));
|
|
16
|
+
this.chunks = [
|
|
17
|
+
...this.chunks.filter((c) => !newPaths.has(c.metadata.filePath) || newIds.has(c.id)),
|
|
18
|
+
...valid,
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
async addChunksBulk(items) {
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
await this.addChunks(item.chunks, { dedup: item.dedup });
|
|
24
|
+
}
|
|
6
25
|
}
|
|
7
26
|
async search(embedding, topK) {
|
|
8
27
|
return this.searchWithFilter(embedding, topK);
|
|
@@ -13,7 +32,11 @@ export class InMemoryVectorStore {
|
|
|
13
32
|
.filter((c) => matchesFilter(c, filter))
|
|
14
33
|
.map((chunk) => {
|
|
15
34
|
const sim = cosineSimilarity(embedding, chunk.embedding);
|
|
16
|
-
|
|
35
|
+
// Normalize cosine [-1,1] into [0,1] so scores are comparable with
|
|
36
|
+
// the LanceDB store's clamped `1 - distance/2` domain (minScore and
|
|
37
|
+
// displayed scores mean the same thing across both stores).
|
|
38
|
+
const normalized = Math.min(1, Math.max(0, (sim + 1) / 2));
|
|
39
|
+
return { chunk, score: normalized };
|
|
17
40
|
})
|
|
18
41
|
.sort((a, b) => b.score - a.score)
|
|
19
42
|
.slice(0, topK);
|
|
@@ -74,7 +97,7 @@ export class InMemoryVectorStore {
|
|
|
74
97
|
async close() {
|
|
75
98
|
}
|
|
76
99
|
/** No-op for the in-memory store. */
|
|
77
|
-
async optimize() {
|
|
100
|
+
async optimize(_options) {
|
|
78
101
|
}
|
|
79
102
|
}
|
|
80
103
|
/**
|
package/dist/watcher.d.ts
CHANGED
|
@@ -40,6 +40,14 @@ export type WatcherStatus = {
|
|
|
40
40
|
/** Timestamp (ms since epoch) of the last completed run, or undefined. */
|
|
41
41
|
lastRunAt: number | undefined;
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Atomically claim the watcher lock for a workspace. Returns true only if
|
|
45
|
+
* this process is now the active watcher. A stale lock (dead PID) or an
|
|
46
|
+
* unreadable/corrupt lock file is cleared and re-claimed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function tryAcquireWatcherLock(storePath: string): boolean;
|
|
49
|
+
/** Release the watcher lock — only if this process owns it. */
|
|
50
|
+
export declare function releaseWatcherLock(storePath: string): void;
|
|
43
51
|
/**
|
|
44
52
|
* Create a background file watcher that automatically re-indexes the
|
|
45
53
|
* workspace when files change. Uses chokidar for file system events and
|