Lies I was told about collab editing, Part 1: Algorithms for offline editing
moment.dev
moment.dev
You write this article like you're disagreeing with me - but I agree completely with what you've said. I've been saying so on HN for years. (Eg, in this comment from 6 years ago[1].)
The way I think about it, the realtime collaborative tools we use today make a lot of sense when everyone is online & editing together. But when users edit content offline, or in long lived branches, you probably want the option to add conflict markers & do manual review when merging. (Especially for code.)
Luckily, algorithms like egwalker have access to all the information they need to do that. We store character-by-character editing traces from all users. And we store when all changes happened (in causal order, like a git DAG). This is far more information than git has. So it should be very possible to build a CRDT which uses this information to detects & mark conflict ranges when branches are merged. Then we can allow users to manually resolve conflicts.
Algorithmically, this is an interesting problem but it should be quite solvable. Just, for some reason, nobody has worked on this yet. So, thanks for writing this post and bringing more attention to this problem!
If anyone is interested in making a unique and valuable contribution to the field, I'd love to see some work on this. Its an important piece thats missing in the CRDT ecosystem - simply because (as far as I know) nobody has tried to solve it yet. At least not for text editing.
[1] Bottom part of this comment: https://news.ycombinator.com/item?id=19889174
Regarding your [1], I had a similar idea and I am beginning to think that only something like an LLM or similar can truly merge conflict free because only they are powerful enough to understand users' intent.
I’ve heard people suggest using LLMs for years for this - but CRDTs work because the same computation on two peers is guaranteed to produce the same result. LLMs can’t guarantee that. You could probably use an llm in leu of a human manually merging conflicts - but in that case we still need the crdt to first generate those conflict ranges to pass to the llm. Essentially an llm could solve the UX problem, but the underlying algorithm still needs this feature first to allow that to be used.
From https://loro.dev/blog/v1.0#next-steps-for-loro:
> When merging extensive concurrent edits, CRDTs can automatically merge changes, but the result may not always meet expectations. Fortunately, Loro stores the complete editing history. This allows us to offer Git-like manual conflict resolution at the application layer when needed.
From https://loro.dev/docs/tutorial/time_travel:
> once we have a more refined version control API in place.
I'm interested to hear your opinion of their partial adoption of Eg-walker: https://loro.dev/blog/v1.0#leveraging-the-potential-of-the-e...
This doesn't seem like a hard requirement. It's a nice property for some academically interesting peer-to-peer trustless algorithm, but in typical software architectures you won't get a situation where Alice receives Bob's edits and Bob receives Alice's edits without them passing through a centralised server that can reconcile them first.
In any case, LLMs can be run deterministically (temperature=0, or use a fixed random seed and a single core where necessary).
I don't expect further serious work in the offline text editing space. The next generation will certainly be some form of "guess, then ask an LLM to guess"
Thats the exact situation these algorithms are all designed to handle. If you require a centralised reconciling server, you’re taking a big step back in functionality - and you no longer have an eventually consistent system.
But as I say, asking an llm - what exactly? Most of the time concurrent edits are in different regions of a document and you want them to be merged automatically in the obvious way. It’s only when the same text (same line) is edited by two peers that you want to invoke a llm (if at all). If that’s the case you still need the crdt to detect conflicting ranges - you’re just using an llm instead of a human to resolve them. And in that case, CRDTs are still missing the important feature this blog post talked about. You’re just proposing a different UX layer on top of that missing feature.
And really, you’ll probably still want human review. LLMs have a tendency to do drive-by edits in situations like this, adding or removing commas in prose, changing formatting in code and so on. I don’t trust LLMs to touch my code or my writing without human review.
I'm skeptical that an algorithmic solution will be possible, but I can see this being handled in a UX layer built on top. For example, a client could detect that there's been a conflict based on the editing traces, and show a conflict resolution dialog that makes a new edit based on the resolution. The tricky part is marking a conflict as resolved. I suspect it could be as simple as adding a field to the crdt, but maybe then it counts as an algorithmic solution?
> it should be very possible to build a CRDT which uses this information to detects & mark conflict ranges when branches are merged
I don't think that the conflict detection/resolution needs to live inside the CRDT data structure. Ultimately you might want to bake it in out of convenience, but it should be possible to handle separately (of course the resolution will ultimately need to be written to the CRDT, but this can be a regular edit).
Keeping the conflict resolution in the application layer allows for CRDT libraries that don't need to be aware of human-in-the-loop conflicts, and can serve a wider range of downstream needs. For example, a note app and a version control system might both be plain text, but conflict resolution needs to be handled completely differently. Another example would be collaborative offline vs. online use cases, as noted above, they are very different use cases.
1. The crdt has an awful lot of information at its disposal while merging branches. I think “branch merging” algorithms should ideally be able to make use of that information.
2. There’s a potential problem in your approach where two users concurrently merge branches together. In that case, you don’t want the merges themselves to also conflict with one another. What you actually want is for the standard crdt convergence properties to hold for the merge operation itself - and if two people concurrently merge branches together (in any order) then it all behaves correctly.
For that to happen, I think you need some coordination between the merging and the crdt’s internal data structures. And why not? They’re right there.
A sketch of a solution would be for the merge operation to perform the normal optimistic merge - but also annotate the document with a set of locations which need human review. If all peers use the same algorithm to figure out where those conflict annotations go, then the merge itself should be idempotent. The conflict annotations can be part of the shared data model.
Another, maybe simpler approach would be for the crdt library itself to just return the list of conflicting locations out of band when you do a merge operation. (Ie, we don’t change the crdt data structure itself - we just also return a list of conflicting lines as a return value from the merge function). The editor takes the list of conflicting locations and builds a ui around the list so the user can do manual review.
No. LLMs definitely have uses where reliability is not a requirement, but that's one requirement which LLMs clearly never meet
(1) might seem stupid but I think just evaluating these systems is a challenging enough technical problem that many teams will struggle with it. I just think they deserve practical advice—I know we would have appreciated it earlier on.
I’m motivated by wanting the problem solved, not getting the most praise by random people on the internet. If today that means being cast in the role of “the old guard who’s missing something important” then so be it. What fun.
Is it aggregate, the people adding to the same with no overlaping elements.
Is it a overlapping set, n-people contributing the same
Is it a xor set where two people want to split up a implementation and go into different directions ?
If one deletes and creates a new as replacement , the other continues to work on the element, that could auto map to an intent above.
And if you know that you can provide much more resonable merges.
The only “offline editing” that I allow on human text documents is having people add comments. So not editing, no automated merging.
For “offline editing” that I allow on automation (source code) is GIT which intentionally does not pretend to solve the merge, it just shows revisions. The merge is an action supervised by humans or specialised automation on a “best guess” effort and still needs reviews and testing to verify success.
Ideally, I want to be able to replace git with something that is built on CRDTs. When branches have no conflicts, CRDTs already work fine - since you merge, run tests, and push when you’re happy.
But right now CRDTs are too optimistic. If two branches edit the same line, we do a best-effort merge and move on. Instead in this case, the algorithm should explicitly mark the region as “in conflict” and needing human review, just like git does. Then humans can manually review the marked ranges (or, as others have suggested, ask an llm to do so or something.). And once they’re happy with the result, clear the conflicting range markers and push.
I would agree that git works more than half the time.
Merge resolution is a problem so hard that even otherwise capable developers fuck it up regularly. And on a large team made of people who intermittently fuck up, we start getting into and aggregate failure rate that feels like a lot.
The whole idea with CRDTs was to make something humans couldn’t fuck up, but that seems unlikely to happen. There’s some undiscovered Gödel out there who needs to tell us why.
And that’s if you swap the default diff algorithm for one of the optional ones. I’ve used patience for years but someone tweaked it and called it “histogram” when I wasn’t looking.
How is this different than git's automatic merging? Or another compatible algorithm
- From CRDTs I want support for realtime collaborative editing. This could even include non-code content (like databases). And some developer tools could use the stream of code changes to implement hot-module reloading and live typechecking.
- From git I want .. well, everything git does. Branches. Pull requests. Conflict detection when branches merge. Issue tracking. (This isn't part of git, but it would be easy to add using json-like CRDTs!)
We can do almost all of that today. The one part that's seriously missing is merge conflicts.
So you end up with “conflict” as part of your data model. Seems an amusing way of achieving the C (“conflict-free”) of CRDT, though perfectly legitimate and probably the only way.
The next fun challenge is when you get conflicts over conflict resolution!
For example, one of the simplest state CRDTs is the MAX function. The commutativity rule requires that if we merge some set of states A, B, C, ... in any order, we get the same result. This requires the MAX function to be commutative - ie, MAX(a, b) == MAX(b, a). (And also MAX(a, b, c) == MAX(c, a, b), and so on.)
This must hold for the CRDT to converge & be correct.
I thought the C stood for commutativity for the longest time after learning CRDTs. I admitted that to Martin Kleppmann the first time I met him. He thought for a solid minute, then laughed and said "yeah, I think that works too!"
We go into a bunch more detail in the Upwelling paper about the differences between (what we call) semantic and syntactic conflicts in writing: https://inkandswitch.com/upwelling/
Ultimately, my feeling is that serious collaboration is a document review problem as much as anything else. That said, this is particularly true in journalism and scientific publishing and can be mostly ignored for your meeting notes...
Anyway, if you see this comment, thanks for a nice piece of writing, Alex. Love to see folks wrestling with these problems.
EDIT: Oh, also I meant to link to Upwelling, but forgot what it was called. I settled for a different link instead because it was deadline.
If you're going to use CRDTs, do yourself a favor and either use Redis or similar (though the amount of memory being consumed is painful to think about), or MyRocks [2] (or anything else based on RocksDB / LevelDB). Whatever you do, do _not_ back it with an RDBMS, and especially not Postgres.
[0]: https://news.ycombinator.com/item?id=40834759
[1]: https://supabase.com/blog/postgres-crdt
[2]: http://myrocks.io
[0] https://www.powersync.com/blog/postgres-and-yjs-crdt-collabo...
As part of my ongoing PhD studies, we have developed our own formal model for structured conflict representation, based on lattice theory: “Lazy Merging: From a Potential of Universes to a Universe of Potentials” [2]. Incidentally, it is also a CRDT, but it does not attempt to resolve conflicts automatically. Instead, it represents them within the collaborative documents. Approaching the problem from mathematics allowed us to arrive at a simple conceptual model that can guarantee strong properties, like, e.g., the completeness, minimality, and uniqueness of merges, even after repeated merges of existing conflicts. And merges can be calculates very easily. I always wanted to make a blog post about it, but I never came around to do it.
> hence also the name, conflict-free replicated data type
When I was introduced to CRDTs the acronym stood for commutative replicated data types (eg in the paper by Shapiro et al). I prefer this actually, despite it being harder to pronounce.
A conflict is a complicated idea, and while "conflict free" is a technically correct way of describing the result, it can be misleading as evidenced by this post and your comment.
Commutativity is the property that when Bob applies changes in the order [Bob, Alice] and Alice applies changes in the order [Alice, Bob] that they both end up with the same document. It doesn't imply that the document is somehow "free" of "conflicts" in a sense that may be meaningful at a higher level of abstraction.
https://pages.lip6.fr/Marc.Shapiro/papers/RR-7687.pdf: the paper clearly calls it Conflict-free Replicated Data Types, though it does also define the two styles:
• State-based Convergent Replicated Data Type (CvRDT)
• Op-based Commutative Replicated Data Type (CmRDT)
And calling this "solvable" is a funny thing to think about, since huge portions of the earth think the chaos output of LLMs could be anywhere near deciding the final output of computation at this point in time
You say this word is only required if it's a part of this whole sentence? OK, the other edit kept the whole sentence, but changed a single other word in it, which happened to be the subject.
The given situation is solvable only by the humans involved. They want different things. Either one of them has authority over the other, or they talk it over.
Agreeing on edits requires a shared context, a shared understanding of the goal and requirements of the text being edited, and a shared projection of how readers should understand the text and how they will understand it instead.
The specific contextual information required for automated editing is dependent on the combined situation of all writers, considering their professional, personal and cultural contexts.
Assuming that context can't be made available in a systematized way, the machine will choose an edit that is not guaranteed to match the intentions and expectations of the people involved. Instead, it will just act as adding one more writer to the mix.
1. Consistency
2. Availability
3. Partition tolerance
You can work around this by not using CRDTs/OT. E.g., the server processes operations in receipt order, applying whatever UX logic it wants; clients use a rebase/prediction strategy to still allow optimistic edits on top (cf. https://doc.replicache.dev/concepts/how-it-works). Doing this for text editing has some challenges, but they're separate from the CRDT/OT challenges discussed here.
Things get way nastier when you start considering trees, e.g. yJS operates on JSON documents. From a UI standpoint (where the UI is showing some shallow level and hasn't been expanded to the deeper level) users could never even see edits that have been deleted.
I think that the class of CRDTs that preserve conflicts (IIRC that is when a register can hold multiple values) hold the most promise. Users should then be presented with those conflicts - and it could even be completely visual. Being able to scrub through history also seems like a viable alternative (allowing the user to figure out how a strange thing happened, or how their changes disappeared).
I think "Git" would be a wonderful name for this type of CRDT.
Git is popular, but it's not particularly great at conflict resolution.
IIRC Torvalds himself was quite pessimistic about what could be achieved with automatic merging. (And in this he was correct.) He said that Git had rejected the idea that a version-control system could, or should attempt to, "solve the merging problem" by finding a sufficiently-smart algorithm which would do the right things automatically.
> Offline editing is a UI/UX problem
True. There are two deeper root causes here:
1) computing's commitment to cargo-culting old solutions and, relatedly,
2) its devotion to the belief "in general 5-lb sacks are nicer to deal with than 10-lb sacks: therefore I should fit my 10 lbs of POL into one 5-lb sack".
The default vision of "text editor" is "Mosaic textarea", "MacWrite" or something in between, so the quest is usually to bolt merging onto something like that with the minimum possible change. Make it a menu item, or a little dialog box with a few options. If there is some kind of GUI support for merging hidden deep in the menus it's a programmer-UI diff-merger horror that barely does the minimum, or a strikethrough-based view which feels treacherous like navigating a ship through a fog. But in fact in text editing with offline collaboration merging, partly-manual merging at that, is a central part of the process and it needs to be central to the design of the editor. Unfortunately MacWrite is a local maximum which isn't easy to get far away from.
For example, often when someone mentions "cargo-culting" and "old solutions", their next words are "stop editing code as text, edit as a syntax tree". But it has the same problem, just replace "character" with "statement"! Bob added a line to "else" branch of "if" statement, Alice deleted the entire statement, along with "else" branch - what is the smart system to do?
Though we seem to have collectively decided it's easier to solve this by forcing everyone to adopt the same auto formatter configs, which, I guess fair enough
Git would never automatically mash your local changes in without your explicit consent. bzr would never have dreamed of it. But things like Google Docs will happily do it.
It's awesome to see all the progress y'all have made! Good luck with early access!
Ultimately though, it is as you and others like @zamalek in this thread have said, the mathematical notion of conflict resolution might not actually mean anything semantically, therefore it is difficult to have long running offline edits merge together cohesively; it works with things like Google Docs where the user can see what other users have written in real time, which works for 99% of use cases, and sometimes I wonder whether one really needs such long running offline syncs, as it seems to be a very niche goal to target. Short running offline is nice to have and even necessary, especially for products one expects to work wholly offline, but it is the long term I don't see much value in, as in not collaborating online for weeks or months at a time but still expecting cohesive edit merges.
[0] https://localfirstweb.dev/
Now, what some Git merge software is doing is using LLMs to coordinate merges, which I think might be useful to do so in plain text editing too, perhaps embedded into these CRDT libraries, but fundamentally, there must be someone to facilitate merges, whether it be a human agent or an AI one. It is impossible to do so with an entirely automated solution simply because the machine does not know the intents of all its users.
Actually, I will make a small deviation here: I think it is a big industry/startup/open source project there, in creating a set of semantic diff algorithms/implementations. For example, due to my present job, I am very interested in collaborative editing of electrical circuits, and layouts for PCB and chips. Altium and KiCad are trying, for exmaple, to store everything in XML/text files and put the text files in Git/SVN and I can tell you a botched C++ program is nothing in comparison to a botched and malformed electrical circuit. So we need diff tools that "know" about a text file, vs rich-text with formatting, vs bitmap vs vector image, vs song, vs English text. Anybody want to start an open source project (DM me or put a comment here).
Anyhow, thanks to the authors on the great insights and let's work on the take home!
My profile has links to webpages with my contact info.
Basic symbol or character-by-character manipulation cannot reveal why the changes occurred, just what happened.
We structure text in an implicit hierarchy given by spacing, margins and a bunch of other subtle things that [Char] doesn't capture, but can encode for other humans. I think that this is were all the problems stem from, and that with right (tm) structure a lot of the operations can be merged trivially with way fewer surprises. Easier said that done for sure, and there will be more weird cases, but I'm guessing they will get closer to being just the conflicts that really need authors to deal with.
I imagine it is somewhat a consequence of the divide between engineering and product.
It can resolve all conflicts, and it's such a holy grail to go decentralized / get Google Docs-like editing down to a a package in your favorite language. But, in practice, its intractable for arbitrary data, even just an arbitrary string.
I do wish there was a formal proof that showed / proved this to share in HN discussions re: CRDTs...but hey, this is great! The "it left a u" example is simple, intuitive, and with a charitable listener, I doubt they'd argue we can't figure out a string unambiguously, but we can figure out JSON unmabiguously.
Feels like https://xkcd.com/1831
https://chatgpt.com/share/67538beb-73e8-800a-b602-fe26b131e5...
HMU if interested in building it
One of the surprising things is that LLMs regularly "fix" things that no other system can fix. Like if we both add the same sentence to a doc. It's interesting stuff.
With that said I am not sure that this specific LLM is providing the "right" answer. It seems like AN answer! But I think the real solution might be to ask the user what to do.
https://chatgpt.com/share/67542c95-fea8-8008-8749-7b7daf355c...
Here is the resulting semantically meaningful diff that reconciles the edits:
- The color of orange is orange
+ The colour of orange is orange
This reflects the change in spelling from "color" to "colour," which is the only meaningful difference between the two edits.
The better the diff captures the intent of the user change, the easier the diff is to read, AND it also becomes easier to understand merge conflicts, as the conflict becomes a conflict of intents and not characters. Sometimes certain conflicts can even be avoided if the diffs are chosen right (or are expressive enough, e.g. when code moves can be detected).
Yours sincerely,
CRDT fanboy
The simple litmustest to understand what i mean is this:
You rename a todo changing its meaning and concurrently mark it as done. Can you tell the sync engine to always disregard the marking as done in case of changing the text. If there is no api for this to fully control the merge semantics, your system is not suitable for application data.
1. Look at the authors changeset to put a “reason”, or similar, to the edit
2. Ask whether either of the edits makes the other edit redundant
3. Ask the LLM which order to replay the changes (addition or deletion) to preserve the reason of both edits, or otherwise pick the best order
I doubt you need even a 70B model to get “90% good results”, which is all you probably need, because lets be honest offline editing is quite an edge-case to begin with in 2025.
How does "LLM tries its best" solve the problem of exactly syncing documents offline? "Tries its best" implies it could fail which is what the problem already is. An LLM only adds a new layer of abstraction, but now the downside is it's impossible to analyze.
Maybe I just have a much higher threshold of "solved" than you do, but anything non-deterministic (without some human judgment a la git) is not a good solve for document syncing.
A "powerful enough LLM" is the equivalent of "and a wizard does it".
Cut & paste all the examples you can come up to Claude and tell me if LLMs are not able to do this kind of merging.
Can we guarantee that an LLM will get exactly the same result merging (A ∨ B) ∨ C as it would merging A ∨ (B ∨ C)? Even when the temperature is 0?
This doesn't help you do merges client-side during live collaboration (for showing your optimistic local updates), but there the low latency reduces conflicts anyway, so you can fall back on a semantically-imperfect CRDT.
> Figma isn't using true CRDTs though. CRDTs are designed for decentralized systems where there is no single central authority to decide what the final state should be. There is some unavoidable performance and memory overhead with doing this. Since Figma is centralized (our server is the central authority), we can simplify our system by removing this extra overhead and benefit from a faster and leaner implementation.
> It’s also worth noting that Figma's data structure isn't a single CRDT. Instead it's inspired by multiple separate CRDTs and uses them in combination to create the final data structure that represents a Figma document (described below).
[0] https://www.figma.com/blog/how-figmas-multiplayer-technology...
One example of this is if you and I both add a similar sentence in different spots in the document, asking an LLM to merge this will often result in only one of the sentences being accepted. It's not perfect but it's the kind of thing you can't get in any other technology!
With that all said I don't think LLMs REPLACE merge algorithms. For one, to get sensible output from the LLMs you generally need a diff of some kind, either git style or as the trace ouput of something like eg-walker.
Etherpad.
Bob’s intent is to edit the word color, and inserting a u. But, he is limited to just expressing “put u here,” which is not at all what he wants to achieve it is just a mechanical description of what operations need to occur.
Alice’s intent is to delete the whole sentence, but she’s similarly limited to just saying “delete delete deleted delete delete…” to a bunch of letters.
Ending up with a u is the obvious dumb result of treating language as a pile of characters. The correct behavior is to say: because the world Bob has edited no longer exists, his edit is clearly nonsense, don’t apply it. Which editor does that?
Heck, even person A cannot identify intentions of person B in a systematic way.
Hence it's a non-solvable problem.