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 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 scored = cached.records
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
- const score = params.vectorWeight * vectorScore + params.bm25Weight * bm25Score;
113
- return { record, score, vectorScore, bm25Score };
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lancedb-opencode-pro",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "LanceDB-backed long-term memory provider for OpenCode",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",