From 2f40ece2ab069f3dd91f9cba4c02947be52b76a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 08:26:04 +0000 Subject: [PATCH] fix: remove FTS5 dependency to fix SqlClient Service not found at startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration 020 created a memory_fts FTS5 virtual table and three triggers. SQLite distributions without FTS5 support (Node.js built-in SQLite, some Bun builds) fail the migration mid-way. Because the migration runs inside the Effect setup layer that is composed with Layer.provideMerge alongside makeRuntimeSqliteLayer, a migration failure causes the entire persistence layer construction to fail — SqlClient is never registered in the Effect context, so every downstream service reports the misleading error: Service not found: effect/sql/SqlClient Fix: - Drop the memory_fts virtual table, insert/delete/update triggers from migration 020 (no schema change is needed for already-run migrations since the tables were in the same transaction and will re-run cleanly on a fresh DB, and users who ran it successfully already have those tables harmlessly present) - Replace the FTS5 MATCH query in ProjectMemoryService.search with three LIKE predicates across title, content, and tags columns - Remove the FTS5 index rebuild from ProjectMemoryService.index (forceReindex becomes a no-op, which is fine for LIKE search) bun typecheck passes (7/7 packages). https://claude.ai/code/session_01Nxa3JfS5jZVHsnJmaaHvbW --- .../memory/Services/ProjectMemoryService.ts | 85 ++++++++++--------- .../Migrations/020_NewFeatureTables.ts | 29 ------- 2 files changed, 44 insertions(+), 70 deletions(-) diff --git a/apps/server/src/memory/Services/ProjectMemoryService.ts b/apps/server/src/memory/Services/ProjectMemoryService.ts index 7463e0deae..fc1605eabe 100644 --- a/apps/server/src/memory/Services/ProjectMemoryService.ts +++ b/apps/server/src/memory/Services/ProjectMemoryService.ts @@ -56,46 +56,52 @@ const makeProjectMemoryService = Effect.gen(function* () { const search: ProjectMemoryServiceShape["search"] = (input) => Effect.gen(function* () { const start = Date.now(); - const kindClause = input.kind ? "AND m.kind = ?" : ""; - const queryParams: Array = input.kind - ? [input.query, input.projectId, input.kind, input.limit] - : [input.query, input.projectId, input.limit]; - // Use FTS5 for full-text search - const rows = yield* sql.unsafe<{ - id: string; - project_id: string; - thread_id: string | null; - kind: string; - title: string; - content: string; - tags: string; - relevance_score: number; - access_count: number; - created_at: string; - updated_at: string; - expires_at: string | null; - rank: number; - }>( - `SELECT m.*, fts.rank - FROM memory_fts fts - JOIN memory_entries m ON m.rowid = fts.rowid - WHERE memory_fts MATCH ? - AND m.project_id = ? - ${kindClause} - AND (m.expires_at IS NULL OR m.expires_at > datetime('now')) - ORDER BY fts.rank - LIMIT ?`, - queryParams, - ); + const pattern = `%${input.query}%`; + const rows = yield* (input.kind + ? sql<{ + id: string; + project_id: string; + thread_id: string | null; + kind: string; + title: string; + content: string; + tags: string; + relevance_score: number; + access_count: number; + created_at: string; + updated_at: string; + expires_at: string | null; + }>`SELECT * FROM memory_entries + WHERE project_id = ${input.projectId} + AND kind = ${input.kind} + AND (title LIKE ${pattern} OR content LIKE ${pattern} OR tags LIKE ${pattern}) + AND (expires_at IS NULL OR expires_at > datetime('now')) + ORDER BY relevance_score DESC + LIMIT ${input.limit}` + : sql<{ + id: string; + project_id: string; + thread_id: string | null; + kind: string; + title: string; + content: string; + tags: string; + relevance_score: number; + access_count: number; + created_at: string; + updated_at: string; + expires_at: string | null; + }>`SELECT * FROM memory_entries + WHERE project_id = ${input.projectId} + AND (title LIKE ${pattern} OR content LIKE ${pattern} OR tags LIKE ${pattern}) + AND (expires_at IS NULL OR expires_at > datetime('now')) + ORDER BY relevance_score DESC + LIMIT ${input.limit}`); // Increment access count if (rows.length > 0) { - const placeholders = rows.map(() => "?").join(","); const ids = rows.map((r) => r.id); - yield* sql.unsafe( - `UPDATE memory_entries SET access_count = access_count + 1 WHERE id IN (${placeholders})`, - ids, - ); + yield* sql`UPDATE memory_entries SET access_count = access_count + 1 WHERE id IN ${sql.in(ids)}`; } const results: MemorySearchResult[] = rows.map((r) => ({ @@ -113,7 +119,7 @@ const makeProjectMemoryService = Effect.gen(function* () { updatedAt: r.updated_at, expiresAt: (r.expires_at ?? null) as MemoryEntry["expiresAt"], } as MemoryEntry, - matchScore: -r.rank, // FTS5 rank is negative; flip for display + matchScore: r.relevance_score, matchSnippet: null, })); @@ -169,10 +175,7 @@ const makeProjectMemoryService = Effect.gen(function* () { const index: ProjectMemoryServiceShape["index"] = (input) => Effect.gen(function* () { const start = Date.now(); - if (input.forceReindex) { - // Rebuild FTS index - yield* sql.unsafe("INSERT INTO memory_fts(memory_fts) VALUES('rebuild')"); - } + // forceReindex is a no-op with LIKE-based search (no external index to rebuild) const rows = yield* sql<{ count: number; }>`SELECT COUNT(*) as count FROM memory_entries WHERE project_id = ${input.projectId}`; diff --git a/apps/server/src/persistence/Migrations/020_NewFeatureTables.ts b/apps/server/src/persistence/Migrations/020_NewFeatureTables.ts index f829eef066..805924136a 100644 --- a/apps/server/src/persistence/Migrations/020_NewFeatureTables.ts +++ b/apps/server/src/persistence/Migrations/020_NewFeatureTables.ts @@ -222,35 +222,6 @@ export default Effect.gen(function* () { CREATE INDEX IF NOT EXISTS idx_memory_entries_kind ON memory_entries(kind) `; - yield* sql` - CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5( - title, - content, - tags, - content=memory_entries, - content_rowid=rowid - ) - `; - - yield* sql.unsafe(` - CREATE TRIGGER IF NOT EXISTS memory_fts_insert AFTER INSERT ON memory_entries BEGIN - INSERT INTO memory_fts(rowid, title, content, tags) VALUES (new.rowid, new.title, new.content, new.tags); - END - `); - - yield* sql.unsafe(` - CREATE TRIGGER IF NOT EXISTS memory_fts_delete AFTER DELETE ON memory_entries BEGIN - INSERT INTO memory_fts(memory_fts, rowid, title, content, tags) VALUES ('delete', old.rowid, old.title, old.content, old.tags); - END - `); - - yield* sql.unsafe(` - CREATE TRIGGER IF NOT EXISTS memory_fts_update AFTER UPDATE ON memory_entries BEGIN - INSERT INTO memory_fts(memory_fts, rowid, title, content, tags) VALUES ('delete', old.rowid, old.title, old.content, old.tags); - INSERT INTO memory_fts(rowid, title, content, tags) VALUES (new.rowid, new.title, new.content, new.tags); - END - `); - // ── Presence / Session Sharing ───────────────────────────────────── yield* sql`