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
|
|
427
|
-
|
|
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
|
-
#
|
|
55
|
-
|
|
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"{
|
|
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])
|