opencode-rag-plugin 1.21.0 → 1.21.1
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/cli/commands/init-helpers.d.ts +5 -2
- package/dist/cli/commands/init-helpers.js +26 -2
- package/dist/core/interfaces.d.ts +5 -0
- package/dist/indexer/pipeline.js +16 -5
- package/dist/vectorstore/lancedb.d.ts +16 -0
- package/dist/vectorstore/lancedb.js +76 -8
- package/dist/vectorstore/memory.d.ts +2 -0
- package/package.json +1 -1
|
@@ -21,8 +21,11 @@ export declare function buildWorkspacePackageJson(existing: Record<string, unkno
|
|
|
21
21
|
/**
|
|
22
22
|
* Build the `.opencode/opencode.json` config object.
|
|
23
23
|
*
|
|
24
|
-
* Ensures the `$schema` key is present and removes
|
|
25
|
-
*
|
|
24
|
+
* Ensures the `$schema` key is present and removes stale `plugin` entries
|
|
25
|
+
* (which would trigger erroneous npm installs) plus stale MCP entries that
|
|
26
|
+
* are not valid local/remote server configs (e.g. a leftover
|
|
27
|
+
* `mcp.<name>: { enabled: true }` that OpenCode ignores with
|
|
28
|
+
* "Ignoring MCP config entry without type").
|
|
26
29
|
*
|
|
27
30
|
* @param existing - The existing opencode.json content (if any).
|
|
28
31
|
* @returns The normalized config object.
|
|
@@ -49,8 +49,11 @@ export function buildWorkspacePackageJson(existing, packageMetadata) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Build the `.opencode/opencode.json` config object.
|
|
51
51
|
*
|
|
52
|
-
* Ensures the `$schema` key is present and removes
|
|
53
|
-
*
|
|
52
|
+
* Ensures the `$schema` key is present and removes stale `plugin` entries
|
|
53
|
+
* (which would trigger erroneous npm installs) plus stale MCP entries that
|
|
54
|
+
* are not valid local/remote server configs (e.g. a leftover
|
|
55
|
+
* `mcp.<name>: { enabled: true }` that OpenCode ignores with
|
|
56
|
+
* "Ignoring MCP config entry without type").
|
|
54
57
|
*
|
|
55
58
|
* @param existing - The existing opencode.json content (if any).
|
|
56
59
|
* @returns The normalized config object.
|
|
@@ -65,6 +68,27 @@ export function buildOpencodeConfig(existing) {
|
|
|
65
68
|
// init versions would trigger npm install (which fails due to native
|
|
66
69
|
// dependencies like sharp) and produce "Plugin export is not a function".
|
|
67
70
|
delete next.plugin;
|
|
71
|
+
// Keep only MCP entries that look like real server configs (have a
|
|
72
|
+
// "type" of "local" or "remote"). Drop shorthand entries such as
|
|
73
|
+
// `{ enabled: true }` that OpenCode ignores at startup.
|
|
74
|
+
if (next.mcp && typeof next.mcp === "object" && !Array.isArray(next.mcp)) {
|
|
75
|
+
const mcp = next.mcp;
|
|
76
|
+
const valid = {};
|
|
77
|
+
for (const [name, value] of Object.entries(mcp)) {
|
|
78
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
79
|
+
const entry = value;
|
|
80
|
+
if (entry.type === "local" || entry.type === "remote") {
|
|
81
|
+
valid[name] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (Object.keys(valid).length === 0) {
|
|
86
|
+
delete next.mcp;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
next.mcp = valid;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
68
92
|
return next;
|
|
69
93
|
}
|
|
70
94
|
/**
|
|
@@ -209,9 +209,14 @@ export interface VectorStore {
|
|
|
209
209
|
* accumulation.
|
|
210
210
|
* @param options - `aggressive: true` prunes all but the current version
|
|
211
211
|
* (only safe when no other process reads the store, e.g. temp rebuilds).
|
|
212
|
+
* `skipIndex: true` skips ANN index creation/repair (private temp stores
|
|
213
|
+
* are never searched — the rebuild pipeline builds the index once at the
|
|
214
|
+
* end). `logger` receives diagnostics instead of the console.
|
|
212
215
|
*/
|
|
213
216
|
optimize?(options?: {
|
|
214
217
|
aggressive?: boolean;
|
|
218
|
+
skipIndex?: boolean;
|
|
219
|
+
logger?: (message: string) => void;
|
|
215
220
|
}): Promise<void>;
|
|
216
221
|
/**
|
|
217
222
|
* Verify that the store's data is actually readable.
|
package/dist/indexer/pipeline.js
CHANGED
|
@@ -528,6 +528,11 @@ async function runIndexPassInner(options, logger) {
|
|
|
528
528
|
prep.chunks = undefined;
|
|
529
529
|
prep.textToEmbed = undefined;
|
|
530
530
|
}
|
|
531
|
+
// Number of files whose chunks were actually bulk-written this window.
|
|
532
|
+
// The caller uses this to skip mid-run compaction on idle passes that
|
|
533
|
+
// wrote nothing (no fragments were added, so nothing needs compacting,
|
|
534
|
+
// and the store avoids unnecessary index-repair churn).
|
|
535
|
+
return storePayloads.length;
|
|
531
536
|
}
|
|
532
537
|
// ── Windowed pipeline ────────────────────────────────────────────────────
|
|
533
538
|
// The prepare→describe→embed→store chain used to materialize ALL chunks,
|
|
@@ -833,12 +838,18 @@ async function runIndexPassInner(options, logger) {
|
|
|
833
838
|
// entries and skip every file ("Store phase: storing N → 0 files stored").
|
|
834
839
|
prevStore = storeWindow(prepared, new Map(earlyWorkerResults));
|
|
835
840
|
// Periodically compact fragments and prune old versions so the store
|
|
836
|
-
// phase doesn't slow down as the index grows during long runs.
|
|
841
|
+
// phase doesn't slow down as the index grows during long runs. Skipped on
|
|
842
|
+
// windows that wrote nothing — an idle pass (no changed files) adds no
|
|
843
|
+
// fragments, so compaction would only churn the index-repair path.
|
|
837
844
|
if (optimizeInterval > 0 && windowCount % optimizeInterval === 0) {
|
|
838
|
-
await prevStore;
|
|
845
|
+
const storedInWindow = await prevStore;
|
|
839
846
|
prevStore = null;
|
|
840
|
-
|
|
841
|
-
|
|
847
|
+
if (storedInWindow > 0) {
|
|
848
|
+
logger.info("Optimizing vector store (mid-run compaction, pruning old versions)...");
|
|
849
|
+
await effectiveStore.optimize?.(tempStorePath
|
|
850
|
+
? { aggressive: true, skipIndex: true, logger: logger.warn }
|
|
851
|
+
: { logger: logger.warn });
|
|
852
|
+
}
|
|
842
853
|
}
|
|
843
854
|
if (abortedInWindow)
|
|
844
855
|
break;
|
|
@@ -915,7 +926,7 @@ async function runIndexPassInner(options, logger) {
|
|
|
915
926
|
const optimizeStart = Date.now();
|
|
916
927
|
try {
|
|
917
928
|
const optimizeTarget = tempStorePath ? options.store : effectiveStore;
|
|
918
|
-
await optimizeTarget.optimize?.(tempStorePath ? { aggressive: true } :
|
|
929
|
+
await optimizeTarget.optimize?.(tempStorePath ? { aggressive: true, logger: logger.warn } : { logger: logger.warn });
|
|
919
930
|
logger.info(`Vector store optimized in ${((Date.now() - optimizeStart) / 1000).toFixed(1)}s`);
|
|
920
931
|
}
|
|
921
932
|
catch (err) {
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { VectorStore, Chunk, ChunkSummary, SearchResult, MetadataFilter } from "../core/interfaces.js";
|
|
2
|
+
/** Minimal warning sink for store diagnostics (defaults to console.warn). */
|
|
3
|
+
export type StoreWarn = (message: string) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Count index-version directories in a LanceDB table directory. Each
|
|
6
|
+
* `createIndex` writes a new `<uuid>/` directory under `_indices`; a healthy
|
|
7
|
+
* store holds one per built index, while a store whose index commits fail
|
|
8
|
+
* accumulates one per attempt (and eventually degrades / corrupts).
|
|
9
|
+
*
|
|
10
|
+
* @param tablePath - Filesystem path of the table directory (e.g. `.../chunks.lance`).
|
|
11
|
+
* @returns The number of index-version directories, or 0 when unavailable.
|
|
12
|
+
*/
|
|
13
|
+
export declare function countIndexVersionDirs(tablePath: string): number;
|
|
2
14
|
/**
|
|
3
15
|
* L2-normalize a vector to unit length. Cosine models require unit vectors
|
|
4
16
|
* for the dot product to equal cosine similarity.
|
|
@@ -62,6 +74,8 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
62
74
|
* back to brute-force. This repairs that stale index on first search.
|
|
63
75
|
*/
|
|
64
76
|
private indexRepairPromise;
|
|
77
|
+
/** Consecutive failed repair attempts — bounded so a broken store cannot retrain forever. */
|
|
78
|
+
private indexRepairFailures;
|
|
65
79
|
/**
|
|
66
80
|
* Execute an async function under an exclusive write lock.
|
|
67
81
|
*
|
|
@@ -245,6 +259,8 @@ export declare class LanceDbStore implements VectorStore {
|
|
|
245
259
|
*/
|
|
246
260
|
optimize(options?: {
|
|
247
261
|
aggressive?: boolean;
|
|
262
|
+
skipIndex?: boolean;
|
|
263
|
+
logger?: StoreWarn;
|
|
248
264
|
}): Promise<void>;
|
|
249
265
|
/**
|
|
250
266
|
* Return all unique file paths currently stored in the index.
|
|
@@ -3,11 +3,45 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as lancedb from "@lancedb/lancedb";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
|
+
import fsSync from "node:fs";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { normalizeFilePath, manifestPathFor } from "../core/manifest.js";
|
|
8
9
|
import { normalizeFileExtensions, matchesFileExtension } from "../core/filters.js";
|
|
9
10
|
const TABLE_NAME = "chunks";
|
|
10
11
|
const QUERY_COLUMNS = ["id", "content", "description", "filePath", "startLine", "endLine", "language", "kind", "quirkType", "tags"];
|
|
12
|
+
/**
|
|
13
|
+
* Upper bound for the number of failed index-creation attempts per process.
|
|
14
|
+
* Beyond this the repair gives up for the process lifetime instead of
|
|
15
|
+
* retraining the IVF index on every optimize/search call (each attempt runs
|
|
16
|
+
* a full KMeans training pass and logs "partition N is empty, skipping").
|
|
17
|
+
*/
|
|
18
|
+
const MAX_INDEX_REPAIR_ATTEMPTS = 3;
|
|
19
|
+
/**
|
|
20
|
+
* Upper bound for stale index-version directories under `chunks.lance/_indices`.
|
|
21
|
+
* Healthy stores have one per built index; a store whose index registration
|
|
22
|
+
* keeps failing accumulates one directory per attempt and never converges.
|
|
23
|
+
* Above this threshold the repair refuses to create yet another version and
|
|
24
|
+
* instead tells the user to rebuild the store.
|
|
25
|
+
*/
|
|
26
|
+
const MAX_STALE_INDEX_VERSIONS = 40;
|
|
27
|
+
/**
|
|
28
|
+
* Count index-version directories in a LanceDB table directory. Each
|
|
29
|
+
* `createIndex` writes a new `<uuid>/` directory under `_indices`; a healthy
|
|
30
|
+
* store holds one per built index, while a store whose index commits fail
|
|
31
|
+
* accumulates one per attempt (and eventually degrades / corrupts).
|
|
32
|
+
*
|
|
33
|
+
* @param tablePath - Filesystem path of the table directory (e.g. `.../chunks.lance`).
|
|
34
|
+
* @returns The number of index-version directories, or 0 when unavailable.
|
|
35
|
+
*/
|
|
36
|
+
export function countIndexVersionDirs(tablePath) {
|
|
37
|
+
try {
|
|
38
|
+
const entries = fsSync.readdirSync(path.join(tablePath, "_indices"), { withFileTypes: true });
|
|
39
|
+
return entries.filter((e) => e.isDirectory()).length;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
11
45
|
/**
|
|
12
46
|
* L2-normalize a vector to unit length. Cosine models require unit vectors
|
|
13
47
|
* for the dot product to equal cosine similarity.
|
|
@@ -108,6 +142,8 @@ export class LanceDbStore {
|
|
|
108
142
|
* back to brute-force. This repairs that stale index on first search.
|
|
109
143
|
*/
|
|
110
144
|
indexRepairPromise = null;
|
|
145
|
+
/** Consecutive failed repair attempts — bounded so a broken store cannot retrain forever. */
|
|
146
|
+
indexRepairFailures = 0;
|
|
111
147
|
/**
|
|
112
148
|
* Execute an async function under an exclusive write lock.
|
|
113
149
|
*
|
|
@@ -817,9 +853,9 @@ export class LanceDbStore {
|
|
|
817
853
|
* Callers must hold the write lock (searchInternal wraps the call in
|
|
818
854
|
* withWriteLock; optimize runs under it already).
|
|
819
855
|
*/
|
|
820
|
-
ensureCosineIndex() {
|
|
856
|
+
ensureCosineIndex(warn) {
|
|
821
857
|
if (!this.indexRepairPromise) {
|
|
822
|
-
this.indexRepairPromise = this.repairIndexMetricOnce();
|
|
858
|
+
this.indexRepairPromise = this.repairIndexMetricOnce(warn);
|
|
823
859
|
}
|
|
824
860
|
return this.indexRepairPromise;
|
|
825
861
|
}
|
|
@@ -831,8 +867,24 @@ export class LanceDbStore {
|
|
|
831
867
|
* stale index in place. On failure the memo is cleared so the next
|
|
832
868
|
* search/optimize retries.
|
|
833
869
|
*/
|
|
834
|
-
async repairIndexMetricOnce() {
|
|
870
|
+
async repairIndexMetricOnce(warn) {
|
|
871
|
+
const report = (message) => {
|
|
872
|
+
(warn ?? console.warn)(message);
|
|
873
|
+
};
|
|
835
874
|
try {
|
|
875
|
+
// Guard against a store that never converges: each failed createIndex
|
|
876
|
+
// leaves a new index-version directory behind. If many stale versions
|
|
877
|
+
// accumulated, building yet another index only entrenches the problem.
|
|
878
|
+
// The store must be rebuilt instead (see doc/troubleshooting.md).
|
|
879
|
+
const staleVersions = this.dbPath.startsWith("memory://")
|
|
880
|
+
? 0
|
|
881
|
+
: countIndexVersionDirs(path.join(this.dbPath, TABLE_NAME + ".lance"));
|
|
882
|
+
if (staleVersions > MAX_STALE_INDEX_VERSIONS) {
|
|
883
|
+
report(`[lancedb] ${staleVersions} stale index versions detected in ${this.dbPath} — ` +
|
|
884
|
+
`skipping index rebuild (the store is corrupted). Delete the rag_db directory and re-index.`);
|
|
885
|
+
// Resolve the memo so no further attempts run this process.
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
836
888
|
const table = await this.getTable();
|
|
837
889
|
const count = await table.countRows().catch(() => 0);
|
|
838
890
|
const indices = await table.listIndices();
|
|
@@ -854,6 +906,7 @@ export class LanceDbStore {
|
|
|
854
906
|
// race a background index-creation from another process.
|
|
855
907
|
waitTimeoutSeconds: 120,
|
|
856
908
|
});
|
|
909
|
+
this.indexRepairFailures = 0;
|
|
857
910
|
return;
|
|
858
911
|
}
|
|
859
912
|
catch (err) {
|
|
@@ -866,9 +919,19 @@ export class LanceDbStore {
|
|
|
866
919
|
}
|
|
867
920
|
}
|
|
868
921
|
catch (err) {
|
|
922
|
+
this.indexRepairFailures++;
|
|
923
|
+
const message = `[lancedb] index metric repair failed (queries continue in brute-force): ` +
|
|
924
|
+
`${err instanceof Error ? err.message : String(err)}`;
|
|
925
|
+
if (this.indexRepairFailures >= MAX_INDEX_REPAIR_ATTEMPTS) {
|
|
926
|
+
// Stop retrying for this process — a store that fails repeatedly would
|
|
927
|
+
// otherwise be retrained (with KMeans "partition empty" warning spam)
|
|
928
|
+
// on every optimize() and search() call.
|
|
929
|
+
report(`${message} — giving up after ${this.indexRepairFailures} attempts this session. ` +
|
|
930
|
+
`Delete the rag_db directory and re-index.`);
|
|
931
|
+
return; // memo stays resolved: no further attempts this process
|
|
932
|
+
}
|
|
869
933
|
this.indexRepairPromise = null; // allow retry on a later search/optimize
|
|
870
|
-
|
|
871
|
-
`${err instanceof Error ? err.message : String(err)}`);
|
|
934
|
+
report(message);
|
|
872
935
|
}
|
|
873
936
|
}
|
|
874
937
|
/**
|
|
@@ -896,13 +959,18 @@ export class LanceDbStore {
|
|
|
896
959
|
}
|
|
897
960
|
// Build the ANN vector index — without it every vectorSearch() is a
|
|
898
961
|
// brute-force O(N) flat scan (slow at 50k+ chunks). Skips early when
|
|
899
|
-
// an index with the correct cosine metric already exists
|
|
900
|
-
|
|
962
|
+
// an index with the correct cosine metric already exists, and can be
|
|
963
|
+
// skipped entirely for a private temporary store that is never
|
|
964
|
+
// searched (the rebuild pipeline builds the index once at the end).
|
|
965
|
+
if (!options?.skipIndex) {
|
|
966
|
+
await this.ensureCosineIndex(options?.logger);
|
|
967
|
+
}
|
|
901
968
|
}
|
|
902
969
|
catch (err) {
|
|
903
970
|
// Optimize is best-effort — must not break indexing, but surface the
|
|
904
971
|
// failure instead of swallowing it silently.
|
|
905
|
-
|
|
972
|
+
const message = `[lancedb] optimize failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
973
|
+
(options?.logger ?? console.warn)(message);
|
|
906
974
|
}
|
|
907
975
|
});
|
|
908
976
|
}
|