Repository navigation
fix(import): an import that changes a stored message's content puts its duplicate flag right - #1978
Merged
Merged
Conversation
…ts duplicate flag right An append with dedupe off could give a stored message the text of a later edit, or add an attachment to it, and leave its duplicate flag as it was, because only the account-wide dedupe pass sets or clears the flag and an import runs that pass only with dedupe on. A message hidden behind another source's copy of its old text stayed hidden, so a search for its new text found nothing, and copies hidden behind it stayed hidden though their text no longer matched. Promotion now collects the stored messages whose content it changed (those in the edit map, those that gained an attachment, and those whose stored attachment took its file) and, whatever the import's dedupe setting, runs dedupe_changed_messages for them inside the import's transaction. It computes their content keys again, runs the exact and near-time passes over the messages with a content key, and writes the flags of the messages tied to a changed one, before or now. A message an import with dedupe off added has no content key, so it stays as it came. An import that changed no stored message's content runs nothing. Closes #1805 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mbeisser1
commented
Oct 7, 2026
The pass an import runs for the stored messages whose content it changed now takes up only messages that had a content key before. A message that only imports with dedupe off brought has none, and an edit to it no longer keys it and hides it behind another source's copy of its new text. `_promote_edit_map` records whether each message had a key and whether its text changed, because promote_later_edits clears the key of a message whose text changes; a change to the earlier versions alone keeps the key and runs no pass. An import that fills content keys skips the pass, because the full dedupe after its commit writes every flag again. The server's import command fills them only when that dedupe follows. The changed ids go to SQL as one JSON array read with json_each, so KeyScope::Changed carries them and the `_dedupe_changed` and `_dedupe_tied` temp tables and fill_id_table are gone. A PromotedContent struct carries what the promotion did to stored content and names the changed messages in one place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
HAS_CONTENT_KEY_SQL in dedupe.rs is now the one place that says what a message with a content key is; the edit map, the changed-message query, the exact pass and the near-time pass use it. reset_id_map takes extra columns, so the edit map is made by it too. The --skip-dedupe help, and the server CLI page generated from it, say that the import then writes no content keys and a later full dedupe writes them. The fill_content_keys doc says what the flag does rather than who passes it, and the changelog entry is split into two sentences. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mbeisser1
marked this pull request as ready for review
October 7, 2026 20:59
mbeisser1
commented
Oct 7, 2026
Member
Author
|
Review summary. Reviewed head a9e1394 (the PR with main merged in), fixes 028aec2 and 2f4e61c, base merged in again as c4a5d9e (no conflicts), CI green on c4a5d9e.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug Description
Expected: An import that changes a stored message's content (a later edit's text, an attachment added, or a stored attachment given its file) leaves the message's duplicate flag matching its new content, whatever the import's dedupe setting.
Actual: An append with dedupe off changed the content and left the flag. Message M hidden behind N, another source's copy of "see you at six", took the text "see you at seven" and stayed hidden behind N, so a search for "seven" found nothing. Copies hidden behind M stayed hidden though their text no longer matched.
Root Cause
Only
dedupe::dedupe_cross_sourcesets or clearsduplicate_of, andrun_import_pathincrates/server/server/src/imports_api/mod.rsruns it only when the Import Run has dedupe on. Since #1801 and #1966,promote_later_editschanges a stored message's text and clears its content key, andpromote_attachmentschanges what the key hashes, but nothing re-evaluated the flag when dedupe was off.Fix Description
Option 1 of the decision on #1805: the import re-runs the dedupe for the changed messages, whatever its setting.
promote_attachments(db/staging.rs) now returns the stored messages whose attachment rows took their file (RETURNING message_id), andstored_messages_with_new_contentnames the messages in_promote_edit_mapand those that gained an attachment row.Promote::run(imports_api/promote.rs) passes those ids to the newdedupe::dedupe_changed_messages, inside the import's transaction, as its last phase. With no changed message it runs nothing.dedupe_changed_messagescomputes the changed messages' content keys again, runs the exact and near-time passes over the messages with a content key, and writes the flags of the messages tied to a changed one: those it hid or was hidden behind, before or now, followed to the end, so no hidden message is left pointing at one that is hidden. A message an import with dedupe off added has no content key, so it is neither hidden nor a winner. Every other flag stays as it is.dedupe_cross_sourceshares the pieces: the exact pass is split into compute and apply, and the near-time pass reads the exact pass's losers from memory instead ofduplicate_of IS NULL(the same rows, since the pass clears every flag first).NEAR_WINDOW_SECSnames the 2-second window both use.docs/architecture/contacts-identities-and-messages.mdstates the rule in place of the "still leaves" note. A CHANGELOG entry is under 0.11.0, Fixes, Importing.Assumptions, stated rather than asked:
importcommand with--skip-dedupe(which fills content keys but runs no pass) is covered as well. With dedupe on it is redundant work, only when stored content changed.How to Reproduce (Before Fix)
sms, then the same message from sourceimessage, both with dedupe on: theimessagecopy is hidden behind thesmsone.imessagecopy was edited to "see you at seven".GET /v1/messages?q=sevenfinds nothing, and theimessagecopy is still hidden.How to Verify (After Fix)
cargo test -p message-crate-server changed_content_deduperuns the four new tests incrates/server/server/src/imports_api/tests/changed_content_dedupe.rs, through the HTTP import routes.an_edit_with_dedupe_off_shows_a_message_hidden_behind_a_copy_of_its_old_textis the issue's scenario: M is shown, a search for "seven" finds it, and a new message the same import brings stays shown beside the copy it duplicates.an_edit_with_dedupe_off_evaluates_the_copies_around_the_message_again: the copy of M's old text hidden behind M is shown again, and a third source's copy of the new text is hidden behind M.an_attachment_with_dedupe_off_keeps_a_copy_that_still_matches_hidden: a copy that still matches M stays hidden, and M's content key hashes its new file.an_append_with_dedupe_off_that_changes_nothing_stored_runs_no_dedupe: no change, no pass.The first three fail without the fix. The fourth and the "new message stays shown" assertion pass without it by their nature: they guard the scope of the fix against an account-wide pass.
Impact Assessment
Regression Risk
Every import that changes a stored message's content now loads the account's keyed messages once for the two passes, which costs about what the full pass costs less the key hashing. Imports that change nothing stored are unaffected. The refactor of
dedupe_cross_sourcekeeps its behaviour: the whole server suite passes, including the generated-database invariant test.Checklist
cargo test -p message-crate-server: 1326 passed, 1 ignored;./scripts/check-pr.shpassed)Related Issues
Closes #1805
Related: #1801, #1757.
🤖 Generated with Claude Code