opencode-rag-plugin 1.18.2 → 1.19.0
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/content/reader.d.ts +2 -1
- package/dist/content/reader.js +9 -15
- package/dist/core/config.d.ts +28 -2
- package/dist/core/config.js +12 -0
- package/dist/core/exclude.d.ts +4 -0
- package/dist/core/exclude.js +45 -0
- package/dist/core/interfaces.d.ts +6 -0
- package/dist/indexer/pipeline.js +21 -0
- package/dist/indexer/watch.js +4 -3
- package/dist/indexer.d.ts +1 -0
- package/dist/indexer.js +1 -0
- package/dist/plugin.js +15 -1
- package/dist/quirks/quirk-store.d.ts +15 -0
- package/dist/quirks/quirk-store.js +27 -0
- package/dist/vectorstore/lancedb.d.ts +33 -0
- package/dist/vectorstore/lancedb.js +169 -80
- package/dist/web/api.d.ts +2 -1
- package/dist/web/api.js +219 -3
- package/dist/web/pca.d.ts +7 -0
- package/dist/web/pca.js +87 -0
- package/dist/web/server.js +23 -8
- package/dist/web/static.d.ts +7 -2
- package/dist/web/static.js +31 -7
- package/dist/web/ui/assets/index-CJBvt6e0.js +3 -0
- package/dist/web/ui/assets/index-CKdp79Tw.css +1 -0
- package/dist/web/ui/assets/vendor-Dy7HKFCY.js +1 -0
- package/dist/web/ui/index.html +9 -1632
- package/package.json +10 -4
- package/dist/web/ui/app.css +0 -1
- package/dist/web/ui/github-dark.css +0 -118
- package/dist/web/ui/highlight.min.js +0 -1213
package/dist/content/reader.d.ts
CHANGED
|
@@ -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:
|
|
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
|
/**
|
package/dist/content/reader.js
CHANGED
|
@@ -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.
|
|
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?.
|
|
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
|
-
|
|
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,
|
|
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
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
package/dist/core/config.js
CHANGED
|
@@ -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",
|
|
@@ -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,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 {
|
package/dist/indexer/pipeline.js
CHANGED
|
@@ -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) {
|
package/dist/indexer/watch.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|