From aaa6f8e3ae206555fc5de7dbd82a8bcfe0d2d5cc Mon Sep 17 00:00:00 2001 From: Sriram Varun Kumar Date: Mon, 5 Oct 2026 18:39:44 +0530 Subject: [PATCH] Doc: Resolves #16603: Add documentation for conflict auto-merge and desktop resolution UI (#16604) Co-authored-by: Laurent Cozic --- readme/dev/spec/conflict_auto_merge.md | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 readme/dev/spec/conflict_auto_merge.md diff --git a/readme/dev/spec/conflict_auto_merge.md b/readme/dev/spec/conflict_auto_merge.md new file mode 100644 index 0000000000..e163d1dd85 --- /dev/null +++ b/readme/dev/spec/conflict_auto_merge.md @@ -0,0 +1,38 @@ +# Conflict auto-merge + +When the same note is edited on two devices before they sync, if a change is made when one of the devices was not fully synced, Joplin creates a conflict note. Auto-merge avoids this when the edits don't overlap, using a three-way merge. It runs on the client that syncs second; other devices just receive the merged note. + +## Base version + +The merge needs three versions: **base** (the last version this device and the sync target agreed on), **local** and **remote**. + +The base is stored per sync target in `sync_items.base_body` and `base_title`. It is recorded after an upload, after a download, and after a conflict, whichever version is left in the original note. If a downloaded note is encrypted, the base is cleared until it is decrypted, so that a conflict before decryption creates a normal conflict note. + +## Merge + +The merge is line-based. Regions changed by only one side are taken as-is. Regions changed by both sides are auto-merged if both made the same change, otherwise they become a conflict. Edits on the same or adjacent lines therefore conflict. Word-level merging was tried and dropped because it could silently duplicate text. Titles follow the same rule. + +If everything merges, the merged note is saved over the local note, becomes the new base, and its `updated_time` is moved past the remote time so it gets uploaded. If some regions conflict, a conflict note is still created, but both notes already include the non-conflicting changes, so they differ only where there's a real conflict. + +## Fallback to a conflict note + +The merge is skipped, and a normal conflict note is created, when: + +- `sync.autoMergeConflicts` setting is off +- there is no recorded base +- the item is read-only or either note is locked +- either note cannot be decrypted +- an edit which touches identical lines, since the diff can't tell which one changed and may duplicate content. +- the diff exceeds its bounds (`maxEditLength` 5000, timeout 1000ms) + +## E2EE + +An encrypted remote note is decrypted in memory for the merge without being saved. If a conflict note is created, the original note is saved with the decrypted (and partially merged) remote content. + +## Code + +- `services/conflict/diffNotes.ts`: merge engine (`autoMerge()`) and the duplicate-line guard +- `services/conflict/boundedDiff3.ts`: fork of `node-diff3`'s `diff3MergeRegions()` using a bounded diff +- `services/conflict/autoMergeNote.ts`: merges title and body of a note +- `services/synchronizer/utils/handleConflictAction.ts`: runs the merge during sync and applies the result +- `models/BaseItem.ts`: stores the base and keeps it when a sync doesn't provide a new one