fix(sync): stop treating list position and hit counts as conflicts - #1138
Open
DepengWang wants to merge 1 commit into
Open
DepengWang wants to merge 1 commit into
DepengWang wants to merge 1 commit into
Conversation
Encrypted sync compared whole records, and every history, dictionary and correction record carried sortIndex: its position in the exporting device's list. History and corrections insert at the front, so each new dictation renumbered every older row on that device. Two devices that had both been used since the last sync, an unequal number of times, therefore disagreed with each other and with the baseline on every shared history row, and the user was asked to pick a side for each one even though the text was identical. Dictionary hit counters did the same per word. The merge now compares what a record says. sortIndex (history, dictionary, corrections) and dictionary hits are left out of the comparison; when both sides hold a dictionary entry the higher hit count is kept, including when a conflict is resolved by choice. Collection order is derived from the records instead of from a device's list: history and corrections newest first, dictionary manual entries newest first ahead of learned entries oldest first, which is how the stores insert. sortIndex stays on the wire as a dense index so older clients keep restoring in order; the incoming value only breaks ties, so rows without a usable createdAt keep their relative order. Real conflicts are unchanged: the same record edited on both sides, and delete against modify, still ask the user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
Encrypted sync asked the user to resolve a conflict for every shared history record whenever two devices had both been used since the last sync. The records were identical on both sides. This PR makes the merge compare what a record says, so ordinary use on several devices merges silently and only real edits conflict.
Root cause
diff_sync_documentscompares whole records.export_snapshotaddssortIndexto every history, dictionary and correction record: the record's position in the exporting device's list.Result: after device A dictates twice and device B once, every shared history row has three different
sortIndexvalues (baseline, A, B) and is reported asboth_modified. If both devices dictate the same number of times, or only one device is used, nothing conflicts, which is why this is easy to miss with a single test device.Dictionary
hitshas the same effect per word: the counter grows on each device as the word is used.Reproduced with the unmodified merge before the fix:
History / both_modifiedDictionary / both_modifiedChanges
merge.rs):sortIndex(history, dictionary, corrections) and dictionaryhitsare left out of the record comparison. When both sides hold a dictionary entry, the higher hit count is kept, also when a conflict is resolved by the user's choice.validate.rs): collection order is derived from the records instead of from one device's list position. History and corrections are newest first; the dictionary keeps manual entries (newest first) ahead of learned entries (oldest first). This mirrors how the stores insert, and neither the dictionary nor corrections has a manual reorder feature. Merged history from two devices is now interleaved by time.sortIndexis still written as a dense index, so older clients keep restoring in the right order. The incoming value now only breaks ties, so rows without a usablecreatedAtkeep their relative order.Real conflicts behave as before: the same record edited on both sides, and delete against modify, still ask the user.
Compatibility
sortIndexvalues in them are simply ignored by the comparison and rewritten on the next upload.Testing
cargo test -p openless-core --lib: 1072 passed, 0 failed. New tests incloud_sync_e2ee_documents/tests.rs:Not tested: an end-to-end sync between two real devices with this build.
Not covered
orderfield. Presets are appended, so existing ones do not shift; left as is.🤖 Generated with Claude Code