lancedb-opencode-pro 0.1.6 → 0.2.1
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 +16 -0
- package/dist/config.js +6 -0
- package/dist/extract.d.ts +2 -0
- package/dist/extract.js +75 -0
- package/dist/index.js +118 -0
- package/dist/store.d.ts +6 -0
- package/dist/store.js +117 -3
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,9 @@ If you already use other plugins, keep them and append `"lancedb-opencode-pro"`.
|
|
|
48
48
|
"importanceWeight": 0.4
|
|
49
49
|
},
|
|
50
50
|
"includeGlobalScope": true,
|
|
51
|
+
"globalDetectionThreshold": 2,
|
|
52
|
+
"globalDiscountFactor": 0.7,
|
|
53
|
+
"unusedDaysThreshold": 30,
|
|
51
54
|
"minCaptureChars": 80,
|
|
52
55
|
"maxEntriesPerScope": 3000
|
|
53
56
|
}
|
|
@@ -184,6 +187,9 @@ Create `~/.config/opencode/lancedb-opencode-pro.json`:
|
|
|
184
187
|
"importanceWeight": 0.4
|
|
185
188
|
},
|
|
186
189
|
"includeGlobalScope": true,
|
|
190
|
+
"globalDetectionThreshold": 2,
|
|
191
|
+
"globalDiscountFactor": 0.7,
|
|
192
|
+
"unusedDaysThreshold": 30,
|
|
187
193
|
"minCaptureChars": 80,
|
|
188
194
|
"maxEntriesPerScope": 3000
|
|
189
195
|
}
|
|
@@ -229,6 +235,9 @@ Supported environment variables:
|
|
|
229
235
|
- `LANCEDB_OPENCODE_PRO_RECENCY_HALF_LIFE_HOURS`
|
|
230
236
|
- `LANCEDB_OPENCODE_PRO_IMPORTANCE_WEIGHT`
|
|
231
237
|
- `LANCEDB_OPENCODE_PRO_INCLUDE_GLOBAL_SCOPE`
|
|
238
|
+
- `LANCEDB_OPENCODE_PRO_GLOBAL_DETECTION_THRESHOLD`
|
|
239
|
+
- `LANCEDB_OPENCODE_PRO_GLOBAL_DISCOUNT_FACTOR`
|
|
240
|
+
- `LANCEDB_OPENCODE_PRO_UNUSED_DAYS_THRESHOLD`
|
|
232
241
|
- `LANCEDB_OPENCODE_PRO_MIN_CAPTURE_CHARS`
|
|
233
242
|
- `LANCEDB_OPENCODE_PRO_MAX_ENTRIES_PER_SCOPE`
|
|
234
243
|
|
|
@@ -237,6 +246,7 @@ Supported environment variables:
|
|
|
237
246
|
- Auto-capture of durable outcomes from completed assistant responses.
|
|
238
247
|
- Hybrid retrieval (vector + lexical) for future context injection.
|
|
239
248
|
- Project-scope memory isolation (`project:*` + optional `global`).
|
|
249
|
+
- Cross-project memory sharing via global scope with automatic detection.
|
|
240
250
|
- Memory tools:
|
|
241
251
|
- `memory_search`
|
|
242
252
|
- `memory_delete`
|
|
@@ -246,6 +256,9 @@ Supported environment variables:
|
|
|
246
256
|
- `memory_feedback_wrong`
|
|
247
257
|
- `memory_feedback_useful`
|
|
248
258
|
- `memory_effectiveness`
|
|
259
|
+
- `memory_scope_promote`
|
|
260
|
+
- `memory_scope_demote`
|
|
261
|
+
- `memory_global_list`
|
|
249
262
|
- `memory_port_plan`
|
|
250
263
|
|
|
251
264
|
## Memory Effectiveness Feedback
|
|
@@ -371,6 +384,9 @@ Example sidecar:
|
|
|
371
384
|
"importanceWeight": 0.4
|
|
372
385
|
},
|
|
373
386
|
"includeGlobalScope": true,
|
|
387
|
+
"globalDetectionThreshold": 2,
|
|
388
|
+
"globalDiscountFactor": 0.7,
|
|
389
|
+
"unusedDaysThreshold": 30,
|
|
374
390
|
"minCaptureChars": 80,
|
|
375
391
|
"maxEntriesPerScope": 3000
|
|
376
392
|
}
|
package/dist/config.js
CHANGED
|
@@ -59,6 +59,9 @@ export function resolveMemoryConfig(config, worktree) {
|
|
|
59
59
|
importanceWeight,
|
|
60
60
|
},
|
|
61
61
|
includeGlobalScope: toBoolean(process.env.LANCEDB_OPENCODE_PRO_INCLUDE_GLOBAL_SCOPE ?? raw.includeGlobalScope, true),
|
|
62
|
+
globalDetectionThreshold: Math.max(1, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_GLOBAL_DETECTION_THRESHOLD ?? raw.globalDetectionThreshold, 2))),
|
|
63
|
+
globalDiscountFactor: clamp(toNumber(process.env.LANCEDB_OPENCODE_PRO_GLOBAL_DISCOUNT_FACTOR ?? raw.globalDiscountFactor, 0.7), 0, 1),
|
|
64
|
+
unusedDaysThreshold: Math.max(1, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_UNUSED_DAYS_THRESHOLD ?? raw.unusedDaysThreshold, 30))),
|
|
62
65
|
minCaptureChars: Math.max(30, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_MIN_CAPTURE_CHARS ?? raw.minCaptureChars, 80))),
|
|
63
66
|
maxEntriesPerScope: Math.max(50, Math.floor(toNumber(process.env.LANCEDB_OPENCODE_PRO_MAX_ENTRIES_PER_SCOPE ?? raw.maxEntriesPerScope, 3000))),
|
|
64
67
|
};
|
|
@@ -83,6 +86,9 @@ function validateEmbeddingConfig(embedding) {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
function loadSidecarConfig(worktree) {
|
|
89
|
+
if (process.env.LANCEDB_OPENCODE_PRO_SKIP_SIDECAR === "true") {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
86
92
|
const configPath = firstString(process.env.LANCEDB_OPENCODE_PRO_CONFIG_PATH);
|
|
87
93
|
const candidates = [
|
|
88
94
|
join(expandHomePath("~/.opencode"), SIDECAR_FILE),
|
package/dist/extract.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { CaptureCandidateResult } from "./types.js";
|
|
2
2
|
export declare function extractCaptureCandidate(text: string, minChars: number): CaptureCandidateResult;
|
|
3
|
+
export declare function detectGlobalWorthiness(content: string): number;
|
|
4
|
+
export declare function isGlobalCandidate(content: string, threshold: number): boolean;
|
package/dist/extract.js
CHANGED
|
@@ -12,6 +12,68 @@ const POSITIVE_SIGNALS = [
|
|
|
12
12
|
const DECISION_SIGNALS = ["decide", "decision", "tradeoff", "architecture", "採用", "決定", "架構"];
|
|
13
13
|
const FACT_SIGNALS = ["because", "root cause", "原因", "由於"];
|
|
14
14
|
const PREF_SIGNALS = ["prefer", "preference", "偏好", "習慣"];
|
|
15
|
+
const GLOBAL_KEYWORDS = [
|
|
16
|
+
// Distributions
|
|
17
|
+
"alpine",
|
|
18
|
+
"debian",
|
|
19
|
+
"ubuntu",
|
|
20
|
+
"centos",
|
|
21
|
+
"fedora",
|
|
22
|
+
"arch",
|
|
23
|
+
// Containers
|
|
24
|
+
"docker",
|
|
25
|
+
"dockerfile",
|
|
26
|
+
"docker-compose",
|
|
27
|
+
"containerd",
|
|
28
|
+
// Orchestration
|
|
29
|
+
"kubernetes",
|
|
30
|
+
"k8s",
|
|
31
|
+
"helm",
|
|
32
|
+
"kubectl",
|
|
33
|
+
// Shells/Systems
|
|
34
|
+
"bash",
|
|
35
|
+
"shell",
|
|
36
|
+
"linux",
|
|
37
|
+
"unix",
|
|
38
|
+
"posix",
|
|
39
|
+
"busybox",
|
|
40
|
+
// Web servers
|
|
41
|
+
"nginx",
|
|
42
|
+
"apache",
|
|
43
|
+
"caddy",
|
|
44
|
+
// Databases
|
|
45
|
+
"postgres",
|
|
46
|
+
"postgresql",
|
|
47
|
+
"mysql",
|
|
48
|
+
"redis",
|
|
49
|
+
"mongodb",
|
|
50
|
+
"sqlite",
|
|
51
|
+
// Cloud
|
|
52
|
+
"aws",
|
|
53
|
+
"gcp",
|
|
54
|
+
"azure",
|
|
55
|
+
"digitalocean",
|
|
56
|
+
// VCS
|
|
57
|
+
"git",
|
|
58
|
+
"github",
|
|
59
|
+
"gitlab",
|
|
60
|
+
"bitbucket",
|
|
61
|
+
// Protocols
|
|
62
|
+
"api",
|
|
63
|
+
"rest",
|
|
64
|
+
"graphql",
|
|
65
|
+
"grpc",
|
|
66
|
+
"http",
|
|
67
|
+
"https",
|
|
68
|
+
// Package managers
|
|
69
|
+
"npm",
|
|
70
|
+
"yarn",
|
|
71
|
+
"pnpm",
|
|
72
|
+
"pip",
|
|
73
|
+
"cargo",
|
|
74
|
+
"make",
|
|
75
|
+
"cmake",
|
|
76
|
+
];
|
|
15
77
|
export function extractCaptureCandidate(text, minChars) {
|
|
16
78
|
const normalized = text.trim();
|
|
17
79
|
if (normalized.length < minChars) {
|
|
@@ -45,3 +107,16 @@ function clipText(text, maxLen) {
|
|
|
45
107
|
return text;
|
|
46
108
|
return `${text.slice(0, maxLen - 3)}...`;
|
|
47
109
|
}
|
|
110
|
+
export function detectGlobalWorthiness(content) {
|
|
111
|
+
const lower = content.toLowerCase();
|
|
112
|
+
let matches = 0;
|
|
113
|
+
for (const keyword of GLOBAL_KEYWORDS) {
|
|
114
|
+
if (lower.includes(keyword)) {
|
|
115
|
+
matches += 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return matches;
|
|
119
|
+
}
|
|
120
|
+
export function isGlobalCandidate(content, threshold) {
|
|
121
|
+
return detectGlobalWorthiness(content) >= threshold;
|
|
122
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,7 @@ const plugin = async (input) => {
|
|
|
60
60
|
recencyBoost: state.config.retrieval.recencyBoost,
|
|
61
61
|
recencyHalfLifeHours: state.config.retrieval.recencyHalfLifeHours,
|
|
62
62
|
importanceWeight: state.config.retrieval.importanceWeight,
|
|
63
|
+
globalDiscountFactor: state.config.globalDiscountFactor,
|
|
63
64
|
});
|
|
64
65
|
await state.store.putEvent({
|
|
65
66
|
id: generateId(),
|
|
@@ -77,6 +78,9 @@ const plugin = async (input) => {
|
|
|
77
78
|
});
|
|
78
79
|
if (results.length === 0)
|
|
79
80
|
return;
|
|
81
|
+
for (const result of results) {
|
|
82
|
+
state.store.updateMemoryUsage(result.record.id, activeScope, scopes).catch(() => { });
|
|
83
|
+
}
|
|
80
84
|
const memoryBlock = [
|
|
81
85
|
"[Memory Recall - optional historical context]",
|
|
82
86
|
...results.map((item, index) => `${index + 1}. [${item.record.id}] (${item.record.scope}) ${item.record.text}`),
|
|
@@ -117,6 +121,7 @@ const plugin = async (input) => {
|
|
|
117
121
|
recencyBoost: state.config.retrieval.recencyBoost,
|
|
118
122
|
recencyHalfLifeHours: state.config.retrieval.recencyHalfLifeHours,
|
|
119
123
|
importanceWeight: state.config.retrieval.importanceWeight,
|
|
124
|
+
globalDiscountFactor: state.config.globalDiscountFactor,
|
|
120
125
|
});
|
|
121
126
|
await state.store.putEvent({
|
|
122
127
|
id: generateId(),
|
|
@@ -131,6 +136,9 @@ const plugin = async (input) => {
|
|
|
131
136
|
});
|
|
132
137
|
if (results.length === 0)
|
|
133
138
|
return "No relevant memory found.";
|
|
139
|
+
for (const result of results) {
|
|
140
|
+
state.store.updateMemoryUsage(result.record.id, activeScope, scopes).catch(() => { });
|
|
141
|
+
}
|
|
134
142
|
return results
|
|
135
143
|
.map((item, idx) => {
|
|
136
144
|
const percent = Math.round(item.score * 100);
|
|
@@ -302,6 +310,110 @@ const plugin = async (input) => {
|
|
|
302
310
|
return JSON.stringify(summary, null, 2);
|
|
303
311
|
},
|
|
304
312
|
}),
|
|
313
|
+
memory_scope_promote: tool({
|
|
314
|
+
description: "Promote a memory from project scope to global scope for cross-project sharing",
|
|
315
|
+
args: {
|
|
316
|
+
id: tool.schema.string().min(6),
|
|
317
|
+
confirm: tool.schema.boolean().default(false),
|
|
318
|
+
},
|
|
319
|
+
execute: async (args, context) => {
|
|
320
|
+
await state.ensureInitialized();
|
|
321
|
+
if (!state.initialized)
|
|
322
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
323
|
+
if (!args.confirm) {
|
|
324
|
+
return "Rejected: memory_scope_promote requires confirm=true.";
|
|
325
|
+
}
|
|
326
|
+
const activeScope = deriveProjectScope(context.worktree);
|
|
327
|
+
const scopes = buildScopeFilter(activeScope, state.config.includeGlobalScope);
|
|
328
|
+
const exists = await state.store.hasMemory(args.id, scopes);
|
|
329
|
+
if (!exists) {
|
|
330
|
+
return `Memory ${args.id} not found in current scope.`;
|
|
331
|
+
}
|
|
332
|
+
const updated = await state.store.updateMemoryScope(args.id, "global", scopes);
|
|
333
|
+
if (!updated) {
|
|
334
|
+
return `Failed to promote memory ${args.id}.`;
|
|
335
|
+
}
|
|
336
|
+
return `Promoted memory ${args.id} to global scope.`;
|
|
337
|
+
},
|
|
338
|
+
}),
|
|
339
|
+
memory_scope_demote: tool({
|
|
340
|
+
description: "Demote a memory from global scope to project scope",
|
|
341
|
+
args: {
|
|
342
|
+
id: tool.schema.string().min(6),
|
|
343
|
+
confirm: tool.schema.boolean().default(false),
|
|
344
|
+
scope: tool.schema.string().optional(),
|
|
345
|
+
},
|
|
346
|
+
execute: async (args, context) => {
|
|
347
|
+
await state.ensureInitialized();
|
|
348
|
+
if (!state.initialized)
|
|
349
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
350
|
+
if (!args.confirm) {
|
|
351
|
+
return "Rejected: memory_scope_demote requires confirm=true.";
|
|
352
|
+
}
|
|
353
|
+
const projectScope = args.scope ?? deriveProjectScope(context.worktree);
|
|
354
|
+
const globalExists = await state.store.hasMemory(args.id, ["global"]);
|
|
355
|
+
if (!globalExists) {
|
|
356
|
+
return `Memory ${args.id} not found in global scope or is not a global memory.`;
|
|
357
|
+
}
|
|
358
|
+
const updated = await state.store.updateMemoryScope(args.id, projectScope, ["global"]);
|
|
359
|
+
if (!updated) {
|
|
360
|
+
return `Failed to demote memory ${args.id}.`;
|
|
361
|
+
}
|
|
362
|
+
return `Demoted memory ${args.id} from global to ${projectScope}.`;
|
|
363
|
+
},
|
|
364
|
+
}),
|
|
365
|
+
memory_global_list: tool({
|
|
366
|
+
description: "List all global-scoped memories, optionally filtered by search query or unused status",
|
|
367
|
+
args: {
|
|
368
|
+
query: tool.schema.string().optional(),
|
|
369
|
+
filter: tool.schema.string().optional(),
|
|
370
|
+
limit: tool.schema.number().int().min(1).max(100).default(20),
|
|
371
|
+
},
|
|
372
|
+
execute: async (args) => {
|
|
373
|
+
await state.ensureInitialized();
|
|
374
|
+
if (!state.initialized)
|
|
375
|
+
return unavailableMessage(state.config.embedding.provider);
|
|
376
|
+
let records;
|
|
377
|
+
if (args.filter === "unused") {
|
|
378
|
+
records = await state.store.getUnusedGlobalMemories(state.config.unusedDaysThreshold, args.limit);
|
|
379
|
+
}
|
|
380
|
+
else if (args.query) {
|
|
381
|
+
let queryVector = [];
|
|
382
|
+
try {
|
|
383
|
+
queryVector = await state.embedder.embed(args.query);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
queryVector = [];
|
|
387
|
+
}
|
|
388
|
+
records = await state.store.search({
|
|
389
|
+
query: args.query,
|
|
390
|
+
queryVector,
|
|
391
|
+
scopes: ["global"],
|
|
392
|
+
limit: args.limit,
|
|
393
|
+
vectorWeight: 0.7,
|
|
394
|
+
bm25Weight: 0.3,
|
|
395
|
+
minScore: 0.2,
|
|
396
|
+
globalDiscountFactor: 1.0,
|
|
397
|
+
}).then((results) => results.map((r) => r.record));
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
records = await state.store.readGlobalMemories(args.limit);
|
|
401
|
+
}
|
|
402
|
+
if (records.length === 0) {
|
|
403
|
+
return "No global memories found.";
|
|
404
|
+
}
|
|
405
|
+
return records
|
|
406
|
+
.map((record, idx) => {
|
|
407
|
+
const date = new Date(record.timestamp).toISOString().split("T")[0];
|
|
408
|
+
const lastRecalled = record.lastRecalled > 0
|
|
409
|
+
? new Date(record.lastRecalled).toISOString().split("T")[0]
|
|
410
|
+
: "never";
|
|
411
|
+
return `${idx + 1}. [${record.id}] ${record.text.slice(0, 80)}...
|
|
412
|
+
Stored: ${date} | Recalled: ${lastRecalled} | Count: ${record.recallCount} | Projects: ${record.projectCount}`;
|
|
413
|
+
})
|
|
414
|
+
.join("\n");
|
|
415
|
+
},
|
|
416
|
+
}),
|
|
305
417
|
memory_port_plan: tool({
|
|
306
418
|
description: "Plan non-conflicting host ports for compose services and optionally persist reservations",
|
|
307
419
|
args: {
|
|
@@ -363,6 +475,9 @@ const plugin = async (input) => {
|
|
|
363
475
|
scope: "global",
|
|
364
476
|
importance: 0.8,
|
|
365
477
|
timestamp: Date.now(),
|
|
478
|
+
lastRecalled: 0,
|
|
479
|
+
recallCount: 0,
|
|
480
|
+
projectCount: 0,
|
|
366
481
|
schemaVersion: SCHEMA_VERSION,
|
|
367
482
|
embeddingModel: state.config.embedding.model,
|
|
368
483
|
vectorDim: vector.length,
|
|
@@ -517,6 +632,9 @@ async function flushAutoCapture(sessionID, state, client) {
|
|
|
517
632
|
scope: activeScope,
|
|
518
633
|
importance: result.candidate.importance,
|
|
519
634
|
timestamp: Date.now(),
|
|
635
|
+
lastRecalled: 0,
|
|
636
|
+
recallCount: 0,
|
|
637
|
+
projectCount: 0,
|
|
520
638
|
schemaVersion: SCHEMA_VERSION,
|
|
521
639
|
embeddingModel: state.config.embedding.model,
|
|
522
640
|
vectorDim: vector.length,
|
package/dist/store.d.ts
CHANGED
|
@@ -23,13 +23,18 @@ export declare class MemoryStore {
|
|
|
23
23
|
recencyBoost?: boolean;
|
|
24
24
|
recencyHalfLifeHours?: number;
|
|
25
25
|
importanceWeight?: number;
|
|
26
|
+
globalDiscountFactor?: number;
|
|
26
27
|
}): Promise<SearchResult[]>;
|
|
27
28
|
deleteById(id: string, scopes: string[]): Promise<boolean>;
|
|
29
|
+
updateMemoryScope(id: string, newScope: string, scopes: string[]): Promise<boolean>;
|
|
30
|
+
readGlobalMemories(limit?: number): Promise<MemoryRecord[]>;
|
|
31
|
+
getUnusedGlobalMemories(unusedDaysThreshold: number, limit?: number): Promise<MemoryRecord[]>;
|
|
28
32
|
clearScope(scope: string): Promise<number>;
|
|
29
33
|
list(scope: string, limit: number): Promise<MemoryRecord[]>;
|
|
30
34
|
pruneScope(scope: string, maxEntries: number): Promise<number>;
|
|
31
35
|
countIncompatibleVectors(scopes: string[], expectedDim: number): Promise<number>;
|
|
32
36
|
hasMemory(id: string, scopes: string[]): Promise<boolean>;
|
|
37
|
+
updateMemoryUsage(id: string, projectScope: string, scopes: string[]): Promise<void>;
|
|
33
38
|
listEvents(scopes: string[], limit: number): Promise<MemoryEffectivenessEvent[]>;
|
|
34
39
|
summarizeEvents(scope: string, includeGlobalScope: boolean): Promise<EffectivenessSummary>;
|
|
35
40
|
getIndexHealth(): {
|
|
@@ -44,5 +49,6 @@ export declare class MemoryStore {
|
|
|
44
49
|
private readEventsByScopes;
|
|
45
50
|
private readByScopes;
|
|
46
51
|
private ensureIndexes;
|
|
52
|
+
private ensureMemoriesTableCompatibility;
|
|
47
53
|
private ensureEventTableCompatibility;
|
|
48
54
|
}
|
package/dist/store.js
CHANGED
|
@@ -36,6 +36,9 @@ export class MemoryStore {
|
|
|
36
36
|
scope: "global",
|
|
37
37
|
importance: 0,
|
|
38
38
|
timestamp: 0,
|
|
39
|
+
lastRecalled: 0,
|
|
40
|
+
recallCount: 0,
|
|
41
|
+
projectCount: 0,
|
|
39
42
|
schemaVersion: 1,
|
|
40
43
|
embeddingModel: "bootstrap",
|
|
41
44
|
vectorDim,
|
|
@@ -70,6 +73,7 @@ export class MemoryStore {
|
|
|
70
73
|
this.eventTable = await this.connection.createTable(EVENTS_TABLE_NAME, [bootstrapEvent]);
|
|
71
74
|
await this.eventTable.delete("id = '__bootstrap__'");
|
|
72
75
|
}
|
|
76
|
+
await this.ensureMemoriesTableCompatibility();
|
|
73
77
|
await this.ensureEventTableCompatibility();
|
|
74
78
|
await this.ensureIndexes();
|
|
75
79
|
}
|
|
@@ -114,13 +118,15 @@ export class MemoryStore {
|
|
|
114
118
|
const recencyBoostEnabled = params.recencyBoost ?? true;
|
|
115
119
|
const recencyHalfLifeHours = Math.max(1, params.recencyHalfLifeHours ?? 72);
|
|
116
120
|
const importanceWeight = clampImportanceWeight(params.importanceWeight ?? 0.4);
|
|
121
|
+
const globalDiscountFactor = params.globalDiscountFactor ?? 1.0;
|
|
117
122
|
const candidates = cached.records
|
|
118
123
|
.filter((record) => params.queryVector.length === 0 || record.vector.length === params.queryVector.length)
|
|
119
124
|
.map((record, index) => {
|
|
120
125
|
const recordNorm = cached.norms.get(record.id) ?? vecNorm(record.vector);
|
|
121
126
|
const vectorScore = useVectorChannel ? fastCosine(params.queryVector, record.vector, queryNorm, recordNorm) : 0;
|
|
122
127
|
const bm25Score = useBm25Channel ? bm25LikeScore(queryTokens, cached.tokenized[index], cached.idf) : 0;
|
|
123
|
-
|
|
128
|
+
const isGlobal = record.scope === "global";
|
|
129
|
+
return { record, vectorScore, bm25Score, isGlobal };
|
|
124
130
|
});
|
|
125
131
|
if (candidates.length === 0)
|
|
126
132
|
return [];
|
|
@@ -144,7 +150,8 @@ export class MemoryStore {
|
|
|
144
150
|
? computeRecencyMultiplier(item.record.timestamp, recencyHalfLifeHours)
|
|
145
151
|
: 1;
|
|
146
152
|
const importanceFactor = 1 + importanceWeight * clampImportance(item.record.importance);
|
|
147
|
-
const
|
|
153
|
+
const scopeFactor = item.isGlobal ? globalDiscountFactor : 1.0;
|
|
154
|
+
const score = rrfScore * recencyFactor * importanceFactor * scopeFactor;
|
|
148
155
|
return {
|
|
149
156
|
record: item.record,
|
|
150
157
|
score,
|
|
@@ -166,6 +173,26 @@ export class MemoryStore {
|
|
|
166
173
|
this.invalidateScope(match.scope);
|
|
167
174
|
return true;
|
|
168
175
|
}
|
|
176
|
+
async updateMemoryScope(id, newScope, scopes) {
|
|
177
|
+
const rows = await this.readByScopes(scopes);
|
|
178
|
+
const match = rows.find((row) => row.id === id);
|
|
179
|
+
if (!match)
|
|
180
|
+
return false;
|
|
181
|
+
await this.requireTable().delete(`id = '${escapeSql(id)}'`);
|
|
182
|
+
this.invalidateScope(match.scope);
|
|
183
|
+
await this.requireTable().add([{ ...match, scope: newScope }]);
|
|
184
|
+
this.invalidateScope(newScope);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
async readGlobalMemories(limit = 100) {
|
|
188
|
+
const rows = await this.readByScopes(["global"]);
|
|
189
|
+
return rows.sort((a, b) => b.timestamp - a.timestamp).slice(0, limit);
|
|
190
|
+
}
|
|
191
|
+
async getUnusedGlobalMemories(unusedDaysThreshold, limit = 100) {
|
|
192
|
+
const cutoffTime = Date.now() - unusedDaysThreshold * 24 * 60 * 60 * 1000;
|
|
193
|
+
const rows = await this.readByScopes(["global"]);
|
|
194
|
+
return rows.filter((row) => row.lastRecalled > 0 && row.lastRecalled < cutoffTime).slice(0, limit);
|
|
195
|
+
}
|
|
169
196
|
async clearScope(scope) {
|
|
170
197
|
const rows = await this.readByScopes([scope]);
|
|
171
198
|
if (rows.length === 0)
|
|
@@ -194,8 +221,51 @@ export class MemoryStore {
|
|
|
194
221
|
return rows.filter((row) => row.vectorDim !== expectedDim).length;
|
|
195
222
|
}
|
|
196
223
|
async hasMemory(id, scopes) {
|
|
224
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
225
|
+
const rows = await this.readByScopes(scopes);
|
|
226
|
+
if (rows.some((row) => row.id === id)) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
if (attempt < 2) {
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
async updateMemoryUsage(id, projectScope, scopes) {
|
|
197
236
|
const rows = await this.readByScopes(scopes);
|
|
198
|
-
|
|
237
|
+
const match = rows.find((row) => row.id === id);
|
|
238
|
+
if (!match)
|
|
239
|
+
return;
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
const newRecallCount = match.recallCount + 1;
|
|
242
|
+
let newProjectCount = match.projectCount;
|
|
243
|
+
let metadataJson = match.metadataJson;
|
|
244
|
+
if (match.scope === "global" && projectScope) {
|
|
245
|
+
const projects = extractRecalledProjects(metadataJson);
|
|
246
|
+
if (!projects.has(projectScope)) {
|
|
247
|
+
projects.add(projectScope);
|
|
248
|
+
if (projects.size > 100) {
|
|
249
|
+
const arr = Array.from(projects);
|
|
250
|
+
arr.splice(0, arr.length - 100);
|
|
251
|
+
metadataJson = JSON.stringify({ recalledProjects: arr });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
metadataJson = JSON.stringify({ recalledProjects: Array.from(projects) });
|
|
255
|
+
}
|
|
256
|
+
newProjectCount = projects.size;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
await this.requireTable().delete(`id = '${escapeSql(id)}'`);
|
|
260
|
+
this.invalidateScope(match.scope);
|
|
261
|
+
await this.requireTable().add([{
|
|
262
|
+
...match,
|
|
263
|
+
lastRecalled: now,
|
|
264
|
+
recallCount: newRecallCount,
|
|
265
|
+
projectCount: newProjectCount,
|
|
266
|
+
metadataJson,
|
|
267
|
+
}]);
|
|
268
|
+
this.invalidateScope(match.scope);
|
|
199
269
|
}
|
|
200
270
|
async listEvents(scopes, limit) {
|
|
201
271
|
const rows = await this.readEventsByScopes(scopes);
|
|
@@ -410,6 +480,9 @@ export class MemoryStore {
|
|
|
410
480
|
"scope",
|
|
411
481
|
"importance",
|
|
412
482
|
"timestamp",
|
|
483
|
+
"lastRecalled",
|
|
484
|
+
"recallCount",
|
|
485
|
+
"projectCount",
|
|
413
486
|
"schemaVersion",
|
|
414
487
|
"embeddingModel",
|
|
415
488
|
"vectorDim",
|
|
@@ -447,6 +520,32 @@ export class MemoryStore {
|
|
|
447
520
|
this.indexState.ftsError = error instanceof Error ? error.message : String(error);
|
|
448
521
|
}
|
|
449
522
|
}
|
|
523
|
+
async ensureMemoriesTableCompatibility() {
|
|
524
|
+
const table = this.requireTable();
|
|
525
|
+
const schema = await table.schema();
|
|
526
|
+
const fieldNames = new Set(schema.fields.map((field) => field.name));
|
|
527
|
+
const missing = [];
|
|
528
|
+
if (!fieldNames.has("lastRecalled")) {
|
|
529
|
+
missing.push({ name: "lastRecalled", valueSql: "CAST(0 AS BIGINT)" });
|
|
530
|
+
}
|
|
531
|
+
if (!fieldNames.has("recallCount")) {
|
|
532
|
+
missing.push({ name: "recallCount", valueSql: "CAST(0 AS INT)" });
|
|
533
|
+
}
|
|
534
|
+
if (!fieldNames.has("projectCount")) {
|
|
535
|
+
missing.push({ name: "projectCount", valueSql: "CAST(0 AS INT)" });
|
|
536
|
+
}
|
|
537
|
+
if (missing.length === 0) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
await table.addColumns(missing);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
545
|
+
const names = missing.map((col) => col.name).join(", ");
|
|
546
|
+
throw new Error(`Failed to patch ${TABLE_NAME} schema for columns [${names}]: ${reason}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
450
549
|
async ensureEventTableCompatibility() {
|
|
451
550
|
const table = this.requireEventTable();
|
|
452
551
|
const schema = await table.schema();
|
|
@@ -477,6 +576,9 @@ function normalizeRow(row) {
|
|
|
477
576
|
scope: row.scope,
|
|
478
577
|
importance: Number(row.importance ?? 0.5),
|
|
479
578
|
timestamp: Number(row.timestamp ?? Date.now()),
|
|
579
|
+
lastRecalled: Number(row.lastRecalled ?? 0),
|
|
580
|
+
recallCount: Number(row.recallCount ?? 0),
|
|
581
|
+
projectCount: Number(row.projectCount ?? 0),
|
|
480
582
|
schemaVersion: Number(row.schemaVersion ?? 1),
|
|
481
583
|
embeddingModel: String(row.embeddingModel ?? "unknown"),
|
|
482
584
|
vectorDim: Number(row.vectorDim ?? vector.length),
|
|
@@ -628,3 +730,15 @@ function bm25LikeScore(query, doc, idf) {
|
|
|
628
730
|
}
|
|
629
731
|
return 1 - Math.exp(-score);
|
|
630
732
|
}
|
|
733
|
+
function extractRecalledProjects(metadataJson) {
|
|
734
|
+
try {
|
|
735
|
+
const metadata = JSON.parse(metadataJson);
|
|
736
|
+
if (metadata && Array.isArray(metadata.recalledProjects)) {
|
|
737
|
+
return new Set(metadata.recalledProjects);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
// ignore parse errors
|
|
742
|
+
}
|
|
743
|
+
return new Set();
|
|
744
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type CaptureOutcome = "considered" | "skipped" | "stored";
|
|
|
5
5
|
export type CaptureSkipReason = "empty-buffer" | "below-min-chars" | "no-positive-signal" | "initialization-unavailable" | "embedding-unavailable" | "empty-embedding";
|
|
6
6
|
export type FeedbackType = "missing" | "wrong" | "useful";
|
|
7
7
|
export type RecallSource = "system-transform" | "manual-search";
|
|
8
|
+
export type MemoryScope = "project" | "global";
|
|
8
9
|
export interface EmbeddingConfig {
|
|
9
10
|
provider: EmbeddingProvider;
|
|
10
11
|
model: string;
|
|
@@ -28,6 +29,9 @@ export interface MemoryRuntimeConfig {
|
|
|
28
29
|
embedding: EmbeddingConfig;
|
|
29
30
|
retrieval: RetrievalConfig;
|
|
30
31
|
includeGlobalScope: boolean;
|
|
32
|
+
globalDetectionThreshold: number;
|
|
33
|
+
globalDiscountFactor: number;
|
|
34
|
+
unusedDaysThreshold: number;
|
|
31
35
|
minCaptureChars: number;
|
|
32
36
|
maxEntriesPerScope: number;
|
|
33
37
|
}
|
|
@@ -39,6 +43,9 @@ export interface MemoryRecord {
|
|
|
39
43
|
scope: string;
|
|
40
44
|
importance: number;
|
|
41
45
|
timestamp: number;
|
|
46
|
+
lastRecalled: number;
|
|
47
|
+
recallCount: number;
|
|
48
|
+
projectCount: number;
|
|
42
49
|
schemaVersion: number;
|
|
43
50
|
embeddingModel: string;
|
|
44
51
|
vectorDim: number;
|