Skip to content

fix(storage): mark beliefs/summaries stale when source memory deleted - #1865

Merged
doobidoo merged 8 commits into
doobidoo:mainfrom
ZYZ666-RGB:fix/delete-stale-beliefs-summaries
Nov 5, 2026
Merged

doobidoo merged 8 commits into
doobidoo:mainfrom
ZYZ666-RGB:fix/delete-stale-beliefs-summaries

Conversation

@ZYZ666-RGB

@ZYZ666-RGB ZYZ666-RGB commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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_by and the dedup copy in metadata.merged_from_hashes kept the hash, and compression and association memories kept it in source_memory_hashes. Retention deletes memories on the normal path, so get_beliefs kept returning a belief whose evidence was gone.

Fixes #1857.

How it was verified

On main (a2ee57d3), with src/ swapped back, scripts/ci/check_tests_prove_fix.sh origin/main ran the new file and reported PASS - the changed tests fail without the change (pytest exit 1):

.venv/bin/pytest tests/consolidation/test_delete_stale_beliefs.py -q --tb=line
15 failed

Eight of those drive real sqlite-vec storage. They fail because derived_from still contains the deleted hash (delete, delete_memory, delete_by_tag, delete_by_tags, delete_by_timeframe, and the remote tombstone), and because _update_belief unions a tombstoned hash back out of merged_from_hashes. The other seven are the pure-function tests; on main they error with ModuleNotFoundError because storage/source_retraction.py does not exist yet.

On this branch:

.venv/bin/pytest tests/consolidation/test_delete_stale_beliefs.py tests/test_belief_dedup_producer.py tests/test_belief_connection_lock.py tests/consolidation/test_dead_storage_calls.py -q
67 passed

The new file alone is 15 passed. bash scripts/pr/pre_pr_check.sh was 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.md

Notes for the reviewer

Status name: ungrounded. A belief with nothing left in derived_from and contradicted_by gets that status, so the default get_beliefs(status='active') stops returning it. superseded stays what dedup uses for a loser. Confidence is not touched; challenge_belief still 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 active with an empty source list, which is the bug. The row is not deleted, and a later challenge_belief can promote it again. merged_from_hashes is stripped in the same write, and _update_belief skips a merged hash that is no longer a live memory, so the next derivation cannot union the deleted hash back. When the memories table cannot be asked (the belief-only test databases), every hash is kept.

Summaries are flagged with metadata.stale_sources and not rewritten. source_memory_hashes still lists the deleted hash. Only memories tagged association, or both cluster and compressed, are touched. A user note that happens to store source_memory_hashes is left alone.

stale_sources is {count, updated_at}. On a belief, count accumulates as hashes are removed from the lists. On a summary, count is how many of the recorded sources are not live, so flagging the same gap twice does not increment it.

Lookup is json_each over the JSON columns, guarded with json_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 no belief_sources table.

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 call delete()), 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, ungrounded already hides beliefs this path has cleaned. A read-time check would still matter for rows deleted before this change.

Open in Web Open in Cursor 

RetriggerView in GreptileConfidence 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.

  • Deleting a memory now clears its belief evidence and flags its summaries.
  • Belief re-derivation now keeps only sources that are still live.

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| G
Loading

Reviews (8) · Last reviewed commit: "Merge branch 'main' into fix/delete-stal..." · Reviewed by Greptile

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]>
@github-actions github-actions Bot added the unclaimed PR closes an issue its author did not claim first (CONTRIBUTING.md) label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

This pull request closes #1857, but @ZYZ666-RGB is not currently assigned to
these issues. Assignment is the maintainer's confirmation that the work is
claimed; a comment alone does not confirm it.

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
assigned, this check runs again on its own and passes. A maintainer can apply
skip-issue as an explicit exception for an approved contribution. Pull requests
for issues that someone else has claimed are usually closed.

@ZYZ666-RGB

Copy link
Copy Markdown
Contributor Author

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]>
greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 5, 2026
@ZYZ666-RGB

Copy link
Copy Markdown
Contributor Author

Follow-up: Greptile is approved and the latest fix (ec7e650) addresses the cleanup-rollback P1. Still blocked only on Claim Check until #1857 is assigned — just pinged @doobidoo there. Thanks for your time.

@github-actions github-actions Bot removed the unclaimed PR closes an issue its author did not claim first (CONTRIBUTING.md) label Nov 5, 2026
@doobidoo

doobidoo commented Nov 5, 2026

Copy link
Copy Markdown
Owner

@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]>
@ZYZ666-RGB

Copy link
Copy Markdown
Contributor Author

@doobidoo both remaining Greptile P1s are addressed in 4223f70. A failed delete rolls back before the connection lock is released, and re-derivation no longer writes deleted sources back onto an ungrounded belief. Current main is merged as well.

@doobidoo
doobidoo enabled auto-merge (squash) November 5, 2026 07:04
@greptile-apps

greptile-apps Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

@doobidoo
doobidoo merged commit 16a0ae9 into doobidoo:main Nov 5, 2026
24 checks passed
doobidoo added a commit that referenced this pull request Nov 5, 2026
…s cut

Main moved by #1865, #1866 and #1868 after release/v12.6.0 was cut. Their
changelog fragments are collected into 12.6.0 and the site test count is
refreshed to 6,214.
@doobidoo doobidoo mentioned this pull request Nov 5, 2026
qwist1233-cpu added a commit to qwist1233-cpu/mcp-memory-service that referenced this pull request Nov 6, 2026
/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]>
doobidoo pushed a commit that referenced this pull request Nov 6, 2026
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deleting a memory leaves beliefs and consolidation summaries built from it untouched

3 participants