diff --git a/src/app/core/persistence/operation-log/docs/operation-log-architecture-diagrams.md b/src/app/core/persistence/operation-log/docs/operation-log-architecture-diagrams.md
index 1eb4b1d39c..4164f26e29 100644
--- a/src/app/core/persistence/operation-log/docs/operation-log-architecture-diagrams.md
+++ b/src/app/core/persistence/operation-log/docs/operation-log-architecture-diagrams.md
@@ -411,9 +411,9 @@ flowchart TB
2. **Efficiency**: Snapshot endpoint is designed for large payloads and stores state directly
3. **Server-Side Handling**: Server creates a synthetic operation record for audit purposes
-## 2c. Late-Joiner Replay with Vector Clock Dominance ✅ IMPLEMENTED
+## 2c. Late-Joiner Replay with UUIDv7 Timestamp Filter ✅ IMPLEMENTED
-When a client receives a SYNC_IMPORT (full state from another client), it must replay any local synced operations that happened "after" the import's vector clock. This ensures local work isn't lost when receiving a full state snapshot.
+When a client receives a SYNC_IMPORT (full state from another client), it must replay any local synced operations created **after** the import (by UUIDv7 timestamp). This ensures local work isn't lost when receiving a full state snapshot.
**Implementation Status:** Complete. See `OperationLogSyncService._replayLocalSyncedOpsAfterImport()`.
@@ -442,35 +442,35 @@ sequenceDiagram
Note over B: Problem: Op3, Op4 were already synced!
If we just apply SYNC_IMPORT, we lose B's work
```
-### The Solution: Vector Clock Dominance Filter
+### The Solution: UUIDv7 Timestamp Filter
-Before replaying local synced ops after a SYNC_IMPORT, we filter out ops that are "dominated" by the SYNC_IMPORT's vector clock. An op is dominated if its vector clock is `LESS_THAN` the SYNC_IMPORT's clock - meaning the op's state is already captured in the imported snapshot.
+Before replaying local synced ops after a SYNC_IMPORT, we filter out ops that were created **before** the SYNC_IMPORT (by comparing UUIDv7 IDs, which embed timestamps). Ops created before the import are already captured in the imported snapshot.
```mermaid
flowchart TD
subgraph Input["SYNC_IMPORT Received"]
- SI["SYNC_IMPORT
vectorClock: A=10, B=5"]
+ SI["SYNC_IMPORT
id: 019abc... (T=1702900000)"]
end
subgraph LocalOps["Local Synced Operations"]
- Op1["Op1: B=1
LESS_THAN → dominated"]
- Op2["Op2: A=5, B=3
LESS_THAN → dominated"]
- Op3["Op3: B=6
GREATER_THAN → NOT dominated"]
- Op4["Op4: A=10, B=5, C=1
CONCURRENT → NOT dominated"]
+ Op1["Op1: id=019aa...
T=1702890000 → BEFORE"]
+ Op2["Op2: id=019ab...
T=1702895000 → BEFORE"]
+ Op3["Op3: id=019abd...
T=1702905000 → AFTER"]
+ Op4["Op4: id=019abe...
T=1702910000 → AFTER"]
end
- subgraph Filter["Vector Clock Comparison"]
- Check["Compare each op's clock
with SYNC_IMPORT clock"]
+ subgraph Filter["UUIDv7 Timestamp Comparison"]
+ Check["Compare each op.id
with SYNC_IMPORT.id"]
end
subgraph Result["Ops to Replay"]
- Replay["Only Op3 and Op4
not dominated"]
+ Replay["Only Op3 and Op4
created after import"]
end
SI --> Check
LocalOps --> Check
- Check --> |"LESS_THAN"| Skip["Skip - already in snapshot"]
- Check --> |"Otherwise"| Replay
+ Check --> |"op.id < import.id"| Skip["Skip - created before import"]
+ Check --> |"op.id >= import.id"| Replay
style Op1 fill:#ffcdd2,stroke:#c62828
style Op2 fill:#ffcdd2,stroke:#c62828
@@ -480,14 +480,12 @@ flowchart TD
style Replay fill:#e8f5e9,stroke:#2e7d32
```
-### Vector Clock Comparison Results
+### UUIDv7 Comparison Results
-| Comparison | Meaning | Action |
-| -------------- | ------------------------------ | ---------------------------------- |
-| `LESS_THAN` | Op happened-before SYNC_IMPORT | Skip (state already captured) |
-| `EQUAL` | Same causal history | Replay (edge case, safe to replay) |
-| `GREATER_THAN` | Op happened-after SYNC_IMPORT | Replay (newer than snapshot) |
-| `CONCURRENT` | Independent changes | Replay (may have unique changes) |
+| Comparison | Meaning | Action |
+| -------------------- | -------------------------------- | ------------------------------------- |
+| `op.id < import.id` | Op created before SYNC_IMPORT | Skip (state already captured) |
+| `op.id >= import.id` | Op created at same time or after | Replay (newer than or equal snapshot) |
### Implementation Details
@@ -502,10 +500,12 @@ const localSyncedOps = allEntries.filter((entry) => {
if (entry.op.opType === OpType.SyncImport || entry.op.opType === OpType.BackupImport)
return false;
- // Must NOT be dominated by the SYNC_IMPORT's vector clock
- const comparison = compareVectorClocks(entry.op.vectorClock, syncImportClock);
- if (comparison === VectorClockComparison.LESS_THAN) {
- return false; // Skip - state already captured in SYNC_IMPORT
+ // Must be created AFTER the SYNC_IMPORT (by UUIDv7 timestamp)
+ // NOTE: We use UUIDv7 comparison instead of vector clock comparison
+ // because imports with isForceConflict=true create a fresh vector clock
+ // with a new client ID, breaking vector clock causality detection.
+ if (entry.op.id < syncImportOp.id) {
+ return false; // Skip - created before SYNC_IMPORT
}
return true;
});
@@ -515,8 +515,12 @@ const localSyncedOps = allEntries.filter((entry) => {
- Only filters local ops (created by this client)
- Only considers synced ops (accepted by server)
-- Uses vector clock comparison to determine dominance
-- `LESS_THAN` means dominated (skip), all other results mean not dominated (replay)
+- Uses **UUIDv7 ID comparison** (not vector clocks) to determine temporal order
+- Ops with ID < import ID are skipped (created before import)
+
+**Why UUIDv7 Instead of Vector Clocks?**
+
+SYNC_IMPORT operations with `isForceConflict=true` create a fresh vector clock with a new client ID. This breaks vector clock causality detection because the import's clock has no causal relationship with existing local operations. UUIDv7 IDs embed millisecond-precision timestamps, providing reliable "created before/after" comparison that works regardless of vector clock semantics.
---