09 — Conflict Resolution

Topic: conflict resolution. Answers: How should conflicts be handled? Extends ADR-0008 with the full taxonomy and policy; recorded in ADR-0026.

The cardinal rule, repeated because it is the product’s trust contract: never lose data. A conflict produces a conflicted copy — never a silent overwrite.


1. When is it a conflict?

From the planner’s decision matrix (05 §2): a node is in conflict when both Local and Remote diverged from Synced and they differ from each other (S≠R ∧ S≠L ∧ R≠L). If they diverged to the same content (R==L), it’s a convergence, not a conflict (no copy, just record).


2. Conflict taxonomy & resolution

# Conflict Resolution Rationale
1 edit / edit (different content) keep both: loser → name (conflicted copy — <device>, <ts>).ext; both become versions never lose either edit
2 edit / edit (same content) converge, no copy nothing to resolve
3 edit / delete edit wins: resurrect the file, surface the (undone) delete a delete must never destroy an edit
4 delete / delete converge (drop node) agreement
5 create / create (same name, diff content) one keeps name, other → conflicted copy both creations preserved
6 create / create (same content) adopt one node identical, dedup
7 rename / rename (same node, diff names) server order wins the name; other rename surfaced (optionally applied as a copy) deterministic via total order
8 rename / edit (same node) both apply (move + content) — not a conflict if separable node-ID identity decouples move from content
9 type change (file ↔ dir at a name) rename one side as conflicted can’t coexist at one name
10 case-only collision (File vs file) keep both with disambiguating suffix on case-insensitive FS; surface cross-platform hazard (11)
11 unicode normalization (NFC vs NFD same name) normalize; if true collision, conflicted suffix macOS NFD vs Linux NFC (11)

Naming follows the Syncthing/Dropbox convention so users recognize it: report (conflicted copy — Alice's MacBook, 2026-06-11 14:03).docx.


3. Where conflicts are resolved (and why all devices agree)

The resolution is anchored at the server’s total order (ADR-0022/0024), which is what makes it deterministic across devices:

sequenceDiagram
    autonumber
    participant A as Device A (offline edit)
    participant S as Server (journal)
    participant B as Device B (offline edit)
    Note over A,B: both edit report.docx from base version V (offline)
    A->>S: reconnect → Commit(base=V) → wins → version V+1 (seq 101)
    S-->>A: ok (Synced advances)
    S-->>B: notify (namespace advanced)
    B->>S: GetChanges → sees report.docx is now V+1 (≠ B's base V)
    B->>B: planner: S≠R and S≠L and R≠L → CONFLICT
    B->>S: Commit B's content as NEW node:<br/>"report (conflicted copy — B, ts).docx"
    S-->>B: ok (seq 102)
    S-->>A: notify → A downloads the conflicted copy
    Note over A,B: both devices now hold report.docx (A's) + the conflicted copy (B's)

The key property: the losing device materializes its version as a brand-new node on the server, so the conflicted copy becomes an ordinary file that propagates uniformly to every device via the normal pull. There is exactly one conflicted copy, not one per device — because creation happens once, at the server, and fans out.


4. Classification decision tree

flowchart TB
    classDef d fill:#fde68a,stroke:#b45309,color:#111827;
    classDef o fill:#bbf7d0,stroke:#15803d,color:#111827;
    classDef c fill:#fecaca,stroke:#b91c1c,color:#111827;
    n["node: compare S, R, L"]:::d --> q1{"both present and both ≠ S?"}:::d
    q1 -- no --> simple["one-sided change → apply ([05])"]:::o
    q1 -- yes --> q2{"R == L?"}:::d
    q2 -- yes --> conv["converged → advance S (no copy)"]:::o
    q2 -- no --> q3{"one side is a delete?"}:::d
    q3 -- yes --> edel["EDIT WINS → resurrect + surface"]:::c
    q3 -- no --> q4{"mergeable type AND auto-merge enabled?"}:::d
    q4 -- yes --> merge["3-way text merge (opt-in)"]:::o
    q4 -- no --> copy["CONFLICTED COPY (keep both)"]:::c

5. Optional auto-merge (opt-in, narrow)

For text-like types, an opt-in 3-way merge (using the Synced base) can auto-resolve non-overlapping edits, falling back to a conflicted copy on overlap. Default off. We explicitly do not do CRDT/OT whole-file or real-time co-editing — that’s a non-goal (NG1, ADR-0008). Binary files are never auto-merged.


6. User experience & recovery


7. Tradeoffs / Alternatives / Scaling

Tradeoffs. Conflicted copies put reconciliation work on the user — a deliberate, honest cost versus the alternative of silent auto-merge that can corrupt data. Version history + clear surfacing keep it humane.

Alternatives considered.

Scaling concerns.