get-claudia 1.55.7 → 1.55.8

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/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to Claudia will be documented in this file.
4
4
 
5
+ ## 1.55.8 (2026-03-15)
6
+
7
+ ### The Vector Search Fix
8
+
9
+ v1.55.7 fixed FTS5 recall but broke vector/semantic search for every user. Three raw `sqlite3.connect()` calls skipped loading the `sqlite_vec` extension, and three KNN queries lacked the required `k = ?` constraint for JOINs. Net effect: 0% embedding coverage and silent fallback to keyword matching.
10
+
11
+ - **`load_sqlite_vec()` helper** -- Extracted the vec0 extension loading logic from `Database._get_connection()` into a standalone public function. Any raw connection that touches vec0 tables calls this one function. Eliminates the entire class of "forgot to load extension" bugs.
12
+ - **Backfill worker fix** -- `_backfill_worker()` now loads sqlite_vec on its raw connection. Previously it crashed within 60ms of starting because it couldn't query or write to vec0 tables. Degrades gracefully if the extension isn't available.
13
+ - **Index repair fix** -- `_check_and_repair_indexes()` now loads sqlite_vec on its raw connection. Previously it always reported 0 embeddings (triggering unnecessary backfill every startup). Improved exception handling distinguishes "no such table" from "no such module."
14
+ - **KNN `k = ?` constraint** -- Added `AND k = ?` to all three `embedding MATCH` queries in recall.py (`recall()`, `recall_episodes()`, `search_reflections()`). vec0's KNN queries require this constraint when JOINs are present because SQLite's query planner can't push an outer `LIMIT` into the virtual table scan.
15
+ - **Smarter briefing message** -- Embedding health check now reads `_meta['indexes_repaired']` to distinguish "backfill in progress" from "backfill never started." No longer tells users to "Start Ollama" when the real problem was a code bug.
16
+ - **9 new tests** -- Helper extraction, Database integration, backfill/repair patterns, KNN constraint behavior (both success and documented failure without `k`). All 615 tests pass.
17
+
5
18
  ## 1.55.7 (2026-03-15)
6
19
 
7
20
  ### The Recall Recovery Release
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
  from .config import get_config, set_project_id
21
21
  from .daemon.health import start_health_server, stop_health_server
22
22
  from .daemon.scheduler import start_scheduler, stop_scheduler
23
- from .database import get_db
23
+ from .database import get_db, load_sqlite_vec
24
24
  from .mcp.server import run_server as run_mcp_server
25
25
 
26
26
  logger = logging.getLogger(__name__)
@@ -401,6 +401,7 @@ def _check_and_repair_indexes(db_path: Path) -> None:
401
401
  try:
402
402
  conn = sqlite3.connect(str(db_path), timeout=10)
403
403
  conn.row_factory = sqlite3.Row
404
+ load_sqlite_vec(conn)
404
405
 
405
406
  # Count memories
406
407
  mem_row = conn.execute("SELECT COUNT(*) as c FROM memories WHERE invalidated_at IS NULL").fetchone()
@@ -423,8 +424,13 @@ def _check_and_repair_indexes(db_path: Path) -> None:
423
424
  try:
424
425
  emb_row = conn.execute("SELECT COUNT(*) as c FROM memory_embeddings").fetchone()
425
426
  emb_count = emb_row["c"] if emb_row else 0
426
- except Exception:
427
- pass # Vec0 table might not exist
427
+ except sqlite3.OperationalError as e:
428
+ if "no such table" in str(e):
429
+ pass # Fresh install, vec0 tables not yet created
430
+ elif "no such module" in str(e):
431
+ logger.debug("sqlite_vec not loaded, cannot count embeddings")
432
+ else:
433
+ logger.warning(f"Unexpected error counting embeddings: {e}")
428
434
 
429
435
  conn.close()
430
436
 
@@ -504,6 +510,13 @@ def _auto_backfill_embeddings(db_path: Path, mem_count: int, emb_count: int) ->
504
510
 
505
511
  conn = sqlite3.connect(str(db_path), timeout=30)
506
512
  conn.row_factory = sqlite3.Row
513
+ if not load_sqlite_vec(conn):
514
+ logger.warning(
515
+ "sqlite_vec not available in backfill thread. "
516
+ "Cannot write to vec0 tables. Skipping embedding backfill."
517
+ )
518
+ conn.close()
519
+ return
507
520
 
508
521
  # Find memories missing embeddings
509
522
  missing = conn.execute(
@@ -25,6 +25,73 @@ from .config import get_config
25
25
  logger = logging.getLogger(__name__)
26
26
 
27
27
 
28
+ def load_sqlite_vec(conn: sqlite3.Connection) -> bool:
29
+ """Load the sqlite-vec extension on a connection.
30
+
31
+ Tries two methods:
32
+ 1. sqlite_vec Python package (recommended, works everywhere including Python 3.13+)
33
+ 2. Native extension loading (for systems with pre-installed sqlite-vec)
34
+
35
+ Returns True if vec0 is available, False otherwise. Never raises.
36
+ """
37
+ # Method 1: Try sqlite_vec Python package
38
+ try:
39
+ import sqlite_vec
40
+ if hasattr(conn, "enable_load_extension"):
41
+ conn.enable_load_extension(True)
42
+ sqlite_vec.load(conn)
43
+ if hasattr(conn, "enable_load_extension"):
44
+ conn.enable_load_extension(False)
45
+ logger.debug("Loaded sqlite-vec via Python package")
46
+ return True
47
+ except ImportError:
48
+ logger.debug("sqlite_vec package not installed")
49
+ except Exception as e:
50
+ logger.warning(f"sqlite_vec package installed but load() failed: {e}")
51
+
52
+ # Method 2: Try native extension loading
53
+ try:
54
+ conn.enable_load_extension(True)
55
+ sqlite_vec_paths = ["vec0"] # System-wide
56
+
57
+ if sys.platform == "win32":
58
+ try:
59
+ import sqlite_vec as _sv
60
+ pkg_dir = Path(_sv.__file__).parent
61
+ for dll in pkg_dir.rglob("vec0*"):
62
+ if dll.suffix in (".dll", ".so"):
63
+ sqlite_vec_paths.append(str(dll.with_suffix("")))
64
+ except ImportError:
65
+ pass
66
+ sqlite_vec_paths.extend([
67
+ str(Path(sys.executable).parent / "DLLs" / "vec0"),
68
+ str(Path.home() / ".local" / "lib" / "sqlite-vec" / "vec0"),
69
+ ])
70
+ else:
71
+ sqlite_vec_paths.extend([
72
+ "/usr/local/lib/sqlite-vec/vec0",
73
+ "/opt/homebrew/lib/sqlite-vec/vec0",
74
+ str(Path.home() / ".local" / "lib" / "sqlite-vec" / "vec0"),
75
+ ])
76
+
77
+ for path in sqlite_vec_paths:
78
+ try:
79
+ conn.load_extension(path)
80
+ logger.debug(f"Loaded sqlite-vec from {path}")
81
+ conn.enable_load_extension(False)
82
+ return True
83
+ except sqlite3.OperationalError:
84
+ continue
85
+
86
+ conn.enable_load_extension(False)
87
+ except AttributeError:
88
+ logger.debug("enable_load_extension not available (Python 3.13+)")
89
+ except Exception as e:
90
+ logger.debug(f"Extension loading failed: {e}")
91
+
92
+ return False
93
+
94
+
28
95
  class Database:
29
96
  """Thread-safe SQLite database with sqlite-vec support"""
30
97
 
@@ -51,72 +118,8 @@ class Database:
51
118
  # Recover any uncommitted WAL writes from a previous crashed daemon
52
119
  conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
53
120
 
54
- # Try to load sqlite-vec for vector search
55
- # Priority: sqlite_vec Python package first (works on Python 3.13+),
56
- # then fall back to native extension loading
57
- loaded = False
58
-
59
- # Method 1: Try sqlite_vec Python package (recommended, works everywhere)
60
- # Python 3.14+ requires explicit enable_load_extension() before any
61
- # extension loading, even via the sqlite_vec helper package.
62
- try:
63
- import sqlite_vec
64
- if hasattr(conn, "enable_load_extension"):
65
- conn.enable_load_extension(True)
66
- sqlite_vec.load(conn)
67
- if hasattr(conn, "enable_load_extension"):
68
- conn.enable_load_extension(False)
69
- loaded = True
70
- logger.debug("Loaded sqlite-vec via Python package")
71
- except ImportError:
72
- logger.debug("sqlite_vec package not installed")
73
- except Exception as e:
74
- logger.warning(f"sqlite_vec package installed but load() failed: {e}")
75
-
76
- # Method 2: Try native extension loading (for systems with pre-installed sqlite-vec)
77
- if not loaded:
78
- try:
79
- conn.enable_load_extension(True)
80
- sqlite_vec_paths = ["vec0"] # System-wide
81
-
82
- if sys.platform == "win32":
83
- # Try to find vec0.dll in the sqlite-vec package directory
84
- try:
85
- import sqlite_vec as _sv
86
- pkg_dir = Path(_sv.__file__).parent
87
- for dll in pkg_dir.rglob("vec0*"):
88
- if dll.suffix in (".dll", ".so"):
89
- sqlite_vec_paths.append(str(dll.with_suffix("")))
90
- except ImportError:
91
- pass
92
- sqlite_vec_paths.extend([
93
- str(Path(sys.executable).parent / "DLLs" / "vec0"),
94
- str(Path.home() / ".local" / "lib" / "sqlite-vec" / "vec0"),
95
- ])
96
- else:
97
- sqlite_vec_paths.extend([
98
- "/usr/local/lib/sqlite-vec/vec0",
99
- "/opt/homebrew/lib/sqlite-vec/vec0",
100
- str(Path.home() / ".local" / "lib" / "sqlite-vec" / "vec0"),
101
- ])
102
-
103
- for path in sqlite_vec_paths:
104
- try:
105
- conn.load_extension(path)
106
- loaded = True
107
- logger.debug(f"Loaded sqlite-vec from {path}")
108
- break
109
- except sqlite3.OperationalError:
110
- continue
111
-
112
- conn.enable_load_extension(False)
113
- except AttributeError:
114
- # Python 3.13+ may not have enable_load_extension
115
- logger.debug("enable_load_extension not available (Python 3.13+)")
116
- except Exception as e:
117
- logger.debug(f"Extension loading failed: {e}")
118
-
119
- if not loaded:
121
+ # Load sqlite-vec for vector search
122
+ if not load_sqlite_vec(conn):
120
123
  if sys.platform == "win32":
121
124
  logger.warning(
122
125
  "sqlite-vec not available. Vector search will be disabled. "
@@ -3346,10 +3346,25 @@ def _build_briefing() -> str:
3346
3346
  coverage = (emb_c / mem_c) * 100
3347
3347
  if coverage < 90:
3348
3348
  gap = mem_c - emb_c
3349
+ # Check _meta for backfill status to give accurate guidance
3350
+ status_hint = "Start Ollama and restart daemon to generate embeddings."
3351
+ try:
3352
+ repair_row = db.execute(
3353
+ "SELECT value FROM _meta WHERE key = 'indexes_repaired'",
3354
+ fetch=True,
3355
+ )
3356
+ if repair_row and repair_row[0]["value"]:
3357
+ repair_info = repair_row[0]["value"]
3358
+ if "backfill started" in repair_info:
3359
+ status_hint = "Backfill was started on this run. Check daemon logs for progress."
3360
+ elif coverage > 0:
3361
+ status_hint = "Partial embeddings exist. Restart daemon to trigger backfill for the rest."
3362
+ except Exception:
3363
+ pass
3349
3364
  lines.append(
3350
3365
  f"**Embedding coverage:** {emb_c}/{mem_c} ({coverage:.0f}%). "
3351
3366
  f"{gap} memories lack vector embeddings. "
3352
- f"{'Backfill is running in background.' if coverage > 0 else 'Start Ollama and restart daemon to generate embeddings.'} "
3367
+ f"{status_hint} "
3353
3368
  f"Recall uses keyword fallback for unembedded memories."
3354
3369
  )
3355
3370
  except Exception as e:
@@ -145,9 +145,11 @@ class RecallService:
145
145
  LEFT JOIN memory_entities me2 ON m.id = me2.memory_id
146
146
  LEFT JOIN entities e ON me2.entity_id = e.id
147
147
  WHERE me.embedding MATCH ?
148
+ AND k = ?
148
149
  """
149
150
  )
150
151
  params.append(json.dumps(query_embedding))
152
+ params.append(limit * 2)
151
153
 
152
154
  self._apply_filters(sql_parts, params, memory_types, min_importance, date_after, date_before, about_entity, include_archived)
153
155
  sql_parts.append("GROUP BY m.id ORDER BY vector_score DESC LIMIT ?")
@@ -1368,11 +1370,12 @@ class RecallService:
1368
1370
  FROM episode_embeddings ee
1369
1371
  JOIN episodes e ON e.id = ee.episode_id
1370
1372
  WHERE ee.embedding MATCH ?
1373
+ AND k = ?
1371
1374
  AND e.is_summarized = 1
1372
1375
  ORDER BY relevance DESC
1373
1376
  LIMIT ?
1374
1377
  """,
1375
- (json.dumps(query_embedding), limit),
1378
+ (json.dumps(query_embedding), limit, limit),
1376
1379
  fetch=True,
1377
1380
  ) or []
1378
1381
  except Exception as e:
@@ -2286,8 +2289,9 @@ class RecallService:
2286
2289
  JOIN reflections r ON r.id = re.reflection_id
2287
2290
  LEFT JOIN entities e ON r.about_entity_id = e.id
2288
2291
  WHERE re.embedding MATCH ?
2292
+ AND k = ?
2289
2293
  """
2290
- params: list = [json.dumps(query_embedding)]
2294
+ params: list = [json.dumps(query_embedding), limit]
2291
2295
 
2292
2296
  if reflection_types:
2293
2297
  placeholders = ", ".join(["?" for _ in reflection_types])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-claudia",
3
- "version": "1.55.7",
3
+ "version": "1.55.8",
4
4
  "description": "An AI assistant who learns how you work.",
5
5
  "keywords": [
6
6
  "claudia",