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 +68 -1
- package/dist/extract.d.ts +2 -2
- package/dist/extract.js +9 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +188 -12
- package/dist/store.d.ts +8 -1
- package/dist/store.js +220 -0
- package/dist/types.d.ts +64 -0
- package/package.json +3 -2
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 +
|
|
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 {
|
|
2
|
-
export declare function extractCaptureCandidate(text: string, minChars: number):
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 {
|
|
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
|
|
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
|
|
491
|
+
const memoryId = generateId();
|
|
339
492
|
await state.store.put({
|
|
340
|
-
id:
|
|
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.
|
|
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:
|
|
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"
|