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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Hall detection — routes drawer content to `emotions` / `technical` / `family` / `memory` / `identity` / `consciousness` / `creative` halls, enabling hall-based graph connectivity within wings (#835)

### Bug Fixes
- Repair `max_seq_id` corruption caused by `_fix_blob_seq_ids` misinterpreting chromadb 1.5.x's sysdb-10 BLOB format (`b'\x11\x11'` + ASCII digits) as legacy 0.6.x big-endian BLOBs. The shim now skips the `max_seq_id` table entirely and guards the `embeddings` branch with a prefix check. New subcommand `mempalace repair --mode max-seq-id [--from-sidecar <path>]` restores affected palaces. Fixes silent drawer-write drops that began after chromadb 1.5.x upgrades on palaces that still had BLOB-typed `max_seq_id` rows at migration time.
- Set `hnsw:space=cosine` metadata on all collection creation sites — fixes broken similarity scoring under ChromaDB's default L2 distance (#807, #218)
- File-level locking prevents duplicate drawers when agents mine the same file concurrently (#784, #826)
- Hybrid closet+drawer retrieval — closets boost ranking, never gate results (#795)
Expand Down
48 changes: 35 additions & 13 deletions mempalace/backends/chroma.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,21 @@ def _fix_blob_seq_ids(palace_path: str) -> None:
the Rust compactor to crash with "mismatched types; Rust type u64 (as SQL
type INTEGER) is not compatible with SQL type BLOB".

Scoped to the ``embeddings`` table only. The ``max_seq_id`` table used
to be included in this loop, but chromadb 1.5.x writes its own BLOB
format there (``b'\\x11\\x11'`` + 6 ASCII digits). Misinterpreting that
format via ``int.from_bytes(..., 'big')`` yields a ~1.23e18 integer
that silently suppresses every subsequent write for the affected
segment (``embeddings_queue`` filters on ``seq_id > start``). chromadb
owns the ``max_seq_id`` column — we leave it alone. Palaces already
poisoned by the old behaviour can be repaired via
``mempalace repair --mode max-seq-id``.

Defense-in-depth: rows with the sysdb-10 ``b'\\x11\\x11'`` prefix in
``embeddings`` are skipped rather than converted. Real 0.6.x BLOBs are
pure big-endian u64 with no text prefix, so the prefix check is a
no-op for genuine legacy data.

Must run BEFORE PersistentClient is created (the compactor fires on init).

Opening a Python sqlite3 connection against a ChromaDB 1.5.x WAL-mode
Expand All @@ -533,19 +548,26 @@ def _fix_blob_seq_ids(palace_path: str) -> None:
return
try:
with sqlite3.connect(db_path) as conn:
for table in ("embeddings", "max_seq_id"):
try:
rows = conn.execute(
f"SELECT rowid, seq_id FROM {table} WHERE typeof(seq_id) = 'blob'"
).fetchall()
except sqlite3.OperationalError:
continue
if not rows:
continue
updates = [(int.from_bytes(blob, byteorder="big"), rowid) for rowid, blob in rows]
conn.executemany(f"UPDATE {table} SET seq_id = ? WHERE rowid = ?", updates)
logger.info("Fixed %d BLOB seq_ids in %s", len(updates), table)
conn.commit()
try:
rows = conn.execute(
"SELECT rowid, seq_id FROM embeddings WHERE typeof(seq_id) = 'blob'"
).fetchall()
except sqlite3.OperationalError:
return
safe_rows = [(rowid, blob) for rowid, blob in rows if not blob.startswith(b"\x11\x11")]
skipped = len(rows) - len(safe_rows)
if skipped:
logger.warning(
"Skipped %d sysdb-10-format BLOB seq_id(s) in embeddings (not converting)",
skipped,
)
if safe_rows:
updates = [
(int.from_bytes(blob, byteorder="big"), rowid) for rowid, blob in safe_rows
]
conn.executemany("UPDATE embeddings SET seq_id = ? WHERE rowid = ?", updates)
logger.info("Fixed %d BLOB seq_ids in embeddings", len(updates))
conn.commit()
except Exception:
logger.exception("Could not fix BLOB seq_ids in %s", db_path)
return
Expand Down
52 changes: 51 additions & 1 deletion mempalace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,20 @@ def cmd_repair(args):
palace_path = os.path.abspath(
os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path
)

if getattr(args, "mode", "legacy") == "max-seq-id":
from .repair import repair_max_seq_id

repair_max_seq_id(
palace_path,
segment=getattr(args, "segment", None),
from_sidecar=getattr(args, "from_sidecar", None),
backup=getattr(args, "backup", True),
dry_run=getattr(args, "dry_run", False),
assume_yes=getattr(args, "yes", False),
)
return

db_path = os.path.join(palace_path, "chroma.sqlite3")

if not os.path.isdir(palace_path):
Expand Down Expand Up @@ -1117,7 +1131,10 @@ def main():
# repair
p_repair = sub.add_parser(
"repair",
help="Rebuild palace vector index from stored data (fixes segfaults after corruption)",
help=(
"Rebuild palace vector index (legacy mode) or un-poison max_seq_id rows "
"(--mode max-seq-id)"
),
)
p_repair.add_argument(
"--yes", action="store_true", help="Skip confirmation for destructive changes"
Expand All @@ -1132,6 +1149,39 @@ def main():
"the palace really contains that count."
),
)
p_repair.add_argument(
"--mode",
choices=["legacy", "max-seq-id"],
default="legacy",
help=(
"legacy: full-palace rebuild (default). "
"max-seq-id: un-poison max_seq_id rows corrupted by the legacy 0.6.x shim."
),
)
p_repair.add_argument(
"--segment",
default=None,
help="Segment UUID filter for --mode max-seq-id (repairs only that segment).",
)
p_repair.add_argument(
"--from-sidecar",
default=None,
help=(
"Path to a pre-corruption chroma.sqlite3 sidecar (for --mode max-seq-id); "
"clean values are copied from its max_seq_id table verbatim."
),
)
p_repair.add_argument(
"--backup",
action=argparse.BooleanOptionalAction,
default=True,
help="Back up SQLite before mutation (default: on)",
)
p_repair.add_argument(
"--dry-run",
action="store_true",
help="Print detected poisoned rows and exit without mutation (--mode max-seq-id only)",
)

# repair-status — read-only HNSW capacity health check (#1222)
sub.add_parser(
Expand Down
Loading