opencode-rag-plugin 1.18.0 → 1.18.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 +44 -1
- package/dist/cli/commands/init-helpers.d.ts +5 -1
- package/dist/cli/commands/init-helpers.js +8 -19
- package/dist/cli/commands/init.js +42 -8
- package/dist/cli/commands/query.js +1 -1
- package/dist/cli/commands/quirk.js +2 -2
- package/dist/cli/commands/setup.js +15 -6
- package/dist/cli/commands/ui.js +1 -1
- package/dist/core/config.d.ts +16 -0
- package/dist/core/config.js +22 -1
- package/dist/core/interfaces.d.ts +9 -0
- package/dist/core/runtime-overrides.d.ts +11 -1
- package/dist/core/runtime-overrides.js +21 -0
- package/dist/describer/anthropic.d.ts +4 -0
- package/dist/describer/anthropic.js +9 -2
- package/dist/describer/describer.d.ts +4 -0
- package/dist/describer/describer.js +8 -0
- package/dist/describer/gemini.d.ts +3 -0
- package/dist/describer/gemini.js +8 -2
- package/dist/indexer/pipeline.js +40 -0
- package/dist/opencode/system-guidance.d.ts +34 -0
- package/dist/opencode/system-guidance.js +127 -0
- package/dist/plugin.js +172 -70
- package/dist/quirks/auto-capture.d.ts +14 -0
- package/dist/quirks/auto-capture.js +112 -0
- package/dist/quirks/prompts.d.ts +1 -0
- package/dist/quirks/prompts.js +13 -0
- package/dist/quirks/quirk-store.d.ts +4 -0
- package/dist/quirks/quirk-store.js +6 -6
- package/dist/tui.js +134 -1
- package/dist/vectorstore/lancedb.d.ts +10 -1
- package/dist/vectorstore/lancedb.js +28 -2
- package/dist/vectorstore/memory.d.ts +2 -0
- package/dist/vectorstore/memory.js +3 -0
- package/dist/web/api.d.ts +3 -1
- package/dist/web/api.js +50 -1
- package/dist/web/server.d.ts +2 -1
- package/dist/web/server.js +3 -2
- package/dist/web/ui/index.html +163 -0
- package/package.json +1 -1
package/dist/indexer/pipeline.js
CHANGED
|
@@ -169,7 +169,10 @@ async function runIndexPassInner(options, logger) {
|
|
|
169
169
|
const workspaceFiles = await scanWorkspaceFiles(options.cwd, options.config, logger, options.force ? undefined : manifest, filterPaths, options.imageVisionProvider, descCache);
|
|
170
170
|
const scanSec = ((Date.now() - scanStart) / 1000).toFixed(1);
|
|
171
171
|
logger.info(`Workspace scan complete: ${workspaceFiles.length} files in ${scanSec}s`);
|
|
172
|
+
logger.info(`Querying vector store for existing chunk count...`);
|
|
173
|
+
const storeCountStart = Date.now();
|
|
172
174
|
const existingCount = await options.store.count();
|
|
175
|
+
logger.info(`Store has ${existingCount} existing chunks (${((Date.now() - storeCountStart) / 1000).toFixed(1)}s)`);
|
|
173
176
|
// Detect data loss: if the store has far fewer chunks than the manifest expects,
|
|
174
177
|
// treat it as a corrupt store (e.g. schema migration dropped the old table).
|
|
175
178
|
if (!options.force && manifestStatus === "ok" && existingCount > 0) {
|
|
@@ -276,6 +279,11 @@ async function runIndexPassInner(options, logger) {
|
|
|
276
279
|
logger.debug(`Processing ${workspaceFiles.length} files (${newCount} new, ${modifiedCount} modified, ${unchangedCount} unchanged)...`);
|
|
277
280
|
const limit = pLimit(options.config.indexing.concurrency);
|
|
278
281
|
const deferDescriptions = !!options.descriptionProvider;
|
|
282
|
+
const activeCount = workspaceFiles.filter((f) => !f.isEmpty && !f.isTooSmall && (!manifest.files[f.normalizedPath] || manifest.files[f.normalizedPath].hash !== f.hash)).length;
|
|
283
|
+
logger.info(`Chunking ${activeCount} file(s)...`);
|
|
284
|
+
let chunkedDone = 0;
|
|
285
|
+
const chunkProgressInterval = Math.max(1, Math.floor(activeCount / 20));
|
|
286
|
+
const chunkPhaseStart = Date.now();
|
|
279
287
|
const prepared = await Promise.all(workspaceFiles.map((file) => limit(async () => {
|
|
280
288
|
const fileLabel = path.relative(options.cwd, file.normalizedPath).replace(/\\/g, "/");
|
|
281
289
|
const isActive = !file.isEmpty && !file.isTooSmall &&
|
|
@@ -284,11 +292,20 @@ async function runIndexPassInner(options, logger) {
|
|
|
284
292
|
options.progress?.startFile(fileLabel);
|
|
285
293
|
}
|
|
286
294
|
const prep = await prepareFile(file, options.cwd, manifest.files[file.normalizedPath], options.config, options.keywordIndex, options.descriptionProvider, logger, deferDescriptions, descHash);
|
|
295
|
+
if (isActive) {
|
|
296
|
+
chunkedDone++;
|
|
297
|
+
if (chunkedDone % chunkProgressInterval === 0 || chunkedDone === activeCount) {
|
|
298
|
+
const pct = ((chunkedDone / activeCount) * 100).toFixed(1);
|
|
299
|
+
logger.info(` Chunking: ${chunkedDone}/${activeCount} files (${pct}%)`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
287
302
|
if (prep.earlyResult && isActive) {
|
|
288
303
|
options.progress?.finishFile(fileLabel);
|
|
289
304
|
}
|
|
290
305
|
return prep;
|
|
291
306
|
})));
|
|
307
|
+
const chunkPhaseSec = ((Date.now() - chunkPhaseStart) / 1000).toFixed(1);
|
|
308
|
+
logger.info(`Chunking complete: ${activeCount} files in ${chunkPhaseSec}s`);
|
|
292
309
|
const aborted = () => options.abortSignal?.aborted ?? false;
|
|
293
310
|
// Shared progress logger: "stage <file> (chunk i/n) — X/total remaining (P%)".
|
|
294
311
|
const logChunkProgress = (stage, fileLabel, index, count, completed, total) => {
|
|
@@ -302,6 +319,7 @@ async function runIndexPassInner(options, logger) {
|
|
|
302
319
|
const allChunks = [];
|
|
303
320
|
const oversizedChunks = [];
|
|
304
321
|
const maxContentChars = options.config.description?.maxContentChars;
|
|
322
|
+
logger.info(`Description phase: ${deferredPreps.length} files with chunks to describe`);
|
|
305
323
|
for (const prep of deferredPreps) {
|
|
306
324
|
for (const chunk of prep.chunks) {
|
|
307
325
|
if (chunk.metadata.contentType !== "image") {
|
|
@@ -469,6 +487,8 @@ async function runIndexPassInner(options, logger) {
|
|
|
469
487
|
for (const prep of deferredPreps) {
|
|
470
488
|
prep.textToEmbed = buildTextsToEmbed(prep.chunks, prep.relPath, prep.metaHeader ?? "", prep.docPrefix ?? "", prep.isImageFile ?? false);
|
|
471
489
|
}
|
|
490
|
+
const totalDescribedChunks = deferredPreps.reduce((s, p) => s + (p.chunks?.length ?? 0), 0);
|
|
491
|
+
logger.info(`Description phase complete: ${totalDescribedChunks} chunks across ${deferredPreps.length} files`);
|
|
472
492
|
}
|
|
473
493
|
}
|
|
474
494
|
// Cross-file embedding batch: collect all texts into a single queue,
|
|
@@ -530,12 +550,15 @@ async function runIndexPassInner(options, logger) {
|
|
|
530
550
|
let embeddedDone = 0;
|
|
531
551
|
const allTexts = embedQueue.map(item => item.text);
|
|
532
552
|
let allEmbeddings = [];
|
|
553
|
+
const embedPhaseStart = Date.now();
|
|
533
554
|
if (allTexts.length > 0) {
|
|
555
|
+
logger.info(`Embedding phase: ${allTexts.length} texts in batches of ${batchSize}...`);
|
|
534
556
|
try {
|
|
535
557
|
allEmbeddings = await embedBatch(options.embedder, allTexts, batchSize, "document", defaultConcurrency, (completed, total) => {
|
|
536
558
|
embeddedDone = completed;
|
|
537
559
|
logChunkProgress("Embedding", "", completed, total, embeddedDone, totalEmbedChunks);
|
|
538
560
|
});
|
|
561
|
+
logger.info(`Embedding complete: ${allTexts.length} texts in ${((Date.now() - embedPhaseStart) / 1000).toFixed(1)}s`);
|
|
539
562
|
}
|
|
540
563
|
catch (err) {
|
|
541
564
|
logger.warn(` Global embedding failed: ${err.message}`);
|
|
@@ -564,6 +587,8 @@ async function runIndexPassInner(options, logger) {
|
|
|
564
587
|
}
|
|
565
588
|
// ── Phase 3: Store + manifest update per file (parallel) ──────────────
|
|
566
589
|
const filesToStore = prepared.filter((p) => !earlyWorkerResults.has(prepared.indexOf(p)) && p.chunks && (p.textToEmbed?.length ?? 0) > 0).length;
|
|
590
|
+
const storePhaseStart = Date.now();
|
|
591
|
+
logger.info(`Store phase: storing ${filesToStore} file(s) into vector database...`);
|
|
567
592
|
let storedFiles = 0;
|
|
568
593
|
const storeLimit = pLimit(options.config.indexing.concurrency);
|
|
569
594
|
const storeResults = await Promise.all(prepared.map((prep, fi) => storeLimit(async () => {
|
|
@@ -648,6 +673,8 @@ async function runIndexPassInner(options, logger) {
|
|
|
648
673
|
entry.size = size;
|
|
649
674
|
}
|
|
650
675
|
}
|
|
676
|
+
const storePhaseSec = ((Date.now() - storePhaseStart) / 1000).toFixed(1);
|
|
677
|
+
logger.info(`Store phase complete: ${storedFiles} files stored in ${storePhaseSec}s (${stats.totalChunks} total chunks)`);
|
|
651
678
|
aggregateStats(stats, finalResults);
|
|
652
679
|
// Update timestamps; advance lastGitCommit ONLY on a complete pass
|
|
653
680
|
manifest.lastIndexedAt = Date.now();
|
|
@@ -696,6 +723,19 @@ async function runIndexPassInner(options, logger) {
|
|
|
696
723
|
// a successful swap this points to the new data; after an abort it's the old).
|
|
697
724
|
await saveManifest(options.storePath, manifest);
|
|
698
725
|
await options.keywordIndex?.save(options.storePath);
|
|
726
|
+
// Compact fragments and prune old version manifests so countRows() can't
|
|
727
|
+
// hang on accumulated versions from many add/delete cycles.
|
|
728
|
+
if (!aborted()) {
|
|
729
|
+
logger.info("Optimizing vector store (compacting fragments, pruning old versions)...");
|
|
730
|
+
const optimizeStart = Date.now();
|
|
731
|
+
try {
|
|
732
|
+
await effectiveStore.optimize?.();
|
|
733
|
+
logger.info(`Vector store optimized in ${((Date.now() - optimizeStart) / 1000).toFixed(1)}s`);
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
logger.warn(`Vector store optimization failed: ${err.message}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
699
739
|
// Persist description cache
|
|
700
740
|
await descCache.save();
|
|
701
741
|
// Count from the store — after a successful swap, the original handle has
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Single source of truth for the mandatory OpenCodeRAG tool-usage guidance
|
|
3
|
+
* that is injected into both the runtime system prompt (via plugin.ts) and the AGENTS.md
|
|
4
|
+
* directive (via init-helpers.ts).
|
|
5
|
+
*/
|
|
6
|
+
/** Sentinel marker that opens the managed section. */
|
|
7
|
+
export declare const BEGIN_MARKER = "<!-- BEGIN opencode-rag -->";
|
|
8
|
+
/** Sentinel marker that closes the managed section. */
|
|
9
|
+
export declare const END_MARKER = "<!-- END opencode-rag -->";
|
|
10
|
+
/**
|
|
11
|
+
* The always-on mandatory guidance lines — tool list, decision tree,
|
|
12
|
+
* proactive triggers, and anti-patterns. Used verbatim by the runtime
|
|
13
|
+
* system prompt injector.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MANDATORY_GUIDANCE_LINES: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* The conditional quirk-capture enforcement lines. Only included when
|
|
18
|
+
* `memory.promptEnforcement` is true.
|
|
19
|
+
*/
|
|
20
|
+
export declare const QUIRK_ENFORCEMENT_LINES: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* Build the flat guidance lines array for the runtime system-prompt injector.
|
|
23
|
+
* Returns a new array each call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildSystemGuidanceLines(opts: {
|
|
26
|
+
promptEnforcement: boolean;
|
|
27
|
+
}): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Build the markdown-formatted AGENTS.md directive section, wrapped in sentinel
|
|
30
|
+
* markers so `mergeAgentsMdContent` can replace it in place on re-runs.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildAgentsMdDirective(opts: {
|
|
33
|
+
promptEnforcement: boolean;
|
|
34
|
+
}): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Single source of truth for the mandatory OpenCodeRAG tool-usage guidance
|
|
3
|
+
* that is injected into both the runtime system prompt (via plugin.ts) and the AGENTS.md
|
|
4
|
+
* directive (via init-helpers.ts).
|
|
5
|
+
*/
|
|
6
|
+
/** Sentinel marker that opens the managed section. */
|
|
7
|
+
export const BEGIN_MARKER = "<!-- BEGIN opencode-rag -->";
|
|
8
|
+
/** Sentinel marker that closes the managed section. */
|
|
9
|
+
export const END_MARKER = "<!-- END opencode-rag -->";
|
|
10
|
+
/**
|
|
11
|
+
* The always-on mandatory guidance lines — tool list, decision tree,
|
|
12
|
+
* proactive triggers, and anti-patterns. Used verbatim by the runtime
|
|
13
|
+
* system prompt injector.
|
|
14
|
+
*/
|
|
15
|
+
export const MANDATORY_GUIDANCE_LINES = [
|
|
16
|
+
"MANDATORY: OpenCodeRAG tools MUST be used before any code task:",
|
|
17
|
+
"- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
|
|
18
|
+
"- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
|
|
19
|
+
"- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
|
|
20
|
+
"- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
|
|
21
|
+
"- `recall_quirks(query)`: query experiential quirk memory (gotchas, preferences, decisions). Call when you hit an error or need to recall known pitfalls.",
|
|
22
|
+
"- `add_quirk(content)`: store a new experiential memory. Call when you discover a non-obvious fact, gotcha, or coding convention.",
|
|
23
|
+
"",
|
|
24
|
+
"Decision tree — ALWAYS follow this order:",
|
|
25
|
+
"1. User mentions code behavior/architecture → `search_semantic(query)`",
|
|
26
|
+
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
27
|
+
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
28
|
+
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
29
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
30
|
+
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
31
|
+
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
32
|
+
"",
|
|
33
|
+
"Proactive triggers — you MUST call these tools when:",
|
|
34
|
+
"- User asks about code behavior, architecture, or implementation details",
|
|
35
|
+
"- User asks to edit, refactor, or fix code — call `find_usages` first",
|
|
36
|
+
"- User references files or functions you haven't read yet",
|
|
37
|
+
"- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
|
|
38
|
+
"- User refers to an image, screenshot, diagram, or visual asset",
|
|
39
|
+
"- Before answering ANY code-related question, retrieve context first",
|
|
40
|
+
"- Before reading ANY file, call `get_file_skeleton` to orient first",
|
|
41
|
+
"",
|
|
42
|
+
"Anti-patterns — NEVER do these:",
|
|
43
|
+
"- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
|
|
44
|
+
"- Editing a function without calling `find_usages` first (breaks call sites)",
|
|
45
|
+
"- Answering code questions without calling `search_semantic` first (you guess at behavior)",
|
|
46
|
+
"- Using `grep`/`glob` when `search_semantic` would find the answer faster",
|
|
47
|
+
"- Treating image files as text — use `describe_image` instead of reading raw bytes",
|
|
48
|
+
"- Using `npx opencode-rag quirk` shell commands instead of the built-in `add_quirk` / `recall_quirks` tools (the tools are faster, already loaded in-process, and go through the trust monitor)",
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* The conditional quirk-capture enforcement lines. Only included when
|
|
52
|
+
* `memory.promptEnforcement` is true.
|
|
53
|
+
*/
|
|
54
|
+
export const QUIRK_ENFORCEMENT_LINES = [
|
|
55
|
+
"",
|
|
56
|
+
"MANDATORY quirk capture rules — you MUST call `add_quirk` when:",
|
|
57
|
+
"- A build, test, or type-check command fails and you resolve it",
|
|
58
|
+
"- You discover an undocumented library constraint, peer dep, or workaround",
|
|
59
|
+
"- You learn an environment-specific requirement (OS, tool version, etc.)",
|
|
60
|
+
"- You make a design decision that future sessions should remember",
|
|
61
|
+
"- You resolve a gotcha that cost more than one attempt",
|
|
62
|
+
"",
|
|
63
|
+
"Anti-pattern — NEVER finish a coding session without adding quirks for resolved errors.",
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* Build the flat guidance lines array for the runtime system-prompt injector.
|
|
67
|
+
* Returns a new array each call.
|
|
68
|
+
*/
|
|
69
|
+
export function buildSystemGuidanceLines(opts) {
|
|
70
|
+
const lines = [...MANDATORY_GUIDANCE_LINES];
|
|
71
|
+
if (opts.promptEnforcement) {
|
|
72
|
+
lines.push(...QUIRK_ENFORCEMENT_LINES);
|
|
73
|
+
}
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the markdown-formatted AGENTS.md directive section, wrapped in sentinel
|
|
78
|
+
* markers so `mergeAgentsMdContent` can replace it in place on re-runs.
|
|
79
|
+
*/
|
|
80
|
+
export function buildAgentsMdDirective(opts) {
|
|
81
|
+
const lines = [
|
|
82
|
+
BEGIN_MARKER,
|
|
83
|
+
"## Code Navigation",
|
|
84
|
+
"",
|
|
85
|
+
"ALWAYS use OpenCodeRAG tools before reading or editing:",
|
|
86
|
+
"- **Search first** — `search_semantic(query)` instead of grep/glob",
|
|
87
|
+
"- **Skeleton before read** — `get_file_skeleton(filePath)` then read specific lines",
|
|
88
|
+
"- **Usages before edit** — `find_usages(symbolName)` before modifying any symbol",
|
|
89
|
+
"- **Images via describe** — `describe_image(filePath)` — never read raw bytes",
|
|
90
|
+
"- **Recall quirks** — `recall_quirks(query)` when you hit a known pitfall",
|
|
91
|
+
"- **Add quirks** — `add_quirk(content)` when you discover a non-obvious fact",
|
|
92
|
+
"",
|
|
93
|
+
"If no results, run `opencode-rag index`.",
|
|
94
|
+
"",
|
|
95
|
+
"### Decision tree — ALWAYS follow this order",
|
|
96
|
+
"1. User mentions code behavior/architecture → `search_semantic(query)`",
|
|
97
|
+
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
98
|
+
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
99
|
+
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
100
|
+
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
101
|
+
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
102
|
+
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
103
|
+
"",
|
|
104
|
+
"### Proactive triggers — you MUST call these tools when",
|
|
105
|
+
"- User asks about code behavior, architecture, or implementation details",
|
|
106
|
+
"- User asks to edit, refactor, or fix code — call `find_usages` first",
|
|
107
|
+
"- User references files or functions you haven't read yet",
|
|
108
|
+
"- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
|
|
109
|
+
"- User refers to an image, screenshot, diagram, or visual asset",
|
|
110
|
+
"- Before answering ANY code-related question, retrieve context first",
|
|
111
|
+
"- Before reading ANY file, call `get_file_skeleton` to orient first",
|
|
112
|
+
"",
|
|
113
|
+
"### Anti-patterns — NEVER do these",
|
|
114
|
+
"- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
|
|
115
|
+
"- Editing a function without calling `find_usages` first (breaks call sites)",
|
|
116
|
+
"- Answering code questions without calling `search_semantic` first (you guess at behavior)",
|
|
117
|
+
"- Using `grep`/`glob` when `search_semantic` would find the answer faster",
|
|
118
|
+
"- Treating image files as text — use `describe_image` instead of reading raw bytes",
|
|
119
|
+
"- Using `npx opencode-rag quirk` shell commands instead of the built-in `add_quirk` / `recall_quirks` tools (the tools are faster, already loaded in-process, and go through the trust monitor)",
|
|
120
|
+
];
|
|
121
|
+
if (opts.promptEnforcement) {
|
|
122
|
+
lines.push("", "### MANDATORY quirk capture rules — you MUST call `add_quirk` when", "- A build, test, or type-check command fails and you resolve it", "- You discover an undocumented library constraint, peer dep, or workaround", "- You learn an environment-specific requirement (OS, tool version, etc.)", "- You make a design decision that future sessions should remember", "- You resolve a gotcha that cost more than one attempt", "- NEVER finish a coding session without adding quirks for resolved errors.");
|
|
123
|
+
}
|
|
124
|
+
lines.push(END_MARKER);
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=system-guidance.js.map
|
package/dist/plugin.js
CHANGED
|
@@ -26,6 +26,8 @@ import { checkForUpdate, getCurrentVersion, installLatestUpdate } from "./core/v
|
|
|
26
26
|
import { loadAutoUpdateState, saveAutoUpdateState, shouldAttemptInstall } from "./core/auto-update-state.js";
|
|
27
27
|
import { destroyAllPooledConnections } from "./embedder/http.js";
|
|
28
28
|
import { listQuirks, lintQuirks, recallQuirks } from "./quirks/quirk-store.js";
|
|
29
|
+
import { autoCaptureQuirks } from "./quirks/auto-capture.js";
|
|
30
|
+
import { buildSystemGuidanceLines } from "./opencode/system-guidance.js";
|
|
29
31
|
import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
30
32
|
import path from "node:path";
|
|
31
33
|
import { fileURLToPath } from "node:url";
|
|
@@ -93,6 +95,15 @@ function boundedSet(map, key, value, maxSize) {
|
|
|
93
95
|
}
|
|
94
96
|
map.set(key, value);
|
|
95
97
|
}
|
|
98
|
+
/** Get or create a Set for a session key in the given Map, bounded by maxSize. */
|
|
99
|
+
function getOrCreateSessionSet(map, key, maxSize) {
|
|
100
|
+
let set = map.get(key);
|
|
101
|
+
if (!set) {
|
|
102
|
+
set = new Set();
|
|
103
|
+
boundedSet(map, key, set, maxSize);
|
|
104
|
+
}
|
|
105
|
+
return set;
|
|
106
|
+
}
|
|
96
107
|
/** Name of the semantic search tool as exposed to the LLM. */
|
|
97
108
|
const CONTEXT_TOOL_NAME = "search_semantic";
|
|
98
109
|
/** Marker string injected into context output to identify RAG-sourced content. */
|
|
@@ -434,6 +445,12 @@ export function createRagHooks(options) {
|
|
|
434
445
|
// Track the current assistant message text per session for 2-message search queries.
|
|
435
446
|
const sessionAssistantMessageId = new Map();
|
|
436
447
|
const sessionAssistantText = new Map();
|
|
448
|
+
// Auto-capture maps: previous user request per session, tool results, and full transcript
|
|
449
|
+
const sessionPrevUserReq = new Map();
|
|
450
|
+
const sessionToolResults = new Map();
|
|
451
|
+
const sessionTranscript = new Map();
|
|
452
|
+
// Track quirk IDs already injected per session to avoid duplicate injection
|
|
453
|
+
const sessionInjectedQuirks = new Map();
|
|
437
454
|
// Evaluation session logger — captures OpenCode events for analysis
|
|
438
455
|
const sessionLogger = createSessionLogger(options.storePath);
|
|
439
456
|
appendDebugLog(options.logFilePath, {
|
|
@@ -686,8 +703,40 @@ export function createRagHooks(options) {
|
|
|
686
703
|
catch {
|
|
687
704
|
// Non-critical — must never throw
|
|
688
705
|
}
|
|
706
|
+
// Session-end extraction: when we see a non-message lifecycle event,
|
|
707
|
+
// attempt to extract quirks from the full transcript.
|
|
708
|
+
try {
|
|
709
|
+
if (!event.type.startsWith("message.")) {
|
|
710
|
+
const sessionID = event.sessionID;
|
|
711
|
+
if (sessionID) {
|
|
712
|
+
const transcript = sessionTranscript.get(sessionID);
|
|
713
|
+
if (transcript && transcript.length > 0) {
|
|
714
|
+
const effCfg = getEffectiveCfg();
|
|
715
|
+
const memCfg = effCfg.memory;
|
|
716
|
+
if (memCfg?.sessionEndExtraction && options.descriptionProvider) {
|
|
717
|
+
const qDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effCfg, storePath: options.storePath };
|
|
718
|
+
void autoCaptureQuirks(qDeps, options.descriptionProvider, transcript, effCfg, { captureAll: true }).catch(() => { });
|
|
719
|
+
}
|
|
720
|
+
boundedSet(sessionTranscript, sessionID, [], MAX_SESSION_MAP_SIZE);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
// Non-critical — must never throw
|
|
727
|
+
}
|
|
689
728
|
},
|
|
690
729
|
tool: tools,
|
|
730
|
+
async "tool.execute.after"(input, output) {
|
|
731
|
+
try {
|
|
732
|
+
const results = sessionToolResults.get(input.sessionID) ?? [];
|
|
733
|
+
results.push({ tool: input.tool, output: output.output ?? "" });
|
|
734
|
+
boundedSet(sessionToolResults, input.sessionID, results, MAX_SESSION_MAP_SIZE);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
// Non-critical — must never throw
|
|
738
|
+
}
|
|
739
|
+
},
|
|
691
740
|
async "experimental.chat.system.transform"(_input, output) {
|
|
692
741
|
appendDebugLog(options.logFilePath, {
|
|
693
742
|
scope: "experimental.chat.system.transform",
|
|
@@ -695,40 +744,7 @@ export function createRagHooks(options) {
|
|
|
695
744
|
});
|
|
696
745
|
const cfg = getEffectiveCfg();
|
|
697
746
|
if (cfg.openCode.injectSystemPrompt !== false) {
|
|
698
|
-
const guidance =
|
|
699
|
-
"MANDATORY: OpenCodeRAG tools MUST be used before any code task:",
|
|
700
|
-
"- `search_semantic(query)`: retrieve relevant code chunks. Call BEFORE planning, editing, or answering. Accepts `pathHints` and `languageHints`.",
|
|
701
|
-
"- `get_file_skeleton(filePath)`: structural overview of a file. Call BEFORE reading any file.",
|
|
702
|
-
"- `find_usages(symbolName)`: find all references. Call BEFORE editing any function, class, or variable.",
|
|
703
|
-
"- `describe_image(filePath)`: describe an image file using a vision model. Call when user refers to a screenshot, diagram, or image.",
|
|
704
|
-
"- `recall_quirks(query)`: query experiential quirk memory (gotchas, preferences, decisions). Call when you hit an error or need to recall known pitfalls.",
|
|
705
|
-
"- `add_quirk(content)`: store a new experiential memory. Call when you discover a non-obvious fact, gotcha, or coding convention.",
|
|
706
|
-
"",
|
|
707
|
-
"Decision tree — ALWAYS follow this order:",
|
|
708
|
-
"1. User mentions code behavior/architecture → `search_semantic(query)`",
|
|
709
|
-
"2. User mentions a file path → `get_file_skeleton(filePath)` THEN `read` on specific lines",
|
|
710
|
-
"3. User mentions a function/class/variable to edit → `find_usages(symbolName)` THEN `search_semantic` THEN `edit`",
|
|
711
|
-
"4. User asks a code question → `search_semantic` to gather context before answering",
|
|
712
|
-
"5. User asks about an image or visual asset → `describe_image(filePath)` to retrieve its generated description, then optionally `search_semantic` for related code",
|
|
713
|
-
"6. You encounter an error or need to recall a known pitfall → `recall_quirks(query)`",
|
|
714
|
-
"7. You discover a non-obvious fact or workaround → `add_quirk(content)` to persist it for future sessions",
|
|
715
|
-
"",
|
|
716
|
-
"Proactive triggers — you MUST call these tools when:",
|
|
717
|
-
"- User asks about code behavior, architecture, or implementation details",
|
|
718
|
-
"- User asks to edit, refactor, or fix code — call `find_usages` first",
|
|
719
|
-
"- User references files or functions you haven't read yet",
|
|
720
|
-
"- User says \"find\", \"search\", \"look up\", \"where is\", \"how does\"",
|
|
721
|
-
"- User refers to an image, screenshot, diagram, or visual asset",
|
|
722
|
-
"- Before answering ANY code-related question, retrieve context first",
|
|
723
|
-
"- Before reading ANY file, call `get_file_skeleton` to orient first",
|
|
724
|
-
"",
|
|
725
|
-
"Anti-patterns — NEVER do these:",
|
|
726
|
-
"- Reading full files without calling `get_file_skeleton` first (wastes tokens)",
|
|
727
|
-
"- Editing a function without calling `find_usages` first (breaks call sites)",
|
|
728
|
-
"- Answering code questions without calling `search_semantic` first (you guess at behavior)",
|
|
729
|
-
"- Using `grep`/`glob` when `search_semantic` would find the answer faster",
|
|
730
|
-
"- Treating image files as text — use `describe_image` instead of reading raw bytes",
|
|
731
|
-
];
|
|
747
|
+
const guidance = buildSystemGuidanceLines({ promptEnforcement: !!cfg.memory?.promptEnforcement });
|
|
732
748
|
output.system.unshift(guidance.join("\n"));
|
|
733
749
|
}
|
|
734
750
|
// Inject a one-time update prompt so the agent asks the user to install.
|
|
@@ -762,6 +778,42 @@ export function createRagHooks(options) {
|
|
|
762
778
|
if (wikiMode?.enabled && wikiMode.systemPrompt) {
|
|
763
779
|
output.system.unshift(wikiMode.systemPrompt);
|
|
764
780
|
}
|
|
781
|
+
// Inject relevant quirks into system prompt when memory.autoInject is enabled
|
|
782
|
+
try {
|
|
783
|
+
const sessionId = _input?.sessionID;
|
|
784
|
+
if (sessionId) {
|
|
785
|
+
const memCfg = getEffectiveCfg().memory;
|
|
786
|
+
if (memCfg?.enabled && memCfg?.autoInject) {
|
|
787
|
+
const assistantText = sessionAssistantText.get(sessionId) ?? "";
|
|
788
|
+
const userReq = sessionPrevUserReq.get(sessionId) ?? "";
|
|
789
|
+
const query = [assistantText, userReq].filter((t) => t.length > 0).join("\n");
|
|
790
|
+
if (query.length > 0) {
|
|
791
|
+
const quirkDeps = { embedder, store, keywordIndex: keywordIndex, cfg: getEffectiveCfg(), storePath: options.storePath };
|
|
792
|
+
const budgetMs = memCfg.autoInjectLatencyBudgetMs ?? 2000;
|
|
793
|
+
let quirkResults = await Promise.race([
|
|
794
|
+
recallQuirks(quirkDeps, query, { topK: memCfg.autoInjectTopK ?? 2, minScore: memCfg.autoInjectMinScore }),
|
|
795
|
+
new Promise((resolve) => setTimeout(() => resolve([]), budgetMs)),
|
|
796
|
+
]);
|
|
797
|
+
// Dedup: skip quirks already injected into this session
|
|
798
|
+
const injectedSet = getOrCreateSessionSet(sessionInjectedQuirks, sessionId, MAX_SESSION_MAP_SIZE);
|
|
799
|
+
quirkResults = quirkResults.filter((qr) => !injectedSet.has(qr.chunk.id));
|
|
800
|
+
if (quirkResults.length > 0) {
|
|
801
|
+
const lines = ["", "⚠ **Relevant quirks for this task:**", ""];
|
|
802
|
+
for (const qr of quirkResults) {
|
|
803
|
+
const m = qr.chunk.metadata;
|
|
804
|
+
const badge = m.quirkType ? `[${m.quirkType}] ` : "";
|
|
805
|
+
lines.push(`- ${badge}${qr.chunk.content}`);
|
|
806
|
+
injectedSet.add(qr.chunk.id);
|
|
807
|
+
}
|
|
808
|
+
output.system.unshift(lines.join("\n"));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
// Non-critical — must never throw
|
|
816
|
+
}
|
|
765
817
|
},
|
|
766
818
|
async "chat.message"(input, output) {
|
|
767
819
|
try {
|
|
@@ -778,6 +830,30 @@ export function createRagHooks(options) {
|
|
|
778
830
|
});
|
|
779
831
|
if (text.length === 0)
|
|
780
832
|
return;
|
|
833
|
+
// Passive capture: process the completed exchange before overwriting session state
|
|
834
|
+
const prevUserReq = sessionPrevUserReq.get(input.sessionID) ?? "";
|
|
835
|
+
if (prevUserReq) {
|
|
836
|
+
const effCfg = getEffectiveCfg();
|
|
837
|
+
const memCfg = effCfg.memory;
|
|
838
|
+
if (options.descriptionProvider) {
|
|
839
|
+
const assistantText = sessionAssistantText.get(input.sessionID) ?? "";
|
|
840
|
+
const toolResults = sessionToolResults.get(input.sessionID) ?? [];
|
|
841
|
+
if (assistantText) {
|
|
842
|
+
const qDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effCfg, storePath: options.storePath };
|
|
843
|
+
const exchange = { userReq: prevUserReq, assistantText, toolResults };
|
|
844
|
+
if (memCfg?.passiveCapture) {
|
|
845
|
+
void autoCaptureQuirks(qDeps, options.descriptionProvider, [exchange], effCfg).catch(() => { });
|
|
846
|
+
}
|
|
847
|
+
if (memCfg?.sessionEndExtraction) {
|
|
848
|
+
const t = sessionTranscript.get(input.sessionID) ?? [];
|
|
849
|
+
t.push(exchange);
|
|
850
|
+
boundedSet(sessionTranscript, input.sessionID, t, MAX_SESSION_MAP_SIZE);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
sessionPrevUserReq.set(input.sessionID, text);
|
|
856
|
+
sessionToolResults.set(input.sessionID, []);
|
|
781
857
|
boundedSet(sessionLastMessage, input.sessionID, text, MAX_SESSION_MAP_SIZE);
|
|
782
858
|
// Handle /doc slash command
|
|
783
859
|
if (text.startsWith("/doc")) {
|
|
@@ -967,10 +1043,20 @@ export function createRagHooks(options) {
|
|
|
967
1043
|
for (const q of quirks) {
|
|
968
1044
|
const badge = q.quirkType ? `[\`${q.quirkType}\`] ` : "";
|
|
969
1045
|
const tags = q.tags.length > 0 ? ` _(${q.tags.join(", ")})_` : "";
|
|
970
|
-
lines.push(`- ${badge}${q.content
|
|
1046
|
+
lines.push(`- ${badge}${q.content}${tags} `);
|
|
971
1047
|
}
|
|
972
|
-
lines.push("", "Commands:", "- `/quirk` — show this status", "- `/quirk lint` — health-check quirks", "- `/quirk add <text>` — instructions to use the add_quirk tool");
|
|
973
1048
|
}
|
|
1049
|
+
const memCfg = getEffectiveCfg().memory;
|
|
1050
|
+
const flags = [
|
|
1051
|
+
memCfg?.autoInject ? "auto-inject" : "",
|
|
1052
|
+
memCfg?.passiveCapture ? "passive-capture" : "",
|
|
1053
|
+
memCfg?.promptEnforcement ? "prompt-enforce" : "",
|
|
1054
|
+
memCfg?.sessionEndExtraction ? "session-end" : "",
|
|
1055
|
+
].filter(Boolean);
|
|
1056
|
+
if (flags.length > 0) {
|
|
1057
|
+
lines.push(`Flags: ${flags.join(", ")}`);
|
|
1058
|
+
}
|
|
1059
|
+
lines.push("", "Commands:", "- `/quirk` — show this status", "- `/quirk lint` — health-check quirks", "- `/quirk add <text>` — instructions to use the add_quirk tool");
|
|
974
1060
|
}
|
|
975
1061
|
const quirkMsg = lines.join("\n");
|
|
976
1062
|
const parts = output?.parts ?? output?.message?.parts;
|
|
@@ -982,6 +1068,56 @@ export function createRagHooks(options) {
|
|
|
982
1068
|
}
|
|
983
1069
|
return;
|
|
984
1070
|
}
|
|
1071
|
+
// Always-on quirk injection when memory.autoInject is enabled
|
|
1072
|
+
// (runs on every non-slash message, not just hotkey injection)
|
|
1073
|
+
const quirkMemoryCfg = getEffectiveCfg().memory;
|
|
1074
|
+
if (quirkMemoryCfg?.enabled && quirkMemoryCfg?.autoInject) {
|
|
1075
|
+
try {
|
|
1076
|
+
const assistantText = sessionAssistantText.get(input.sessionID) ?? "";
|
|
1077
|
+
const quirkQuery = [assistantText, text].filter((t) => t.length > 0).join("\n");
|
|
1078
|
+
if (quirkQuery.length > 0) {
|
|
1079
|
+
const quirkDeps = { embedder, store, keywordIndex: keywordIndex, cfg: getEffectiveCfg(), storePath: options.storePath };
|
|
1080
|
+
const budgetMs = quirkMemoryCfg.autoInjectLatencyBudgetMs ?? 2000;
|
|
1081
|
+
// Use the stricter recallMinScore for user-prompt injection
|
|
1082
|
+
// (system prompt injection uses the lower autoInjectMinScore)
|
|
1083
|
+
const minScore = quirkMemoryCfg.recallMinScore ?? 0.72;
|
|
1084
|
+
let quirkResults = await Promise.race([
|
|
1085
|
+
recallQuirks(quirkDeps, quirkQuery, { topK: quirkMemoryCfg.autoInjectTopK ?? 2, minScore }),
|
|
1086
|
+
new Promise((resolve) => setTimeout(() => resolve([]), budgetMs)),
|
|
1087
|
+
]);
|
|
1088
|
+
// Dedup: skip quirks already injected into this session
|
|
1089
|
+
const injectedSet = getOrCreateSessionSet(sessionInjectedQuirks, input.sessionID, MAX_SESSION_MAP_SIZE);
|
|
1090
|
+
quirkResults = quirkResults.filter((qr) => !injectedSet.has(qr.chunk.id));
|
|
1091
|
+
if (quirkResults.length > 0) {
|
|
1092
|
+
const quirkLines = ["", "---", "⚠ **Quirk memory**", ""];
|
|
1093
|
+
for (const qr of quirkResults) {
|
|
1094
|
+
const m = qr.chunk.metadata;
|
|
1095
|
+
const badge = m.quirkType ? `[\`${m.quirkType}\`] ` : "";
|
|
1096
|
+
quirkLines.push(`- ${badge}${qr.chunk.content}`);
|
|
1097
|
+
injectedSet.add(qr.chunk.id);
|
|
1098
|
+
}
|
|
1099
|
+
const quirkBlock = quirkLines.join("\n");
|
|
1100
|
+
const parts = output?.parts ?? output?.message?.parts;
|
|
1101
|
+
if (Array.isArray(parts) && parts.length > 0) {
|
|
1102
|
+
const first = parts[0];
|
|
1103
|
+
if (typeof first.text === "string") {
|
|
1104
|
+
parts[0] = { ...first, text: first.text + "\n\n" + quirkBlock };
|
|
1105
|
+
}
|
|
1106
|
+
const msgParts = output?.message?.parts;
|
|
1107
|
+
if (Array.isArray(msgParts) && msgParts !== parts) {
|
|
1108
|
+
const mfirst = msgParts[0];
|
|
1109
|
+
if (typeof mfirst.text === "string") {
|
|
1110
|
+
msgParts[0] = { ...mfirst, text: mfirst.text + "\n\n" + quirkBlock };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
catch {
|
|
1118
|
+
// Non-critical — must never throw
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
985
1121
|
// Handle hotkey-triggered RAG injection (Ctrl+Enter / Ctrl+Alt+Enter)
|
|
986
1122
|
const pendingInjection = consumePendingRagInjection(options.storePath);
|
|
987
1123
|
if (pendingInjection) {
|
|
@@ -1038,40 +1174,6 @@ export function createRagHooks(options) {
|
|
|
1038
1174
|
});
|
|
1039
1175
|
}
|
|
1040
1176
|
}
|
|
1041
|
-
// Auto-inject quirks when memory.autoInject is enabled
|
|
1042
|
-
const memoryCfg = effectiveCfg.memory;
|
|
1043
|
-
if (memoryCfg?.autoInject) {
|
|
1044
|
-
try {
|
|
1045
|
-
const quirkDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effectiveCfg, storePath: options.storePath };
|
|
1046
|
-
const quirkResults = await recallQuirks(quirkDeps, searchQuery, { topK: 3 });
|
|
1047
|
-
if (quirkResults.length > 0) {
|
|
1048
|
-
const quirkLines = ["", "---", "⚠ **Quirk memory**", ""];
|
|
1049
|
-
for (const qr of quirkResults) {
|
|
1050
|
-
const m = qr.chunk.metadata;
|
|
1051
|
-
const badge = m.quirkType ? `[\`${m.quirkType}\`] ` : "";
|
|
1052
|
-
quirkLines.push(`- ${badge}${qr.chunk.content.slice(0, 200)}`);
|
|
1053
|
-
}
|
|
1054
|
-
const quirkBlock = quirkLines.join("\n");
|
|
1055
|
-
const parts = output?.parts ?? output?.message?.parts;
|
|
1056
|
-
if (Array.isArray(parts) && parts.length > 0) {
|
|
1057
|
-
const first = parts[0];
|
|
1058
|
-
if (typeof first.text === "string") {
|
|
1059
|
-
parts[0] = { ...first, text: first.text + "\n\n" + quirkBlock };
|
|
1060
|
-
}
|
|
1061
|
-
const msgParts = output?.message?.parts;
|
|
1062
|
-
if (Array.isArray(msgParts) && msgParts !== parts) {
|
|
1063
|
-
const mfirst = msgParts[0];
|
|
1064
|
-
if (typeof mfirst.text === "string") {
|
|
1065
|
-
msgParts[0] = { ...mfirst, text: mfirst.text + "\n\n" + quirkBlock };
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
catch {
|
|
1072
|
-
// Non-critical — must never throw
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
1177
|
}
|
|
1076
1178
|
appendDebugLog(options.logFilePath, {
|
|
1077
1179
|
scope: "chat.message",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DescriptionProvider } from "../core/interfaces.js";
|
|
2
|
+
import type { RagConfig } from "../core/config.js";
|
|
3
|
+
import { type QuirkStoreDeps } from "./quirk-store.js";
|
|
4
|
+
export interface CaptureExchange {
|
|
5
|
+
userReq: string;
|
|
6
|
+
assistantText: string;
|
|
7
|
+
toolResults: {
|
|
8
|
+
tool: string;
|
|
9
|
+
output: string;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
export declare function autoCaptureQuirks(deps: QuirkStoreDeps, descriptionProvider: DescriptionProvider, exchanges: CaptureExchange[], cfg: RagConfig, opts?: {
|
|
13
|
+
captureAll?: boolean;
|
|
14
|
+
}): Promise<void>;
|