Skip to content

ADR-0005 — Last-write-wins sync with cursor-kind awareness, no CRDTs in v0

Multiple Hakiri replicas — laptops, daemons, edge workers — can push to the same shared bucket. Conflicts arise on:

  • run files (two replicas wrote under the same logical run id),
  • snapshot files (two replicas compacted at the same time),
  • cursors (two replicas advanced the same pipeline),
  • config.toml (two replicas edited project settings),
  • schema (two replicas evolved the same table differently).

Two extremes were available: a full CRDT layer over all metadata (always-mergeable, formally correct), or “last write wins, document the edge cases” (simple, occasionally requires human reconciliation).

Last-write-wins (LWW) with cursor-kind awareness, append-only data, and node-id-prefixed paths. Connectors declare a cursor-kind in their WIT export that determines whether concurrent runs are safe:

cursor-kindMulti-writer behavior
monotonic (timestamp, autoincrement)LWW safe — concurrent runs allowed
opaque-token (vendor page tokens)Single-writer lease required
snapshot-id (Postgres LSN, S3 version)Single-writer lease required

The catalog holds a pipeline_lease row for non-LWW cursors; replicas acquire via atomic CAS with a TTL. Run and snapshot file paths include the writer’s node id, so two replicas under the same logical run id produce disjoint files instead of colliding. config.toml conflicts surface in hakiri sync status with git-style markers. Divergent schema evolutions require hakiri schema reconcile.

No CRDTs in v0.

This ADR applies to the data-sync layer: run files, snapshot files, cursors, config.toml reconciliation between replicas (daemons / cluster workers), and schema-evolution decisions. These are written by daemons and reconciled against an S3-compatible bucket — the 04-context-store.md data plane.

A separate decision in ADR-0013 (2026-05-12) introduces a Loro CRDT for the collaborative config-editing surface between human operators editing the manifest from the Electron app and the web UI (13-team-surfaces.md, 14-collab-config.md). The two scopes are orthogonal:

LayerConflict modelWhere
Data sync (daemons ↔ bucket)LWW + lease-based single-writerthis ADR
Config editing (humans + Electron / web ↔ control plane)Loro CRDT, materialized to TOML snapshots on applyADR-0013

The daemon never imports a CRDT library — it consumes materialized TOML manifest snapshots. The CRDT lives entirely above the data plane.

Positive

  • Implementation is small and well-trodden: atomic CAS for leases, UUIDv7 for run ids, node id in paths.
  • No CRDT library dependency. The conflict-resolution rules are inspectable in plain Rust.
  • The “common case” (one writer, occasional sync) is zero overhead.
  • Connectors that can safely run concurrent (the majority — anything timestamp-based) do so without coordination.

Negative

  • Simultaneous schema renames across replicas require human reconciliation. Documented as “use one writer at a time when evolving schemas.”
  • A misbehaving connector that fails to declare cursor-kind defaults to opaque-token (safe-by-default) and pays the leasing cost unnecessarily.
  • Lease TTL is a tunable; too short and crashed replicas lock out healthy ones briefly, too long and concurrent runs serialize unnecessarily. Default 10 minutes; revisit with usage data.

Neutral

  • CRDT-shaped problems (true multi-writer simultaneous schema evolution, multi-region active-active) are explicitly out of scope. Teams that need them can run a single writer per pipeline.

Full CRDT metadata layer (automerge or yrs). Formally correct simultaneous edits to manifests, cursors, and schemas with no conflict surface at all. Rejected for the data-sync layer because the implementation cost is large, the dependency surface is large, and the use case (concurrent schema edits from two laptops) is rare enough that “ask the team to coordinate” is acceptable.

Partially reconsidered for config editing (2026-05-12). Once the day-1 team product landed — see 13-team-surfaces.md — a collaborative editor did become a workflow (multiplayer pipeline editing from Electron and web). The CRDT decision for that surface lives in ADR-0013, which chose Loro for its MovableList / Tree types and Rust-native fit. The choice is scoped: it applies only above the data plane (in hakiri-control and the Electron / web renderer) and does not enter the daemon or the sync engine. This ADR’s LWW + lease-based decision still governs everything below the editing surface.

Pessimistic locking on every write. Trivial to reason about, but kills the local-first property — a laptop without network connectivity can’t acquire a lock, so it can’t run anything. Off-table for Pillar 2 (local-first).

Operational Transform. Same correctness goals as CRDTs with more implementation pain and a less mature Rust ecosystem. Strict downgrade.