Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/critical-version-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"loro-crdt": patch
---

Retreat the conservative replay base to the latest single-head critical
version of the two versions' combined history instead of the beginning of
history. When an import or checkout involves a genuinely concurrent branch,
the causal replay now starts at the most recent point that no concurrency
crosses (in the sense of Eg-walker's critical versions), skipping the
fully-synced common prefix that the old empty-version fallback replayed.
13 changes: 13 additions & 0 deletions .changeset/igu-entry-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"loro-crdt": patch
---

Fix a convergence bug where a movable tree's incrementally maintained state
could diverge from a full replay of its own oplog. When newly imported
operations were concurrent with part of the receiving peer's multi-head
frontier, the diff mode was misclassified as concurrency-free and the tree
fast path applied the new moves without adjudicating them against the
existing concurrent branch. The classifier now verifies that every entry
point of the imported region causally covers the whole current frontier,
and otherwise retreats the replay base to the latest critical version so
the competing branches are replayed together.
9 changes: 9 additions & 0 deletions .changeset/quiet-imports-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"loro-crdt": patch
---

Correct LCA unmatched-branch detection when an explicit dependency already
covers the same peer's implicit predecessor. Keep those causally newer imports
on the current replay base, and avoid rebuilding unchanged list-like containers
when a genuinely concurrent update needs a conservative base containing a large
common history.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ loom_test.json
.env
sponsorkit/.cache.json
.claude/
.gstack/
28 changes: 27 additions & 1 deletion context/internal-encoding.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Internal Encoding Context

Verified against code 2026-07-21.
Verified against code 2026-07-31.

Loro has one binary blob envelope, two current binary body formats, two
recognized-but-unsupported legacy top-level modes, and a separate JSON updates
Expand Down Expand Up @@ -149,6 +149,32 @@ Because the set is only ever conservative, a rollback needs no invalidation —
stale names just force the general diff path. Decode failures or exceeding the
name-byte cap permanently disable the optimization for that store.

The general diff path may choose a replay base older than the current state
(the latest single-head critical version, Eg-walker §3.5) so list-like trackers
have enough position context. When that happens,
`DiffCalculator::calc_diff_internal` still walks the common causal history, but
routes it only to containers that have operations in the version-vector
difference between `before` and `after`. Do not treat every container seen since
the conservative base as changed: the List/Text/MovableList safety fallback can
otherwise replay the full history once per unchanged container.

The replay-base walk expands both explicit change dependencies and the implicit previous
counter of the same peer. A change from an existing peer can therefore produce
two paths: an explicit relay dependency and an implicit same-peer predecessor.
The relay may already contain that predecessor. In that case the second path can
reach the end of the queue without meeting the other side even though it is not
concurrent.

To distinguish those cases, `_find_common_ancestor_new` carries the dependency
tip where each path split. When a path remains unmatched, it checks only that tip
against the ancestors of the candidate common frontiers. A covered tip is a
redundant route and does not lower the replay base; an uncovered tip is a real
concurrent branch and keeps the conservative fallback. This is a targeted DAG
reachability check with visited-node and Lamport pruning. Do not replace it with
a complete version-vector containment check for every new peer range: that work
scales with both the update's peer count and the size of the current version
vector, and it duplicates the causal decision the walk is already making.

For a large snapshot regression check, first build the Node package, then run:

```sh
Expand Down
13 changes: 13 additions & 0 deletions crates/loro-internal/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ over graceful degradation.
`MapHandler::ensure_mergeable_*`.
- `src/diff_calc/`: diff calculation when moving between versions.
- `docs/diff_calc.md`: design notes for diff calculation.
- `docs/critical-version-spec.md`: specification and proof skeleton for
replay-base selection (Eg-walker-aligned terminology; defines critical
version, the entry check, and the fallback sweep).
- `docs/mergeable-container-id.md`: current mergeable container id encoding.
- `tests/mergeable_container/` and `tests/mergeable_cid_encoding.rs`: focused
mergeable container regression tests.
Expand All @@ -50,6 +53,16 @@ coverage under `crates/fuzz` and ask before running long fuzz targets.

## Working Rules

- Replay-base selection uses Eg-walker terminology (arXiv:2409.14252 §3.5):
a version V is **critical** when every event outside `Events(V)` happened
after all of `Events(V)` — no concurrency crosses the cut. Non-`Checkout`
diff modes and the tree calculator's lamport windows are only sound when
the base satisfies this; `dag.rs` enforces it via the
`ImportGreaterUpdates` entry check and the
`latest_single_head_critical_version` fallback. Do not use "LCA" in new
code or docs: the meet of two versions is generally NOT a safe replay
base. Read `docs/critical-version-spec.md` before touching
`find_common_ancestor`, diff modes, or `diff_calc/tree.rs` windows.
- Internal invariant violation should fail fast. Invalid external bytes or JSON
should return `Err`.
- Do not silently skip ops, containers, state entries, diffs, or pending changes.
Expand Down
Loading
Loading