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.
@@ -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 any stale `plugin`
25
- * entries that would trigger erroneous npm installs.
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 any stale `plugin`
53
- * entries that would trigger erroneous npm installs.
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.
@@ -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
- logger.info("Optimizing vector store (mid-run compaction, pruning old versions)...");
841
- await effectiveStore.optimize?.(tempStorePath ? { aggressive: true } : undefined);
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 } : undefined);
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
- console.warn(`[lancedb] index metric repair failed (queries continue in brute-force): ` +
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
- await this.ensureCosineIndex();
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
- console.warn(`[lancedb] optimize failed: ${err instanceof Error ? err.message : String(err)}`);
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
  }
@@ -27,5 +27,7 @@ export declare class InMemoryVectorStore implements VectorStore {
27
27
  /** No-op for the in-memory store. */
28
28
  optimize(_options?: {
29
29
  aggressive?: boolean;
30
+ skipIndex?: boolean;
31
+ logger?: (message: string) => void;
30
32
  }): Promise<void>;
31
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-rag-plugin",
3
- "version": "1.21.0",
3
+ "version": "1.21.1",
4
4
  "description": "OpenCode plugin for local-first RAG-based semantic code search",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin-entry.js",