[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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View 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.