lancedb-opencode-pro 0.1.3 → 0.1.4
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 +35 -3
- package/dist/config.js +8 -0
- package/dist/index.js +8 -0
- package/dist/store.d.ts +4 -0
- package/dist/store.js +78 -5
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,11 @@ If you already use other plugins, keep them and append `"lancedb-opencode-pro"`.
|
|
|
41
41
|
"mode": "hybrid",
|
|
42
42
|
"vectorWeight": 0.7,
|
|
43
43
|
"bm25Weight": 0.3,
|
|
44
|
-
"minScore": 0.2
|
|
44
|
+
"minScore": 0.2,
|
|
45
|
+
"rrfK": 60,
|
|
46
|
+
"recencyBoost": true,
|
|
47
|
+
"recencyHalfLifeHours": 72,
|
|
48
|
+
"importanceWeight": 0.4
|
|
45
49
|
},
|
|
46
50
|
"includeGlobalScope": true,
|
|
47
51
|
"minCaptureChars": 80,
|
|
@@ -173,7 +177,11 @@ Create `~/.config/opencode/lancedb-opencode-pro.json`:
|
|
|
173
177
|
"mode": "hybrid",
|
|
174
178
|
"vectorWeight": 0.7,
|
|
175
179
|
"bm25Weight": 0.3,
|
|
176
|
-
"minScore": 0.2
|
|
180
|
+
"minScore": 0.2,
|
|
181
|
+
"rrfK": 60,
|
|
182
|
+
"recencyBoost": true,
|
|
183
|
+
"recencyHalfLifeHours": 72,
|
|
184
|
+
"importanceWeight": 0.4
|
|
177
185
|
},
|
|
178
186
|
"includeGlobalScope": true,
|
|
179
187
|
"minCaptureChars": 80,
|
|
@@ -216,6 +224,10 @@ Supported environment variables:
|
|
|
216
224
|
- `LANCEDB_OPENCODE_PRO_VECTOR_WEIGHT`
|
|
217
225
|
- `LANCEDB_OPENCODE_PRO_BM25_WEIGHT`
|
|
218
226
|
- `LANCEDB_OPENCODE_PRO_MIN_SCORE`
|
|
227
|
+
- `LANCEDB_OPENCODE_PRO_RRF_K`
|
|
228
|
+
- `LANCEDB_OPENCODE_PRO_RECENCY_BOOST`
|
|
229
|
+
- `LANCEDB_OPENCODE_PRO_RECENCY_HALF_LIFE_HOURS`
|
|
230
|
+
- `LANCEDB_OPENCODE_PRO_IMPORTANCE_WEIGHT`
|
|
219
231
|
- `LANCEDB_OPENCODE_PRO_INCLUDE_GLOBAL_SCOPE`
|
|
220
232
|
- `LANCEDB_OPENCODE_PRO_MIN_CAPTURE_CHARS`
|
|
221
233
|
- `LANCEDB_OPENCODE_PRO_MAX_ENTRIES_PER_SCOPE`
|
|
@@ -298,6 +310,22 @@ Key fields:
|
|
|
298
310
|
- `feedback.falsePositiveRate`: wrong-memory reports divided by stored memories.
|
|
299
311
|
- `feedback.falseNegativeRate`: missing-memory reports relative to capture attempts.
|
|
300
312
|
|
|
313
|
+
### Interpreting Low-Feedback Results
|
|
314
|
+
|
|
315
|
+
In real OpenCode usage, auto-capture and recall happen in the background, so explicit `memory_feedback_*` events are often sparse.
|
|
316
|
+
|
|
317
|
+
- Treat `capture.*` and `recall.*` as system-health metrics: they show whether the memory pipeline is running.
|
|
318
|
+
- Treat repeated-context reduction, clarification burden, manual memory rescue, correction signals, and sampled audits as product-value signals: they show whether memory actually helped the user.
|
|
319
|
+
- Treat `feedback.* = 0` as insufficient evidence, not proof that memory quality is good.
|
|
320
|
+
- Treat a high `recall.hitRate` or `recall.injectionRate` as recall availability only; those values do not prove usefulness by themselves.
|
|
321
|
+
|
|
322
|
+
Recommended review order in low-feedback environments:
|
|
323
|
+
|
|
324
|
+
1. Check `capture.successRate`, `capture.skipReasons`, `recall.hitRate`, and `recall.injectionRate` for operational health.
|
|
325
|
+
2. Review whether users repeated background context less often or needed fewer clarification turns.
|
|
326
|
+
3. Check whether users still needed manual rescue through `memory_search` or issued correction-like responses.
|
|
327
|
+
4. Run a bounded audit of recalled memories or skipped captures before concluding the system is helping.
|
|
328
|
+
|
|
301
329
|
## OpenAI Embedding Configuration
|
|
302
330
|
|
|
303
331
|
Default behavior stays on Ollama. To use OpenAI embeddings, set `embedding.provider` to `openai` and provide API key + model.
|
|
@@ -318,7 +346,11 @@ Example sidecar:
|
|
|
318
346
|
"mode": "hybrid",
|
|
319
347
|
"vectorWeight": 0.7,
|
|
320
348
|
"bm25Weight": 0.3,
|
|
321
|
-
"minScore": 0.2
|
|
349
|
+
"minScore": 0.2,
|
|
350
|
+
"rrfK": 60,
|
|
351
|
+
"recencyBoost": true,
|
|
352
|
+
"recencyHalfLifeHours": 72,
|
|
353
|
+
"importanceWeight": 0.4
|
|
322
354
|
},
|
|
323
355
|
"includeGlobalScope": true,
|
|
324
356
|
"minCaptureChars": 80,
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,10 @@ export function resolveMemoryConfig(config, worktree) {
|
|
|
20
20
|
const weightSum = vectorWeight + bm25Weight;
|
|
21
21
|
const normalizedVectorWeight = weightSum > 0 ? vectorWeight / weightSum : 0.7;
|
|
22
22
|
const normalizedBm25Weight = weightSum > 0 ? bm25Weight / weightSum : 0.3;
|
|
23
|
+
const rrfK = Math.max(1, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_RRF_K ?? retrievalRaw.rrfK, 60)));
|
|
24
|
+
const recencyBoost = toBoolean(process.env.LANCEDB_OPENCODE_PRO_RECENCY_BOOST ?? retrievalRaw.recencyBoost, true);
|
|
25
|
+
const recencyHalfLifeHours = Math.max(1, toNumber(process.env.LANCEDB_OPENCODE_PRO_RECENCY_HALF_LIFE_HOURS ?? retrievalRaw.recencyHalfLifeHours, 72));
|
|
26
|
+
const importanceWeight = clamp(toNumber(process.env.LANCEDB_OPENCODE_PRO_IMPORTANCE_WEIGHT ?? retrievalRaw.importanceWeight, 0.4), 0, 2);
|
|
23
27
|
const embeddingProvider = resolveEmbeddingProvider(firstString(process.env.LANCEDB_OPENCODE_PRO_EMBEDDING_PROVIDER, embeddingRaw.provider));
|
|
24
28
|
const embeddingModel = embeddingProvider === "openai"
|
|
25
29
|
? firstString(process.env.LANCEDB_OPENCODE_PRO_OPENAI_MODEL, process.env.LANCEDB_OPENCODE_PRO_EMBEDDING_MODEL, embeddingRaw.model)
|
|
@@ -49,6 +53,10 @@ export function resolveMemoryConfig(config, worktree) {
|
|
|
49
53
|
vectorWeight: normalizedVectorWeight,
|
|
50
54
|
bm25Weight: normalizedBm25Weight,
|
|
51
55
|
minScore: clamp(toNumber(process.env.LANCEDB_OPENCODE_PRO_MIN_SCORE ?? retrievalRaw.minScore, 0.2), 0, 1),
|
|
56
|
+
rrfK,
|
|
57
|
+
recencyBoost,
|
|
58
|
+
recencyHalfLifeHours,
|
|
59
|
+
importanceWeight,
|
|
52
60
|
},
|
|
53
61
|
includeGlobalScope: toBoolean(process.env.LANCEDB_OPENCODE_PRO_INCLUDE_GLOBAL_SCOPE ?? raw.includeGlobalScope, true),
|
|
54
62
|
minCaptureChars: Math.max(30, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_MIN_CAPTURE_CHARS ?? raw.minCaptureChars, 80))),
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,10 @@ const plugin = async (input) => {
|
|
|
56
56
|
vectorWeight: state.config.retrieval.mode === "vector" ? 1 : state.config.retrieval.vectorWeight,
|
|
57
57
|
bm25Weight: state.config.retrieval.mode === "vector" ? 0 : state.config.retrieval.bm25Weight,
|
|
58
58
|
minScore: state.config.retrieval.minScore,
|
|
59
|
+
rrfK: state.config.retrieval.rrfK,
|
|
60
|
+
recencyBoost: state.config.retrieval.recencyBoost,
|
|
61
|
+
recencyHalfLifeHours: state.config.retrieval.recencyHalfLifeHours,
|
|
62
|
+
importanceWeight: state.config.retrieval.importanceWeight,
|
|
59
63
|
});
|
|
60
64
|
await state.store.putEvent({
|
|
61
65
|
id: generateId(),
|
|
@@ -108,6 +112,10 @@ const plugin = async (input) => {
|
|
|
108
112
|
vectorWeight: state.config.retrieval.mode === "vector" ? 1 : state.config.retrieval.vectorWeight,
|
|
109
113
|
bm25Weight: state.config.retrieval.mode === "vector" ? 0 : state.config.retrieval.bm25Weight,
|
|
110
114
|
minScore: state.config.retrieval.minScore,
|
|
115
|
+
rrfK: state.config.retrieval.rrfK,
|
|
116
|
+
recencyBoost: state.config.retrieval.recencyBoost,
|
|
117
|
+
recencyHalfLifeHours: state.config.retrieval.recencyHalfLifeHours,
|
|
118
|
+
importanceWeight: state.config.retrieval.importanceWeight,
|
|
111
119
|
});
|
|
112
120
|
if (results.length === 0)
|
|
113
121
|
return "No relevant memory found.";
|
package/dist/store.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export declare class MemoryStore {
|
|
|
19
19
|
vectorWeight: number;
|
|
20
20
|
bm25Weight: number;
|
|
21
21
|
minScore: number;
|
|
22
|
+
rrfK?: number;
|
|
23
|
+
recencyBoost?: boolean;
|
|
24
|
+
recencyHalfLifeHours?: number;
|
|
25
|
+
importanceWeight?: number;
|
|
22
26
|
}): Promise<SearchResult[]>;
|
|
23
27
|
deleteById(id: string, scopes: string[]): Promise<boolean>;
|
|
24
28
|
clearScope(scope: string): Promise<number>;
|
package/dist/store.js
CHANGED
|
@@ -103,14 +103,50 @@ export class MemoryStore {
|
|
|
103
103
|
return [];
|
|
104
104
|
const queryTokens = tokenize(params.query);
|
|
105
105
|
const queryNorm = vecNorm(params.queryVector);
|
|
106
|
-
const
|
|
106
|
+
const useVectorChannel = params.queryVector.length > 0 && params.vectorWeight > 0;
|
|
107
|
+
const useBm25Channel = queryTokens.length > 0 && params.bm25Weight > 0;
|
|
108
|
+
const { vectorWeight, bm25Weight } = normalizeChannelWeights(useVectorChannel ? params.vectorWeight : 0, useBm25Channel ? params.bm25Weight : 0);
|
|
109
|
+
const rrfK = Math.max(1, Math.floor(params.rrfK ?? 60));
|
|
110
|
+
const recencyBoostEnabled = params.recencyBoost ?? true;
|
|
111
|
+
const recencyHalfLifeHours = Math.max(1, params.recencyHalfLifeHours ?? 72);
|
|
112
|
+
const importanceWeight = clampImportanceWeight(params.importanceWeight ?? 0.4);
|
|
113
|
+
const candidates = cached.records
|
|
107
114
|
.filter((record) => params.queryVector.length === 0 || record.vector.length === params.queryVector.length)
|
|
108
115
|
.map((record, index) => {
|
|
109
116
|
const recordNorm = cached.norms.get(record.id) ?? vecNorm(record.vector);
|
|
110
|
-
const vectorScore = fastCosine(params.queryVector, record.vector, queryNorm, recordNorm);
|
|
111
|
-
const bm25Score = bm25LikeScore(queryTokens, cached.tokenized[index], cached.idf);
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
const vectorScore = useVectorChannel ? fastCosine(params.queryVector, record.vector, queryNorm, recordNorm) : 0;
|
|
118
|
+
const bm25Score = useBm25Channel ? bm25LikeScore(queryTokens, cached.tokenized[index], cached.idf) : 0;
|
|
119
|
+
return { record, vectorScore, bm25Score };
|
|
120
|
+
});
|
|
121
|
+
if (candidates.length === 0)
|
|
122
|
+
return [];
|
|
123
|
+
const vectorRanks = useVectorChannel ? buildRankMap(candidates, (item) => item.vectorScore) : null;
|
|
124
|
+
const bm25Ranks = useBm25Channel ? buildRankMap(candidates, (item) => item.bm25Score) : null;
|
|
125
|
+
const scored = candidates
|
|
126
|
+
.map((item) => {
|
|
127
|
+
let rrfScore = 0;
|
|
128
|
+
if (vectorRanks) {
|
|
129
|
+
const rank = vectorRanks.get(item.record.id);
|
|
130
|
+
if (rank !== undefined)
|
|
131
|
+
rrfScore += vectorWeight / (rrfK + rank);
|
|
132
|
+
}
|
|
133
|
+
if (bm25Ranks) {
|
|
134
|
+
const rank = bm25Ranks.get(item.record.id);
|
|
135
|
+
if (rank !== undefined)
|
|
136
|
+
rrfScore += bm25Weight / (rrfK + rank);
|
|
137
|
+
}
|
|
138
|
+
rrfScore *= rrfK + 1;
|
|
139
|
+
const recencyFactor = recencyBoostEnabled
|
|
140
|
+
? computeRecencyMultiplier(item.record.timestamp, recencyHalfLifeHours)
|
|
141
|
+
: 1;
|
|
142
|
+
const importanceFactor = 1 + importanceWeight * clampImportance(item.record.importance);
|
|
143
|
+
const score = rrfScore * recencyFactor * importanceFactor;
|
|
144
|
+
return {
|
|
145
|
+
record: item.record,
|
|
146
|
+
score,
|
|
147
|
+
vectorScore: item.vectorScore,
|
|
148
|
+
bm25Score: item.bm25Score,
|
|
149
|
+
};
|
|
114
150
|
})
|
|
115
151
|
.filter((item) => item.score >= params.minScore)
|
|
116
152
|
.sort((a, b) => b.score - a.score)
|
|
@@ -445,6 +481,43 @@ function normalizeEventRow(row) {
|
|
|
445
481
|
function escapeSql(value) {
|
|
446
482
|
return value.replace(/'/g, "''");
|
|
447
483
|
}
|
|
484
|
+
function buildRankMap(items, scoreOf) {
|
|
485
|
+
const ranked = [...items].sort((a, b) => scoreOf(b) - scoreOf(a));
|
|
486
|
+
const ranks = new Map();
|
|
487
|
+
for (let i = 0; i < ranked.length; i += 1) {
|
|
488
|
+
ranks.set(ranked[i].record.id, i + 1);
|
|
489
|
+
}
|
|
490
|
+
return ranks;
|
|
491
|
+
}
|
|
492
|
+
function normalizeChannelWeights(vectorWeight, bm25Weight) {
|
|
493
|
+
const sum = vectorWeight + bm25Weight;
|
|
494
|
+
if (sum <= 0) {
|
|
495
|
+
return { vectorWeight: 0.5, bm25Weight: 0.5 };
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
vectorWeight: vectorWeight / sum,
|
|
499
|
+
bm25Weight: bm25Weight / sum,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function computeRecencyMultiplier(timestamp, halfLifeHours) {
|
|
503
|
+
const now = Date.now();
|
|
504
|
+
const ageMs = Math.max(0, now - timestamp);
|
|
505
|
+
const ageHours = ageMs / 3_600_000;
|
|
506
|
+
if (ageHours === 0)
|
|
507
|
+
return 1;
|
|
508
|
+
const decay = Math.pow(0.5, ageHours / halfLifeHours);
|
|
509
|
+
return 0.5 + 0.5 * decay;
|
|
510
|
+
}
|
|
511
|
+
function clampImportance(value) {
|
|
512
|
+
if (!Number.isFinite(value))
|
|
513
|
+
return 0;
|
|
514
|
+
return Math.max(0, Math.min(1, value));
|
|
515
|
+
}
|
|
516
|
+
function clampImportanceWeight(value) {
|
|
517
|
+
if (!Number.isFinite(value))
|
|
518
|
+
return 0.4;
|
|
519
|
+
return Math.max(0, Math.min(2, value));
|
|
520
|
+
}
|
|
448
521
|
function computeIdf(docs) {
|
|
449
522
|
const df = new Map();
|
|
450
523
|
for (const doc of docs) {
|
package/dist/types.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface RetrievalConfig {
|
|
|
16
16
|
vectorWeight: number;
|
|
17
17
|
bm25Weight: number;
|
|
18
18
|
minScore: number;
|
|
19
|
+
rrfK: number;
|
|
20
|
+
recencyBoost: boolean;
|
|
21
|
+
recencyHalfLifeHours: number;
|
|
22
|
+
importanceWeight: number;
|
|
19
23
|
}
|
|
20
24
|
export interface MemoryRuntimeConfig {
|
|
21
25
|
provider: string;
|