docs/sync-and-op-log/background-info/vector-clock-history-and-alternatives.md
Research compiled Feb 2026 to validate and contextualize the pruning strategy used in Super Productivity's sync system.
Vector clocks grow linearly with the number of participating clients. In a system where users may use 10–20+ devices over time, unbounded clocks waste storage and bandwidth. Pruning reduces clock size at the cost of false concurrency — pruned entries can make two causally ordered events appear concurrent.
Key insight from all sources: False concurrency is safe (creates conflicts to resolve), while false ordering (incorrectly declaring one clock greater than another) causes silent data loss.
small_vclock / big_vclock — size thresholdsyoung_vclock / old_vclock — age thresholdssmall_vclock are never prunedyoung_vclock are never prunedold_vclock are aggressively prunedbig_vclock are pruned regardless of ageThis is the single most important finding, confirmed across multiple sources:
Never prune a vector clock before using it in a comparison.
Pruning removes information. If clock A has entries {X:1, Y:2, Z:3} and you prune Z before comparing against {X:1, Y:2, Z:3}, clock A appears to lack knowledge of Z — making the comparison return CONCURRENT instead of EQUAL.
1. Receive full clock from client
2. Compare full (unpruned) clock against stored clock
3. If accepted: prune THEN store
4. If rejected: return rejection with stored clock for client-side resolution
When both clocks have been pruned, standard comparison is unreliable because missing entries could mean either "never knew about this client" or "entry was pruned." Two approaches exist:
When both clocks are at MAX size:
Super Productivity's conservative approach generates more conflicts but never produces false ordering. Riak's approach generates fewer conflicts but requires a robust sibling merge mechanism.
The project's current architecture uses a simple 2+1 layer approach:
| Layer | Current Mechanism | Precedent |
|---|---|---|
| 1. Server prunes after comparison | Full clock comparison, prune before storage | Dynamo, Riak (post-#613 fix) |
| 2. Same-client check | Monotonic counter comparison for import client's own ops | Novel — always mathematically correct |
The original 4-layer defense (protected client IDs, pruning-aware comparison, isLikelyPruningArtifact, same-client check) was designed to work around a root cause: MAX=10 was too small, making pruning a frequent occurrence that interacted badly with SYNC_IMPORT operations. Commit d70f18a94d increased MAX from 10 to 30, which was later reduced to 20 (a 20-entry clock is ~333 bytes — negligible overhead), and removed the defense layers that were treating symptoms rather than the cause. With MAX=20, pruning requires 21+ unique client IDs — an extremely rare scenario for a personal productivity app. The isLikelyPruningArtifact heuristic was also removed since it had known false positives and was unnecessary at MAX=20. Only the same-client check remains as a safety net — it's always mathematically correct (monotonic counters are definitive).
If the architecture evolves toward a server-centric model where the server is the primary coordinator (rather than a relay), Dotted Version Vectors could bound clock size to the number of active server "vnodes" rather than client devices.
Assign clients numeric IDs from a small, bounded set (e.g., 0–15). When a client retires, its ID can be reclaimed. This bounds clock size without pruning but requires a registration/retirement protocol.
If clients periodically report their known clock state to the server, the server could compute a stable cut and notify clients which entries are safe to GC. This eliminates false concurrency from pruning but requires all-to-all communication.