Repository navigation
Conversation
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
🤖 Open Code ReviewTarget: PR #2533 🔍 OpenCodeReview found 4 issue(s) in this PR. 1.
|
…s memories delete_node_by_prams deleted the graph nodes but never purged the matching vectors from vec_db, so a deleted memory stayed retrievable through vector search while the delete API answered success. The sibling paths clear() and delete_node_by_mem_cube_id() already purge vec_db; this one omitted the step. The node ids are collected and the nodes deleted in a single statement, so the ids that are purged are exactly the nodes this call removed - two separate auto-commit statements leave a window in which a concurrent writer adds nodes matching the same filter, whose vectors would then be orphaned. Also drops the debug print() calls from this method (the repo logs via get_logger).
dad234f to
fa103c0
Compare
|
Thanks for the review. I have reworked the change; all five points are addressed in #1 and #3 (TOCTOU between the collect query and the delete query) - fixed, and thank you, this was a real defect in my first version. I had collected the ids and then deleted in two separate auto-commit statements. Both paths now do it in one statement, so the ids that get purged from MATCH (n:Memory) WHERE {where} WITH n DETACH DELETE n RETURN n.id AS idI used delete_query = (
f"MATCH (n:Memory) WHERE {ids_where} WITH n DETACH DELETE n RETURN n.id AS id"
)
logger.info(f"[delete_node_by_prams] delete_query: {delete_query}")
with self.driver.session(database=self.db_name) as session:
deleted_ids = [record["id"] for record in session.run(delete_query, **params)]
deleted_count = len(deleted_ids)It is also one round trip instead of two. #2 and #5 (debug #4 (result of Verification on the new revision
The behaviour under test is unchanged: deleting by |
Fix #2331: purge embeddings when delete_node_by_prams removes memories
Fixes #2331.
Neo4jCommunityGraphDB.delete_node_by_prams()deletes the matching graph nodesbut never purges their embeddings from
vec_db. Retrieval reads the vector storefirst, so a deleted memory stays retrievable while the delete API still answers
success — the symptom in the report. The sibling paths already do this:
clear()anddelete_node_by_mem_cube_id()both purgevec_db, anddelete_node_by_pramsis the one that was missed.With a
filter, the stale points also accumulate: they keep occupying the vectortop-k window, which is how a commenter saw search degrade to zero results after
roughly 520 orphans.
Change
Both implementations of
delete_node_by_pramsnow collect the matching node idsinside the same session, before the
DELETE, and purge them afterwards through anew
_delete_vectors_for_deleted_nodes()helper onNeo4jGraphDB:a vector-store outage must not turn a successful delete into "Failed to delete
memories". This matches what
clear()already does.Neo4jGraphDBhas novec_db(vec_configlives only onthe community config), so the helper resolves it with
getattr(self, "vec_db", None)and degrades to a no-op instead of raisingAttributeError.collected rather than running a separate
count(n)query — the same number,one query fewer.
round trip.
Testing
tests/graph_dbs/test_delete_vec_cleanup.py(new, no Neo4j, Qdrant or network —the driver and the vector store are fakes):
assert {'mem-1','mem-2','mem-3'} == {'mem-2'}in the filter path, and the sameshape for the memory-id path and the return-value assertion
tests/graph_dbs→ 39 passed, 3 skippedvec_db,nothing matched, vector-store failure) which legitimately hold before and after
tests/memories tests/api tests/mem_reader→ 256 passed, 5 failed, and the same5 fail on the untouched base tree (Ollama/environment), so they are pre-existing
ruffat the pinned0.11.8passescheckandformat --checkon all threefiles; repo-wide
format --checkreports 631 files already formattedCallers checked
Every caller of the changed method was grepped and checked for a changed
contract:
memories/textual/tree.py:415,435(ignore the return value),api/handlers/memory_handler.py:441-462(the delete API path — returns successor failure purely on exception, which is exactly what the fix makes truthful),
and
mem_reader/read_skill_memory/process_skill_memory.py:1184(deletes areplaced memory, now also purged).
vec_db.deleteaccepts an unbounded id list(
vec_dbs/qdrant.py:347, and the base class takes the same list signature), whichthe new caller relies on.
Two other methods share this gap — the inherited
Neo4jGraphDB.delete_node()andremove_oldest_memory(), used by L3 maintenance and eviction. They are outsidethe delete-memory API path this issue reports, so they are deliberately not
touched here; happy to file that separately if you want it.
AI assistance: this change was produced with AI assistance (an autonomous coding
agent) from the linked issue; the analysis, the reproduction, the before/after
test runs and the lint runs were executed locally, and a human reviewed the
result before submission.