* feat: MCP reliability — inode detection, WAL rotation, metadata cache, search limits Infrastructure hardening for the MCP server: - Detect palace DB replacement via inode tracking (repair command support) - WAL rotation to prevent unbounded WAL growth - _fetch_all_metadata() + _get_cached_metadata() with 60s TTL for taxonomy/status - _MAX_RESULTS cap (100) with limit clamping [1, _MAX_RESULTS] - max_distance parameter for similarity threshold in search - Handle all notifications/* methods, null arguments, method=None - Remove duplicate _client_cache = None declarations - searcher.py max_distance parameter passthrough Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: new MCP tools (get/list/update drawer, hook settings, memories filed), export, normalize New MCP tools: - mempalace_get_drawer: fetch single drawer by ID with full content - mempalace_list_drawers: paginated listing with wing/room filter - mempalace_update_drawer: update content/wing/room on existing drawers - mempalace_hook_settings: get/set hook behavior (silent_save, desktop_toast) - mempalace_memories_filed_away: check latest checkpoint status Also includes: - exporter.py: export palace as browsable markdown files - normalize.py: tool_use/tool_result capture for richer transcript mining - layers.py: updated for new tool integration - config.py: hook settings properties (hook_silent_save, hook_desktop_toast) Depends on PR 3 (reliability) for _MAX_RESULTS, _metadata_cache, WAL logging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: normalize.py handles string messages and Read offset type mismatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: params null guard, L2→cosine docs, empty tool_use_map key guard - Handle explicit null in MCP params (request.get("params") or {}) - Fix search tool description: L2 → cosine distance (collection uses hnsw:space=cosine) - Guard against empty string key in tool_use_map from malformed JSONL entries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: rename ambiguous var 'l' to 'line' (E741 lint) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address code review findings (5 issues) 1. min_similarity backwards-compat: convert similarity to distance scale (1.0 - similarity) instead of passing raw value as max_distance 2. Restore structured error reporting (error + partial fields) in tool_status, tool_list_wings, tool_list_rooms, tool_get_taxonomy — reverts silent except:pass that dropped #647 security hardening 3. inode cache: remove falsy-zero short-circuit so missing DB file triggers reconnect instead of reusing stale client 4. _fetch_all_metadata: check for empty batch before extending/advancing offset to prevent infinite loop on concurrent deletion 5. KG initialization: only override path when --palace is explicit; default runs use KnowledgeGraph's built-in default path Co-authored-by: jphein <jphein@users.noreply.github.com> --------- Co-authored-by: jp <jp@jphein.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: jphein <jphein@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from mempalace.miner import mine
|
||||
from mempalace.exporter import export_palace
|
||||
|
||||
|
||||
def write_file(path: Path, content: str):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def _setup_palace(tmpdir):
|
||||
"""Create a small palace with drawers across two wings for testing."""
|
||||
project_a = Path(tmpdir) / "project_a"
|
||||
project_b = Path(tmpdir) / "project_b"
|
||||
palace_path = str(Path(tmpdir) / "palace")
|
||||
|
||||
# Project A: wing=alpha, rooms=backend,frontend
|
||||
os.makedirs(project_a / "backend")
|
||||
os.makedirs(project_a / "frontend")
|
||||
write_file(project_a / "backend" / "server.py", "def serve():\n return 'ok'\n" * 20)
|
||||
write_file(project_a / "frontend" / "app.js", "function render() { return 'hi'; }\n" * 20)
|
||||
with open(project_a / "mempalace.yaml", "w") as f:
|
||||
yaml.dump(
|
||||
{
|
||||
"wing": "alpha",
|
||||
"rooms": [
|
||||
{"name": "backend", "description": "Backend code"},
|
||||
{"name": "frontend", "description": "Frontend code"},
|
||||
],
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
# Project B: wing=beta, rooms=docs
|
||||
os.makedirs(project_b / "docs")
|
||||
write_file(project_b / "docs" / "guide.md", "# Guide\n\nThis explains things.\n" * 20)
|
||||
with open(project_b / "mempalace.yaml", "w") as f:
|
||||
yaml.dump(
|
||||
{
|
||||
"wing": "beta",
|
||||
"rooms": [{"name": "docs", "description": "Documentation"}],
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
mine(str(project_a), palace_path)
|
||||
mine(str(project_b), palace_path)
|
||||
|
||||
return palace_path
|
||||
|
||||
|
||||
def test_export_creates_structure():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
palace_path = _setup_palace(tmpdir)
|
||||
output_dir = os.path.join(tmpdir, "export")
|
||||
|
||||
stats = export_palace(palace_path, output_dir)
|
||||
|
||||
# Should have two wings
|
||||
assert stats["wings"] == 2
|
||||
assert stats["rooms"] >= 2
|
||||
assert stats["drawers"] >= 3
|
||||
|
||||
# Directory structure
|
||||
assert os.path.isfile(os.path.join(output_dir, "index.md"))
|
||||
assert os.path.isdir(os.path.join(output_dir, "alpha"))
|
||||
assert os.path.isdir(os.path.join(output_dir, "beta"))
|
||||
|
||||
# Room files exist
|
||||
assert os.path.isfile(os.path.join(output_dir, "alpha", "backend.md"))
|
||||
assert os.path.isfile(os.path.join(output_dir, "alpha", "frontend.md"))
|
||||
assert os.path.isfile(os.path.join(output_dir, "beta", "docs.md"))
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_export_markdown_content():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
palace_path = _setup_palace(tmpdir)
|
||||
output_dir = os.path.join(tmpdir, "export")
|
||||
|
||||
export_palace(palace_path, output_dir)
|
||||
|
||||
# Check that room files contain expected markdown elements
|
||||
backend_md = Path(output_dir) / "alpha" / "backend.md"
|
||||
content = backend_md.read_text(encoding="utf-8")
|
||||
|
||||
assert content.startswith("# alpha / backend\n")
|
||||
assert "## drawer_" in content
|
||||
assert "| Field | Value |" in content
|
||||
assert "| Source |" in content
|
||||
assert "| Filed |" in content
|
||||
assert "| Added by |" in content
|
||||
assert "---" in content
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_export_index_content():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
palace_path = _setup_palace(tmpdir)
|
||||
output_dir = os.path.join(tmpdir, "export")
|
||||
|
||||
export_palace(palace_path, output_dir)
|
||||
|
||||
index_md = Path(output_dir) / "index.md"
|
||||
content = index_md.read_text(encoding="utf-8")
|
||||
|
||||
assert "# Palace Export" in content
|
||||
assert "| Wing | Rooms | Drawers |" in content
|
||||
assert "[alpha](alpha/)" in content
|
||||
assert "[beta](beta/)" in content
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
||||
def test_export_empty_palace():
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
try:
|
||||
palace_path = os.path.join(tmpdir, "empty_palace")
|
||||
output_dir = os.path.join(tmpdir, "export")
|
||||
|
||||
stats = export_palace(palace_path, output_dir)
|
||||
|
||||
assert stats == {"wings": 0, "rooms": 0, "drawers": 0}
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
Reference in New Issue
Block a user