Two people can update the same field record while their devices are offline. When the devices reconnect, a later wall-clock timestamp does not tell me whether one edit actually saw the other. Before choosing a winner, I need to know whether the updates are causally ordered or concurrent.

In MESH, I explored that question with version vectors and a small conflict resolver. The useful distinction is between “this update follows the other one” and “both updates branched from state neither device had seen.”

MESH / CAUSALITY BEFORE MERGE

Both edits happened. Neither replica saw the other.
VIRTUAL REPLICA MODEL
SHARED BASE / LAST OBSERVED STATEA 01 · B 01
OFFLINE PARTITION
ALPHA / DEVICE A
{ A: 02, B: 01 }

PATCH status → IN_PROGRESS

BRAVO / DEVICE B
{ A: 01, B: 02 }

PATCH location → 52.23, 21.01

CONCURRENTNeither vector dominates the other.
max(A, B) = { A: 02, B: 02 }
STRATEGYFIELD MERGENO OVERLAP

Keep both independent changes.

status = IN_PROGRESS+location = 52.23, 21.01→vector = A 02 · B 02

Disjoint fields: both edits survive and the joined vector records both histories.

A small source-level model of MESH’s version-vector comparison and two concurrent-payload outcomes. It is not a Bluetooth trace or a device test.

A clock per device records what a replica has observed

The example begins from a shared task state with vector { ALPHA: 1, BRAVO: 1 }. While disconnected, Alpha records a status change and advances its own counter to { ALPHA: 2, BRAVO: 1 }. Bravo records a location change and reaches { ALPHA: 1, BRAVO: 2 }.

Neither vector dominates the other. Alpha has the newer Alpha counter, but it has not observed Bravo’s second event; Bravo has not observed Alpha’s second event either. The two edits are concurrent even if one device’s calendar clock is ahead.

Merging the vectors takes the maximum counter for each device, producing { ALPHA: 2, BRAVO: 2 }. That joined vector says the merged replica has observed both histories. It does not, by itself, decide which field values to keep. Causality detection and payload resolution are separate jobs.

This is why a vector clock is useful here: it gives each replica a partial order. A dominating vector can fast-forward a stale replica. Equal or subordinate input can be ignored as already known. Only when neither side dominates does the resolver need a concurrent-update policy.

Merge independent patches, inspect overlapping fields

The TypeScript ConflictResolver compares the incoming and existing operation vectors first. For concurrent operations, it looks at the keys present in each payload. If the payloads have no field with different values, it combines them and joins the vectors. Alpha’s status and Bravo’s coordinates can both survive in one result.

If both payloads change the same field to different values, MESH marks a conflict and applies a deterministic policy. The default compares the operation’s Lamport time; equal times use the origin device ID as a tie-breaker. Non-overlapping keys from the other payload are preserved, but the winning operation supplies the contested key.

For a disagreement that looks safety-critical, such as one status containing HAZARD while the other does not, the default resolver can quarantine the result instead of silently selecting a value. The quarantine retains both operation DTOs and the conflicting field names for review. That is a guardrail in this prototype, not a complete safety workflow or a domain-approved hazard classifier.

There is an important precision limit in the current implementation: this is a merge of the changed keys supplied in two payloads, not a schema-aware, base-relative three-way merge. It relies on operations carrying useful patches. If callers send full snapshots, unchanged fields may appear to overlap and trigger a conflict. A production design would make that operation contract explicit, validate it, and test it against the application’s actual entity schemas.

Determinism does not mean semantic correctness

The Lamport-time and device-ID rule makes a collision resolve the same way when replicas process the same pair in either order. That property helps replicas converge. It does not prove that the chosen value is the one a technician intended. A timestamp can order two conflicting edits; it cannot decide whether a reported hazard is true.

That is why the quarantine path matters, and why it must remain narrow and reviewable. A real application would need a defined human resolution flow, audit retention, access control, and tests for retries, reordered delivery, deletion, and schema changes. The string check in this prototype should not be reused as a universal safety policy.

What I checked, and what remains a prototype

At private source snapshot ca4c660, the repository contains focused tests for vector dominance, concurrency, missing updates, vector joins, fast-forward, stale input, disjoint patches, last-write-wins, and hazard quarantine. Its property tests generate vectors and check reflexivity, merge commutativity, idempotency, associativity, least-upper-bound behavior, transitivity, and the causal-or-concurrent classification.

I reran a narrow check against the TypeScript VersionVector implementation: the two divergent vectors above were classified as concurrent, their element-wise join was { ALPHA: 2, BRAVO: 2 }, and that result dominated both inputs. I did not run the full Jest suite or build the mobile targets for this review, so I am not claiming fresh suite or device-validation results.

MESH is a prototype. The repository’s walkthrough uses in-memory SQLite, virtual peers and a mock sync transport; it does not send Bluetooth radio packets. Its native bridge examples do not establish Android or iOS background-sync behavior, and the demo storage is not encrypted. The vector-clock example demonstrates a small ordering rule, not a qualified emergency-response system.

This note complements FieldCore’s idempotent retry handling: that article asks what happens when the server receives one operation twice; this one asks how two different operations should be compared after independent edits.

If you are building offline workflows where a conflict needs an understandable audit trail, I’d be glad to compare the trade-offs.

← All notes