Skip to content

Commit bf88daa

Browse files
committed
fix: address review — re-mine modified files, idempotent add_drawer, cleanup ChromaDB handles
1 parent a4149ab commit bf88daa

4 files changed

Lines changed: 79 additions & 63 deletions

File tree

mempalace/mcp_server.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -283,17 +283,16 @@ def tool_add_drawer(
283283
if not col:
284284
return _no_palace()
285285

286-
# Duplicate check
287-
dup = tool_check_duplicate(content, threshold=0.9)
288-
if dup.get("is_duplicate"):
289-
return {
290-
"success": False,
291-
"reason": "duplicate",
292-
"matches": dup["matches"],
293-
}
294-
295286
drawer_id = f"drawer_{wing}_{room}_{hashlib.md5(content.encode()).hexdigest()[:16]}"
296287

288+
# Idempotency: if the deterministic ID already exists, return success as a no-op.
289+
try:
290+
existing = col.get(ids=[drawer_id])
291+
if existing and existing["ids"]:
292+
return {"success": True, "reason": "already_exists", "drawer_id": drawer_id}
293+
except Exception:
294+
pass
295+
297296
try:
298297
col.upsert(
299298
ids=[drawer_id],

mempalace/miner.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -403,10 +403,22 @@ def get_collection(palace_path: str):
403403

404404

405405
def file_already_mined(collection, source_file: str) -> bool:
406-
"""Fast check: has this file been filed before?"""
406+
"""Fast check: has this file been filed before and is unchanged?
407+
408+
Compares the stored mtime in drawer metadata against the file's current
409+
mtime. Returns False (needs re-mining) when the file has been modified
410+
since it was last mined, or when no mtime was stored.
411+
"""
407412
try:
408413
results = collection.get(where={"source_file": source_file}, limit=1)
409-
return len(results.get("ids", [])) > 0
414+
if not results.get("ids"):
415+
return False
416+
stored_meta = results["metadatas"][0] if results.get("metadatas") else {}
417+
stored_mtime = stored_meta.get("source_mtime")
418+
if stored_mtime is None:
419+
return False
420+
current_mtime = os.path.getmtime(source_file)
421+
return float(stored_mtime) == current_mtime
410422
except Exception:
411423
return False
412424

@@ -417,19 +429,23 @@ def add_drawer(
417429
"""Add one drawer to the palace."""
418430
drawer_id = f"drawer_{wing}_{room}_{hashlib.md5((source_file + str(chunk_index)).encode(), usedforsecurity=False).hexdigest()[:16]}"
419431
try:
432+
metadata = {
433+
"wing": wing,
434+
"room": room,
435+
"source_file": source_file,
436+
"chunk_index": chunk_index,
437+
"added_by": agent,
438+
"filed_at": datetime.now().isoformat(),
439+
}
440+
# Store file mtime so we can detect modifications later.
441+
try:
442+
metadata["source_mtime"] = os.path.getmtime(source_file)
443+
except OSError:
444+
pass
420445
collection.upsert(
421446
documents=[content],
422447
ids=[drawer_id],
423-
metadatas=[
424-
{
425-
"wing": wing,
426-
"room": room,
427-
"source_file": source_file,
428-
"chunk_index": chunk_index,
429-
"added_by": agent,
430-
"filed_at": datetime.now().isoformat(),
431-
}
432-
],
448+
metadatas=[metadata],
433449
)
434450
return True
435451
except Exception:

tests/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ def collection(palace_path):
102102
"""A ChromaDB collection pre-seeded in the temp palace."""
103103
client = chromadb.PersistentClient(path=palace_path)
104104
col = client.get_or_create_collection("mempalace_drawers")
105-
return col
105+
yield col
106+
client.delete_collection("mempalace_drawers")
107+
del client
106108

107109

108110
@pytest.fixture

tests/test_mcp_server.py

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,26 @@
99
import json
1010

1111

12-
def _patch_mcp_server(monkeypatch, config, palace_path, kg):
12+
def _patch_mcp_server(monkeypatch, config, kg):
1313
"""Patch the mcp_server module globals to use test fixtures."""
1414
from mempalace import mcp_server
1515

16-
assert getattr(config, "palace_path", None) == palace_path, (
17-
f"config.palace_path ({getattr(config, 'palace_path', None)!r}) does not match palace_path fixture ({palace_path!r})"
18-
)
1916
monkeypatch.setattr(mcp_server, "_config", config)
2017
monkeypatch.setattr(mcp_server, "_kg", kg)
2118

2219

2320
def _get_collection(palace_path, create=False):
24-
"""Helper to get collection from test palace."""
21+
"""Helper to get collection from test palace.
22+
23+
Returns (client, collection) so callers can clean up the client
24+
when they are done.
25+
"""
2526
import chromadb
2627

2728
client = chromadb.PersistentClient(path=palace_path)
2829
if create:
29-
return client.get_or_create_collection("mempalace_drawers")
30-
return client.get_collection("mempalace_drawers")
30+
return client, client.get_or_create_collection("mempalace_drawers")
31+
return client, client.get_collection("mempalace_drawers")
3132

3233

3334
# ── Protocol Layer ──────────────────────────────────────────────────────
@@ -77,11 +78,11 @@ def test_unknown_method(self):
7778
assert resp["error"]["code"] == -32601
7879

7980
def test_tools_call_dispatches(self, monkeypatch, config, palace_path, seeded_kg):
80-
_patch_mcp_server(monkeypatch, config, palace_path, seeded_kg)
81+
_patch_mcp_server(monkeypatch, config, seeded_kg)
8182
from mempalace.mcp_server import handle_request
8283

8384
# Create a collection so status works
84-
_get_collection(palace_path, create=True)
85+
_client, _col = _get_collection(palace_path, create=True); del _client
8586

8687
resp = handle_request(
8788
{
@@ -100,16 +101,16 @@ def test_tools_call_dispatches(self, monkeypatch, config, palace_path, seeded_kg
100101

101102
class TestReadTools:
102103
def test_status_empty_palace(self, monkeypatch, config, palace_path, kg):
103-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
104-
_get_collection(palace_path, create=True)
104+
_patch_mcp_server(monkeypatch, config, kg)
105+
_client, _col = _get_collection(palace_path, create=True); del _client
105106
from mempalace.mcp_server import tool_status
106107

107108
result = tool_status()
108109
assert result["total_drawers"] == 0
109110
assert result["wings"] == {}
110111

111112
def test_status_with_data(self, monkeypatch, config, palace_path, seeded_collection, kg):
112-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
113+
_patch_mcp_server(monkeypatch, config, kg)
113114
from mempalace.mcp_server import tool_status
114115

115116
result = tool_status()
@@ -118,15 +119,15 @@ def test_status_with_data(self, monkeypatch, config, palace_path, seeded_collect
118119
assert "notes" in result["wings"]
119120

120121
def test_list_wings(self, monkeypatch, config, palace_path, seeded_collection, kg):
121-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
122+
_patch_mcp_server(monkeypatch, config, kg)
122123
from mempalace.mcp_server import tool_list_wings
123124

124125
result = tool_list_wings()
125126
assert result["wings"]["project"] == 3
126127
assert result["wings"]["notes"] == 1
127128

128129
def test_list_rooms_all(self, monkeypatch, config, palace_path, seeded_collection, kg):
129-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
130+
_patch_mcp_server(monkeypatch, config, kg)
130131
from mempalace.mcp_server import tool_list_rooms
131132

132133
result = tool_list_rooms()
@@ -135,26 +136,24 @@ def test_list_rooms_all(self, monkeypatch, config, palace_path, seeded_collectio
135136
assert "planning" in result["rooms"]
136137

137138
def test_list_rooms_filtered(self, monkeypatch, config, palace_path, seeded_collection, kg):
138-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
139+
_patch_mcp_server(monkeypatch, config, kg)
139140
from mempalace.mcp_server import tool_list_rooms
140141

141142
result = tool_list_rooms(wing="project")
142143
assert "backend" in result["rooms"]
143144
assert "planning" not in result["rooms"]
144145

145146
def test_get_taxonomy(self, monkeypatch, config, palace_path, seeded_collection, kg):
146-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
147+
_patch_mcp_server(monkeypatch, config, kg)
147148
from mempalace.mcp_server import tool_get_taxonomy
148149

149150
result = tool_get_taxonomy()
150151
assert result["taxonomy"]["project"]["backend"] == 2
151152
assert result["taxonomy"]["project"]["frontend"] == 1
152153
assert result["taxonomy"]["notes"]["planning"] == 1
153154

154-
def test_no_palace_returns_error(self, monkeypatch, config, kg, tmp_path):
155-
missing = str(tmp_path / "missing")
156-
config._file_config["palace_path"] = missing
157-
_patch_mcp_server(monkeypatch, config, missing, kg)
155+
def test_no_palace_returns_error(self, monkeypatch, config, kg):
156+
_patch_mcp_server(monkeypatch, config, kg)
158157
from mempalace.mcp_server import tool_status
159158

160159
result = tool_status()
@@ -166,7 +165,7 @@ def test_no_palace_returns_error(self, monkeypatch, config, kg, tmp_path):
166165

167166
class TestSearchTool:
168167
def test_search_basic(self, monkeypatch, config, palace_path, seeded_collection, kg):
169-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
168+
_patch_mcp_server(monkeypatch, config, kg)
170169
from mempalace.mcp_server import tool_search
171170

172171
result = tool_search(query="JWT authentication tokens")
@@ -177,14 +176,14 @@ def test_search_basic(self, monkeypatch, config, palace_path, seeded_collection,
177176
assert "JWT" in top["text"] or "authentication" in top["text"].lower()
178177

179178
def test_search_with_wing_filter(self, monkeypatch, config, palace_path, seeded_collection, kg):
180-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
179+
_patch_mcp_server(monkeypatch, config, kg)
181180
from mempalace.mcp_server import tool_search
182181

183182
result = tool_search(query="planning", wing="notes")
184183
assert all(r["wing"] == "notes" for r in result["results"])
185184

186185
def test_search_with_room_filter(self, monkeypatch, config, palace_path, seeded_collection, kg):
187-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
186+
_patch_mcp_server(monkeypatch, config, kg)
188187
from mempalace.mcp_server import tool_search
189188

190189
result = tool_search(query="database", room="backend")
@@ -196,8 +195,8 @@ def test_search_with_room_filter(self, monkeypatch, config, palace_path, seeded_
196195

197196
class TestWriteTools:
198197
def test_add_drawer(self, monkeypatch, config, palace_path, kg):
199-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
200-
_get_collection(palace_path, create=True)
198+
_patch_mcp_server(monkeypatch, config, kg)
199+
_client, _col = _get_collection(palace_path, create=True); del _client
201200
from mempalace.mcp_server import tool_add_drawer
202201

203202
result = tool_add_drawer(
@@ -211,35 +210,35 @@ def test_add_drawer(self, monkeypatch, config, palace_path, kg):
211210
assert result["drawer_id"].startswith("drawer_test_wing_test_room_")
212211

213212
def test_add_drawer_duplicate_detection(self, monkeypatch, config, palace_path, kg):
214-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
215-
_get_collection(palace_path, create=True)
213+
_patch_mcp_server(monkeypatch, config, kg)
214+
_client, _col = _get_collection(palace_path, create=True); del _client
216215
from mempalace.mcp_server import tool_add_drawer
217216

218217
content = "This is a unique test memory about Rust ownership and borrowing."
219218
result1 = tool_add_drawer(wing="w", room="r", content=content)
220219
assert result1["success"] is True
221220

222221
result2 = tool_add_drawer(wing="w", room="r", content=content)
223-
assert result2["success"] is False
224-
assert result2["reason"] == "duplicate"
222+
assert result2["success"] is True
223+
assert result2["reason"] == "already_exists"
225224

226225
def test_delete_drawer(self, monkeypatch, config, palace_path, seeded_collection, kg):
227-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
226+
_patch_mcp_server(monkeypatch, config, kg)
228227
from mempalace.mcp_server import tool_delete_drawer
229228

230229
result = tool_delete_drawer("drawer_proj_backend_aaa")
231230
assert result["success"] is True
232231
assert seeded_collection.count() == 3
233232

234233
def test_delete_drawer_not_found(self, monkeypatch, config, palace_path, seeded_collection, kg):
235-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
234+
_patch_mcp_server(monkeypatch, config, kg)
236235
from mempalace.mcp_server import tool_delete_drawer
237236

238237
result = tool_delete_drawer("nonexistent_drawer")
239238
assert result["success"] is False
240239

241240
def test_check_duplicate(self, monkeypatch, config, palace_path, seeded_collection, kg):
242-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
241+
_patch_mcp_server(monkeypatch, config, kg)
243242
from mempalace.mcp_server import tool_check_duplicate
244243

245244
# Exact match text from seeded_collection should be flagged
@@ -263,7 +262,7 @@ def test_check_duplicate(self, monkeypatch, config, palace_path, seeded_collecti
263262

264263
class TestKGTools:
265264
def test_kg_add(self, monkeypatch, config, palace_path, kg):
266-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
265+
_patch_mcp_server(monkeypatch, config, kg)
267266
from mempalace.mcp_server import tool_kg_add
268267

269268
result = tool_kg_add(
@@ -275,14 +274,14 @@ def test_kg_add(self, monkeypatch, config, palace_path, kg):
275274
assert result["success"] is True
276275

277276
def test_kg_query(self, monkeypatch, config, palace_path, seeded_kg):
278-
_patch_mcp_server(monkeypatch, config, palace_path, seeded_kg)
277+
_patch_mcp_server(monkeypatch, config, seeded_kg)
279278
from mempalace.mcp_server import tool_kg_query
280279

281280
result = tool_kg_query(entity="Max")
282281
assert result["count"] > 0
283282

284283
def test_kg_invalidate(self, monkeypatch, config, palace_path, seeded_kg):
285-
_patch_mcp_server(monkeypatch, config, palace_path, seeded_kg)
284+
_patch_mcp_server(monkeypatch, config, seeded_kg)
286285
from mempalace.mcp_server import tool_kg_invalidate
287286

288287
result = tool_kg_invalidate(
@@ -294,14 +293,14 @@ def test_kg_invalidate(self, monkeypatch, config, palace_path, seeded_kg):
294293
assert result["success"] is True
295294

296295
def test_kg_timeline(self, monkeypatch, config, palace_path, seeded_kg):
297-
_patch_mcp_server(monkeypatch, config, palace_path, seeded_kg)
296+
_patch_mcp_server(monkeypatch, config, seeded_kg)
298297
from mempalace.mcp_server import tool_kg_timeline
299298

300299
result = tool_kg_timeline(entity="Alice")
301300
assert result["count"] > 0
302301

303302
def test_kg_stats(self, monkeypatch, config, palace_path, seeded_kg):
304-
_patch_mcp_server(monkeypatch, config, palace_path, seeded_kg)
303+
_patch_mcp_server(monkeypatch, config, seeded_kg)
305304
from mempalace.mcp_server import tool_kg_stats
306305

307306
result = tool_kg_stats()
@@ -313,8 +312,8 @@ def test_kg_stats(self, monkeypatch, config, palace_path, seeded_kg):
313312

314313
class TestDiaryTools:
315314
def test_diary_write_and_read(self, monkeypatch, config, palace_path, kg):
316-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
317-
_get_collection(palace_path, create=True)
315+
_patch_mcp_server(monkeypatch, config, kg)
316+
_client, _col = _get_collection(palace_path, create=True); del _client
318317
from mempalace.mcp_server import tool_diary_write, tool_diary_read
319318

320319
w = tool_diary_write(
@@ -331,8 +330,8 @@ def test_diary_write_and_read(self, monkeypatch, config, palace_path, kg):
331330
assert "authentication" in r["entries"][0]["content"]
332331

333332
def test_diary_read_empty(self, monkeypatch, config, palace_path, kg):
334-
_patch_mcp_server(monkeypatch, config, palace_path, kg)
335-
_get_collection(palace_path, create=True)
333+
_patch_mcp_server(monkeypatch, config, kg)
334+
_client, _col = _get_collection(palace_path, create=True); del _client
336335
from mempalace.mcp_server import tool_diary_read
337336

338337
r = tool_diary_read(agent_name="Nobody")

0 commit comments

Comments
 (0)