[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
152
docs/designs/convergent-sync-crdt.md
Normal file
152
docs/designs/convergent-sync-crdt.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Convergent Sync CRDT Core
|
||||
|
||||
Status: experimental core; not connected to persistence or cloud providers yet.
|
||||
|
||||
Issue: [#2245](https://github.com/binaricat/Netcatty/issues/2245)
|
||||
|
||||
## Goal
|
||||
|
||||
The existing sync engine compares local and remote snapshots against a stored
|
||||
base. That is useful for two replicas, but folding more replicas or providers in
|
||||
different orders is not algebraically safe. The v2 core defines a state-based
|
||||
join so every replica reaches the same state regardless of message order,
|
||||
duplication, or grouping.
|
||||
|
||||
This first change intentionally contains only pure domain logic. Encryption,
|
||||
legacy migration, provider verification, persistence, and UI are separate
|
||||
follow-up changes after this core is reviewed.
|
||||
|
||||
Mutation callers must provide the wall-clock sample used to advance the HLC.
|
||||
The domain layer never reads `Date.now()`, so replaying the same state, device,
|
||||
mutation batch, and timestamp produces identical serialized state.
|
||||
|
||||
## State model
|
||||
|
||||
Each device owns a monotonically increasing counter. A write allocates a unique
|
||||
dot `(deviceId, counter)`. The global version vector allocates collision-free
|
||||
dots, while each candidate records the exact prior dots observed in its own
|
||||
register. The register context is an exact dot set rather than another compact
|
||||
version vector because one device's global counters can interleave writes to
|
||||
different registers. A Hybrid Logical Clock (HLC) supplies a user-facing
|
||||
ordering hint without defining causality.
|
||||
|
||||
The replica contains:
|
||||
|
||||
- one global dotted version vector and HLC;
|
||||
- a compact dot-origin index mapping every device counter to its register;
|
||||
- an MV-register for entity presence;
|
||||
- an MV-register for collection position;
|
||||
- an MV-register for every top-level entity field;
|
||||
- an MV-register for every settings leaf path (arrays are atomic leaves);
|
||||
- observed-remove string entries with their own presence and position
|
||||
registers.
|
||||
|
||||
A string-entry remove is emitted only after the replica has observed a currently
|
||||
visible add. Deleting a locally absent entry is a no-op, so a concurrent add on
|
||||
another replica survives without creating an artificial value/tombstone conflict.
|
||||
|
||||
Mutation application is idempotent for already-selected entity fields, string
|
||||
entries, and settings. A same-value settings write still tombstones active
|
||||
ancestor or descendant paths, and a same-value write against an MV-register
|
||||
conflict still emits a causally dominating resolution.
|
||||
|
||||
A full entity upsert follows the same rule for presence, fields, and collection
|
||||
position: unchanged conflict-free registers are preserved, while accepting a
|
||||
currently selected conflicted value emits a new candidate that dominates every
|
||||
retained alternative.
|
||||
|
||||
String-entry add mutations likewise resolve visible presence and position
|
||||
conflicts even when the selected values are unchanged; conflict-free repeated
|
||||
adds remain no-ops.
|
||||
|
||||
Deletion is a register candidate, not absence from the serialized structure.
|
||||
Tombstones are retained indefinitely in v2. A later recreation replaces a
|
||||
tombstone only when its new dot causally observes the deletion.
|
||||
|
||||
Settings writes keep the active leaf set prefix-free. Replacing an object leaf
|
||||
with an atomic parent (or the reverse) causally tombstones the overlapping
|
||||
paths. Deleting a settings path tombstones that path and every causally observed
|
||||
descendant, so deleting a subtree cannot leave stale leaf registers visible;
|
||||
deleting a nested path does not implicitly remove an atomic ancestor.
|
||||
Independent replicas can still create a parent/descendant shape conflict;
|
||||
materialization then selects a deterministic maximal prefix-free set, keeps
|
||||
non-overlapping siblings, and reports the competing paths and candidates for
|
||||
explicit resolution.
|
||||
|
||||
Entity field updates also write a fresh present candidate. Consequently, an
|
||||
offline deletion racing an offline edit becomes a presence conflict; it cannot
|
||||
silently hide the edit. No-op field writes allocate no dot and do not refresh
|
||||
presence. Deleting a field from a non-present entity may tombstone stale field
|
||||
data but never recreates the entity.
|
||||
|
||||
## Join
|
||||
|
||||
For each register, the join keeps:
|
||||
|
||||
1. candidates present on both sides;
|
||||
2. left-only candidates not covered by the right register's causal context;
|
||||
3. right-only candidates not covered by the left register's causal context.
|
||||
|
||||
Candidates causally dominated by another surviving candidate are removed. The
|
||||
replica vector is the pointwise maximum. A global vector is never used as proof
|
||||
that a candidate from an absent register was superseded; only a candidate in
|
||||
that same register can carry such proof. This makes partial provider states
|
||||
fail validation instead of silently deleting unrelated local data. Join remains
|
||||
commutative, associative, and idempotent. Property tests exercise those laws
|
||||
directly and also reduce 2-20 randomly generated offline replicas using
|
||||
reordered, partitioned, and duplicated joins.
|
||||
|
||||
Reusing a dot for different data or different register addresses is an
|
||||
invariant violation and fails closed. Every global vector counter must also be
|
||||
witnessed by a retained candidate dot or same-register candidate context.
|
||||
Hydration rejects dangling observations so a malformed or partial state cannot
|
||||
use an unsubstantiated vector to discard local candidates during join. It also
|
||||
rejects a context that references any currently retained candidate dot: exact
|
||||
contexts contain dominated history only, so retained references would represent
|
||||
invalid causal dominance or a cycle. The permanent dot-origin index proves that
|
||||
each candidate and context dot belongs to the register claiming it, so copying
|
||||
an omitted register's dot into unrelated context cannot satisfy validation.
|
||||
Origin indexes join by dot and reject conflicting register identities.
|
||||
|
||||
## Materialization and conflicts
|
||||
|
||||
Concurrent candidates remain in the CRDT state. A deterministic materialized
|
||||
snapshot is selected for legacy readers and immediate application:
|
||||
|
||||
1. a value sorts after a tombstone;
|
||||
2. then HLC wall time and logical counter;
|
||||
3. then device ID using locale-independent UTF-16 code-unit order;
|
||||
4. then device counter.
|
||||
|
||||
Candidate ordering in canonical serialization uses the dot, not the selected
|
||||
winner order. Dot and HLC objects are rebuilt with fixed property order so
|
||||
provider JSON key ordering cannot change identity or serialized bytes.
|
||||
Conflicts are emitted in collection, entity, field-path, and dot order.
|
||||
Resolving a conflict creates a new write whose causal context covers all
|
||||
observed candidates, so the resolution remains stable when stale replicas
|
||||
return.
|
||||
|
||||
Internal field and position conflicts are emitted only while their parent
|
||||
entity or string entry is materialized. An accepted parent deletion retains
|
||||
the underlying causal metadata for future explicit recreation but suppresses
|
||||
non-actionable child conflicts from the current conflict list. A concurrent
|
||||
delete/update whose selected presence remains visible still exposes both the
|
||||
presence conflict and all active internal conflicts.
|
||||
|
||||
## Complexity
|
||||
|
||||
State validation and canonical serialization are linear in registers,
|
||||
candidates, and retained context dots. Join additionally compares the bounded
|
||||
set of concurrent candidates within each register. Sorting is bounded by keys
|
||||
within each map. Batch mutation clones the replica once, avoiding a full-state
|
||||
copy per imported entity. `npm run bench:sync-crdt` reports non-gating
|
||||
measurements for 1,000, 5,000, and 10,000 entities so accidental quadratic
|
||||
behavior is visible during review.
|
||||
|
||||
## Follow-up boundaries
|
||||
|
||||
The encrypted v2 envelope, legacy baselines, migration preview, protection
|
||||
snapshots, key rotation, and fail-closed protocol rules are described in
|
||||
[`convergent-sync-protocol-v2.md`](./convergent-sync-protocol-v2.md). The final
|
||||
change will integrate provider read-merge-write-verify loops, multi-window
|
||||
locking, conflict resolution state, and localized settings UI.
|
||||
Reference in New Issue
Block a user