Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). Versions follow

---

## [0.2.1] - 2026-03-21

### Fixed

- Schema migration: databases created with v0.1.x now automatically receive the `lastRecalled`, `recallCount`, and `projectCount` columns on first startup with v0.2.x. Without this patch, any existing database would fail with a LanceDB schema error (`No field named "lastRecalled"`) on every query.

---

## [0.2.0] - 2026-03-21

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lancedb-opencode-pro",
"version": "0.2.0",
"version": "0.2.1",
"description": "LanceDB-backed long-term memory provider for OpenCode",
"type": "module",
"main": "dist/index.js",
Expand Down
32 changes: 32 additions & 0 deletions src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class MemoryStore {
await this.eventTable.delete("id = '__bootstrap__'");
}

await this.ensureMemoriesTableCompatibility();
await this.ensureEventTableCompatibility();

await this.ensureIndexes();
Expand Down Expand Up @@ -593,6 +594,37 @@ export class MemoryStore {
}
}

private async ensureMemoriesTableCompatibility(): Promise<void> {
const table = this.requireTable();
const schema = await table.schema();
const fieldNames = new Set(schema.fields.map((field) => field.name));

const missing: Array<{ name: string; valueSql: string }> = [];
if (!fieldNames.has("lastRecalled")) {
missing.push({ name: "lastRecalled", valueSql: "CAST(0 AS BIGINT)" });
}
if (!fieldNames.has("recallCount")) {
missing.push({ name: "recallCount", valueSql: "CAST(0 AS INT)" });
}
if (!fieldNames.has("projectCount")) {
missing.push({ name: "projectCount", valueSql: "CAST(0 AS INT)" });
}

if (missing.length === 0) {
return;
}

try {
await table.addColumns(missing);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
const names = missing.map((col) => col.name).join(", ");
throw new Error(
`Failed to patch ${TABLE_NAME} schema for columns [${names}]: ${reason}`,
);
}
}

private async ensureEventTableCompatibility(): Promise<void> {
const table = this.requireEventTable();
const schema = await table.schema();
Expand Down
29 changes: 29 additions & 0 deletions test/foundation/foundation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createTestStore,
createVector,
seedLegacyEffectivenessEventsTable,
seedLegacyMemoriesTable,
} from "../setup.js";

test("write-read persistence keeps field integrity across multiple scopes", async () => {
Expand Down Expand Up @@ -302,6 +303,34 @@ test("store init patches legacy effectiveness_events schema before writing recal
}
});

test("store init patches legacy memories schema by adding lastRecalled recallCount and projectCount", async () => {
const dbPath = await createTempDbPath();

try {
await seedLegacyMemoriesTable(dbPath);
const { store } = await createTestStore(dbPath);

const results = await store.search({
query: "legacy memory",
queryVector: createVector(384, 0.5),
scopes: ["project:legacy"],
limit: 5,
vectorWeight: 0.7,
bm25Weight: 0.3,
minScore: 0,
});

assert.equal(results.length, 1);
const record = results[0].record;
assert.equal(record.id, "legacy-memory-1");
assert.equal(record.lastRecalled, 0, "patched column should default to 0");
assert.equal(record.recallCount, 0, "patched column should default to 0");
assert.equal(record.projectCount, 0, "patched column should default to 0");
} finally {
await cleanupDbPath(dbPath);
}
});

test("search scoring uses normalized RRF fusion when recency and importance boosts are disabled", async () => {
const { store, dbPath } = await createTestStore();

Expand Down
20 changes: 20 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ export async function cleanupDbPath(dbPath: string): Promise<void> {
}
}

export async function seedLegacyMemoriesTable(dbPath: string, scope = "project:legacy"): Promise<void> {
const lancedb = await import("@lancedb/lancedb");
const connection = await lancedb.connect(dbPath);
await connection.createTable("memories", [
{
id: "legacy-memory-1",
text: "Legacy memory without usage tracking fields",
vector: Array.from({ length: DEFAULT_VECTOR_DIM }, () => 0.1),
category: "fact",
scope,
importance: 0.5,
timestamp: 1_000,
schemaVersion: 1,
embeddingModel: "test-embedding-model",
vectorDim: DEFAULT_VECTOR_DIM,
metadataJson: "{}",
},
]);
}

export async function seedLegacyEffectivenessEventsTable(dbPath: string, scope = "project:legacy"): Promise<void> {
const lancedb = await import("@lancedb/lancedb");
const connection = await lancedb.connect(dbPath);
Expand Down
Loading