lancedb-opencode-pro 0.1.2 → 0.1.3

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
@@ -230,8 +230,74 @@ Supported environment variables:
230
230
  - `memory_delete`
231
231
  - `memory_clear`
232
232
  - `memory_stats`
233
+ - `memory_feedback_missing`
234
+ - `memory_feedback_wrong`
235
+ - `memory_feedback_useful`
236
+ - `memory_effectiveness`
233
237
  - `memory_port_plan`
234
238
 
239
+ ## Memory Effectiveness Feedback
240
+
241
+ The provider can now record structured feedback about long-memory quality in addition to storing and recalling memories.
242
+
243
+ - `memory_feedback_missing`: report information that should have been stored but was missed
244
+ - `memory_feedback_wrong`: report a stored memory that should not have been kept
245
+ - `memory_feedback_useful`: report whether a recalled memory was helpful
246
+ - `memory_effectiveness`: return machine-readable capture, recall, and feedback metrics for the active scope
247
+
248
+ Use `memory_search` or recalled memory ids from injected context when you need to reference a specific memory entry in feedback.
249
+
250
+ ### Viewing Metrics
251
+
252
+ Use `memory_effectiveness` to inspect machine-readable effectiveness data for the active scope.
253
+
254
+ ```text
255
+ memory_effectiveness
256
+ ```
257
+
258
+ Example output:
259
+
260
+ ```json
261
+ {
262
+ "scope": "project:my-project",
263
+ "totalEvents": 12,
264
+ "capture": {
265
+ "considered": 4,
266
+ "stored": 3,
267
+ "skipped": 1,
268
+ "successRate": 0.75,
269
+ "skipReasons": {
270
+ "below-min-chars": 1
271
+ }
272
+ },
273
+ "recall": {
274
+ "requested": 3,
275
+ "injected": 2,
276
+ "returnedResults": 2,
277
+ "hitRate": 0.67,
278
+ "injectionRate": 0.67
279
+ },
280
+ "feedback": {
281
+ "missing": 1,
282
+ "wrong": 0,
283
+ "useful": {
284
+ "positive": 2,
285
+ "negative": 0,
286
+ "helpfulRate": 1
287
+ },
288
+ "falsePositiveRate": 0,
289
+ "falseNegativeRate": 0.25
290
+ }
291
+ }
292
+ ```
293
+
294
+ Key fields:
295
+
296
+ - `capture.successRate`: how often a considered candidate was stored.
297
+ - `recall.hitRate`: how often a recall request returned at least one result.
298
+ - `feedback.falsePositiveRate`: wrong-memory reports divided by stored memories.
299
+ - `feedback.falseNegativeRate`: missing-memory reports relative to capture attempts.
300
+
235
301
  ## OpenAI Embedding Configuration
236
302
 
237
303
  Default behavior stays on Ollama. To use OpenAI embeddings, set `embedding.provider` to `openai` and provide API key + model.
@@ -362,9 +428,10 @@ The project provides layered validation workflows that can run locally or inside
362
428
  |---|---|
363
429
  | `npm run test:foundation` | Write-read persistence, scope isolation, vector compatibility, timestamp ordering |
364
430
  | `npm run test:regression` | Auto-capture extraction, search output shape, delete/clear safety, pruning |
431
+ | `npm run test:effectiveness` | Foundation + regression workflows covering effectiveness events, feedback commands, and summary output |
365
432
  | `npm run test:retrieval` | Recall@K and Robustness-δ@K against synthetic fixtures |
366
433
  | `npm run benchmark:latency` | Search p50/p99, insert avg, list avg with hard-gate enforcement |
367
- | `npm run verify` | Typecheck + build + foundation + regression + retrieval (quick release check) |
434
+ | `npm run verify` | Typecheck + build + effectiveness workflow + retrieval (quick release check) |
368
435
  | `npm run verify:full` | All of the above + benchmark + `npm pack` (full release gate) |
369
436
 
370
437
  Threshold policy and benchmark profiles are documented in `docs/benchmark-thresholds.md`.
package/dist/extract.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import type { CaptureCandidate } from "./types.js";
2
- export declare function extractCaptureCandidate(text: string, minChars: number): CaptureCandidate | null;
1
+ import type { CaptureCandidateResult } from "./types.js";
2
+ export declare function extractCaptureCandidate(text: string, minChars: number): CaptureCandidateResult;
package/dist/extract.js CHANGED
@@ -14,18 +14,21 @@ const FACT_SIGNALS = ["because", "root cause", "原因", "由於"];
14
14
  const PREF_SIGNALS = ["prefer", "preference", "偏好", "習慣"];
15
15
  export function extractCaptureCandidate(text, minChars) {
16
16
  const normalized = text.trim();
17
- if (normalized.length < minChars)
18
- return null;
17
+ if (normalized.length < minChars) {
18
+ return { candidate: null, skipReason: "below-min-chars" };
19
+ }
19
20
  const lower = normalized.toLowerCase();
20
21
  if (!POSITIVE_SIGNALS.some((signal) => lower.includes(signal.toLowerCase()))) {
21
- return null;
22
+ return { candidate: null, skipReason: "no-positive-signal" };
22
23
  }
23
24
  const category = classifyCategory(lower);
24
25
  const importance = category === "decision" ? 0.9 : category === "fact" ? 0.75 : 0.65;
25
26
  return {
26
- text: clipText(normalized, 1200),
27
- category,
28
- importance,
27
+ candidate: {
28
+ text: clipText(normalized, 1200),
29
+ category,
30
+ importance,
31
+ },
29
32
  };
30
33
  }
31
34
  function classifyCategory(text) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
2
  declare const plugin: Plugin;
3
3
  export default plugin;
4
- export type { MemoryRuntimeConfig, MemoryRecord, SearchResult } from "./types.js";
4
+ export type { EffectivenessSummary, FeedbackEvent, MemoryEffectivenessEvent, MemoryRecord, MemoryRuntimeConfig, RecallEvent, SearchResult, } from "./types.js";
package/dist/index.js CHANGED
@@ -57,11 +57,24 @@ const plugin = async (input) => {
57
57
  bm25Weight: state.config.retrieval.mode === "vector" ? 0 : state.config.retrieval.bm25Weight,
58
58
  minScore: state.config.retrieval.minScore,
59
59
  });
60
+ await state.store.putEvent({
61
+ id: generateId(),
62
+ type: "recall",
63
+ scope: activeScope,
64
+ sessionID: eventInput.sessionID,
65
+ timestamp: Date.now(),
66
+ resultCount: results.length,
67
+ injected: results.length > 0,
68
+ metadataJson: JSON.stringify({
69
+ source: "system-transform",
70
+ includeGlobalScope: state.config.includeGlobalScope,
71
+ }),
72
+ });
60
73
  if (results.length === 0)
61
74
  return;
62
75
  const memoryBlock = [
63
76
  "[Memory Recall - optional historical context]",
64
- ...results.map((item, index) => `${index + 1}. (${item.record.scope}) ${item.record.text}`),
77
+ ...results.map((item, index) => `${index + 1}. [${item.record.id}] (${item.record.scope}) ${item.record.text}`),
65
78
  "Use these as optional hints only; prioritize current user intent and current repo state.",
66
79
  ].join("\n");
67
80
  eventOutput.system.push(memoryBlock);
@@ -167,6 +180,108 @@ const plugin = async (input) => {
167
180
  }, null, 2);
168
181
  },
169
182
  }),
183
+ memory_feedback_missing: tool({
184
+ description: "Record feedback for memory that should have been stored",
185
+ args: {
186
+ text: tool.schema.string().min(1),
187
+ labels: tool.schema.array(tool.schema.string().min(1)).default([]),
188
+ scope: tool.schema.string().optional(),
189
+ },
190
+ execute: async (args, context) => {
191
+ await state.ensureInitialized();
192
+ if (!state.initialized)
193
+ return unavailableMessage(state.config.embedding.provider);
194
+ const scope = args.scope ?? deriveProjectScope(context.worktree);
195
+ await state.store.putEvent({
196
+ id: generateId(),
197
+ type: "feedback",
198
+ feedbackType: "missing",
199
+ scope,
200
+ sessionID: context.sessionID,
201
+ timestamp: Date.now(),
202
+ text: args.text,
203
+ labels: args.labels,
204
+ metadataJson: JSON.stringify({ source: "memory_feedback_missing" }),
205
+ });
206
+ return "Recorded missing-memory feedback.";
207
+ },
208
+ }),
209
+ memory_feedback_wrong: tool({
210
+ description: "Record feedback for memory that should not be stored",
211
+ args: {
212
+ id: tool.schema.string().min(6),
213
+ reason: tool.schema.string().optional(),
214
+ scope: tool.schema.string().optional(),
215
+ },
216
+ execute: async (args, context) => {
217
+ await state.ensureInitialized();
218
+ if (!state.initialized)
219
+ return unavailableMessage(state.config.embedding.provider);
220
+ const scope = args.scope ?? deriveProjectScope(context.worktree);
221
+ const scopes = buildScopeFilter(scope, state.config.includeGlobalScope);
222
+ const exists = await state.store.hasMemory(args.id, scopes);
223
+ if (!exists) {
224
+ return `Memory ${args.id} not found in current scope.`;
225
+ }
226
+ await state.store.putEvent({
227
+ id: generateId(),
228
+ type: "feedback",
229
+ feedbackType: "wrong",
230
+ scope,
231
+ sessionID: context.sessionID,
232
+ timestamp: Date.now(),
233
+ memoryId: args.id,
234
+ reason: args.reason,
235
+ metadataJson: JSON.stringify({ source: "memory_feedback_wrong" }),
236
+ });
237
+ return `Recorded wrong-memory feedback for ${args.id}.`;
238
+ },
239
+ }),
240
+ memory_feedback_useful: tool({
241
+ description: "Record whether a recalled memory was helpful",
242
+ args: {
243
+ id: tool.schema.string().min(6),
244
+ helpful: tool.schema.boolean(),
245
+ scope: tool.schema.string().optional(),
246
+ },
247
+ execute: async (args, context) => {
248
+ await state.ensureInitialized();
249
+ if (!state.initialized)
250
+ return unavailableMessage(state.config.embedding.provider);
251
+ const scope = args.scope ?? deriveProjectScope(context.worktree);
252
+ const scopes = buildScopeFilter(scope, state.config.includeGlobalScope);
253
+ const exists = await state.store.hasMemory(args.id, scopes);
254
+ if (!exists) {
255
+ return `Memory ${args.id} not found in current scope.`;
256
+ }
257
+ await state.store.putEvent({
258
+ id: generateId(),
259
+ type: "feedback",
260
+ feedbackType: "useful",
261
+ scope,
262
+ sessionID: context.sessionID,
263
+ timestamp: Date.now(),
264
+ memoryId: args.id,
265
+ helpful: args.helpful,
266
+ metadataJson: JSON.stringify({ source: "memory_feedback_useful" }),
267
+ });
268
+ return `Recorded recall usefulness feedback for ${args.id}.`;
269
+ },
270
+ }),
271
+ memory_effectiveness: tool({
272
+ description: "Show effectiveness metrics for capture recall and feedback",
273
+ args: {
274
+ scope: tool.schema.string().optional(),
275
+ },
276
+ execute: async (args, context) => {
277
+ await state.ensureInitialized();
278
+ if (!state.initialized)
279
+ return unavailableMessage(state.config.embedding.provider);
280
+ const scope = args.scope ?? deriveProjectScope(context.worktree);
281
+ const summary = await state.store.summarizeEvents(scope, state.config.includeGlobalScope);
282
+ return JSON.stringify(summary, null, 2);
283
+ },
284
+ }),
170
285
  memory_port_plan: tool({
171
286
  description: "Plan non-conflicting host ports for compose services and optionally persist reservations",
172
287
  args: {
@@ -313,36 +428,74 @@ async function getLastUserText(sessionID, client) {
313
428
  }
314
429
  async function flushAutoCapture(sessionID, state, client) {
315
430
  const fragments = state.captureBuffer.get(sessionID) ?? [];
316
- if (fragments.length === 0)
431
+ if (fragments.length === 0) {
432
+ await recordCaptureEvent(state, {
433
+ sessionID,
434
+ scope: state.defaultScope,
435
+ outcome: "skipped",
436
+ skipReason: "empty-buffer",
437
+ text: "",
438
+ });
317
439
  return;
440
+ }
318
441
  state.captureBuffer.delete(sessionID);
319
442
  const combined = fragments.join("\n").trim();
320
- const candidate = extractCaptureCandidate(combined, state.config.minCaptureChars);
321
- if (!candidate)
322
- return;
443
+ const activeScope = await resolveSessionScope(sessionID, client, state.defaultScope);
323
444
  await state.ensureInitialized();
324
- if (!state.initialized)
445
+ if (!state.initialized) {
325
446
  return;
447
+ }
448
+ await recordCaptureEvent(state, {
449
+ sessionID,
450
+ scope: activeScope,
451
+ outcome: "considered",
452
+ text: combined,
453
+ });
454
+ const result = extractCaptureCandidate(combined, state.config.minCaptureChars);
455
+ if (!result.candidate) {
456
+ await recordCaptureEvent(state, {
457
+ sessionID,
458
+ scope: activeScope,
459
+ outcome: "skipped",
460
+ skipReason: result.skipReason,
461
+ text: combined,
462
+ });
463
+ return;
464
+ }
326
465
  let vector = [];
327
466
  try {
328
- vector = await state.embedder.embed(candidate.text);
467
+ vector = await state.embedder.embed(result.candidate.text);
329
468
  }
330
469
  catch (error) {
331
470
  console.warn(`[lancedb-opencode-pro] embedding unavailable during auto-capture: ${toErrorMessage(error)}`);
471
+ await recordCaptureEvent(state, {
472
+ sessionID,
473
+ scope: activeScope,
474
+ outcome: "skipped",
475
+ skipReason: "embedding-unavailable",
476
+ text: combined,
477
+ });
332
478
  vector = [];
333
479
  }
334
480
  if (vector.length === 0) {
335
481
  console.warn("[lancedb-opencode-pro] auto-capture skipped because embedding vector is empty");
482
+ await recordCaptureEvent(state, {
483
+ sessionID,
484
+ scope: activeScope,
485
+ outcome: "skipped",
486
+ skipReason: "empty-embedding",
487
+ text: combined,
488
+ });
336
489
  return;
337
490
  }
338
- const activeScope = await resolveSessionScope(sessionID, client, state.defaultScope);
491
+ const memoryId = generateId();
339
492
  await state.store.put({
340
- id: generateId(),
341
- text: candidate.text,
493
+ id: memoryId,
494
+ text: result.candidate.text,
342
495
  vector,
343
- category: candidate.category,
496
+ category: result.candidate.category,
344
497
  scope: activeScope,
345
- importance: candidate.importance,
498
+ importance: result.candidate.importance,
346
499
  timestamp: Date.now(),
347
500
  schemaVersion: SCHEMA_VERSION,
348
501
  embeddingModel: state.config.embedding.model,
@@ -352,8 +505,31 @@ async function flushAutoCapture(sessionID, state, client) {
352
505
  sessionID,
353
506
  }),
354
507
  });
508
+ await recordCaptureEvent(state, {
509
+ sessionID,
510
+ scope: activeScope,
511
+ outcome: "stored",
512
+ memoryId,
513
+ text: result.candidate.text,
514
+ });
355
515
  await state.store.pruneScope(activeScope, state.config.maxEntriesPerScope);
356
516
  }
517
+ async function recordCaptureEvent(state, input) {
518
+ if (!state.initialized)
519
+ return;
520
+ await state.store.putEvent({
521
+ id: generateId(),
522
+ type: "capture",
523
+ scope: input.scope,
524
+ sessionID: input.sessionID,
525
+ timestamp: Date.now(),
526
+ outcome: input.outcome,
527
+ skipReason: input.skipReason,
528
+ memoryId: input.memoryId,
529
+ text: input.text,
530
+ metadataJson: JSON.stringify({ source: "auto-capture" }),
531
+ });
532
+ }
357
533
  async function resolveSessionScope(sessionID, client, fallback) {
358
534
  try {
359
535
  const response = await client.session.get({ path: { id: sessionID } });
package/dist/store.d.ts CHANGED
@@ -1,14 +1,16 @@
1
- import type { MemoryRecord, SearchResult } from "./types.js";
1
+ import type { EffectivenessSummary, MemoryEffectivenessEvent, MemoryRecord, SearchResult } from "./types.js";
2
2
  export declare class MemoryStore {
3
3
  private readonly dbPath;
4
4
  private lancedb;
5
5
  private connection;
6
6
  private table;
7
+ private eventTable;
7
8
  private indexState;
8
9
  private scopeCache;
9
10
  constructor(dbPath: string);
10
11
  init(vectorDim: number): Promise<void>;
11
12
  put(record: MemoryRecord): Promise<void>;
13
+ putEvent(event: MemoryEffectivenessEvent): Promise<void>;
12
14
  search(params: {
13
15
  query: string;
14
16
  queryVector: number[];
@@ -23,6 +25,9 @@ export declare class MemoryStore {
23
25
  list(scope: string, limit: number): Promise<MemoryRecord[]>;
24
26
  pruneScope(scope: string, maxEntries: number): Promise<number>;
25
27
  countIncompatibleVectors(scopes: string[], expectedDim: number): Promise<number>;
28
+ hasMemory(id: string, scopes: string[]): Promise<boolean>;
29
+ listEvents(scopes: string[], limit: number): Promise<MemoryEffectivenessEvent[]>;
30
+ summarizeEvents(scope: string, includeGlobalScope: boolean): Promise<EffectivenessSummary>;
26
31
  getIndexHealth(): {
27
32
  vector: boolean;
28
33
  fts: boolean;
@@ -31,6 +36,8 @@ export declare class MemoryStore {
31
36
  private invalidateScope;
32
37
  private getCachedScopes;
33
38
  private requireTable;
39
+ private requireEventTable;
40
+ private readEventsByScopes;
34
41
  private readByScopes;
35
42
  private ensureIndexes;
36
43
  }
package/dist/store.js CHANGED
@@ -2,11 +2,13 @@ import { mkdir } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  import { tokenize } from "./utils.js";
4
4
  const TABLE_NAME = "memories";
5
+ const EVENTS_TABLE_NAME = "effectiveness_events";
5
6
  export class MemoryStore {
6
7
  dbPath;
7
8
  lancedb = null;
8
9
  connection = null;
9
10
  table = null;
11
+ eventTable = null;
10
12
  indexState = {
11
13
  vector: false,
12
14
  fts: false,
@@ -41,6 +43,31 @@ export class MemoryStore {
41
43
  this.table = await this.connection.createTable(TABLE_NAME, [bootstrap]);
42
44
  await this.table.delete("id = '__bootstrap__'");
43
45
  }
46
+ try {
47
+ this.eventTable = await this.connection.openTable(EVENTS_TABLE_NAME);
48
+ }
49
+ catch {
50
+ const bootstrapEvent = {
51
+ id: "__bootstrap__",
52
+ type: "capture",
53
+ scope: "global",
54
+ sessionID: "",
55
+ timestamp: 0,
56
+ memoryId: "",
57
+ text: "",
58
+ outcome: "considered",
59
+ skipReason: "",
60
+ resultCount: 0,
61
+ injected: false,
62
+ feedbackType: "",
63
+ helpful: -1,
64
+ reason: "",
65
+ labelsJson: "[]",
66
+ metadataJson: "{}",
67
+ };
68
+ this.eventTable = await this.connection.createTable(EVENTS_TABLE_NAME, [bootstrapEvent]);
69
+ await this.eventTable.delete("id = '__bootstrap__'");
70
+ }
44
71
  await this.ensureIndexes();
45
72
  }
46
73
  async put(record) {
@@ -48,6 +75,28 @@ export class MemoryStore {
48
75
  await table.add([record]);
49
76
  this.invalidateScope(record.scope);
50
77
  }
78
+ async putEvent(event) {
79
+ await this.requireEventTable().add([
80
+ {
81
+ id: event.id,
82
+ type: event.type,
83
+ scope: event.scope,
84
+ sessionID: event.sessionID ?? "",
85
+ timestamp: event.timestamp,
86
+ memoryId: event.memoryId ?? "",
87
+ text: event.text ?? "",
88
+ outcome: event.type === "capture" ? event.outcome : "",
89
+ skipReason: event.type === "capture" ? event.skipReason ?? "" : "",
90
+ resultCount: event.type === "recall" ? event.resultCount : 0,
91
+ injected: event.type === "recall" ? event.injected : false,
92
+ feedbackType: event.type === "feedback" ? event.feedbackType : "",
93
+ helpful: event.type === "feedback" ? (event.helpful === undefined ? -1 : event.helpful ? 1 : 0) : -1,
94
+ reason: event.type === "feedback" ? event.reason ?? "" : "",
95
+ labelsJson: event.type === "feedback" ? JSON.stringify(event.labels ?? []) : "[]",
96
+ metadataJson: event.metadataJson,
97
+ },
98
+ ]);
99
+ }
51
100
  async search(params) {
52
101
  const cached = await this.getCachedScopes(params.scopes);
53
102
  if (cached.records.length === 0)
@@ -104,6 +153,93 @@ export class MemoryStore {
104
153
  const rows = await this.readByScopes(scopes);
105
154
  return rows.filter((row) => row.vectorDim !== expectedDim).length;
106
155
  }
156
+ async hasMemory(id, scopes) {
157
+ const rows = await this.readByScopes(scopes);
158
+ return rows.some((row) => row.id === id);
159
+ }
160
+ async listEvents(scopes, limit) {
161
+ const rows = await this.readEventsByScopes(scopes);
162
+ return rows.sort((a, b) => b.timestamp - a.timestamp).slice(0, limit);
163
+ }
164
+ async summarizeEvents(scope, includeGlobalScope) {
165
+ const scopes = includeGlobalScope && scope !== "global" ? [scope, "global"] : [scope];
166
+ const events = await this.readEventsByScopes(scopes);
167
+ const captureSkipReasons = {};
168
+ let captureConsidered = 0;
169
+ let captureStored = 0;
170
+ let captureSkipped = 0;
171
+ let recallRequested = 0;
172
+ let recallInjected = 0;
173
+ let recallReturnedResults = 0;
174
+ let feedbackMissing = 0;
175
+ let feedbackWrong = 0;
176
+ let feedbackUsefulPositive = 0;
177
+ let feedbackUsefulNegative = 0;
178
+ for (const event of events) {
179
+ if (event.type === "capture") {
180
+ if (event.outcome === "considered")
181
+ captureConsidered += 1;
182
+ if (event.outcome === "stored")
183
+ captureStored += 1;
184
+ if (event.outcome === "skipped") {
185
+ captureSkipped += 1;
186
+ if (event.skipReason) {
187
+ captureSkipReasons[event.skipReason] = (captureSkipReasons[event.skipReason] ?? 0) + 1;
188
+ }
189
+ }
190
+ }
191
+ if (event.type === "recall") {
192
+ recallRequested += 1;
193
+ if (event.resultCount > 0)
194
+ recallReturnedResults += 1;
195
+ if (event.injected)
196
+ recallInjected += 1;
197
+ }
198
+ if (event.type === "feedback") {
199
+ if (event.feedbackType === "missing")
200
+ feedbackMissing += 1;
201
+ if (event.feedbackType === "wrong")
202
+ feedbackWrong += 1;
203
+ if (event.feedbackType === "useful") {
204
+ if (event.helpful)
205
+ feedbackUsefulPositive += 1;
206
+ else
207
+ feedbackUsefulNegative += 1;
208
+ }
209
+ }
210
+ }
211
+ const totalCaptureAttempts = captureStored + captureSkipped;
212
+ const totalUsefulFeedback = feedbackUsefulPositive + feedbackUsefulNegative;
213
+ return {
214
+ scope,
215
+ totalEvents: events.length,
216
+ capture: {
217
+ considered: captureConsidered,
218
+ stored: captureStored,
219
+ skipped: captureSkipped,
220
+ successRate: totalCaptureAttempts === 0 ? 0 : captureStored / totalCaptureAttempts,
221
+ skipReasons: captureSkipReasons,
222
+ },
223
+ recall: {
224
+ requested: recallRequested,
225
+ injected: recallInjected,
226
+ returnedResults: recallReturnedResults,
227
+ hitRate: recallRequested === 0 ? 0 : recallReturnedResults / recallRequested,
228
+ injectionRate: recallRequested === 0 ? 0 : recallInjected / recallRequested,
229
+ },
230
+ feedback: {
231
+ missing: feedbackMissing,
232
+ wrong: feedbackWrong,
233
+ useful: {
234
+ positive: feedbackUsefulPositive,
235
+ negative: feedbackUsefulNegative,
236
+ helpfulRate: totalUsefulFeedback === 0 ? 0 : feedbackUsefulPositive / totalUsefulFeedback,
237
+ },
238
+ falsePositiveRate: captureStored === 0 ? 0 : feedbackWrong / captureStored,
239
+ falseNegativeRate: totalCaptureAttempts === 0 ? 0 : feedbackMissing / totalCaptureAttempts,
240
+ },
241
+ };
242
+ }
107
243
  getIndexHealth() {
108
244
  return {
109
245
  vector: this.indexState.vector,
@@ -148,6 +284,44 @@ export class MemoryStore {
148
284
  }
149
285
  return this.table;
150
286
  }
287
+ requireEventTable() {
288
+ if (!this.eventTable) {
289
+ throw new Error("MemoryStore event table is not initialized");
290
+ }
291
+ return this.eventTable;
292
+ }
293
+ async readEventsByScopes(scopes) {
294
+ const table = this.requireEventTable();
295
+ if (scopes.length === 0)
296
+ return [];
297
+ const whereExpr = scopes.map((scope) => `scope = '${escapeSql(scope)}'`).join(" OR ");
298
+ const rows = await table
299
+ .query()
300
+ .where(`(${whereExpr})`)
301
+ .select([
302
+ "id",
303
+ "type",
304
+ "scope",
305
+ "sessionID",
306
+ "timestamp",
307
+ "memoryId",
308
+ "text",
309
+ "outcome",
310
+ "skipReason",
311
+ "resultCount",
312
+ "injected",
313
+ "feedbackType",
314
+ "helpful",
315
+ "reason",
316
+ "labelsJson",
317
+ "metadataJson",
318
+ ])
319
+ .limit(100000)
320
+ .toArray();
321
+ return rows
322
+ .map((row) => normalizeEventRow(row))
323
+ .filter((row) => row !== null);
324
+ }
151
325
  async readByScopes(scopes) {
152
326
  const table = this.requireTable();
153
327
  if (scopes.length === 0)
@@ -222,6 +396,52 @@ function normalizeRow(row) {
222
396
  metadataJson: String(row.metadataJson ?? "{}"),
223
397
  };
224
398
  }
399
+ function normalizeEventRow(row) {
400
+ if (typeof row.id !== "string" || typeof row.type !== "string" || typeof row.scope !== "string") {
401
+ return null;
402
+ }
403
+ const base = {
404
+ id: row.id,
405
+ scope: row.scope,
406
+ sessionID: typeof row.sessionID === "string" && row.sessionID.length > 0 ? row.sessionID : undefined,
407
+ timestamp: Number(row.timestamp ?? Date.now()),
408
+ memoryId: typeof row.memoryId === "string" && row.memoryId.length > 0 ? row.memoryId : undefined,
409
+ text: typeof row.text === "string" && row.text.length > 0 ? row.text : undefined,
410
+ metadataJson: String(row.metadataJson ?? "{}"),
411
+ };
412
+ if (row.type === "capture") {
413
+ return {
414
+ ...base,
415
+ type: "capture",
416
+ outcome: row.outcome === "stored" || row.outcome === "skipped" ? row.outcome : "considered",
417
+ skipReason: typeof row.skipReason === "string" && row.skipReason.length > 0
418
+ ? row.skipReason
419
+ : undefined,
420
+ };
421
+ }
422
+ if (row.type === "recall") {
423
+ return {
424
+ ...base,
425
+ type: "recall",
426
+ resultCount: Number(row.resultCount ?? 0),
427
+ injected: Boolean(row.injected),
428
+ };
429
+ }
430
+ if (row.type === "feedback") {
431
+ const labelsJson = typeof row.labelsJson === "string" ? row.labelsJson : "[]";
432
+ const labels = JSON.parse(labelsJson);
433
+ const helpfulValue = Number(row.helpful ?? -1);
434
+ return {
435
+ ...base,
436
+ type: "feedback",
437
+ feedbackType: row.feedbackType === "missing" || row.feedbackType === "wrong" ? row.feedbackType : "useful",
438
+ helpful: helpfulValue < 0 ? undefined : helpfulValue === 1,
439
+ labels: Array.isArray(labels) ? labels.filter((item) => typeof item === "string") : [],
440
+ reason: typeof row.reason === "string" && row.reason.length > 0 ? row.reason : undefined,
441
+ };
442
+ }
443
+ return null;
444
+ }
225
445
  function escapeSql(value) {
226
446
  return value.replace(/'/g, "''");
227
447
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export type EmbeddingProvider = "ollama" | "openai";
2
2
  export type RetrievalMode = "hybrid" | "vector";
3
3
  export type MemoryCategory = "preference" | "fact" | "decision" | "entity" | "other";
4
+ export type CaptureOutcome = "considered" | "skipped" | "stored";
5
+ export type CaptureSkipReason = "empty-buffer" | "below-min-chars" | "no-positive-signal" | "initialization-unavailable" | "embedding-unavailable" | "empty-embedding";
6
+ export type FeedbackType = "missing" | "wrong" | "useful";
4
7
  export interface EmbeddingConfig {
5
8
  provider: EmbeddingProvider;
6
9
  model: string;
@@ -47,3 +50,64 @@ export interface CaptureCandidate {
47
50
  category: MemoryCategory;
48
51
  importance: number;
49
52
  }
53
+ export interface CaptureCandidateResult {
54
+ candidate: CaptureCandidate | null;
55
+ skipReason?: CaptureSkipReason;
56
+ }
57
+ interface MemoryEffectivenessEventBase {
58
+ id: string;
59
+ scope: string;
60
+ sessionID?: string;
61
+ timestamp: number;
62
+ memoryId?: string;
63
+ text?: string;
64
+ metadataJson: string;
65
+ }
66
+ export interface CaptureEvent extends MemoryEffectivenessEventBase {
67
+ type: "capture";
68
+ outcome: CaptureOutcome;
69
+ skipReason?: CaptureSkipReason;
70
+ }
71
+ export interface RecallEvent extends MemoryEffectivenessEventBase {
72
+ type: "recall";
73
+ resultCount: number;
74
+ injected: boolean;
75
+ }
76
+ export interface FeedbackEvent extends MemoryEffectivenessEventBase {
77
+ type: "feedback";
78
+ feedbackType: FeedbackType;
79
+ helpful?: boolean;
80
+ labels?: string[];
81
+ reason?: string;
82
+ }
83
+ export type MemoryEffectivenessEvent = CaptureEvent | RecallEvent | FeedbackEvent;
84
+ export interface EffectivenessSummary {
85
+ scope: string;
86
+ totalEvents: number;
87
+ capture: {
88
+ considered: number;
89
+ stored: number;
90
+ skipped: number;
91
+ successRate: number;
92
+ skipReasons: Partial<Record<CaptureSkipReason, number>>;
93
+ };
94
+ recall: {
95
+ requested: number;
96
+ injected: number;
97
+ returnedResults: number;
98
+ hitRate: number;
99
+ injectionRate: number;
100
+ };
101
+ feedback: {
102
+ missing: number;
103
+ wrong: number;
104
+ useful: {
105
+ positive: number;
106
+ negative: number;
107
+ helpfulRate: number;
108
+ };
109
+ falsePositiveRate: number;
110
+ falseNegativeRate: number;
111
+ };
112
+ }
113
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lancedb-opencode-pro",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "LanceDB-backed long-term memory provider for OpenCode",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -46,10 +46,11 @@
46
46
  "test": "npm run typecheck",
47
47
  "test:foundation": "npm run build:test && node --test dist-test/test/foundation/foundation.test.js",
48
48
  "test:regression": "npm run build:test && node --test dist-test/test/regression/plugin.test.js",
49
+ "test:effectiveness": "npm run test:foundation && npm run test:regression",
49
50
  "test:retrieval": "npm run build:test && node --test dist-test/test/retrieval/retrieval.test.js",
50
51
  "benchmark:latency": "npm run build:test && node dist-test/test/benchmark/latency.js",
51
52
  "test:e2e": "node scripts/e2e-opencode-memory.mjs",
52
- "verify": "npm run typecheck && npm run build && npm run test:foundation && npm run test:regression && npm run test:retrieval",
53
+ "verify": "npm run typecheck && npm run build && npm run test:effectiveness && npm run test:retrieval",
53
54
  "verify:full": "npm run verify && npm run benchmark:latency && npm pack",
54
55
  "release:check": "npm run verify:full && npm publish --dry-run",
55
56
  "prepublishOnly": "npm run verify:full"