opencode-rag-plugin 1.18.1 → 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 +7 -4
- package/dist/cli/commands/query.js +1 -1
- package/dist/cli/commands/quirk.js +2 -2
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +16 -0
- package/dist/core/runtime-overrides.d.ts +4 -1
- package/dist/core/runtime-overrides.js +6 -0
- package/dist/plugin.js +98 -35
- package/dist/quirks/prompts.js +1 -1
- package/dist/quirks/quirk-store.d.ts +2 -0
- package/dist/quirks/quirk-store.js +5 -5
- package/dist/tui.js +134 -1
- package/package.json +1 -1
package/ReadMe.md
CHANGED
|
@@ -128,7 +128,7 @@ See [Plugin documentation](doc/plugin.md#6-wiki-mode--slash-command-wiki) for th
|
|
|
128
128
|
|
|
129
129
|
## Quirk Memory
|
|
130
130
|
|
|
131
|
-
OpenCodeRAG gives your agent **persistent, cross-session memory** of non-obvious facts — gotchas, preferences, decisions, and environment constraints — that it can recall and extend over time. Quirks are embedded and stored in the same vector store as your code, then
|
|
131
|
+
OpenCodeRAG gives your agent **persistent, cross-session memory** of non-obvious facts — gotchas, preferences, decisions, and environment constraints — that it can recall and extend over time. Quirks are embedded and stored in the same vector store as your code, then **automatically injected into the agent's context on every user message** when their relevance score exceeds the configured threshold. Injection uses two thresholds: the stricter `recallMinScore` (0.72) for the user message context, and the more permissive `autoInjectMinScore` (0.45) for the system prompt. This lets the agent avoid repeating mistakes and accumulate project knowledge across sessions without manual recall.
|
|
132
132
|
|
|
133
133
|
**Enabled by default.** Turn it on/off in `opencode-rag.json`:
|
|
134
134
|
|
|
@@ -138,8 +138,11 @@ OpenCodeRAG gives your agent **persistent, cross-session memory** of non-obvious
|
|
|
138
138
|
"enabled": true,
|
|
139
139
|
"autoInject": true,
|
|
140
140
|
"minConfidence": 0.5,
|
|
141
|
-
"recallMinScore": 0.
|
|
142
|
-
"
|
|
141
|
+
"recallMinScore": 0.8,
|
|
142
|
+
"autoInjectMinScore": 0.6,
|
|
143
|
+
"autoInjectTopK": 2,
|
|
144
|
+
"autoInjectLatencyBudgetMs": 2000,
|
|
145
|
+
"decay": { "enabled": false, "halfLifeDays": 30 }
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
148
|
```
|
|
@@ -163,7 +166,7 @@ opencode-rag quirk test "npm needs --legacy-peer-deps"
|
|
|
163
166
|
# 99% confidence
|
|
164
167
|
```
|
|
165
168
|
|
|
166
|
-
When `memory.autoInject` is `true`, relevant quirks are
|
|
169
|
+
When `memory.autoInject` is `true`, the plugin checks for relevant quirks on every user message using the combined agent-response + user-query as the search query. Quirks are only injected when their relevance score exceeds the threshold — `recallMinScore` (default 0.72) for the user message, `autoInjectMinScore` (default 0.45) for the system prompt. A latency budget (`autoInjectLatencyBudgetMs`, default 2000ms) prevents slow embedders from blocking message processing. To avoid polluting the context window, each quirk is injected **at most once per session** — once recalled, it is filtered out from all subsequent auto-injections. Every `add_quirk` is vetted by an immutable trust monitor that rejects destructive patterns (e.g. `rm -rf`, `force push`, `bypass security`). See [Plugin documentation](doc/plugin.md#9-quirk-memory-experiential-memory) and [CLI Reference: `quirk`](doc/cli.md#quirk).
|
|
167
170
|
|
|
168
171
|
## MCP Server (Optional)
|
|
169
172
|
|
|
@@ -74,7 +74,7 @@ export function registerQueryCommand(program) {
|
|
|
74
74
|
logCliInfo(logFilePath, "query", ` ${c.label(" Matched:")} ${c.lang(r.explanation.matchedTerms.join(", "))}`);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
logCliInfo(logFilePath, "query", ` ${pc.dim(r.chunk.content.
|
|
77
|
+
logCliInfo(logFilePath, "query", ` ${pc.dim(r.chunk.content.replace(/\n/g, "\n "))}`);
|
|
78
78
|
}
|
|
79
79
|
await cleanupContext(ctx);
|
|
80
80
|
}
|
|
@@ -51,7 +51,7 @@ export function registerQuirkCommand(program) {
|
|
|
51
51
|
const badge = q.quirkType ? `[${q.quirkType}] ` : "";
|
|
52
52
|
const tags = q.tags.length > 0 ? ` (${q.tags.join(", ")})` : "";
|
|
53
53
|
const date = q.lastObserved ? new Date(q.lastObserved).toLocaleDateString() : "";
|
|
54
|
-
logCliInfo(ctx.logFilePath, "quirk list", ` ${c.value(badge)}${c.file(q.content
|
|
54
|
+
logCliInfo(ctx.logFilePath, "quirk list", ` ${c.value(badge)}${c.file(q.content)}${c.dim(tags)} ${c.dim(date)}`);
|
|
55
55
|
}
|
|
56
56
|
await cleanupContext(ctx);
|
|
57
57
|
}
|
|
@@ -119,7 +119,7 @@ export function registerQuirkCommand(program) {
|
|
|
119
119
|
const badge = r.chunk.metadata.quirkType ? `[${r.chunk.metadata.quirkType}] ` : "";
|
|
120
120
|
const tags = r.chunk.metadata.tags?.length ? ` (${r.chunk.metadata.tags.join(", ")})` : "";
|
|
121
121
|
const confidence = r.chunk.metadata.confidence ? `${(r.chunk.metadata.confidence * 100).toFixed(0)}% confidence` : "";
|
|
122
|
-
logCliInfo(ctx.logFilePath, "quirk test", ` ${c.value(badge)}${c.file(r.chunk.content
|
|
122
|
+
logCliInfo(ctx.logFilePath, "quirk test", ` ${c.value(badge)}${c.file(r.chunk.content)}${c.dim(tags)}`);
|
|
123
123
|
logCliInfo(ctx.logFilePath, "quirk test", ` ${c.dim(confidence)}`);
|
|
124
124
|
logCliInfo(ctx.logFilePath, "quirk test", "");
|
|
125
125
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -131,6 +131,12 @@ export interface MemoryConfig {
|
|
|
131
131
|
minConfidence: number;
|
|
132
132
|
/** Minimum query-relevance score (0-1) for a quirk to be recalled. */
|
|
133
133
|
recallMinScore: number;
|
|
134
|
+
/** Minimum relevance score for auto-injected quirks (lower than recallMinScore for manual calls). */
|
|
135
|
+
autoInjectMinScore: number;
|
|
136
|
+
/** Maximum latency budget (ms) for auto-inject quirk recall. If exceeded, injection is skipped. */
|
|
137
|
+
autoInjectLatencyBudgetMs: number;
|
|
138
|
+
/** Max quirks to auto-inject per turn (default 2). */
|
|
139
|
+
autoInjectTopK: number;
|
|
134
140
|
/** Automatically extract quirks from each completed agent turn. */
|
|
135
141
|
passiveCapture: boolean;
|
|
136
142
|
/** Upgrade the system-prompt nudge into a mandatory trigger. */
|
package/dist/core/config.js
CHANGED
|
@@ -258,6 +258,9 @@ export const DEFAULT_CONFIG = {
|
|
|
258
258
|
autoInject: false,
|
|
259
259
|
minConfidence: 0.5,
|
|
260
260
|
recallMinScore: 0.72,
|
|
261
|
+
autoInjectMinScore: 0.6,
|
|
262
|
+
autoInjectLatencyBudgetMs: 2000,
|
|
263
|
+
autoInjectTopK: 2,
|
|
261
264
|
passiveCapture: false,
|
|
262
265
|
promptEnforcement: true,
|
|
263
266
|
sessionEndExtraction: true,
|
|
@@ -350,6 +353,19 @@ export function validateConfig(config) {
|
|
|
350
353
|
if (r < 0 || r > 1)
|
|
351
354
|
warnings.push("memory.recallMinScore must be between 0 and 1");
|
|
352
355
|
}
|
|
356
|
+
if (config.memory?.autoInjectMinScore != null) {
|
|
357
|
+
const r = config.memory.autoInjectMinScore;
|
|
358
|
+
if (r < 0 || r > 1)
|
|
359
|
+
warnings.push("memory.autoInjectMinScore must be between 0 and 1");
|
|
360
|
+
}
|
|
361
|
+
if (config.memory?.autoInjectLatencyBudgetMs != null) {
|
|
362
|
+
const r = config.memory.autoInjectLatencyBudgetMs;
|
|
363
|
+
if (r < 0)
|
|
364
|
+
warnings.push("memory.autoInjectLatencyBudgetMs must be >= 0");
|
|
365
|
+
}
|
|
366
|
+
if (config.memory?.autoInjectTopK != null && config.memory.autoInjectTopK < 1) {
|
|
367
|
+
warnings.push("memory.autoInjectTopK must be >= 1");
|
|
368
|
+
}
|
|
353
369
|
if (config.openCode.maxContextChunks <= 0) {
|
|
354
370
|
warnings.push("openCode.maxContextChunks must be > 0");
|
|
355
371
|
}
|
|
@@ -48,6 +48,9 @@ export interface RuntimeOverrides {
|
|
|
48
48
|
sessionEndExtraction?: boolean;
|
|
49
49
|
autoCaptureMaxPerTurn?: number;
|
|
50
50
|
autoCaptureDedupThreshold?: number;
|
|
51
|
+
autoInjectMinScore?: number;
|
|
52
|
+
autoInjectLatencyBudgetMs?: number;
|
|
53
|
+
autoInjectTopK?: number;
|
|
51
54
|
};
|
|
52
55
|
tui?: {
|
|
53
56
|
fileListKeybinding?: string;
|
|
@@ -57,6 +60,6 @@ export interface RuntimeOverrides {
|
|
|
57
60
|
/** Load runtime overrides from the store directory. Returns empty object if none exist. */
|
|
58
61
|
export declare function loadRuntimeOverrides(storePath: string): RuntimeOverrides;
|
|
59
62
|
/** Save a single runtime override value at a dotted path. Creates intermediate objects as needed. */
|
|
60
|
-
export declare function saveRuntimeOverride(storePath: string, path: string[], value: boolean | number | string): void;
|
|
63
|
+
export declare function saveRuntimeOverride(storePath: string, path: string[], value: boolean | number | string | Record<string, unknown>): void;
|
|
61
64
|
/** Deep-merge runtime overrides into a config object. Returns a new object without mutating the original. */
|
|
62
65
|
export declare function applyRuntimeOverrides(cfg: RagConfig, overrides: RuntimeOverrides): RagConfig;
|
|
@@ -136,6 +136,12 @@ export function applyRuntimeOverrides(cfg, overrides) {
|
|
|
136
136
|
m.autoCaptureMaxPerTurn = overrides.memory.autoCaptureMaxPerTurn;
|
|
137
137
|
if (overrides.memory.autoCaptureDedupThreshold !== undefined)
|
|
138
138
|
m.autoCaptureDedupThreshold = overrides.memory.autoCaptureDedupThreshold;
|
|
139
|
+
if (overrides.memory.autoInjectMinScore !== undefined)
|
|
140
|
+
m.autoInjectMinScore = overrides.memory.autoInjectMinScore;
|
|
141
|
+
if (overrides.memory.autoInjectLatencyBudgetMs !== undefined)
|
|
142
|
+
m.autoInjectLatencyBudgetMs = overrides.memory.autoInjectLatencyBudgetMs;
|
|
143
|
+
if (overrides.memory.autoInjectTopK !== undefined)
|
|
144
|
+
m.autoInjectTopK = overrides.memory.autoInjectTopK;
|
|
139
145
|
}
|
|
140
146
|
if (overrides.tui) {
|
|
141
147
|
merged.tui = {
|
package/dist/plugin.js
CHANGED
|
@@ -95,6 +95,15 @@ function boundedSet(map, key, value, maxSize) {
|
|
|
95
95
|
}
|
|
96
96
|
map.set(key, value);
|
|
97
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
|
+
}
|
|
98
107
|
/** Name of the semantic search tool as exposed to the LLM. */
|
|
99
108
|
const CONTEXT_TOOL_NAME = "search_semantic";
|
|
100
109
|
/** Marker string injected into context output to identify RAG-sourced content. */
|
|
@@ -440,6 +449,8 @@ export function createRagHooks(options) {
|
|
|
440
449
|
const sessionPrevUserReq = new Map();
|
|
441
450
|
const sessionToolResults = new Map();
|
|
442
451
|
const sessionTranscript = new Map();
|
|
452
|
+
// Track quirk IDs already injected per session to avoid duplicate injection
|
|
453
|
+
const sessionInjectedQuirks = new Map();
|
|
443
454
|
// Evaluation session logger — captures OpenCode events for analysis
|
|
444
455
|
const sessionLogger = createSessionLogger(options.storePath);
|
|
445
456
|
appendDebugLog(options.logFilePath, {
|
|
@@ -767,6 +778,42 @@ export function createRagHooks(options) {
|
|
|
767
778
|
if (wikiMode?.enabled && wikiMode.systemPrompt) {
|
|
768
779
|
output.system.unshift(wikiMode.systemPrompt);
|
|
769
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
|
+
}
|
|
770
817
|
},
|
|
771
818
|
async "chat.message"(input, output) {
|
|
772
819
|
try {
|
|
@@ -996,7 +1043,7 @@ export function createRagHooks(options) {
|
|
|
996
1043
|
for (const q of quirks) {
|
|
997
1044
|
const badge = q.quirkType ? `[\`${q.quirkType}\`] ` : "";
|
|
998
1045
|
const tags = q.tags.length > 0 ? ` _(${q.tags.join(", ")})_` : "";
|
|
999
|
-
lines.push(`- ${badge}${q.content
|
|
1046
|
+
lines.push(`- ${badge}${q.content}${tags} `);
|
|
1000
1047
|
}
|
|
1001
1048
|
}
|
|
1002
1049
|
const memCfg = getEffectiveCfg().memory;
|
|
@@ -1021,6 +1068,56 @@ export function createRagHooks(options) {
|
|
|
1021
1068
|
}
|
|
1022
1069
|
return;
|
|
1023
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
|
+
}
|
|
1024
1121
|
// Handle hotkey-triggered RAG injection (Ctrl+Enter / Ctrl+Alt+Enter)
|
|
1025
1122
|
const pendingInjection = consumePendingRagInjection(options.storePath);
|
|
1026
1123
|
if (pendingInjection) {
|
|
@@ -1077,40 +1174,6 @@ export function createRagHooks(options) {
|
|
|
1077
1174
|
});
|
|
1078
1175
|
}
|
|
1079
1176
|
}
|
|
1080
|
-
// Auto-inject quirks when memory.autoInject is enabled
|
|
1081
|
-
const memoryCfg = effectiveCfg.memory;
|
|
1082
|
-
if (memoryCfg?.autoInject) {
|
|
1083
|
-
try {
|
|
1084
|
-
const quirkDeps = { embedder, store, keywordIndex: keywordIndex, cfg: effectiveCfg, storePath: options.storePath };
|
|
1085
|
-
const quirkResults = await recallQuirks(quirkDeps, searchQuery, { topK: 3 });
|
|
1086
|
-
if (quirkResults.length > 0) {
|
|
1087
|
-
const quirkLines = ["", "---", "⚠ **Quirk memory**", ""];
|
|
1088
|
-
for (const qr of quirkResults) {
|
|
1089
|
-
const m = qr.chunk.metadata;
|
|
1090
|
-
const badge = m.quirkType ? `[\`${m.quirkType}\`] ` : "";
|
|
1091
|
-
quirkLines.push(`- ${badge}${qr.chunk.content.slice(0, 200)}`);
|
|
1092
|
-
}
|
|
1093
|
-
const quirkBlock = quirkLines.join("\n");
|
|
1094
|
-
const parts = output?.parts ?? output?.message?.parts;
|
|
1095
|
-
if (Array.isArray(parts) && parts.length > 0) {
|
|
1096
|
-
const first = parts[0];
|
|
1097
|
-
if (typeof first.text === "string") {
|
|
1098
|
-
parts[0] = { ...first, text: first.text + "\n\n" + quirkBlock };
|
|
1099
|
-
}
|
|
1100
|
-
const msgParts = output?.message?.parts;
|
|
1101
|
-
if (Array.isArray(msgParts) && msgParts !== parts) {
|
|
1102
|
-
const mfirst = msgParts[0];
|
|
1103
|
-
if (typeof mfirst.text === "string") {
|
|
1104
|
-
msgParts[0] = { ...mfirst, text: mfirst.text + "\n\n" + quirkBlock };
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
catch {
|
|
1111
|
-
// Non-critical — must never throw
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
1177
|
}
|
|
1115
1178
|
appendDebugLog(options.logFilePath, {
|
|
1116
1179
|
scope: "chat.message",
|
package/dist/quirks/prompts.js
CHANGED
|
@@ -3,7 +3,7 @@ export const MEMORY_CAPTURE_SYSTEM_PROMPT = "You are a quirk-extraction assistan
|
|
|
3
3
|
"Rules:\n" +
|
|
4
4
|
"- Output exactly one line per quirk in the format: TYPE|content\n" +
|
|
5
5
|
"- TYPE must be one of: gotcha, preference, decision, environment-constraint\n" +
|
|
6
|
-
"- content
|
|
6
|
+
"- Keep content brief and to the point — one short sentence capturing the essential fact\n" +
|
|
7
7
|
"- If no quirks are present, output exactly: NOTHING\n" +
|
|
8
8
|
"- Do not include explanations, numbering, or markdown\n\n" +
|
|
9
9
|
"Examples:\n" +
|
|
@@ -20,6 +20,8 @@ export declare function recallQuirks(deps: QuirkStoreDeps, query: string, option
|
|
|
20
20
|
topK?: number;
|
|
21
21
|
quirkType?: string;
|
|
22
22
|
tags?: string[];
|
|
23
|
+
/** Override recallMinScore from config. Used by auto-inject for a lower threshold. */
|
|
24
|
+
minScore?: number;
|
|
23
25
|
}): Promise<SearchResult[]>;
|
|
24
26
|
/** Lint quirks for low confidence, staleness, duplicates, and orphan source refs. */
|
|
25
27
|
export declare function lintQuirks(deps: QuirkStoreDeps): Promise<string[]>;
|
|
@@ -146,13 +146,13 @@ export async function recallQuirks(deps, query, options) {
|
|
|
146
146
|
if (options?.quirkType) {
|
|
147
147
|
filter.languages = [options.quirkType];
|
|
148
148
|
}
|
|
149
|
-
const recallMinScore = deps.cfg.memory?.recallMinScore ?? 0.72;
|
|
149
|
+
const recallMinScore = options?.minScore ?? deps.cfg.memory?.recallMinScore ?? 0.72;
|
|
150
150
|
const raw = await retrieve(query, deps.embedder, deps.store, {
|
|
151
151
|
topK: topK * 3,
|
|
152
152
|
minScore: recallMinScore,
|
|
153
153
|
keywordIndex: deps.keywordIndex,
|
|
154
154
|
keywordWeight: deps.cfg.retrieval.hybridSearch?.keywordWeight,
|
|
155
|
-
hybridEnabled:
|
|
155
|
+
hybridEnabled: true,
|
|
156
156
|
queryPrefix: deps.cfg.embedding.queryPrefix,
|
|
157
157
|
filter,
|
|
158
158
|
});
|
|
@@ -185,13 +185,13 @@ export async function lintQuirks(deps) {
|
|
|
185
185
|
const cfg = deps.cfg.memory;
|
|
186
186
|
for (const q of quirks) {
|
|
187
187
|
if (q.confidence < (cfg?.minConfidence ?? 0.5)) {
|
|
188
|
-
issues.push(`Low confidence (${q.confidence}): "${q.content
|
|
188
|
+
issues.push(`Low confidence (${q.confidence}): "${q.content}" [${q.id}]`);
|
|
189
189
|
}
|
|
190
190
|
if (cfg?.decay?.enabled && q.lastObserved) {
|
|
191
191
|
const ageDays = (Date.now() - new Date(q.lastObserved).getTime()) / 86_400_000;
|
|
192
192
|
const halfLife = cfg.decay.halfLifeDays;
|
|
193
193
|
if (halfLife > 0 && ageDays > halfLife * 2) {
|
|
194
|
-
issues.push(`Stale (${Math.round(ageDays)}d old): "${q.content
|
|
194
|
+
issues.push(`Stale (${Math.round(ageDays)}d old): "${q.content}" [${q.id}]`);
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -200,7 +200,7 @@ export async function lintQuirks(deps) {
|
|
|
200
200
|
for (let j = i + 1; j < quirks.length; j++) {
|
|
201
201
|
const sim = lexicalSimilarity(quirks[i].content, quirks[j].content);
|
|
202
202
|
if (sim > 0.85) {
|
|
203
|
-
issues.push(`Near-duplicate (${(sim * 100).toFixed(0)}% similar): "${quirks[i].content
|
|
203
|
+
issues.push(`Near-duplicate (${(sim * 100).toFixed(0)}% similar): "${quirks[i].content}" ↔ "${quirks[j].content}"`);
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
}
|
package/dist/tui.js
CHANGED
|
@@ -345,10 +345,16 @@ function buildSettingCategories(cfg, ro, providers) {
|
|
|
345
345
|
const docModeRo = (ro.documentationMode ?? {});
|
|
346
346
|
const wikiModeCfg = (cfg.wikiMode ?? {});
|
|
347
347
|
const wikiModeRo = (ro.wikiMode ?? {});
|
|
348
|
+
const memoryCfg = (cfg.memory ?? {});
|
|
349
|
+
const memoryRo = (ro.memory ?? {});
|
|
348
350
|
const embeddingCfg = (cfg.embedding ?? {});
|
|
349
351
|
const embeddingRo = (ro.embedding ?? {});
|
|
350
352
|
const tuiCfg = (cfg.tui ?? {});
|
|
351
353
|
const tuiRo = (ro.tui ?? {});
|
|
354
|
+
const indexingCfg = (cfg.indexing ?? {});
|
|
355
|
+
const indexingRo = (ro.indexing ?? {});
|
|
356
|
+
const chunkingCfg = (cfg.chunking ?? {});
|
|
357
|
+
const chunkingRo = (ro.chunking ?? {});
|
|
352
358
|
const modelOptions = providers ? buildModelOptions(providers) : undefined;
|
|
353
359
|
function displayModel(roProvider, roModel, cfgProvider, cfgModel, defaultProvider, defaultModel) {
|
|
354
360
|
const p = roProvider ?? cfgProvider ?? defaultProvider;
|
|
@@ -416,6 +422,31 @@ function buildSettingCategories(cfg, ro, providers) {
|
|
|
416
422
|
},
|
|
417
423
|
],
|
|
418
424
|
},
|
|
425
|
+
{
|
|
426
|
+
id: "chunking",
|
|
427
|
+
label: "Chunking",
|
|
428
|
+
description: "Configure how files are split into chunks",
|
|
429
|
+
entries: [
|
|
430
|
+
{
|
|
431
|
+
path: ["indexing", "chunkOverlap"],
|
|
432
|
+
label: "Chunk overlap (lines)",
|
|
433
|
+
type: "number",
|
|
434
|
+
currentValue: indexingRo.chunkOverlap ?? indexingCfg.chunkOverlap ?? 0,
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
path: ["indexing", "maxSvgSizeBytes"],
|
|
438
|
+
label: "Max SVG size (bytes)",
|
|
439
|
+
type: "number",
|
|
440
|
+
currentValue: indexingRo.maxSvgSizeBytes ?? indexingCfg.maxSvgSizeBytes ?? 1_048_576,
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
path: ["chunking", "nodeTypes"],
|
|
444
|
+
label: "Node types (JSON)",
|
|
445
|
+
type: "json",
|
|
446
|
+
currentValue: chunkingRo.nodeTypes ?? chunkingCfg.nodeTypes ?? {},
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
},
|
|
419
450
|
{
|
|
420
451
|
id: "embedding",
|
|
421
452
|
label: "Embedding",
|
|
@@ -488,6 +519,67 @@ function buildSettingCategories(cfg, ro, providers) {
|
|
|
488
519
|
},
|
|
489
520
|
],
|
|
490
521
|
},
|
|
522
|
+
{
|
|
523
|
+
id: "memory",
|
|
524
|
+
label: "Quirk Memory",
|
|
525
|
+
description: "Configure experiential memory — gotchas, preferences, decisions recalled across sessions",
|
|
526
|
+
entries: [
|
|
527
|
+
{
|
|
528
|
+
path: ["memory", "enabled"],
|
|
529
|
+
label: "Quirk memory enabled",
|
|
530
|
+
type: "boolean",
|
|
531
|
+
currentValue: memoryRo.enabled ?? memoryCfg.enabled ?? true,
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
path: ["memory", "autoInject"],
|
|
535
|
+
label: "Auto-inject quirks",
|
|
536
|
+
type: "boolean",
|
|
537
|
+
currentValue: memoryRo.autoInject ?? memoryCfg.autoInject ?? false,
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
path: ["memory", "recallMinScore"],
|
|
541
|
+
label: "User prompt min score",
|
|
542
|
+
type: "number",
|
|
543
|
+
currentValue: memoryRo.recallMinScore ?? memoryCfg.recallMinScore ?? 0.72,
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
path: ["memory", "autoInjectMinScore"],
|
|
547
|
+
label: "System prompt min score",
|
|
548
|
+
type: "number",
|
|
549
|
+
currentValue: memoryRo.autoInjectMinScore ?? memoryCfg.autoInjectMinScore ?? 0.6,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
path: ["memory", "autoInjectLatencyBudgetMs"],
|
|
553
|
+
label: "Latency budget (ms)",
|
|
554
|
+
type: "number",
|
|
555
|
+
currentValue: memoryRo.autoInjectLatencyBudgetMs ?? memoryCfg.autoInjectLatencyBudgetMs ?? 2000,
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
path: ["memory", "minConfidence"],
|
|
559
|
+
label: "Min confidence",
|
|
560
|
+
type: "number",
|
|
561
|
+
currentValue: memoryRo.minConfidence ?? memoryCfg.minConfidence ?? 0.5,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
path: ["memory", "passiveCapture"],
|
|
565
|
+
label: "Passive capture",
|
|
566
|
+
type: "boolean",
|
|
567
|
+
currentValue: memoryRo.passiveCapture ?? memoryCfg.passiveCapture ?? false,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
path: ["memory", "promptEnforcement"],
|
|
571
|
+
label: "Prompt enforcement",
|
|
572
|
+
type: "boolean",
|
|
573
|
+
currentValue: memoryRo.promptEnforcement ?? memoryCfg.promptEnforcement ?? true,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
path: ["memory", "sessionEndExtraction"],
|
|
577
|
+
label: "Session-end extraction",
|
|
578
|
+
type: "boolean",
|
|
579
|
+
currentValue: memoryRo.sessionEndExtraction ?? memoryCfg.sessionEndExtraction ?? true,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
},
|
|
491
583
|
{
|
|
492
584
|
id: "keybindings",
|
|
493
585
|
label: "Keybindings",
|
|
@@ -564,7 +656,7 @@ async function openSettingsDialog(api) {
|
|
|
564
656
|
function showSettingMenu(cat) {
|
|
565
657
|
const options = [
|
|
566
658
|
...cat.entries.map((s) => ({
|
|
567
|
-
title: `${s.label}: ${s.type === "boolean" ? (s.currentValue ? "Yes" : "No") : String(s.currentValue)}`,
|
|
659
|
+
title: `${s.label}: ${s.type === "boolean" ? (s.currentValue ? "Yes" : "No") : s.type === "json" ? JSON.stringify(s.currentValue) : String(s.currentValue)}`,
|
|
568
660
|
value: s.path.join("."),
|
|
569
661
|
description: s.options ? "Select to open model picker" : (s.type === "boolean" ? "Select to toggle" : "Select to edit"),
|
|
570
662
|
})),
|
|
@@ -594,6 +686,9 @@ async function openSettingsDialog(api) {
|
|
|
594
686
|
title: "Settings",
|
|
595
687
|
message: `${entry.label}: ${newVal ? "Yes" : "No"}`,
|
|
596
688
|
});
|
|
689
|
+
if (entry.path[0] === "indexing" || entry.path[0] === "chunking") {
|
|
690
|
+
api.ui.toast({ variant: "warning", title: "Settings", message: "Chunking changed. Re-index required." });
|
|
691
|
+
}
|
|
597
692
|
entry.currentValue = newVal;
|
|
598
693
|
showSettingMenu(cat);
|
|
599
694
|
}
|
|
@@ -612,6 +707,9 @@ async function openSettingsDialog(api) {
|
|
|
612
707
|
title: "Settings",
|
|
613
708
|
message: `${entry.label}: ${num}`,
|
|
614
709
|
});
|
|
710
|
+
if (entry.path[0] === "indexing" || entry.path[0] === "chunking") {
|
|
711
|
+
api.ui.toast({ variant: "warning", title: "Settings", message: "Chunking changed. Re-index required." });
|
|
712
|
+
}
|
|
615
713
|
entry.currentValue = num;
|
|
616
714
|
}
|
|
617
715
|
showSettingMenu(cat);
|
|
@@ -621,6 +719,41 @@ async function openSettingsDialog(api) {
|
|
|
621
719
|
},
|
|
622
720
|
}));
|
|
623
721
|
}
|
|
722
|
+
else if (entry.type === "json") {
|
|
723
|
+
api.ui.dialog.replace(() => api.ui.DialogPrompt({
|
|
724
|
+
title: `Edit ${entry.label}`,
|
|
725
|
+
placeholder: "JSON",
|
|
726
|
+
value: JSON.stringify(entry.currentValue, null, 2),
|
|
727
|
+
onConfirm: (input) => {
|
|
728
|
+
try {
|
|
729
|
+
const parsed = JSON.parse(input);
|
|
730
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
731
|
+
api.ui.toast({ variant: "error", title: "Settings", message: "Value must be a JSON object" });
|
|
732
|
+
showSettingMenu(cat);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
saveRuntimeOverride(storePath, entry.path, parsed);
|
|
736
|
+
saveConfigValue(configPath, entry.path, parsed);
|
|
737
|
+
api.ui.toast({
|
|
738
|
+
variant: "success",
|
|
739
|
+
title: "Settings",
|
|
740
|
+
message: `${entry.label}: updated`,
|
|
741
|
+
});
|
|
742
|
+
if (entry.path[0] === "indexing" || entry.path[0] === "chunking") {
|
|
743
|
+
api.ui.toast({ variant: "warning", title: "Settings", message: "Chunking changed. Re-index required." });
|
|
744
|
+
}
|
|
745
|
+
entry.currentValue = parsed;
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
api.ui.toast({ variant: "error", title: "Settings", message: "Invalid JSON" });
|
|
749
|
+
}
|
|
750
|
+
showSettingMenu(cat);
|
|
751
|
+
},
|
|
752
|
+
onCancel: () => {
|
|
753
|
+
showSettingMenu(cat);
|
|
754
|
+
},
|
|
755
|
+
}));
|
|
756
|
+
}
|
|
624
757
|
else {
|
|
625
758
|
api.ui.dialog.replace(() => api.ui.DialogPrompt({
|
|
626
759
|
title: `Edit ${entry.label}`,
|