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
8 changes: 6 additions & 2 deletions mempalace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,10 @@ def cmd_repair(args):
check_extraction_safety,
)

config = MempalaceConfig()
collection_name = config.collection_name
palace_path = os.path.abspath(
os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path
os.path.expanduser(args.palace) if args.palace else config.palace_path
)

if getattr(args, "mode", "legacy") == "max-seq-id":
Expand Down Expand Up @@ -749,7 +751,7 @@ def cmd_repair(args):

# Try to read existing drawers
try:
col = backend.get_collection(palace_path, "mempalace_drawers")
col = backend.get_collection(palace_path, collection_name)
total = col.count()
print(f" Drawers found: {total}")
except Exception as e:
Expand Down Expand Up @@ -784,6 +786,7 @@ def cmd_repair(args):
palace_path,
len(all_ids),
confirm_truncation_ok=getattr(args, "confirm_truncation_ok", False),
collection_name=collection_name,
)
except TruncationDetected as e:
print(e.message)
Expand All @@ -810,6 +813,7 @@ def cmd_repair(args):
all_docs,
all_metas,
batch_size,
collection_name=collection_name,
progress=print,
)
except RebuildCollectionError as e:
Expand Down
8 changes: 8 additions & 0 deletions mempalace/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import json
import os
import re
from functools import lru_cache
from pathlib import Path


Expand Down Expand Up @@ -127,6 +128,13 @@ def sanitize_content(value: str, max_length: int = 100_000) -> str:
DEFAULT_PALACE_PATH = os.path.expanduser("~/.mempalace/palace")
DEFAULT_COLLECTION_NAME = "mempalace_drawers"


@lru_cache(maxsize=1)
def get_configured_collection_name() -> str:
"""Return the configured drawer collection name without repeated config-file reads."""
return MempalaceConfig().collection_name


DEFAULT_TOPIC_WINGS = [
"emotions",
"consciousness",
Expand Down
61 changes: 53 additions & 8 deletions mempalace/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def _refresh_vector_disabled_flag() -> None:
"""
global _vector_disabled, _vector_disabled_reason, _vector_capacity_status
try:
info = hnsw_capacity_status(_config.palace_path, "mempalace_drawers")
info = hnsw_capacity_status(_config.palace_path, _config.collection_name)
except Exception:
logger.debug("HNSW capacity probe raised", exc_info=True)
return
Expand Down Expand Up @@ -490,6 +490,7 @@ def _tool_status_via_sqlite() -> dict:
db_path = os.path.join(_config.palace_path, "chroma.sqlite3")
if not os.path.isfile(db_path):
return _no_palace()
collection_name = _config.collection_name

wings: dict = {}
rooms: dict = {}
Expand All @@ -503,8 +504,9 @@ def _tool_status_via_sqlite() -> dict:
FROM embeddings e
JOIN segments s ON e.segment_id = s.id
JOIN collections c ON s.collection = c.id
WHERE c.name = 'mempalace_drawers'
"""
WHERE c.name = ?
""",
(collection_name,),
).fetchone()
total = int(row[0]) if row and row[0] is not None else 0
for key, target in (("wing", wings), ("room", rooms)):
Expand All @@ -515,12 +517,12 @@ def _tool_status_via_sqlite() -> dict:
JOIN embeddings e ON em.id = e.id
JOIN segments s ON e.segment_id = s.id
JOIN collections c ON s.collection = c.id
WHERE c.name = 'mempalace_drawers'
WHERE c.name = ?
AND em.key = ?
AND em.string_value IS NOT NULL
GROUP BY em.string_value
""",
(key,),
(collection_name, key),
):
target[value] = count
finally:
Expand Down Expand Up @@ -720,6 +722,7 @@ def tool_search(
n_results=limit,
max_distance=dist,
vector_disabled=_vector_disabled,
collection_name=_config.collection_name,
)
if _vector_disabled:
result["vector_disabled"] = True
Expand Down Expand Up @@ -922,8 +925,8 @@ def tool_add_drawer(

# Idempotency: if the deterministic ID already exists, return success as a no-op.
try:
existing = col.get(ids=[drawer_id])
if existing and existing["ids"]:
existing = col.get(ids=[drawer_id], include=[])
if existing.ids:
return {"success": True, "reason": "already_exists", "drawer_id": drawer_id}
except Exception:
logger.debug("Idempotency pre-check failed for %s", drawer_id, exc_info=True)
Expand All @@ -943,6 +946,12 @@ def tool_add_drawer(
}
],
)
inserted = col.get(ids=[drawer_id], include=[])
if not inserted.ids:
raise RuntimeError(
"Drawer write was acknowledged but the new ID is not readable. "
"The palace index may be stale; run reconnect or repair."
)
_metadata_cache = None
logger.info(f"Filed drawer: {drawer_id} → {wing}/{room}")
return {"success": True, "drawer_id": drawer_id, "wing": wing, "room": room}
Expand Down Expand Up @@ -1506,6 +1515,30 @@ def tool_reconnect():
_palace_db_mtime, \
_vector_disabled, \
_vector_disabled_reason
from . import palace as palace_module

close_errors = []
try:
palace_module._DEFAULT_BACKEND.close_palace(_config.palace_path)
except Exception as exc:
logger.debug("Failed to close shared palace backend during reconnect", exc_info=True)
close_errors.append(f"backend close_palace failed: {exc}")
try:
from chromadb.api.client import SharedSystemClient

clear_system_cache = getattr(SharedSystemClient, "clear_system_cache", None)
if callable(clear_system_cache):
clear_system_cache()
else:
logger.debug(
"SharedSystemClient.clear_system_cache is unavailable; skipping shared Chroma cache clear during reconnect"
)
except Exception as exc:
logger.debug(
"Failed to clear Chroma shared system cache during reconnect",
exc_info=True,
)
close_errors.append(f"shared Chroma cache clear failed: {exc}")
_client_cache = None
_collection_cache = None
_palace_db_inode = 0
Expand All @@ -1527,12 +1560,24 @@ def tool_reconnect():
try:
col = _get_collection()
if col is None:
return {
result = {
"success": False,
"message": "No palace found after reconnect",
"drawers": 0,
"vector_disabled": _vector_disabled,
}
if close_errors:
result["error"] = "; ".join(close_errors)
return result
if close_errors:
return {
"success": False,
"message": "Reconnect reopened the palace but failed to fully reset cached handles",
"drawers": col.count(),
"vector_disabled": _vector_disabled,
"vector_disabled_reason": _vector_disabled_reason,
"error": "; ".join(close_errors),
}
return {
"success": True,
"message": "Reconnected to palace",
Expand Down
7 changes: 6 additions & 1 deletion mempalace/palace.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import re
import threading
from typing import Optional

from .backends.chroma import ChromaBackend

Expand Down Expand Up @@ -56,10 +57,14 @@

def get_collection(
palace_path: str,
collection_name: str = "mempalace_drawers",
collection_name: Optional[str] = None,
create: bool = True,
):
"""Get the palace collection through the backend layer."""
if collection_name is None:
from .config import get_configured_collection_name

collection_name = get_configured_collection_name()
return _DEFAULT_BACKEND.get_collection(
palace_path,
collection_name=collection_name,
Expand Down
Loading
Loading