Repository navigation
fix(storage): mark beliefs/summaries stale when source memory deleted - #1865
Conversation
Deleting a memory removed its graph edges and left derived_from, contradicted_by, merged_from_hashes and source_memory_hashes pointing at the hash. The delete transaction now drops the hash from belief provenance, flags compression and association memories, and marks a belief ungrounded when no sources remain. Fixes doobidoo#1857 Co-authored-by: yuzhou <[email protected]>
|
This pull request closes #1857, but @ZYZ666-RGB is not currently assigned to CONTRIBUTING.md asks for a comment on the issue and a maintainer's confirmation before any code is written, so that two people do not work on the same thing: https://github.com/doobidoo/mcp-memory-service/blob/main/CONTRIBUTING.md#before-you-start-claim-the-issue Please comment on the issue and wait for a maintainer to assign you. Once |
|
Noted on the Claim Check — claim comment is on #1857 and I'm waiting for maintainer assignment. Once assigned, this check should re-run and pass. Thanks. |
Re-deriving a belief whose sources came back promotes it from ungrounded to candidate, or to active when the usual floors are met. A delta-sync tombstone runs the same citation cleanup as a local delete. Summary flags advance updated_at and record an update_metadata event. Fixes doobidoo#1857 Co-authored-by: yuzhou <[email protected]>
A delta-sync delete committed the tombstone and the sync_events row before checking that belief and summary cleanup finished. The retry then looked like a duplicate. Apply now commits only a finished materialize, keeps the skippable empty create, and rolls back when cleanup raises, including on a losing delete. Fixes doobidoo#1857 Co-authored-by: yuzhou <[email protected]>
|
@ZYZ666-RGB thanks for contributing! Will you please resolve any remaining Greptile code review? |
Citation cleanup runs after the tombstone write. Rolling back only after the connection lock is released lets another waiter commit that tombstone. delete, delete_by_tag, and delete_by_tags now roll back inside the worker. Re-derivation re-checks which loaded observations are still live and writes sources, confidence, and status in one locked update, so a delete in between cannot promote an ungrounded belief. Fixes doobidoo#1857 Co-authored-by: yuzhou <[email protected]>
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
/api/manage/delete-untagged tombstoned untagged rows with a raw UPDATE on the shared SQLite connection, so a delete there skipped everything the storage layer does around a tombstone: graph edges stayed, beliefs and summaries kept citing the deleted hashes, delta-sync peers never saw the deletes, and the write went out without the connection lock. DeleteMixin now owns the operation. delete_untagged_memories() collects the hashes, drops their embeddings and graph edges, tombstones them, retracts the citations (doobidoo#1865), appends one delete event per hash, and commits once under the connection lock - the same body as delete_by_tags(). count_untagged_memories() shares its predicate, so the confirmation count the caller is held to and the rows that go are decided by one expression. The endpoint resolves the backend and calls it: hybrid storage keeps SQLite as primary and does not re-export its bulk deletes, so the resolver makes the same hop the raw-connection path used to make. Closes doobidoo#1870 Co-Authored-By: Claude Opus 5 <[email protected]>
* fix(manage): delete untagged memories through the storage layer /api/manage/delete-untagged tombstoned untagged rows with a raw UPDATE on the shared SQLite connection, so a delete there skipped everything the storage layer does around a tombstone: graph edges stayed, beliefs and summaries kept citing the deleted hashes, delta-sync peers never saw the deletes, and the write went out without the connection lock. DeleteMixin now owns the operation. delete_untagged_memories() collects the hashes, drops their embeddings and graph edges, tombstones them, retracts the citations (#1865), appends one delete event per hash, and commits once under the connection lock - the same body as delete_by_tags(). count_untagged_memories() shares its predicate, so the confirmation count the caller is held to and the rows that go are decided by one expression. The endpoint resolves the backend and calls it: hybrid storage keeps SQLite as primary and does not re-export its bulk deletes, so the resolver makes the same hop the raw-connection path used to make. Closes #1870 Co-Authored-By: Claude Opus 5 <[email protected]> * fix(manage): hold delete-untagged to its confirmation count at the delete count-untagged and delete-untagged are two requests, and inside the delete endpoint the count and the delete are two awaits apart. A request that strips the last tag off another memory in that window makes the untagged set larger than the count the user agreed to, and the delete takes the extra row with it. The raw-SQL path this endpoint used before did not yield between counting and deleting, so moving the work into the storage layer opened the window. confirm_count now travels down to the backend. On SQLite it is checked against the rows the SELECT just returned - inside the connection lock, inside the transaction, before the first write - and a mismatch raises UntaggedCountChanged with nothing written. Milvus has no transaction to hold it in, so it checks the ids it collected before the first delete chunk goes out. The endpoint keeps its early check for a stale count, which answers a wrong number without touching the store, and turns UntaggedCountChanged into the same 400. The changelog fragment also gains the list marker the collector needs; without it scripts/ci/check_changelog_entry.sh rejects the entry. Closes #1870 Co-Authored-By: Claude Opus 5 <[email protected]> --------- Co-authored-by: Claude Opus 5 <[email protected]> Co-authored-by: qwist1233-cpu <[email protected]>
What this changes
Deleting a memory now updates the beliefs and consolidation summaries built from it, in the same transaction that already deletes the graph edges.
Why
Graph cleanup removed edges that touch a deleted hash and left everything derived from it in place.
derived_from,contradicted_byand the dedup copy inmetadata.merged_from_hasheskept the hash, and compression and association memories kept it insource_memory_hashes. Retention deletes memories on the normal path, soget_beliefskept returning a belief whose evidence was gone.Fixes #1857.
How it was verified
On
main(a2ee57d3), withsrc/swapped back,scripts/ci/check_tests_prove_fix.sh origin/mainran the new file and reportedPASS - the changed tests fail without the change (pytest exit 1):Eight of those drive real sqlite-vec storage. They fail because
derived_fromstill contains the deleted hash (delete,delete_memory,delete_by_tag,delete_by_tags,delete_by_timeframe, and the remote tombstone), and because_update_beliefunions a tombstoned hash back out ofmerged_from_hashes. The other seven are the pure-function tests; onmainthey error withModuleNotFoundErrorbecausestorage/source_retraction.pydoes not exist yet.On this branch:
The new file alone is
15 passed.bash scripts/pr/pre_pr_check.shwas not run. The complexity and security half of that gate needs the local model, which is not available in this environment.Changelog fragment
changelog.d/1857.fixed.mdNotes for the reviewer
Status name:
ungrounded. A belief with nothing left inderived_fromandcontradicted_bygets that status, so the defaultget_beliefs(status='active')stops returning it.supersededstays what dedup uses for a loser. Confidence is not touched;challenge_beliefstill recalculates it.The note on the issue suggests leaving that status flip for the next consolidation run, so a re-ingest under a new hash does not throw the belief away while consolidation is still catching up. This flips it at delete time. Otherwise the belief stays
activewith an empty source list, which is the bug. The row is not deleted, and a laterchallenge_beliefcan promote it again.merged_from_hashesis stripped in the same write, and_update_beliefskips a merged hash that is no longer a live memory, so the next derivation cannot union the deleted hash back. When thememoriestable cannot be asked (the belief-only test databases), every hash is kept.Summaries are flagged with
metadata.stale_sourcesand not rewritten.source_memory_hashesstill lists the deleted hash. Only memories taggedassociation, or bothclusterandcompressed, are touched. A user note that happens to storesource_memory_hashesis left alone.stale_sourcesis{count, updated_at}. On a belief,countaccumulates as hashes are removed from the lists. On a summary,countis how many of the recorded sources are not live, so flagging the same gap twice does not increment it.Lookup is
json_eachover the JSON columns, guarded withjson_valid, against a temp table of the hashes just deleted. A SQLite without JSON functions falls back to a scan. On an in-memory store of 4000 beliefs, that lookup returned 240 citing rows in about 2.7 ms, so there is nobelief_sourcestable.The same helper runs from
delete(),delete_memory()(the retention/forgetting path),delete_by_tag,delete_by_tags,delete_by_timeframe/delete_before_date(they calldelete()), and the remote tombstone path, which already deletes graph edges in that transaction.#1580 (evidence on inferred edges) is untouched.
The issue names draft #1714 as a read-time filter on
get_beliefs. The PR open at that number today is the store-scope change, not that filter, and there is no read-time filter in tree to reconcile with. If one lands later,ungroundedalready hides beliefs this path has cleaned. A read-time check would still matter for rows deleted before this change.

Confidence Score: 5/5
The PR appears safe to merge; no new actionable issue was established.Summary
Deleting a memory now removes its hashes from belief evidence and flags consolidation summaries in the same transaction.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Local or peer delete] --> B[Write tombstone] B --> C[Remove deleted belief evidence] C --> D[Flag affected summaries] D --> E[Record enabled sync events] E --> F[Commit together] C -->|Failure| G[Roll back and allow retry] D -->|Failure| G E -->|Failure| GReviews (8) · Last reviewed commit: "Merge branch 'main' into fix/delete-stal..." · Reviewed by Greptile