lancedb-opencode-pro 0.1.5 → 0.2.0

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
@@ -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,4 +49,5 @@ export declare class MemoryStore {
44
49
  private readEventsByScopes;
45
50
  private readByScopes;
46
51
  private ensureIndexes;
52
+ private ensureEventTableCompatibility;
47
53
  }
package/dist/store.js CHANGED
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
3
3
  import { tokenize } from "./utils.js";
4
4
  const TABLE_NAME = "memories";
5
5
  const EVENTS_TABLE_NAME = "effectiveness_events";
6
+ const EVENTS_SOURCE_COLUMN = "source";
6
7
  export class MemoryStore {
7
8
  dbPath;
8
9
  lancedb = null;
@@ -35,6 +36,9 @@ export class MemoryStore {
35
36
  scope: "global",
36
37
  importance: 0,
37
38
  timestamp: 0,
39
+ lastRecalled: 0,
40
+ recallCount: 0,
41
+ projectCount: 0,
38
42
  schemaVersion: 1,
39
43
  embeddingModel: "bootstrap",
40
44
  vectorDim,
@@ -69,6 +73,7 @@ export class MemoryStore {
69
73
  this.eventTable = await this.connection.createTable(EVENTS_TABLE_NAME, [bootstrapEvent]);
70
74
  await this.eventTable.delete("id = '__bootstrap__'");
71
75
  }
76
+ await this.ensureEventTableCompatibility();
72
77
  await this.ensureIndexes();
73
78
  }
74
79
  async put(record) {
@@ -112,13 +117,15 @@ export class MemoryStore {
112
117
  const recencyBoostEnabled = params.recencyBoost ?? true;
113
118
  const recencyHalfLifeHours = Math.max(1, params.recencyHalfLifeHours ?? 72);
114
119
  const importanceWeight = clampImportanceWeight(params.importanceWeight ?? 0.4);
120
+ const globalDiscountFactor = params.globalDiscountFactor ?? 1.0;
115
121
  const candidates = cached.records
116
122
  .filter((record) => params.queryVector.length === 0 || record.vector.length === params.queryVector.length)
117
123
  .map((record, index) => {
118
124
  const recordNorm = cached.norms.get(record.id) ?? vecNorm(record.vector);
119
125
  const vectorScore = useVectorChannel ? fastCosine(params.queryVector, record.vector, queryNorm, recordNorm) : 0;
120
126
  const bm25Score = useBm25Channel ? bm25LikeScore(queryTokens, cached.tokenized[index], cached.idf) : 0;
121
- return { record, vectorScore, bm25Score };
127
+ const isGlobal = record.scope === "global";
128
+ return { record, vectorScore, bm25Score, isGlobal };
122
129
  });
123
130
  if (candidates.length === 0)
124
131
  return [];
@@ -142,7 +149,8 @@ export class MemoryStore {
142
149
  ? computeRecencyMultiplier(item.record.timestamp, recencyHalfLifeHours)
143
150
  : 1;
144
151
  const importanceFactor = 1 + importanceWeight * clampImportance(item.record.importance);
145
- const score = rrfScore * recencyFactor * importanceFactor;
152
+ const scopeFactor = item.isGlobal ? globalDiscountFactor : 1.0;
153
+ const score = rrfScore * recencyFactor * importanceFactor * scopeFactor;
146
154
  return {
147
155
  record: item.record,
148
156
  score,
@@ -164,6 +172,26 @@ export class MemoryStore {
164
172
  this.invalidateScope(match.scope);
165
173
  return true;
166
174
  }
175
+ async updateMemoryScope(id, newScope, scopes) {
176
+ const rows = await this.readByScopes(scopes);
177
+ const match = rows.find((row) => row.id === id);
178
+ if (!match)
179
+ return false;
180
+ await this.requireTable().delete(`id = '${escapeSql(id)}'`);
181
+ this.invalidateScope(match.scope);
182
+ await this.requireTable().add([{ ...match, scope: newScope }]);
183
+ this.invalidateScope(newScope);
184
+ return true;
185
+ }
186
+ async readGlobalMemories(limit = 100) {
187
+ const rows = await this.readByScopes(["global"]);
188
+ return rows.sort((a, b) => b.timestamp - a.timestamp).slice(0, limit);
189
+ }
190
+ async getUnusedGlobalMemories(unusedDaysThreshold, limit = 100) {
191
+ const cutoffTime = Date.now() - unusedDaysThreshold * 24 * 60 * 60 * 1000;
192
+ const rows = await this.readByScopes(["global"]);
193
+ return rows.filter((row) => row.lastRecalled > 0 && row.lastRecalled < cutoffTime).slice(0, limit);
194
+ }
167
195
  async clearScope(scope) {
168
196
  const rows = await this.readByScopes([scope]);
169
197
  if (rows.length === 0)
@@ -192,8 +220,51 @@ export class MemoryStore {
192
220
  return rows.filter((row) => row.vectorDim !== expectedDim).length;
193
221
  }
194
222
  async hasMemory(id, scopes) {
223
+ for (let attempt = 0; attempt < 3; attempt++) {
224
+ const rows = await this.readByScopes(scopes);
225
+ if (rows.some((row) => row.id === id)) {
226
+ return true;
227
+ }
228
+ if (attempt < 2) {
229
+ await new Promise((resolve) => setTimeout(resolve, 50));
230
+ }
231
+ }
232
+ return false;
233
+ }
234
+ async updateMemoryUsage(id, projectScope, scopes) {
195
235
  const rows = await this.readByScopes(scopes);
196
- return rows.some((row) => row.id === id);
236
+ const match = rows.find((row) => row.id === id);
237
+ if (!match)
238
+ return;
239
+ const now = Date.now();
240
+ const newRecallCount = match.recallCount + 1;
241
+ let newProjectCount = match.projectCount;
242
+ let metadataJson = match.metadataJson;
243
+ if (match.scope === "global" && projectScope) {
244
+ const projects = extractRecalledProjects(metadataJson);
245
+ if (!projects.has(projectScope)) {
246
+ projects.add(projectScope);
247
+ if (projects.size > 100) {
248
+ const arr = Array.from(projects);
249
+ arr.splice(0, arr.length - 100);
250
+ metadataJson = JSON.stringify({ recalledProjects: arr });
251
+ }
252
+ else {
253
+ metadataJson = JSON.stringify({ recalledProjects: Array.from(projects) });
254
+ }
255
+ newProjectCount = projects.size;
256
+ }
257
+ }
258
+ await this.requireTable().delete(`id = '${escapeSql(id)}'`);
259
+ this.invalidateScope(match.scope);
260
+ await this.requireTable().add([{
261
+ ...match,
262
+ lastRecalled: now,
263
+ recallCount: newRecallCount,
264
+ projectCount: newProjectCount,
265
+ metadataJson,
266
+ }]);
267
+ this.invalidateScope(match.scope);
197
268
  }
198
269
  async listEvents(scopes, limit) {
199
270
  const rows = await this.readEventsByScopes(scopes);
@@ -408,6 +479,9 @@ export class MemoryStore {
408
479
  "scope",
409
480
  "importance",
410
481
  "timestamp",
482
+ "lastRecalled",
483
+ "recallCount",
484
+ "projectCount",
411
485
  "schemaVersion",
412
486
  "embeddingModel",
413
487
  "vectorDim",
@@ -445,6 +519,21 @@ export class MemoryStore {
445
519
  this.indexState.ftsError = error instanceof Error ? error.message : String(error);
446
520
  }
447
521
  }
522
+ async ensureEventTableCompatibility() {
523
+ const table = this.requireEventTable();
524
+ const schema = await table.schema();
525
+ const hasSourceColumn = schema.fields.some((field) => field.name === EVENTS_SOURCE_COLUMN);
526
+ if (hasSourceColumn) {
527
+ return;
528
+ }
529
+ try {
530
+ await table.addColumns([{ name: EVENTS_SOURCE_COLUMN, valueSql: "CAST(NULL AS STRING)" }]);
531
+ }
532
+ catch (error) {
533
+ const reason = error instanceof Error ? error.message : String(error);
534
+ throw new Error(`Failed to patch ${EVENTS_TABLE_NAME} schema for ${EVENTS_SOURCE_COLUMN}: ${reason}`);
535
+ }
536
+ }
448
537
  }
449
538
  function normalizeRow(row) {
450
539
  const vectorRaw = row.vector;
@@ -460,6 +549,9 @@ function normalizeRow(row) {
460
549
  scope: row.scope,
461
550
  importance: Number(row.importance ?? 0.5),
462
551
  timestamp: Number(row.timestamp ?? Date.now()),
552
+ lastRecalled: Number(row.lastRecalled ?? 0),
553
+ recallCount: Number(row.recallCount ?? 0),
554
+ projectCount: Number(row.projectCount ?? 0),
463
555
  schemaVersion: Number(row.schemaVersion ?? 1),
464
556
  embeddingModel: String(row.embeddingModel ?? "unknown"),
465
557
  vectorDim: Number(row.vectorDim ?? vector.length),
@@ -611,3 +703,15 @@ function bm25LikeScore(query, doc, idf) {
611
703
  }
612
704
  return 1 - Math.exp(-score);
613
705
  }
706
+ function extractRecalledProjects(metadataJson) {
707
+ try {
708
+ const metadata = JSON.parse(metadataJson);
709
+ if (metadata && Array.isArray(metadata.recalledProjects)) {
710
+ return new Set(metadata.recalledProjects);
711
+ }
712
+ }
713
+ catch {
714
+ // ignore parse errors
715
+ }
716
+ return new Set();
717
+ }
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lancedb-opencode-pro",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "LanceDB-backed long-term memory provider for OpenCode",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",