Skip to content

Commit d3a2d22

Browse files
jpheinclaude
andcommitted
fix: block ChromaDB 1.5.7 segfault triggers and search None crash
Two in-code triggers for ChromaDB 1.5.7 Rust-bindings segfaults were collapsed under the fork's generic "call mempalace_reconnect" workaround. Both are fixable: - get_or_create_collection(name, metadata={"hnsw:space":"cosine"}) on an existing collection with differing metadata crashes the Rust compactor. Fetch first; only pass metadata on fresh creation. Applied in ChromaBackend.get_collection and mcp_server._get_collection. - Opening a Python sqlite3 connection against the live chroma.sqlite3 corrupts the next PersistentClient call. _fix_blob_seq_ids now writes a .blob_seq_ids_migrated marker after success and skips the sqlite open on subsequent runs; already-migrated palaces can touch the marker to opt into the fast path. Also: - searcher.print-path guards against None drawer metadata (was raising AttributeError mid-render after partial results). - hook scripts marked executable. - gitignore the .claude-plugin/venv symlink used to point hook auto-detection at the repo's venv. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7a454f6 commit d3a2d22

6 files changed

Lines changed: 39 additions & 8 deletions

File tree

.claude-plugin/hooks/mempal-precompact-hook.sh

100644100755
File mode changed.

.claude-plugin/hooks/mempal-stop-hook.sh

100644100755
File mode changed.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ coverage.xml
3333
# Virtual environments
3434
.venv/
3535
venv/
36+
.claude-plugin/venv
3637

3738
# ChromaDB local data
3839
*.sqlite3-journal

mempalace/backends/chroma.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
logger = logging.getLogger(__name__)
1212

1313

14+
_BLOB_FIX_MARKER = ".blob_seq_ids_migrated"
15+
16+
1417
def _fix_blob_seq_ids(palace_path: str):
1518
"""Fix ChromaDB 0.6.x -> 1.5.x migration bug: BLOB seq_ids -> INTEGER.
1619
@@ -20,25 +23,40 @@ def _fix_blob_seq_ids(palace_path: str):
2023
type INTEGER) is not compatible with SQL type BLOB".
2124
2225
Must run BEFORE PersistentClient is created (the compactor fires on init).
26+
27+
Opening a Python sqlite3 connection against a ChromaDB 1.5.x WAL-mode
28+
database leaves state that segfaults the next PersistentClient call.
29+
After the migration has run once successfully, a marker file is written
30+
so subsequent opens skip the sqlite connection entirely.
2331
"""
2432
db_path = os.path.join(palace_path, "chroma.sqlite3")
2533
if not os.path.isfile(db_path):
2634
return
35+
marker = os.path.join(palace_path, _BLOB_FIX_MARKER)
36+
if os.path.isfile(marker):
37+
return
2738
try:
2839
with sqlite3.connect(db_path) as conn:
40+
table_rows: dict = {}
2941
for table in ("embeddings", "max_seq_id"):
3042
try:
3143
rows = conn.execute(
3244
f"SELECT rowid, seq_id FROM {table} WHERE typeof(seq_id) = 'blob'"
3345
).fetchall()
3446
except sqlite3.OperationalError:
3547
continue
36-
if not rows:
37-
continue
48+
if rows:
49+
table_rows[table] = rows
50+
for table, rows in table_rows.items():
3851
updates = [(int.from_bytes(blob, byteorder="big"), rowid) for rowid, blob in rows]
3952
conn.executemany(f"UPDATE {table} SET seq_id = ? WHERE rowid = ?", updates)
4053
logger.info("Fixed %d BLOB seq_ids in %s", len(updates), table)
4154
conn.commit()
55+
try:
56+
with open(marker, "w", encoding="utf-8") as f:
57+
f.write("migrated\n")
58+
except OSError:
59+
pass
4260
except Exception:
4361
logger.exception("Could not fix BLOB seq_ids in %s", db_path)
4462

@@ -125,9 +143,15 @@ def get_collection(self, palace_path: str, collection_name: str, create: bool =
125143

126144
client = self._client(palace_path)
127145
if create:
128-
collection = client.get_or_create_collection(
129-
collection_name, metadata={"hnsw:space": "cosine"}
130-
)
146+
# ChromaDB 1.5.x segfaults when get_or_create_collection is called
147+
# with metadata that differs from an existing collection's metadata.
148+
# Fetch first; only pass hnsw:space when actually creating fresh.
149+
try:
150+
collection = client.get_collection(collection_name)
151+
except Exception:
152+
collection = client.create_collection(
153+
collection_name, metadata={"hnsw:space": "cosine"}
154+
)
131155
else:
132156
collection = client.get_collection(collection_name)
133157
return ChromaCollection(collection)

mempalace/mcp_server.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,16 @@ def _get_collection(create=False):
202202
try:
203203
client = _get_client()
204204
if create:
205-
_collection_cache = ChromaCollection(
206-
client.get_or_create_collection(
205+
# ChromaDB 1.5.x segfaults when get_or_create_collection is called
206+
# with metadata that differs from an existing collection's metadata.
207+
# Fetch first; only pass hnsw:space when actually creating fresh.
208+
try:
209+
raw_col = client.get_collection(_config.collection_name)
210+
except Exception:
211+
raw_col = client.create_collection(
207212
_config.collection_name, metadata={"hnsw:space": "cosine"}
208213
)
209-
)
214+
_collection_cache = ChromaCollection(raw_col)
210215
_metadata_cache = None
211216
_metadata_cache_time = 0
212217
elif _collection_cache is None:

mempalace/searcher.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ def search(query: str, palace_path: str, wing: str = None, room: str = None, n_r
270270

271271
for i, (doc, meta, dist) in enumerate(zip(docs, metas, dists), 1):
272272
similarity = round(max(0.0, 1 - dist), 3)
273+
meta = meta or {}
273274
source = Path(meta.get("source_file", "?")).name
274275
wing_name = meta.get("wing", "?")
275276
room_name = meta.get("room", "?")

0 commit comments

Comments
 (0)