opencode-rag-plugin 1.18.2 → 1.19.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/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
- ![OpenCodeRAG Web UI](doc/assets/eval.png)
58
+ ![OpenCodeRAG Web UI](doc/assets/webui-dashboard.png)
59
59
 
60
60
  Launch with `opencode-rag ui`. See [Web UI documentation](doc/webui.md) for details.
61
61
 
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { RagConfig } from "../core/config.js";
5
5
  import { type FileManifest } from "../core/manifest.js";
6
+ import { type ExcludeMatcher } from "../core/exclude.js";
6
7
  import { DescriptionCache } from "../core/desc-cache.js";
7
8
  import { type ImageVisionProvider } from "../chunker/image.js";
8
9
  /** Metadata and extracted content for a single workspace file discovered during scanning. */
@@ -27,7 +28,7 @@ interface Logger {
27
28
  * Recursively walk a directory tree and collect paths matching the given extension set,
28
29
  * respecting exclusion lists and configurable limits for max directories and results.
29
30
  */
30
- export declare function walkFiles(dir: string, extensions: Set<string>, excludeDirs: Set<string>, excludeFiles?: Set<string>, logger?: Logger, dirCount?: {
31
+ export declare function walkFiles(dir: string, extensions: Set<string>, excludeDirs: ExcludeMatcher, excludeFiles?: ExcludeMatcher, rootDir?: string, logger?: Logger, dirCount?: {
31
32
  value: number;
32
33
  }, maxDirs?: number, maxResults?: number): Promise<string[]>;
33
34
  /**
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import pLimit from "p-limit";
7
7
  import { computeFileHash, computeDescriptionConfigHash, normalizeFilePath } from "../core/manifest.js";
8
+ import { createExcludeMatcher } from "../core/exclude.js";
8
9
  import { DescriptionCache } from "../core/desc-cache.js";
9
10
  import { createImageVisionProvider, } from "../chunker/image.js";
10
11
  import * as pdfExtractor from "./pdf.js";
@@ -16,15 +17,13 @@ import * as imageExtractor from "./image.js";
16
17
  * Recursively walk a directory tree and collect paths matching the given extension set,
17
18
  * respecting exclusion lists and configurable limits for max directories and results.
18
19
  */
19
- export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, logger, dirCount, maxDirs = 10_000, maxResults = 100_000) {
20
+ export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, rootDir = dir, logger, dirCount, maxDirs = 10_000, maxResults = 100_000) {
20
21
  const results = [];
21
22
  const entries = await fs.readdir(dir, { withFileTypes: true });
22
23
  for (const entry of entries) {
23
24
  const fullPath = path.join(dir, entry.name);
24
25
  if (entry.isDirectory()) {
25
- if (excludeDirs.has(entry.name))
26
- continue;
27
- if (entry.name.startsWith("."))
26
+ if (excludeDirs.excluded(path.relative(rootDir, fullPath)))
28
27
  continue;
29
28
  if (dirCount) {
30
29
  dirCount.value++;
@@ -40,7 +39,7 @@ export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, logg
40
39
  logger?.warn(`Exceeded ${maxResults} matching files — truncating walk at ${fullPath}`);
41
40
  return results;
42
41
  }
43
- results.push(...(await walkFiles(fullPath, extensions, excludeDirs, excludeFiles, logger, dirCount, maxDirs, maxResults)));
42
+ results.push(...(await walkFiles(fullPath, extensions, excludeDirs, excludeFiles, rootDir, logger, dirCount, maxDirs, maxResults)));
44
43
  }
45
44
  else if (entry.isFile()) {
46
45
  if (results.length >= maxResults) {
@@ -49,7 +48,7 @@ export async function walkFiles(dir, extensions, excludeDirs, excludeFiles, logg
49
48
  }
50
49
  const ext = path.extname(entry.name).toLowerCase();
51
50
  const basename = entry.name.toLowerCase();
52
- if ((extensions.has(ext) || extensions.has(basename)) && !excludeFiles?.has(basename)) {
51
+ if ((extensions.has(ext) || extensions.has(basename)) && !excludeFiles?.excluded(path.relative(rootDir, fullPath))) {
53
52
  results.push(fullPath);
54
53
  }
55
54
  }
@@ -103,10 +102,10 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
103
102
  imagePrompt = imageCfg.prompt;
104
103
  imageResizeMaxDimension = imageCfg.resizeMaxDimension;
105
104
  }
105
+ const excludeDirMatcher = createExcludeMatcher(config.indexing.excludeDirs);
106
+ const excludeFileMatcher = createExcludeMatcher(config.indexing.excludeFiles ?? []);
106
107
  let files;
107
108
  if (filterPaths && filterPaths.length > 0) {
108
- const excludeDirs = new Set(config.indexing.excludeDirs);
109
- const excludeFiles = new Set(config.indexing.excludeFiles?.map((f) => f.toLowerCase()) ?? []);
110
109
  files = filterPaths
111
110
  .map((p) => path.resolve(cwd, p))
112
111
  .filter((fp) => {
@@ -114,22 +113,17 @@ export async function scanWorkspaceFiles(cwd, config, logger, manifest, filterPa
114
113
  const basename = path.basename(fp).toLowerCase();
115
114
  if (!extensions.has(ext) && !extensions.has(basename))
116
115
  return false;
117
- if (excludeFiles.has(basename))
118
- return false;
119
116
  const rel = path.relative(cwd, fp);
120
117
  if (rel.startsWith(".."))
121
118
  return false;
122
- const parts = rel.split(path.sep);
123
- if (parts.some((part) => excludeDirs.has(part)))
124
- return false;
125
- return true;
119
+ return !excludeDirMatcher.excluded(rel) && !excludeFileMatcher.excluded(rel);
126
120
  });
127
121
  }
128
122
  else {
129
123
  logger?.info("Walking directory tree...");
130
124
  const walkStart = Date.now();
131
125
  const dirCount = { value: 0 };
132
- files = await walkFiles(cwd, extensions, new Set(config.indexing.excludeDirs), new Set(config.indexing.excludeFiles?.map((f) => f.toLowerCase()) ?? []), logger, dirCount);
126
+ files = await walkFiles(cwd, extensions, excludeDirMatcher, excludeFileMatcher, cwd, logger, dirCount);
133
127
  const walkSec = ((Date.now() - walkStart) / 1000).toFixed(1);
134
128
  logger?.info(`Found ${files.length} matching files in ${walkSec}s (${dirCount.value} dirs traversed)`);
135
129
  }
@@ -137,6 +137,13 @@ export interface MemoryConfig {
137
137
  autoInjectLatencyBudgetMs: number;
138
138
  /** Max quirks to auto-inject per turn (default 2). */
139
139
  autoInjectTopK: number;
140
+ /**
141
+ * Minimum number of shared word tokens (≥3 chars) between a candidate quirk's
142
+ * content and the user's *current* message for the quirk to be auto-injected
143
+ * (default 1). Acts as a relevance gate against meta-quirks that match only
144
+ * the prior assistant text in the combined recall query. Set to `0` to disable.
145
+ */
146
+ autoInjectMinTokenOverlap: number;
140
147
  /** Automatically extract quirks from each completed agent turn. */
141
148
  passiveCapture: boolean;
142
149
  /** Upgrade the system-prompt nudge into a mandatory trigger. */
@@ -218,9 +225,28 @@ export interface RagConfig {
218
225
  indexing: {
219
226
  /** File extensions to include in indexing. */
220
227
  includeExtensions: string[];
221
- /** Directory name patterns to exclude. */
228
+ /**
229
+ * Directory/file name patterns to exclude.
230
+ *
231
+ * Semantics (same for `excludeFiles`):
232
+ * - Plain name (no path separator, no glob chars `* ? [ { (`):
233
+ * matches that **basename** at **any depth** (case-insensitive).
234
+ * - Basename glob (glob chars, no separator):
235
+ * matches the segment at any depth via glob.
236
+ * - Path pattern (contains a separator or `**`):
237
+ * **anchored to the workspace root** — matched as a glob against each
238
+ * ancestor prefix (so it excludes the matched directory and all contents).
239
+ *
240
+ * Supports standard wildcards: `*`, `?`, `[...]`, `{a,b}`, `(pattern)`.
241
+ * Use `/` as path separator (converted automatically). Matching is
242
+ * case-insensitive and dot-inclusive.
243
+ */
222
244
  excludeDirs: string[];
223
- /** Specific filenames (basenames, case-insensitive) to exclude from indexing. */
245
+ /**
246
+ * File-name patterns to exclude (same semantics as `excludeDirs`).
247
+ * Plain names match any file with that basename at any depth; path
248
+ * patterns are anchored to the workspace root.
249
+ */
224
250
  excludeFiles?: string[];
225
251
  /** Number of overlapping lines between adjacent chunks. */
226
252
  chunkOverlap: number;
@@ -95,6 +95,17 @@ export const DEFAULT_CONFIG = {
95
95
  ".commandcode",
96
96
  ".agents",
97
97
  "graphify-out",
98
+ ".vscode",
99
+ ".vs",
100
+ ".idea",
101
+ ".next",
102
+ ".nuxt",
103
+ ".turbo",
104
+ ".angular",
105
+ ".svelte-kit",
106
+ ".gradle",
107
+ ".dart_tool",
108
+ ".cache",
98
109
  ],
99
110
  excludeFiles: [
100
111
  "package-lock.json",
@@ -103,7 +114,7 @@ export const DEFAULT_CONFIG = {
103
114
  minFileSizeBytes: 0,
104
115
  concurrency: 8,
105
116
  embedBatchSize: 100,
106
- embedConcurrency: 6,
117
+ embedConcurrency: 3,
107
118
  ollamaMaxBatchSize: 500,
108
119
  descriptionConcurrency: 4,
109
120
  maxSvgSizeBytes: 1_048_576,
@@ -261,6 +272,7 @@ export const DEFAULT_CONFIG = {
261
272
  autoInjectMinScore: 0.6,
262
273
  autoInjectLatencyBudgetMs: 2000,
263
274
  autoInjectTopK: 2,
275
+ autoInjectMinTokenOverlap: 1,
264
276
  passiveCapture: false,
265
277
  promptEnforcement: true,
266
278
  sessionEndExtraction: true,
@@ -0,0 +1,4 @@
1
+ export interface ExcludeMatcher {
2
+ excluded(relPath: string): boolean;
3
+ }
4
+ export declare function createExcludeMatcher(patterns: string[]): ExcludeMatcher;
@@ -0,0 +1,45 @@
1
+ import { Minimatch } from "minimatch";
2
+ const GLOB_MAGIC = /[*?{}()\[\]]/;
3
+ export function createExcludeMatcher(patterns) {
4
+ const plainBasenames = new Set();
5
+ const basenameGlobMatchers = [];
6
+ const pathMatchers = [];
7
+ for (const pattern of patterns) {
8
+ const pat = pattern.trim().replace(/\\/g, "/");
9
+ if (!pat)
10
+ continue;
11
+ const hasSep = pat.includes("/");
12
+ const isGlob = GLOB_MAGIC.test(pat);
13
+ if (!hasSep && !isGlob) {
14
+ plainBasenames.add(pat.toLowerCase());
15
+ }
16
+ else if (!hasSep && isGlob) {
17
+ basenameGlobMatchers.push(new Minimatch(pat, { nocase: true, dot: true }));
18
+ }
19
+ else {
20
+ pathMatchers.push(new Minimatch(pat, { nocase: true, dot: true }));
21
+ }
22
+ }
23
+ function excluded(relPath) {
24
+ const normalized = relPath.replace(/\\/g, "/");
25
+ const segments = normalized.split("/");
26
+ let prefix = "";
27
+ for (const seg of segments) {
28
+ const segLower = seg.toLowerCase();
29
+ prefix = prefix ? `${prefix}/${seg}` : seg;
30
+ if (plainBasenames.has(segLower))
31
+ return true;
32
+ for (const mm of basenameGlobMatchers) {
33
+ if (mm.match(seg))
34
+ return true;
35
+ }
36
+ for (const mm of pathMatchers) {
37
+ if (mm.match(prefix))
38
+ return true;
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+ return { excluded };
44
+ }
45
+ //# sourceMappingURL=exclude.js.map
@@ -185,6 +185,12 @@ export interface VectorStore {
185
185
  reopen?(newPath?: string): Promise<void>;
186
186
  /** Compact fragments and prune old versions to prevent version-manifest accumulation. */
187
187
  optimize?(): Promise<void>;
188
+ /**
189
+ * Verify that the store's data is actually readable.
190
+ * Returns false if data integrity is compromised (e.g., data files missing from disk).
191
+ * Used by the index pipeline to detect silent corruption before scanning.
192
+ */
193
+ checkIntegrity?(): Promise<boolean>;
188
194
  }
189
195
  /** Filter criteria for narrowing search results by file path, language, or kind. */
190
196
  export interface MetadataFilter {
@@ -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
- * @returns A promise resolving to a flat array of embedding vectors (one per input text)
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[][]>;
@@ -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
- * @returns A promise resolving to a flat array of embedding vectors (one per input text)
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 embedder.embed(batch.texts, purpose);
64
- results.push(...embeddings);
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 embedder.embed(batch.texts, purpose);
73
- completedCount += embeddings.length;
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 = [];
@@ -173,6 +173,27 @@ async function runIndexPassInner(options, logger) {
173
173
  const storeCountStart = Date.now();
174
174
  const existingCount = await options.store.count();
175
175
  logger.info(`Store has ${existingCount} existing chunks (${((Date.now() - storeCountStart) / 1000).toFixed(1)}s)`);
176
+ // Direct data-integrity check: try to actually read a row from the store.
177
+ // LanceDB's countRows() may return a value from the version manifest
178
+ // even when the underlying data files are missing from disk, so we need
179
+ // an explicit probe. If this fails, treat the store as corrupt.
180
+ if (!options.force && manifestStatus === "ok" && Object.keys(manifest.files).length > 0) {
181
+ try {
182
+ const isIntact = await options.store.checkIntegrity?.();
183
+ if (isIntact === false) {
184
+ logger.warn("Vector store data integrity check failed — data files are missing " +
185
+ `(${existingCount} chunks in manifest but store can't be read). Re-indexing all files.`);
186
+ for (const key of Object.keys(manifest.files)) {
187
+ delete manifest.files[key];
188
+ }
189
+ manifest.lastIndexedAt = undefined;
190
+ manifestStatus = "missing";
191
+ }
192
+ }
193
+ catch {
194
+ // checkIntegrity not available (older VectorStore impl) — skip
195
+ }
196
+ }
176
197
  // Detect data loss: if the store has far fewer chunks than the manifest expects,
177
198
  // treat it as a corrupt store (e.g. schema migration dropped the old table).
178
199
  if (!options.force && manifestStatus === "ok" && existingCount > 0) {
@@ -569,7 +590,7 @@ async function runIndexPassInner(options, logger) {
569
590
  hash: prepared[fileIdx].hash,
570
591
  chunkCount: 0, fileLabel: prepared[fileIdx].fileLabel,
571
592
  isNew: false, isModified: false, isUnchanged: false, isEmpty: false,
572
- isTooSmall: false, isRemoved: true, hadChunks: false,
593
+ isTooSmall: false, isRemoved: false, hadChunks: false,
573
594
  descriptionFailed: prepared[fileIdx].descriptionFailed,
574
595
  });
575
596
  }
@@ -618,14 +639,14 @@ async function runIndexPassInner(options, logger) {
618
639
  const result = {
619
640
  normalizedPath: prep.normalizedPath,
620
641
  hash: prep.hash,
621
- chunkCount: prep.chunks?.length ?? 0,
642
+ chunkCount: validChunks.length,
622
643
  fileLabel: prep.fileLabel,
623
644
  isNew: !prep.isModified,
624
645
  isModified: prep.isModified,
625
646
  isUnchanged: false,
626
647
  isEmpty: false,
627
648
  isTooSmall: false,
628
- isRemoved: validChunks.length === 0,
649
+ isRemoved: (prep.chunks?.length ?? 0) > 0 && validChunks.length === 0 ? false : validChunks.length === 0,
629
650
  hadChunks: (prep.chunks?.length ?? 0) > 0,
630
651
  descriptionFailed: prep.descriptionFailed,
631
652
  descHash: prep.descHash,
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import path from "node:path";
5
5
  import { manifestPathFor } from "../core/manifest.js";
6
+ import { createExcludeMatcher } from "../core/exclude.js";
6
7
  /**
7
8
  * Create a scheduler that debounces calls to a re-index pass. While a pass is
8
9
  * running, subsequent notifications queue a single rerun. Useful for watching
@@ -117,7 +118,8 @@ export function createWatchPassScheduler(runPass, onError, debounceMs = 300) {
117
118
  */
118
119
  export function createWatchIgnore(cwd, config, storePath) {
119
120
  const manifestPath = manifestPathFor(storePath);
120
- const excludeDirs = new Set(config.indexing.excludeDirs);
121
+ const dirMatcher = createExcludeMatcher(config.indexing.excludeDirs);
122
+ const fileMatcher = createExcludeMatcher(config.indexing.excludeFiles ?? []);
121
123
  return (watchedPath) => {
122
124
  const resolved = path.resolve(watchedPath);
123
125
  if (resolved.startsWith(storePath))
@@ -127,8 +129,7 @@ export function createWatchIgnore(cwd, config, storePath) {
127
129
  const relative = path.relative(cwd, resolved);
128
130
  if (!relative || relative.startsWith(".."))
129
131
  return false;
130
- const segments = relative.split(path.sep);
131
- return segments.some((segment) => excludeDirs.has(segment));
132
+ return dirMatcher.excluded(relative) || fileMatcher.excluded(relative);
132
133
  };
133
134
  }
134
135
  //# sourceMappingURL=watch.js.map
package/dist/indexer.d.ts CHANGED
@@ -6,6 +6,7 @@ export type { IndexRunStats, IndexStatusSummary } from "./indexer/stats.js";
6
6
  export { createIndexStats } from "./indexer/stats.js";
7
7
  export type { WorkspaceFile } from "./content/reader.js";
8
8
  export { scanWorkspaceFiles, walkFiles } from "./content/reader.js";
9
+ export { createExcludeMatcher, type ExcludeMatcher } from "./core/exclude.js";
9
10
  export type { RunIndexPassOptions, WatchPassScheduler } from "./indexer/pipeline.js";
10
11
  export { runIndexPass, getIndexStatusSummary, createWatchPassScheduler, createWatchIgnore, type Logger, } from "./indexer/pipeline.js";
11
12
  import type { RagConfig } from "./core/config.js";
package/dist/indexer.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export { createIndexStats } from "./indexer/stats.js";
6
6
  export { scanWorkspaceFiles, walkFiles } from "./content/reader.js";
7
+ export { createExcludeMatcher } from "./core/exclude.js";
7
8
  export { runIndexPass, getIndexStatusSummary, createWatchPassScheduler, createWatchIgnore, } from "./indexer/pipeline.js";
8
9
  import { scanWorkspaceFiles } from "./content/reader.js";
9
10
  /**
package/dist/plugin.js CHANGED
@@ -25,7 +25,7 @@ import { countTokens } from "./eval/token-counter.js";
25
25
  import { checkForUpdate, getCurrentVersion, installLatestUpdate } from "./core/version-check.js";
26
26
  import { loadAutoUpdateState, saveAutoUpdateState, shouldAttemptInstall } from "./core/auto-update-state.js";
27
27
  import { destroyAllPooledConnections } from "./embedder/http.js";
28
- import { listQuirks, lintQuirks, recallQuirks } from "./quirks/quirk-store.js";
28
+ import { listQuirks, lintQuirks, recallQuirks, sharedWords } from "./quirks/quirk-store.js";
29
29
  import { autoCaptureQuirks } from "./quirks/auto-capture.js";
30
30
  import { buildSystemGuidanceLines } from "./opencode/system-guidance.js";
31
31
  import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
@@ -797,6 +797,13 @@ export function createRagHooks(options) {
797
797
  // Dedup: skip quirks already injected into this session
798
798
  const injectedSet = getOrCreateSessionSet(sessionInjectedQuirks, sessionId, MAX_SESSION_MAP_SIZE);
799
799
  quirkResults = quirkResults.filter((qr) => !injectedSet.has(qr.chunk.id));
800
+ // Lexical gate: drop quirks that match only the prior assistant text (no token
801
+ // overlap with the user's actual request) — prevents meta-quirks (quirks about
802
+ // quirks) from being injected into unrelated tasks. Disabled when autoInjectMinTokenOverlap=0.
803
+ const minTokenOverlap = memCfg.autoInjectMinTokenOverlap ?? 1;
804
+ if (minTokenOverlap > 0 && userReq.length > 0) {
805
+ quirkResults = quirkResults.filter((qr) => sharedWords(qr.chunk.content, userReq) >= minTokenOverlap);
806
+ }
800
807
  if (quirkResults.length > 0) {
801
808
  const lines = ["", "⚠ **Relevant quirks for this task:**", ""];
802
809
  for (const qr of quirkResults) {
@@ -1088,6 +1095,13 @@ export function createRagHooks(options) {
1088
1095
  // Dedup: skip quirks already injected into this session
1089
1096
  const injectedSet = getOrCreateSessionSet(sessionInjectedQuirks, input.sessionID, MAX_SESSION_MAP_SIZE);
1090
1097
  quirkResults = quirkResults.filter((qr) => !injectedSet.has(qr.chunk.id));
1098
+ // Lexical gate: drop quirks that match only the prior assistant text (no token
1099
+ // overlap with the user's current message) — prevents meta-quirks (quirks about
1100
+ // quirks) from being injected into unrelated tasks. Disabled when autoInjectMinTokenOverlap=0.
1101
+ const minTokenOverlap = quirkMemoryCfg.autoInjectMinTokenOverlap ?? 1;
1102
+ if (minTokenOverlap > 0 && text.length > 0) {
1103
+ quirkResults = quirkResults.filter((qr) => sharedWords(qr.chunk.content, text) >= minTokenOverlap);
1104
+ }
1091
1105
  if (quirkResults.length > 0) {
1092
1106
  const quirkLines = ["", "---", "⚠ **Quirk memory**", ""];
1093
1107
  for (const qr of quirkResults) {
@@ -27,3 +27,18 @@ export declare function recallQuirks(deps: QuirkStoreDeps, query: string, option
27
27
  export declare function lintQuirks(deps: QuirkStoreDeps): Promise<string[]>;
28
28
  /** Simple Jaccard-based lexical similarity (word overlap). */
29
29
  export declare function lexicalSimilarity(a: string, b: string): number;
30
+ /**
31
+ * Count of meaningful word tokens shared between two texts (Jaccard numerator).
32
+ *
33
+ * Tokens are whitespace/punctuation-split, lowercased, and filtered to those
34
+ * with length ≥ `minTokenLen` (default 3 — skips short filler like "the").
35
+ *
36
+ * Used by the quirk auto-inject gate: candidate quirks that share no tokens
37
+ * with the user's *current* message (i.e. they matched only against the prior
38
+ * assistant text in the combined recall query) are filtered out. This prevents
39
+ * meta-quirks (quirks about quirks themselves) from being injected into
40
+ * unrelated tasks, e.g. when the agent previously explained how quirks work.
41
+ *
42
+ * Set `memory.autoInjectMinTokenOverlap` to `0` to disable the gate.
43
+ */
44
+ export declare function sharedWords(a: string, b: string, minTokenLen?: number): number;
@@ -214,4 +214,31 @@ export function lexicalSimilarity(a, b) {
214
214
  const union = new Set([...wordsA, ...wordsB]);
215
215
  return union.size === 0 ? 0 : intersection.size / union.size;
216
216
  }
217
+ /**
218
+ * Count of meaningful word tokens shared between two texts (Jaccard numerator).
219
+ *
220
+ * Tokens are whitespace/punctuation-split, lowercased, and filtered to those
221
+ * with length ≥ `minTokenLen` (default 3 — skips short filler like "the").
222
+ *
223
+ * Used by the quirk auto-inject gate: candidate quirks that share no tokens
224
+ * with the user's *current* message (i.e. they matched only against the prior
225
+ * assistant text in the combined recall query) are filtered out. This prevents
226
+ * meta-quirks (quirks about quirks themselves) from being injected into
227
+ * unrelated tasks, e.g. when the agent previously explained how quirks work.
228
+ *
229
+ * Set `memory.autoInjectMinTokenOverlap` to `0` to disable the gate.
230
+ */
231
+ export function sharedWords(a, b, minTokenLen = 3) {
232
+ const tokensA = a.toLowerCase().split(/\W+/).filter((w) => w.length >= minTokenLen);
233
+ const wordsB = new Set(b.toLowerCase().split(/\W+/).filter((w) => w.length >= minTokenLen));
234
+ let count = 0;
235
+ const seen = new Set();
236
+ for (const tok of tokensA) {
237
+ if (wordsB.has(tok) && !seen.has(tok)) {
238
+ seen.add(tok);
239
+ count++;
240
+ }
241
+ }
242
+ return count;
243
+ }
217
244
  //# sourceMappingURL=quirk-store.js.map
@@ -90,6 +90,20 @@ export declare class LanceDbStore implements VectorStore {
90
90
  * @returns An array of chunk summaries.
91
91
  */
92
92
  getChunks(offset: number, limit: number): Promise<ChunkSummary[]>;
93
+ /**
94
+ * Fetch chunks with their embedding vectors included (for embedding projection).
95
+ * @param limit - Maximum number of rows to return.
96
+ * @returns Array of { id, filePath, language, startLine, endLine, description, embedding }.
97
+ */
98
+ getChunksWithEmbeddings(limit: number): Promise<{
99
+ id: string;
100
+ filePath: string;
101
+ language: string;
102
+ startLine: number;
103
+ endLine: number;
104
+ description: string;
105
+ embedding: number[];
106
+ }[]>;
93
107
  /**
94
108
  * Perform ANN search internally, returning results scored as cosine similarity (0-1).
95
109
  * This method is called by `search()` and handles the actual query logic.
@@ -157,5 +171,24 @@ export declare class LanceDbStore implements VectorStore {
157
171
  */
158
172
  deleteByFilePath(filePath: string): Promise<void>;
159
173
  private deleteByFilePathInternal;
174
+ /**
175
+ * Verify that data in the store is actually readable.
176
+ * Reads the `content` column (a large text field stored in data fragments,
177
+ * not in the version manifest) for the first few rows. If any of the
178
+ * underlying `.lance` data files are missing from disk, LanceDB throws a
179
+ * corruption error ("Not found: ... .lance") and we return false.
180
+ *
181
+ * This catches silent corruption where countRows() returns metadata from
182
+ * the version manifest while the actual row data on disk is gone.
183
+ */
184
+ checkIntegrity(): Promise<boolean>;
185
+ /**
186
+ * Execute an async function with automatic corruption recovery.
187
+ * If the function throws a LanceDB corruption error, tryRepair() is run
188
+ * and the function is retried once. If repair fails, the original error
189
+ * is re-thrown so callers/higher layers can handle it (e.g. return
190
+ * fallback data, clear the manifest, or trigger a rebuild).
191
+ */
192
+ private withCorruptionRecovery;
160
193
  private tryRepair;
161
194
  }