镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(sync): stop treating list position and hit counts as conflicts - #1138

Open
DepengWang wants to merge 1 commit into
Open-Less:betafrom
DepengWang:fix/sync-false-conflicts
Open

DepengWang wants to merge 1 commit into
Open-Less:betafrom
DepengWang:fix/sync-false-conflicts

Conversation

@DepengWang

Copy link
Copy Markdown
Contributor

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_documents compares whole records.
  • export_snapshot adds sortIndex to every history, dictionary and correction record: the record's position in the exporting device's list.
  • The history and correction stores insert new rows at the front, so each dictation renumbers every older row on that device.

Result: after device A dictates twice and device B once, every shared history row has three different sortIndex values (baseline, A, B) and is reported as both_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 hits has the same effect per word: the counter grows on each device as the word is used.

Reproduced with the unmodified merge before the fix:

Scenario Conflicts reported
A dictates 2x, B dictates 1x, 50 shared history rows 50, all History / both_modified
A dictates 1x, B dictates 1x 0
Only B dictates 0
One dictionary word used 3x on A and 5x on B, edited on neither 1 Dictionary / both_modified

Changes

  • Merge (merge.rs): sortIndex (history, dictionary, corrections) and dictionary hits are 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.
  • Order (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.
  • Wire format unchanged: sortIndex is 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 usable createdAt keep their relative order.

Real conflicts behave as before: the same record edited on both sides, and delete against modify, still ask the user.

Compatibility

  • No schema or protocol version change. Existing vaults need no migration; stale sortIndex values in them are simply ignored by the comparison and rewritten on the next upload.
  • An older client still reports these false conflicts when it is the one merging. Updating that device fixes it.

Testing

cargo test -p openless-core --lib: 1072 passed, 0 failed. New tests in cloud_sync_e2ee_documents/tests.rs:

  • dictating on two devices between syncs is not a conflict, both sides converge on the same order;
  • records that differ only in list position merge without a baseline;
  • editing the same record on both devices is still a conflict;
  • dictionary hits keep the highest count, including alongside a rename and after a chosen conflict;
  • collection order follows the records (manual before learned, offsets compared as instants, undated rows last).

Not tested: an end-to-end sync between two real devices with this build.

Not covered

  • Hit counts take the maximum of the two sides, which undercounts when both devices used a word since the last sync. It is idempotent, and the counter is only used for hotword ranking.
  • Custom vocabulary presets carry a similar positional order field. Presets are appended, so existing ones do not shift; left as is.

🤖 Generated with Claude Code

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>
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.

1 participant