opencode-rag-plugin 1.19.0 → 1.19.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 +1 -1
- package/dist/cli/commands/status.js +4 -2
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/core/config.js +5 -5
- package/dist/embedder/factory.d.ts +10 -3
- package/dist/embedder/factory.js +38 -9
- package/dist/indexer/pipeline.js +3 -3
- package/dist/vectorstore/lancedb.d.ts +36 -0
- package/dist/vectorstore/lancedb.js +128 -43
- package/package.json +1 -1
package/ReadMe.md
CHANGED
|
@@ -55,7 +55,7 @@ opencode-rag query "authentication middleware"
|
|
|
55
55
|
|
|
56
56
|
A browser-based dashboard for exploring the indexed vector database - browse and inspect chunks and evaluate the OpenCode sessions in terms of retrieved chunks, consumed tokens and more.
|
|
57
57
|
|
|
58
|
-

|
|
59
59
|
|
|
60
60
|
Launch with `opencode-rag ui`. See [Web UI documentation](doc/webui.md) for details.
|
|
61
61
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import fs from "node:fs";
|
|
10
|
-
import { c, resolveCliContext,
|
|
10
|
+
import { c, resolveCliContext, logCliError, logCliInfo, formatTimestamp } from "../format.js";
|
|
11
11
|
import { getIndexStatusSummary } from "../../indexer.js";
|
|
12
12
|
import { getPackageMetadata } from "../helpers.js";
|
|
13
13
|
import { checkForUpdate } from "../../core/version-check.js";
|
|
@@ -156,7 +156,9 @@ export function registerStatusCommand(program) {
|
|
|
156
156
|
}
|
|
157
157
|
}).catch(() => { });
|
|
158
158
|
}
|
|
159
|
-
|
|
159
|
+
// Force exit — avoid LanceDB close() hanging on Windows native bindings.
|
|
160
|
+
// Status is read-only so there's no state to lose.
|
|
161
|
+
process.exit(0);
|
|
160
162
|
}
|
|
161
163
|
catch (err) {
|
|
162
164
|
const message = err.message || String(err);
|
package/dist/cli/index.d.ts
CHANGED
package/dist/cli/index.js
CHANGED
package/dist/core/config.js
CHANGED
|
@@ -114,7 +114,7 @@ export const DEFAULT_CONFIG = {
|
|
|
114
114
|
minFileSizeBytes: 0,
|
|
115
115
|
concurrency: 8,
|
|
116
116
|
embedBatchSize: 100,
|
|
117
|
-
embedConcurrency:
|
|
117
|
+
embedConcurrency: 3,
|
|
118
118
|
ollamaMaxBatchSize: 500,
|
|
119
119
|
descriptionConcurrency: 4,
|
|
120
120
|
maxSvgSizeBytes: 1_048_576,
|
|
@@ -266,14 +266,14 @@ export const DEFAULT_CONFIG = {
|
|
|
266
266
|
},
|
|
267
267
|
memory: {
|
|
268
268
|
enabled: true,
|
|
269
|
-
autoInject:
|
|
269
|
+
autoInject: true,
|
|
270
270
|
minConfidence: 0.5,
|
|
271
|
-
recallMinScore: 0.
|
|
272
|
-
autoInjectMinScore: 0.
|
|
271
|
+
recallMinScore: 0.6,
|
|
272
|
+
autoInjectMinScore: 0.5,
|
|
273
273
|
autoInjectLatencyBudgetMs: 2000,
|
|
274
274
|
autoInjectTopK: 2,
|
|
275
275
|
autoInjectMinTokenOverlap: 1,
|
|
276
|
-
passiveCapture:
|
|
276
|
+
passiveCapture: true,
|
|
277
277
|
promptEnforcement: true,
|
|
278
278
|
sessionEndExtraction: true,
|
|
279
279
|
autoCaptureMaxPerTurn: 2,
|
|
@@ -16,12 +16,16 @@ import type { RagConfig } from "../core/config.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function createEmbedder(config: RagConfig): EmbeddingProvider;
|
|
18
18
|
/**
|
|
19
|
-
* Embed a list of texts in batches with optional concurrency control.
|
|
19
|
+
* Embed a list of texts in batches with optional concurrency control and per-batch retry.
|
|
20
20
|
*
|
|
21
21
|
* Splits the input texts into chunks of `batchSize` and embeds them sequentially
|
|
22
22
|
* (or concurrently when `concurrency > 1`). When concurrency is limited, uses
|
|
23
23
|
* `p-limit` to cap the number of in-flight requests.
|
|
24
24
|
*
|
|
25
|
+
* Each batch is retried up to `retryMax` times with exponential backoff. If all
|
|
26
|
+
* retries are exhausted, the batch is skipped and empty arrays are returned for
|
|
27
|
+
* those texts so the caller can still process successfully embedded batches.
|
|
28
|
+
*
|
|
25
29
|
* @param embedder - The embedding provider to use
|
|
26
30
|
* @param texts - Array of text strings to embed
|
|
27
31
|
* @param batchSize - Number of texts per batch (default 10)
|
|
@@ -29,6 +33,9 @@ export declare function createEmbedder(config: RagConfig): EmbeddingProvider;
|
|
|
29
33
|
* @param concurrency - Maximum number of concurrent batch requests (default 1)
|
|
30
34
|
* @param onProgress - Optional callback invoked after each batch with the running
|
|
31
35
|
* completed count and total; per-text granularity when `concurrency <= 1`.
|
|
32
|
-
* @
|
|
36
|
+
* @param retryMax - Maximum retry attempts per batch (default 3)
|
|
37
|
+
* @param retryBaseDelayMs - Base delay for exponential backoff (default 1000)
|
|
38
|
+
* @returns A promise resolving to a flat array of embedding vectors (one per input text);
|
|
39
|
+
* failed batches return empty arrays
|
|
33
40
|
*/
|
|
34
|
-
export declare function embedBatch(embedder: EmbeddingProvider, texts: string[], batchSize?: number, purpose?: "query" | "document", concurrency?: number, onProgress?: (completed: number, total: number) => void): Promise<number[][]>;
|
|
41
|
+
export declare function embedBatch(embedder: EmbeddingProvider, texts: string[], batchSize?: number, purpose?: "query" | "document", concurrency?: number, onProgress?: (completed: number, total: number) => void, retryMax?: number, retryBaseDelayMs?: number): Promise<number[][]>;
|
package/dist/embedder/factory.js
CHANGED
|
@@ -35,12 +35,16 @@ export function createEmbedder(config) {
|
|
|
35
35
|
throw new Error(`Unknown embedding provider: ${provider}`);
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* Embed a list of texts in batches with optional concurrency control.
|
|
38
|
+
* Embed a list of texts in batches with optional concurrency control and per-batch retry.
|
|
39
39
|
*
|
|
40
40
|
* Splits the input texts into chunks of `batchSize` and embeds them sequentially
|
|
41
41
|
* (or concurrently when `concurrency > 1`). When concurrency is limited, uses
|
|
42
42
|
* `p-limit` to cap the number of in-flight requests.
|
|
43
43
|
*
|
|
44
|
+
* Each batch is retried up to `retryMax` times with exponential backoff. If all
|
|
45
|
+
* retries are exhausted, the batch is skipped and empty arrays are returned for
|
|
46
|
+
* those texts so the caller can still process successfully embedded batches.
|
|
47
|
+
*
|
|
44
48
|
* @param embedder - The embedding provider to use
|
|
45
49
|
* @param texts - Array of text strings to embed
|
|
46
50
|
* @param batchSize - Number of texts per batch (default 10)
|
|
@@ -48,31 +52,56 @@ export function createEmbedder(config) {
|
|
|
48
52
|
* @param concurrency - Maximum number of concurrent batch requests (default 1)
|
|
49
53
|
* @param onProgress - Optional callback invoked after each batch with the running
|
|
50
54
|
* completed count and total; per-text granularity when `concurrency <= 1`.
|
|
51
|
-
* @
|
|
55
|
+
* @param retryMax - Maximum retry attempts per batch (default 3)
|
|
56
|
+
* @param retryBaseDelayMs - Base delay for exponential backoff (default 1000)
|
|
57
|
+
* @returns A promise resolving to a flat array of embedding vectors (one per input text);
|
|
58
|
+
* failed batches return empty arrays
|
|
52
59
|
*/
|
|
53
|
-
export async function embedBatch(embedder, texts, batchSize = 10, purpose, concurrency = 1, onProgress) {
|
|
60
|
+
export async function embedBatch(embedder, texts, batchSize = 10, purpose, concurrency = 1, onProgress, retryMax = 3, retryBaseDelayMs = 1000) {
|
|
54
61
|
if (texts.length === 0)
|
|
55
62
|
return [];
|
|
56
63
|
const batches = [];
|
|
57
64
|
for (let i = 0; i < texts.length; i += batchSize) {
|
|
58
65
|
batches.push({ index: i, texts: texts.slice(i, i + batchSize) });
|
|
59
66
|
}
|
|
67
|
+
async function embedWithRetry(batchTexts) {
|
|
68
|
+
for (let attempt = 0; attempt <= retryMax; attempt++) {
|
|
69
|
+
try {
|
|
70
|
+
return await embedder.embed(batchTexts, purpose);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (attempt < retryMax) {
|
|
74
|
+
const delay = retryBaseDelayMs * Math.pow(2, attempt);
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
60
81
|
if (concurrency <= 1 || batches.length <= 1) {
|
|
61
82
|
const results = [];
|
|
62
83
|
for (const batch of batches) {
|
|
63
|
-
const embeddings = await
|
|
64
|
-
|
|
84
|
+
const embeddings = await embedWithRetry(batch.texts);
|
|
85
|
+
if (embeddings) {
|
|
86
|
+
results.push(...embeddings);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
for (let i = 0; i < batch.texts.length; i++) {
|
|
90
|
+
results.push([]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
65
93
|
onProgress?.(results.length, texts.length);
|
|
66
94
|
}
|
|
67
95
|
return results;
|
|
68
96
|
}
|
|
69
|
-
const limit = pLimit(concurrency);
|
|
70
97
|
let completedCount = 0;
|
|
98
|
+
const limit = pLimit(concurrency);
|
|
71
99
|
const batchResults = await Promise.all(batches.map((batch) => limit(async () => {
|
|
72
|
-
const embeddings = await
|
|
73
|
-
|
|
100
|
+
const embeddings = await embedWithRetry(batch.texts);
|
|
101
|
+
const flatResult = embeddings ?? batch.texts.map(() => []);
|
|
102
|
+
completedCount += embeddings?.length ?? 0;
|
|
74
103
|
onProgress?.(completedCount, texts.length);
|
|
75
|
-
return { index: batch.index, embeddings };
|
|
104
|
+
return { index: batch.index, embeddings: flatResult };
|
|
76
105
|
})));
|
|
77
106
|
batchResults.sort((a, b) => a.index - b.index);
|
|
78
107
|
const results = [];
|
package/dist/indexer/pipeline.js
CHANGED
|
@@ -590,7 +590,7 @@ async function runIndexPassInner(options, logger) {
|
|
|
590
590
|
hash: prepared[fileIdx].hash,
|
|
591
591
|
chunkCount: 0, fileLabel: prepared[fileIdx].fileLabel,
|
|
592
592
|
isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
|
|
593
|
-
isTooSmall: false, isRemoved:
|
|
593
|
+
isTooSmall: false, isRemoved: false, hadChunks: false,
|
|
594
594
|
descriptionFailed: prepared[fileIdx].descriptionFailed,
|
|
595
595
|
});
|
|
596
596
|
}
|
|
@@ -639,14 +639,14 @@ async function runIndexPassInner(options, logger) {
|
|
|
639
639
|
const result = {
|
|
640
640
|
normalizedPath: prep.normalizedPath,
|
|
641
641
|
hash: prep.hash,
|
|
642
|
-
chunkCount:
|
|
642
|
+
chunkCount: validChunks.length,
|
|
643
643
|
fileLabel: prep.fileLabel,
|
|
644
644
|
isNew: !prep.isModified,
|
|
645
645
|
isModified: prep.isModified,
|
|
646
646
|
isUnchanged: false,
|
|
647
647
|
isEmpty: false,
|
|
648
648
|
isTooSmall: false,
|
|
649
|
-
isRemoved: validChunks.length === 0,
|
|
649
|
+
isRemoved: (prep.chunks?.length ?? 0) > 0 && validChunks.length === 0 ? false : validChunks.length === 0,
|
|
650
650
|
hadChunks: (prep.chunks?.length ?? 0) > 0,
|
|
651
651
|
descriptionFailed: prep.descriptionFailed,
|
|
652
652
|
descHash: prep.descHash,
|
|
@@ -10,6 +10,19 @@ export declare function l2Normalize(vec: number[]): number[];
|
|
|
10
10
|
* @returns True if the error matches a known corruption pattern.
|
|
11
11
|
*/
|
|
12
12
|
export declare function isCorruptionError(err: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Check whether an error is a LanceDB transient transaction conflict
|
|
15
|
+
* (e.g. "Incompatible transaction: This Append transaction is incompatible
|
|
16
|
+
* with concurrent transaction Restore at version ...").
|
|
17
|
+
*
|
|
18
|
+
* These are recoverable by retrying after the conflicting transaction finishes.
|
|
19
|
+
* Cross-process writes are the primary source; in-process writes are serialized
|
|
20
|
+
* by the write lock.
|
|
21
|
+
*
|
|
22
|
+
* @param err - The error to inspect.
|
|
23
|
+
* @returns True if the error matches a transient transaction conflict.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isTransientConflictError(err: unknown): boolean;
|
|
13
26
|
/**
|
|
14
27
|
* Atomically replace one LanceDB store directory with another.
|
|
15
28
|
* Swaps the real directory with a temporary one that was built during a rebuild.
|
|
@@ -31,6 +44,23 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
31
44
|
private table;
|
|
32
45
|
private tableInit;
|
|
33
46
|
private writeLock;
|
|
47
|
+
/**
|
|
48
|
+
* Execute an async function under an exclusive write lock.
|
|
49
|
+
*
|
|
50
|
+
* All write operations (addChunks, deleteByFilePath, optimize, tryRepair) must
|
|
51
|
+
* go through this helper to prevent concurrent LanceDB transactions from
|
|
52
|
+
* conflicting (e.g. Append vs Restore, which produces the "Incompatible
|
|
53
|
+
* transaction" error).
|
|
54
|
+
*
|
|
55
|
+
* The lock is a Promise chain: each caller chains onto `this.writeLock` and
|
|
56
|
+
* sets it to a new promise that resolves only when its operation finishes
|
|
57
|
+
* (or throws). This guarantees FIFO serialization without any busy-waiting
|
|
58
|
+
* or timers.
|
|
59
|
+
*
|
|
60
|
+
* @param fn - The async function to execute under the lock.
|
|
61
|
+
* @returns The result of `fn`.
|
|
62
|
+
*/
|
|
63
|
+
private withWriteLock;
|
|
34
64
|
/**
|
|
35
65
|
* @param dbPath - Filesystem path to the LanceDB database directory.
|
|
36
66
|
* @param vectorDimension - Dimension of the embedding vectors. Default: 384.
|
|
@@ -191,4 +221,10 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
191
221
|
*/
|
|
192
222
|
private withCorruptionRecovery;
|
|
193
223
|
private tryRepair;
|
|
224
|
+
/**
|
|
225
|
+
* Drop the existing chunks table and let getTable() create a fresh one.
|
|
226
|
+
* All indexed data is lost — callers should detect the empty table and
|
|
227
|
+
* trigger a re-index if needed.
|
|
228
|
+
*/
|
|
229
|
+
private tryRebuildTable;
|
|
194
230
|
}
|
|
@@ -27,9 +27,32 @@ export function l2Normalize(vec) {
|
|
|
27
27
|
*/
|
|
28
28
|
export function isCorruptionError(err) {
|
|
29
29
|
if (err instanceof Error) {
|
|
30
|
-
return (err.message.includes("Not found") &&
|
|
30
|
+
return ((err.message.includes("Not found") &&
|
|
31
31
|
err.message.includes(".lance") &&
|
|
32
|
-
err.message.includes("lance error"))
|
|
32
|
+
err.message.includes("lance error")) ||
|
|
33
|
+
// Database has an incompatible transaction (e.g. a Restore from a prior
|
|
34
|
+
// version that conflicts with new Appends). This is a recoverable
|
|
35
|
+
// corruption — tryRepair() iterates prior versions to find a consistent one.
|
|
36
|
+
(err.message.includes("Incompatible transaction") &&
|
|
37
|
+
err.message.includes("version")));
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check whether an error is a LanceDB transient transaction conflict
|
|
43
|
+
* (e.g. "Incompatible transaction: This Append transaction is incompatible
|
|
44
|
+
* with concurrent transaction Restore at version ...").
|
|
45
|
+
*
|
|
46
|
+
* These are recoverable by retrying after the conflicting transaction finishes.
|
|
47
|
+
* Cross-process writes are the primary source; in-process writes are serialized
|
|
48
|
+
* by the write lock.
|
|
49
|
+
*
|
|
50
|
+
* @param err - The error to inspect.
|
|
51
|
+
* @returns True if the error matches a transient transaction conflict.
|
|
52
|
+
*/
|
|
53
|
+
export function isTransientConflictError(err) {
|
|
54
|
+
if (err instanceof Error) {
|
|
55
|
+
return err.message.includes("Incompatible transaction");
|
|
33
56
|
}
|
|
34
57
|
return false;
|
|
35
58
|
}
|
|
@@ -76,6 +99,43 @@ export class LanceDbStore {
|
|
|
76
99
|
table = null;
|
|
77
100
|
tableInit = null;
|
|
78
101
|
writeLock = Promise.resolve(void 0);
|
|
102
|
+
/**
|
|
103
|
+
* Execute an async function under an exclusive write lock.
|
|
104
|
+
*
|
|
105
|
+
* All write operations (addChunks, deleteByFilePath, optimize, tryRepair) must
|
|
106
|
+
* go through this helper to prevent concurrent LanceDB transactions from
|
|
107
|
+
* conflicting (e.g. Append vs Restore, which produces the "Incompatible
|
|
108
|
+
* transaction" error).
|
|
109
|
+
*
|
|
110
|
+
* The lock is a Promise chain: each caller chains onto `this.writeLock` and
|
|
111
|
+
* sets it to a new promise that resolves only when its operation finishes
|
|
112
|
+
* (or throws). This guarantees FIFO serialization without any busy-waiting
|
|
113
|
+
* or timers.
|
|
114
|
+
*
|
|
115
|
+
* @param fn - The async function to execute under the lock.
|
|
116
|
+
* @returns The result of `fn`.
|
|
117
|
+
*/
|
|
118
|
+
async withWriteLock(fn) {
|
|
119
|
+
const prev = this.writeLock;
|
|
120
|
+
let release = () => { };
|
|
121
|
+
this.writeLock = new Promise((resolve) => { release = resolve; });
|
|
122
|
+
await prev;
|
|
123
|
+
try {
|
|
124
|
+
return await fn();
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
// Cross-process transient conflict (e.g. CLI vs plugin):
|
|
128
|
+
// wait briefly and retry once, still under the same lock hold.
|
|
129
|
+
if (isTransientConflictError(err)) {
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
131
|
+
return await fn();
|
|
132
|
+
}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
release();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
79
139
|
/**
|
|
80
140
|
* @param dbPath - Filesystem path to the LanceDB database directory.
|
|
81
141
|
* @param vectorDimension - Dimension of the embedding vectors. Default: 384.
|
|
@@ -242,21 +302,18 @@ export class LanceDbStore {
|
|
|
242
302
|
async addChunks(chunks) {
|
|
243
303
|
if (chunks.length === 0)
|
|
244
304
|
return;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
await done;
|
|
249
|
-
}
|
|
250
|
-
catch (err) {
|
|
251
|
-
this.writeLock = Promise.resolve();
|
|
252
|
-
if (isCorruptionError(err) && await this.tryRepair()) {
|
|
253
|
-
const retry = this.addChunksInternal(chunks);
|
|
254
|
-
this.writeLock = retry.catch(() => { });
|
|
255
|
-
await retry;
|
|
256
|
-
return;
|
|
305
|
+
await this.withWriteLock(async () => {
|
|
306
|
+
try {
|
|
307
|
+
await this.addChunksInternal(chunks);
|
|
257
308
|
}
|
|
258
|
-
|
|
259
|
-
|
|
309
|
+
catch (err) {
|
|
310
|
+
if (isCorruptionError(err) && await this.tryRepair()) {
|
|
311
|
+
await this.addChunksInternal(chunks);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
260
317
|
}
|
|
261
318
|
async addChunksInternal(chunks) {
|
|
262
319
|
const table = await this.getTable();
|
|
@@ -343,7 +400,7 @@ export class LanceDbStore {
|
|
|
343
400
|
}
|
|
344
401
|
catch (err) {
|
|
345
402
|
if (isCorruptionError(err)) {
|
|
346
|
-
const repaired = await this.tryRepair();
|
|
403
|
+
const repaired = await this.withWriteLock(() => this.tryRepair());
|
|
347
404
|
if (repaired) {
|
|
348
405
|
return this.searchInternal(embedding, topK, filter);
|
|
349
406
|
}
|
|
@@ -577,19 +634,21 @@ export class LanceDbStore {
|
|
|
577
634
|
* Should be called at the end of a successful index pass.
|
|
578
635
|
*/
|
|
579
636
|
async optimize() {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
637
|
+
await this.withWriteLock(async () => {
|
|
638
|
+
try {
|
|
639
|
+
const table = await this.getTable();
|
|
640
|
+
// Clean up versions older than 1 hour �?" not "right now" �?" so in-flight
|
|
641
|
+
// queries (e.g. Web UI search, background auto-index) can finish before
|
|
642
|
+
// their data files are reclaimed. Using new Date() here caused data-file
|
|
643
|
+
// race conditions where a reader got "Not found: �?� .lance" because the
|
|
644
|
+
// GC deleted fragments that the current version still referenced.
|
|
645
|
+
const threshold = new Date(Date.now() - 60 * 60 * 1000);
|
|
646
|
+
await table.optimize({ cleanupOlderThan: threshold, deleteUnverified: false });
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
// Optimize is best-effort �?" must not break indexing.
|
|
650
|
+
}
|
|
651
|
+
});
|
|
593
652
|
}
|
|
594
653
|
/**
|
|
595
654
|
* Return all unique file paths currently stored in the index.
|
|
@@ -727,16 +786,18 @@ export class LanceDbStore {
|
|
|
727
786
|
* @param filePath - The file path whose chunks should be deleted.
|
|
728
787
|
*/
|
|
729
788
|
async deleteByFilePath(filePath) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
}
|
|
733
|
-
catch (err) {
|
|
734
|
-
if (isCorruptionError(err) && await this.tryRepair()) {
|
|
789
|
+
await this.withWriteLock(async () => {
|
|
790
|
+
try {
|
|
735
791
|
await this.deleteByFilePathInternal(filePath);
|
|
736
|
-
return;
|
|
737
792
|
}
|
|
738
|
-
|
|
739
|
-
|
|
793
|
+
catch (err) {
|
|
794
|
+
if (isCorruptionError(err) && await this.tryRepair()) {
|
|
795
|
+
await this.deleteByFilePathInternal(filePath);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
throw err;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
740
801
|
}
|
|
741
802
|
async deleteByFilePathInternal(filePath) {
|
|
742
803
|
const db = await this.getDb();
|
|
@@ -791,8 +852,13 @@ export class LanceDbStore {
|
|
|
791
852
|
return await fn();
|
|
792
853
|
}
|
|
793
854
|
catch (err) {
|
|
794
|
-
if (isCorruptionError(err)
|
|
795
|
-
|
|
855
|
+
if (isCorruptionError(err)) {
|
|
856
|
+
// Repair must be under writeLock to prevent Restore from conflicting
|
|
857
|
+
// with concurrent Append transactions (addChunks / deleteByFilePath).
|
|
858
|
+
const repaired = await this.withWriteLock(() => this.tryRepair());
|
|
859
|
+
if (repaired) {
|
|
860
|
+
return fn();
|
|
861
|
+
}
|
|
796
862
|
}
|
|
797
863
|
throw err;
|
|
798
864
|
}
|
|
@@ -823,7 +889,7 @@ export class LanceDbStore {
|
|
|
823
889
|
return false;
|
|
824
890
|
}
|
|
825
891
|
if (versions.length <= 1) {
|
|
826
|
-
return
|
|
892
|
+
return this.tryRebuildTable(db);
|
|
827
893
|
}
|
|
828
894
|
const sorted = [...versions].sort((a, b) => b.version - a.version);
|
|
829
895
|
for (const ver of sorted.slice(1)) {
|
|
@@ -839,10 +905,29 @@ export class LanceDbStore {
|
|
|
839
905
|
continue;
|
|
840
906
|
}
|
|
841
907
|
}
|
|
842
|
-
|
|
843
|
-
|
|
908
|
+
// All version-restore attempts failed (likely corrupted version graph
|
|
909
|
+
// with incompatible Restore transactions). Drop and recreate the table.
|
|
910
|
+
console.warn("[lancedb] Version restore failed. Dropping and recreating table to recover from corrupt version graph.");
|
|
911
|
+
return this.tryRebuildTable(db);
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
844
914
|
return false;
|
|
845
915
|
}
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Drop the existing chunks table and let getTable() create a fresh one.
|
|
919
|
+
* All indexed data is lost — callers should detect the empty table and
|
|
920
|
+
* trigger a re-index if needed.
|
|
921
|
+
*/
|
|
922
|
+
async tryRebuildTable(db) {
|
|
923
|
+
try {
|
|
924
|
+
await db.dropTable(TABLE_NAME).catch(() => { });
|
|
925
|
+
this.table = null;
|
|
926
|
+
// Re-create fresh via getTable → initTable
|
|
927
|
+
await this.getTable();
|
|
928
|
+
console.warn("[lancedb] Table recreated from scratch after corruption recovery.");
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
846
931
|
catch {
|
|
847
932
|
return false;
|
|
848
933
|
}
|