opencode-rag-plugin 1.18.0 → 1.18.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.
Files changed (37) hide show
  1. package/ReadMe.md +41 -1
  2. package/dist/cli/commands/init-helpers.d.ts +5 -1
  3. package/dist/cli/commands/init-helpers.js +8 -19
  4. package/dist/cli/commands/init.js +42 -8
  5. package/dist/cli/commands/setup.js +15 -6
  6. package/dist/cli/commands/ui.js +1 -1
  7. package/dist/core/config.d.ts +10 -0
  8. package/dist/core/config.js +6 -1
  9. package/dist/core/interfaces.d.ts +9 -0
  10. package/dist/core/runtime-overrides.d.ts +7 -0
  11. package/dist/core/runtime-overrides.js +15 -0
  12. package/dist/describer/anthropic.d.ts +4 -0
  13. package/dist/describer/anthropic.js +9 -2
  14. package/dist/describer/describer.d.ts +4 -0
  15. package/dist/describer/describer.js +8 -0
  16. package/dist/describer/gemini.d.ts +3 -0
  17. package/dist/describer/gemini.js +8 -2
  18. package/dist/indexer/pipeline.js +40 -0
  19. package/dist/opencode/system-guidance.d.ts +34 -0
  20. package/dist/opencode/system-guidance.js +127 -0
  21. package/dist/plugin.js +74 -35
  22. package/dist/quirks/auto-capture.d.ts +14 -0
  23. package/dist/quirks/auto-capture.js +112 -0
  24. package/dist/quirks/prompts.d.ts +1 -0
  25. package/dist/quirks/prompts.js +13 -0
  26. package/dist/quirks/quirk-store.d.ts +2 -0
  27. package/dist/quirks/quirk-store.js +1 -1
  28. package/dist/vectorstore/lancedb.d.ts +10 -1
  29. package/dist/vectorstore/lancedb.js +28 -2
  30. package/dist/vectorstore/memory.d.ts +2 -0
  31. package/dist/vectorstore/memory.js +3 -0
  32. package/dist/web/api.d.ts +3 -1
  33. package/dist/web/api.js +50 -1
  34. package/dist/web/server.d.ts +2 -1
  35. package/dist/web/server.js +3 -2
  36. package/dist/web/ui/index.html +163 -0
  37. package/package.json +1 -1
@@ -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";
@@ -434,6 +436,10 @@ export function createRagHooks(options) {
434
436
  // Track the current assistant message text per session for 2-message search queries.
435
437
  const sessionAssistantMessageId = new Map();
436
438
  const sessionAssistantText = new Map();
439
+ // Auto-capture maps: previous user request per session, tool results, and full transcript
440
+ const sessionPrevUserReq = new Map();
441
+ const sessionToolResults = new Map();
442
+ const sessionTranscript = new Map();
437
443
  // Evaluation session logger — captures OpenCode events for analysis
438
444
  const sessionLogger = createSessionLogger(options.storePath);
439
445
  appendDebugLog(options.logFilePath, {
@@ -686,8 +692,40 @@ export function createRagHooks(options) {
686
692
  catch {
687
693
  // Non-critical — must never throw
688
694
  }
695
+ // Session-end extraction: when we see a non-message lifecycle event,
696
+ // attempt to extract quirks from the full transcript.
697
+ try {
698
+ if (!event.type.startsWith("message.")) {
699
+ const sessionID = event.sessionID;
700
+ if (sessionID) {
701
+ const transcript = sessionTranscript.get(sessionID);
702
+ if (transcript && transcript.length > 0) {
703
+ const effCfg = getEffectiveCfg();
704
+ const memCfg = effCfg.memory;
705
+ if (memCfg?.sessionEndExtraction && options.descriptionProvider) {
706
+ const qDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effCfg, storePath: options.storePath };
707
+ void autoCaptureQuirks(qDeps, options.descriptionProvider, transcript, effCfg, { captureAll: true }).catch(() => { });
708
+ }
709
+ boundedSet(sessionTranscript, sessionID, [], MAX_SESSION_MAP_SIZE);
710
+ }
711
+ }
712
+ }
713
+ }
714
+ catch {
715
+ // Non-critical — must never throw
716
+ }
689
717
  },
690
718
  tool: tools,
719
+ async "tool.execute.after"(input, output) {
720
+ try {
721
+ const results = sessionToolResults.get(input.sessionID) ?? [];
722
+ results.push({ tool: input.tool, output: output.output ?? "" });
723
+ boundedSet(sessionToolResults, input.sessionID, results, MAX_SESSION_MAP_SIZE);
724
+ }
725
+ catch {
726
+ // Non-critical — must never throw
727
+ }
728
+ },
691
729
  async "experimental.chat.system.transform"(_input, output) {
692
730
  appendDebugLog(options.logFilePath, {
693
731
  scope: "experimental.chat.system.transform",
@@ -695,40 +733,7 @@ export function createRagHooks(options) {
695
733
  });
696
734
  const cfg = getEffectiveCfg();
697
735
  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
- ];
736
+ const guidance = buildSystemGuidanceLines({ promptEnforcement: !!cfg.memory?.promptEnforcement });
732
737
  output.system.unshift(guidance.join("\n"));
733
738
  }
734
739
  // Inject a one-time update prompt so the agent asks the user to install.
@@ -778,6 +783,30 @@ export function createRagHooks(options) {
778
783
  });
779
784
  if (text.length === 0)
780
785
  return;
786
+ // Passive capture: process the completed exchange before overwriting session state
787
+ const prevUserReq = sessionPrevUserReq.get(input.sessionID) ?? "";
788
+ if (prevUserReq) {
789
+ const effCfg = getEffectiveCfg();
790
+ const memCfg = effCfg.memory;
791
+ if (options.descriptionProvider) {
792
+ const assistantText = sessionAssistantText.get(input.sessionID) ?? "";
793
+ const toolResults = sessionToolResults.get(input.sessionID) ?? [];
794
+ if (assistantText) {
795
+ const qDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effCfg, storePath: options.storePath };
796
+ const exchange = { userReq: prevUserReq, assistantText, toolResults };
797
+ if (memCfg?.passiveCapture) {
798
+ void autoCaptureQuirks(qDeps, options.descriptionProvider, [exchange], effCfg).catch(() => { });
799
+ }
800
+ if (memCfg?.sessionEndExtraction) {
801
+ const t = sessionTranscript.get(input.sessionID) ?? [];
802
+ t.push(exchange);
803
+ boundedSet(sessionTranscript, input.sessionID, t, MAX_SESSION_MAP_SIZE);
804
+ }
805
+ }
806
+ }
807
+ }
808
+ sessionPrevUserReq.set(input.sessionID, text);
809
+ sessionToolResults.set(input.sessionID, []);
781
810
  boundedSet(sessionLastMessage, input.sessionID, text, MAX_SESSION_MAP_SIZE);
782
811
  // Handle /doc slash command
783
812
  if (text.startsWith("/doc")) {
@@ -969,8 +998,18 @@ export function createRagHooks(options) {
969
998
  const tags = q.tags.length > 0 ? ` _(${q.tags.join(", ")})_` : "";
970
999
  lines.push(`- ${badge}${q.content.slice(0, 100)}${tags} `);
971
1000
  }
972
- lines.push("", "Commands:", "- `/quirk` — show this status", "- `/quirk lint` — health-check quirks", "- `/quirk add <text>` — instructions to use the add_quirk tool");
973
1001
  }
1002
+ const memCfg = getEffectiveCfg().memory;
1003
+ const flags = [
1004
+ memCfg?.autoInject ? "auto-inject" : "",
1005
+ memCfg?.passiveCapture ? "passive-capture" : "",
1006
+ memCfg?.promptEnforcement ? "prompt-enforce" : "",
1007
+ memCfg?.sessionEndExtraction ? "session-end" : "",
1008
+ ].filter(Boolean);
1009
+ if (flags.length > 0) {
1010
+ lines.push(`Flags: ${flags.join(", ")}`);
1011
+ }
1012
+ lines.push("", "Commands:", "- `/quirk` — show this status", "- `/quirk lint` — health-check quirks", "- `/quirk add <text>` — instructions to use the add_quirk tool");
974
1013
  }
975
1014
  const quirkMsg = lines.join("\n");
976
1015
  const parts = output?.parts ?? output?.message?.parts;
@@ -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>;
@@ -0,0 +1,112 @@
1
+ import { addQuirk, recallQuirks, lexicalSimilarity } from "./quirk-store.js";
2
+ import { isQuirkAllowed } from "./monitor.js";
3
+ import { MEMORY_CAPTURE_SYSTEM_PROMPT } from "./prompts.js";
4
+ const ERROR_SIGNAL_RE = /(error|failure|fail|failed|err(?:or)?\s*:|stack\s*trace|exit\s+code\s*[1-9]|npm\s+ERR|build\s+failed|test\s+fail|cannot\s+find\s+module|command\s+failed|unresolved|conflict|peer.dep|not\s+found|enoent|eaccess|econnrefused)/i;
5
+ function detectErrorSignal(text) {
6
+ return ERROR_SIGNAL_RE.test(text);
7
+ }
8
+ function buildExchangeText(exchanges) {
9
+ const lines = [];
10
+ for (const ex of exchanges) {
11
+ lines.push("=== User ===");
12
+ lines.push(ex.userReq);
13
+ if (ex.toolResults.length > 0) {
14
+ for (const tr of ex.toolResults) {
15
+ lines.push(`--- Tool: ${tr.tool} ---`);
16
+ lines.push(tr.output.slice(0, 500));
17
+ }
18
+ }
19
+ lines.push("=== Assistant ===");
20
+ lines.push(ex.assistantText);
21
+ }
22
+ return lines.join("\n");
23
+ }
24
+ function parseExtractionOutput(text) {
25
+ const results = [];
26
+ for (const line of text.split("\n")) {
27
+ const trimmed = line.trim();
28
+ if (!trimmed || trimmed === "NOTHING")
29
+ continue;
30
+ const pipeIdx = trimmed.indexOf("|");
31
+ if (pipeIdx <= 0)
32
+ continue;
33
+ const quirkType = trimmed.slice(0, pipeIdx).trim();
34
+ const content = trimmed.slice(pipeIdx + 1).trim();
35
+ if (!quirkType || !content || content.length > 200)
36
+ continue;
37
+ results.push({ quirkType, content });
38
+ }
39
+ return results;
40
+ }
41
+ async function extractQuirksFromText(provider, exchangeText) {
42
+ const raw = await provider.generateText(MEMORY_CAPTURE_SYSTEM_PROMPT, exchangeText);
43
+ return parseExtractionOutput(raw);
44
+ }
45
+ async function dedupCandidates(deps, exchangeHead, candidates, threshold) {
46
+ const existing = await recallQuirks(deps, exchangeHead, { topK: 5 });
47
+ if (existing.length === 0)
48
+ return candidates;
49
+ const existingContent = existing.map((r) => r.chunk.content);
50
+ return candidates.filter((c) => {
51
+ for (const ec of existingContent) {
52
+ if (lexicalSimilarity(c.content, ec) >= threshold) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ });
58
+ }
59
+ export async function autoCaptureQuirks(deps, descriptionProvider, exchanges, cfg, opts) {
60
+ const memoryCfg = cfg.memory;
61
+ if (!memoryCfg)
62
+ return;
63
+ const maxPerTurn = memoryCfg.autoCaptureMaxPerTurn ?? 2;
64
+ const dedupThreshold = memoryCfg.autoCaptureDedupThreshold ?? 0.85;
65
+ const captureAll = opts?.captureAll ?? false;
66
+ if (!captureAll) {
67
+ const combined = exchanges.map((e) => e.userReq + "\n" + e.assistantText + "\n" + e.toolResults.map((t) => t.output).join("\n")).join("\n");
68
+ if (!detectErrorSignal(combined))
69
+ return;
70
+ }
71
+ const exchangeText = buildExchangeText(exchanges);
72
+ if (!exchangeText.trim())
73
+ return;
74
+ let candidates;
75
+ try {
76
+ candidates = await extractQuirksFromText(descriptionProvider, exchangeText);
77
+ }
78
+ catch {
79
+ return;
80
+ }
81
+ if (candidates.length === 0)
82
+ return;
83
+ const exchangeHead = exchanges.map((e) => e.userReq).join(" ");
84
+ let deduped;
85
+ try {
86
+ deduped = await dedupCandidates(deps, exchangeHead, candidates, dedupThreshold);
87
+ }
88
+ catch {
89
+ deduped = candidates;
90
+ }
91
+ let added = 0;
92
+ for (const c of deduped) {
93
+ if (added >= maxPerTurn)
94
+ break;
95
+ const allowed = isQuirkAllowed(c.content);
96
+ if (!allowed.ok)
97
+ continue;
98
+ try {
99
+ await addQuirk(deps, {
100
+ content: c.content,
101
+ quirkType: c.quirkType,
102
+ tags: ["auto-captured"],
103
+ confidence: 0.7,
104
+ });
105
+ added++;
106
+ }
107
+ catch {
108
+ // skip individual failures
109
+ }
110
+ }
111
+ }
112
+ //# sourceMappingURL=auto-capture.js.map
@@ -0,0 +1 @@
1
+ export declare const MEMORY_CAPTURE_SYSTEM_PROMPT: string;
@@ -0,0 +1,13 @@
1
+ export const MEMORY_CAPTURE_SYSTEM_PROMPT = "You are a quirk-extraction assistant. Given a transcript excerpt of an AI agent resolving a " +
2
+ "coding problem, extract concise experiential quirks that are worth remembering.\n\n" +
3
+ "Rules:\n" +
4
+ "- Output exactly one line per quirk in the format: TYPE|content\n" +
5
+ "- TYPE must be one of: gotcha, preference, decision, environment-constraint\n" +
6
+ "- content is a single short sentence (max 200 chars)\n" +
7
+ "- If no quirks are present, output exactly: NOTHING\n" +
8
+ "- Do not include explanations, numbering, or markdown\n\n" +
9
+ "Examples:\n" +
10
+ "gotcha|npm install needs --legacy-peer-deps due to LanceDB peer dep conflicts\n" +
11
+ "preference|Use tsx over ts-node for running TypeScript scripts\n" +
12
+ "NOTHING";
13
+ //# sourceMappingURL=prompts.js.map
@@ -23,3 +23,5 @@ export declare function recallQuirks(deps: QuirkStoreDeps, query: string, option
23
23
  }): Promise<SearchResult[]>;
24
24
  /** Lint quirks for low confidence, staleness, duplicates, and orphan source refs. */
25
25
  export declare function lintQuirks(deps: QuirkStoreDeps): Promise<string[]>;
26
+ /** Simple Jaccard-based lexical similarity (word overlap). */
27
+ export declare function lexicalSimilarity(a: string, b: string): number;
@@ -207,7 +207,7 @@ export async function lintQuirks(deps) {
207
207
  return issues;
208
208
  }
209
209
  /** Simple Jaccard-based lexical similarity (word overlap). */
210
- function lexicalSimilarity(a, b) {
210
+ export function lexicalSimilarity(a, b) {
211
211
  const wordsA = new Set(a.toLowerCase().split(/\W+/).filter(Boolean));
212
212
  const wordsB = new Set(b.toLowerCase().split(/\W+/).filter(Boolean));
213
213
  const intersection = new Set([...wordsA].filter((w) => wordsB.has(w)));
@@ -100,9 +100,18 @@ export declare class LanceDbStore implements VectorStore {
100
100
  private searchInternal;
101
101
  /**
102
102
  * Return the total number of chunks stored in the table.
103
- * @returns The chunk count, or 0 if the table does not exist.
103
+ * Guarded by a 30s timeout — returns 0 if the store is unresponsive
104
+ * (e.g. corrupted version-manifest accumulation) so the pipeline can
105
+ * proceed with a fresh index instead of hanging forever.
106
+ * @returns The chunk count, or 0 if the table does not exist or times out.
104
107
  */
105
108
  count(): Promise<number>;
109
+ /**
110
+ * Compact fragments and prune old version manifests to prevent the
111
+ * version-manifest accumulation that causes countRows() to hang.
112
+ * Should be called at the end of a successful index pass.
113
+ */
114
+ optimize(): Promise<void>;
106
115
  /**
107
116
  * Return all unique file paths currently stored in the index.
108
117
  * @returns An array of normalized file paths.
@@ -513,7 +513,10 @@ export class LanceDbStore {
513
513
  }
514
514
  /**
515
515
  * Return the total number of chunks stored in the table.
516
- * @returns The chunk count, or 0 if the table does not exist.
516
+ * Guarded by a 30s timeout — returns 0 if the store is unresponsive
517
+ * (e.g. corrupted version-manifest accumulation) so the pipeline can
518
+ * proceed with a fresh index instead of hanging forever.
519
+ * @returns The chunk count, or 0 if the table does not exist or times out.
517
520
  */
518
521
  async count() {
519
522
  try {
@@ -522,12 +525,35 @@ export class LanceDbStore {
522
525
  if (!tableNames.includes(TABLE_NAME))
523
526
  return 0;
524
527
  const table = await this.getTable();
525
- return await table.countRows();
528
+ const COUNT_TIMEOUT_MS = 30_000;
529
+ const result = await Promise.race([
530
+ table.countRows(),
531
+ new Promise((resolve) => setTimeout(() => resolve(null), COUNT_TIMEOUT_MS)),
532
+ ]);
533
+ if (result === null) {
534
+ console.warn(`[lancedb] count() timed out after ${COUNT_TIMEOUT_MS / 1000}s — treating store as empty.`);
535
+ return 0;
536
+ }
537
+ return result;
526
538
  }
527
539
  catch {
528
540
  return 0;
529
541
  }
530
542
  }
543
+ /**
544
+ * Compact fragments and prune old version manifests to prevent the
545
+ * version-manifest accumulation that causes countRows() to hang.
546
+ * Should be called at the end of a successful index pass.
547
+ */
548
+ async optimize() {
549
+ try {
550
+ const table = await this.getTable();
551
+ await table.optimize({ cleanupOlderThan: new Date(), deleteUnverified: false });
552
+ }
553
+ catch {
554
+ // Optimize is best-effort — must not break indexing.
555
+ }
556
+ }
531
557
  /**
532
558
  * Return all unique file paths currently stored in the index.
533
559
  * @returns An array of normalized file paths.
@@ -21,4 +21,6 @@ export declare class InMemoryVectorStore implements VectorStore {
21
21
  getChunksByFilePath(filePath: string): Promise<Chunk[]>;
22
22
  /** Release any held resources. No-op for the in-memory store. */
23
23
  close(): Promise<void>;
24
+ /** No-op for the in-memory store. */
25
+ optimize(): Promise<void>;
24
26
  }
@@ -73,6 +73,9 @@ export class InMemoryVectorStore {
73
73
  /** Release any held resources. No-op for the in-memory store. */
74
74
  async close() {
75
75
  }
76
+ /** No-op for the in-memory store. */
77
+ async optimize() {
78
+ }
76
79
  }
77
80
  /**
78
81
  * Compute the cosine similarity between two equal-length vectors.
package/dist/web/api.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import type { IncomingMessage, ServerResponse } from "node:http";
5
5
  import { LanceDbStore } from "../vectorstore/lancedb.js";
6
6
  import { KeywordIndex } from "../retriever/keyword-index.js";
7
+ import type { RagConfig } from "../core/config.js";
7
8
  /** Internal shape for a JSON API response: an HTTP status code and a serialisable body. */
8
9
  interface ApiResponse {
9
10
  status: number;
@@ -23,9 +24,10 @@ interface ApiResponse {
23
24
  * @param keywordIndex - The keyword-index instance for text search.
24
25
  * @param storePath - Filesystem path to the store directory (used by eval endpoints).
25
26
  * @param cwd - Optional workspace root for resolving file paths.
27
+ * @param cfg - Active RAG configuration (used by quirk endpoints).
26
28
  * @returns An async handler that returns `true` when a route matched or `false` otherwise.
27
29
  */
28
- export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
30
+ export declare function createApiHandler(store: LanceDbStore, keywordIndex: KeywordIndex, storePath: string, cwd?: string, cfg?: RagConfig): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
29
31
  /**
30
32
  * Perform token-usage analysis for a single evaluation session.
31
33
  *
package/dist/web/api.js CHANGED
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { extname, resolve as resolvePathModule } from "node:path";
3
3
  import { listSessions, getSession, deleteSession, compareSessions, validateSessionID } from "../eval/storage.js";
4
4
  import { analyzeTokenUsage, compareTokenAnalyses, projectTokenSavings } from "../eval/token-analysis.js";
5
+ import { listQuirks, lintQuirks, removeQuirk } from "../quirks/quirk-store.js";
5
6
  const FILE_MIME_TYPES = {
6
7
  ".png": "image/png",
7
8
  ".jpg": "image/jpeg",
@@ -11,6 +12,13 @@ const FILE_MIME_TYPES = {
11
12
  ".bmp": "image/bmp",
12
13
  ".svg": "image/svg+xml",
13
14
  };
15
+ /** No-op embedder used by quirk endpoints that never need to embed text in the read-only UI context. */
16
+ const stubEmbedder = {
17
+ name: "stub",
18
+ embed: async () => {
19
+ throw new Error("Embedder is not available in the Web UI context");
20
+ },
21
+ };
14
22
  /** Split a raw URL into its pathname and parsed query-string parameters. */
15
23
  function parseQuery(url) {
16
24
  const [path, queryString] = url.split("?");
@@ -43,13 +51,22 @@ function sendJson(res, response) {
43
51
  * @param keywordIndex - The keyword-index instance for text search.
44
52
  * @param storePath - Filesystem path to the store directory (used by eval endpoints).
45
53
  * @param cwd - Optional workspace root for resolving file paths.
54
+ * @param cfg - Active RAG configuration (used by quirk endpoints).
46
55
  * @returns An async handler that returns `true` when a route matched or `false` otherwise.
47
56
  */
48
- export function createApiHandler(store, keywordIndex, storePath, cwd) {
57
+ export function createApiHandler(store, keywordIndex, storePath, cwd, cfg) {
49
58
  return async (req, res) => {
50
59
  const url = req.url ?? "/";
51
60
  const method = req.method ?? "GET";
52
61
  const { path, params } = parseQuery(url);
62
+ // Quirk store dependencies (embedder is a no-op stub for the read-only UI context).
63
+ const quirkDeps = {
64
+ embedder: stubEmbedder,
65
+ store,
66
+ keywordIndex,
67
+ cfg: cfg ?? {},
68
+ storePath,
69
+ };
53
70
  // CORS preflight
54
71
  if (method === "OPTIONS") {
55
72
  res.writeHead(204, {
@@ -143,6 +160,22 @@ export function createApiHandler(store, keywordIndex, storePath, cwd) {
143
160
  const body = await readBody(req);
144
161
  response = handleEvalProjectSavings(body);
145
162
  }
163
+ // Quirk memory endpoints
164
+ else if (path === "/api/quirks" && method === "GET") {
165
+ response = await handleQuirks(quirkDeps);
166
+ }
167
+ else if (path === "/api/quirks/lint" && method === "GET") {
168
+ response = await handleQuirkLint(quirkDeps);
169
+ }
170
+ else if (path.startsWith("/api/quirks/") && method === "DELETE") {
171
+ const id = path.slice("/api/quirks/".length);
172
+ if (!id) {
173
+ response = { status: 400, body: { error: "Missing quirk ID" } };
174
+ }
175
+ else {
176
+ response = await handleQuirkDelete(quirkDeps, id);
177
+ }
178
+ }
146
179
  else {
147
180
  return false;
148
181
  }
@@ -181,6 +214,22 @@ async function handleFiles(store) {
181
214
  const files = await store.listFiles();
182
215
  return { status: 200, body: files };
183
216
  }
217
+ // ── Quirk Memory API ─────────────────────────────────────────────────
218
+ /** Respond with all stored quirks, sorted by last-observed time (most recent first). */
219
+ async function handleQuirks(deps) {
220
+ const quirks = await listQuirks(deps);
221
+ return { status: 200, body: { quirks } };
222
+ }
223
+ /** Respond with the health-check issues for the quirk store (confidence, staleness, duplicates). */
224
+ async function handleQuirkLint(deps) {
225
+ const issues = await lintQuirks(deps);
226
+ return { status: 200, body: { issues } };
227
+ }
228
+ /** Delete a single quirk by its ID from the store, index, and audit log. */
229
+ async function handleQuirkDelete(deps, id) {
230
+ await removeQuirk(deps, id);
231
+ return { status: 200, body: { deleted: true, id } };
232
+ }
184
233
  /**
185
234
  * Respond with a paginated, optionally filtered list of chunks.
186
235
  *