KVS concurrent-merge & mergeable refs (conflict-as-data)
Corpo da especificação
Spec — KVS concurrent-merge & mergeable refs (resolves kvs-RFC-002 OQ-2)
The differentiator: a fleet of AI agents pushing to the same branch at high
frequency must all land without non-ff rejection and without silent overwrite.
This spec is the normative contract for ref advancement, server-side merge, and
the conflict-as-data outcome. It is the research-y core; the existing kvs client
merge engine (KVS#179/#202) is the head-start.
R1 — Objects never conflict (CAS, content-addressed)
Writing a git object is never blocked or rejected: identical content → identical
hash (idempotent); different content → different hash (both stored). Object upload
is concurrency-free by construction (kdb-obj CAS, kdb-RFC-006). The only
contention point in the system is the mutable ref.
R2 — Ref advance is non-blocking + auto-merging (no locks)
There is no per-repo and no per-ref lock. Concurrent pushes to the same branch do not serialize behind a lock and do not get a non-ff rejection. Each push attaches its commit(s) to the branch DAG and triggers a server-side merge (R3) that advances the branch tip. Fast-forward (incoming is a descendant of tip) skips merge and advances directly (R7).
R3 — Merge mechanism (Driver-selected, RFC-006 §5.2)
The server computes a three-way merge merge(base, tip, incoming):
- Per-file merge driver selected via
.gitattributes(the Driver mechanism, RFC-006 §5.2 — zero git-core fork). Default driver = git textual three-way. - AST/structure-aware drivers per language where available (the
kvsengine, KVS#179) — disjoint structural edits to the same file auto-merge where textual three-way would conflict. - Drivers are deterministic + pure (same inputs → same merged tree/conflict set) so the optimistic retry (R5) converges and replays are stable.
R4 — Conflict = first-class committable object (NEVER reject, NEVER overwrite)
When a driver cannot fully resolve, the merge does not reject the push and does
not silently pick a side. It produces a conflict object (jj model, KVS#179):
the merge commit records the unresolved regions as structured data (both sides +
base, per region), the branch tip advances to that commit, and the ref enters an
explicit needs-resolution state surfaced via API/UI/MCP. Downstream reads see
a well-formed tree (conflict regions rendered deterministically) tagged
has_conflicts=true; resolution is a normal follow-up commit that clears the
markers. Silent index/tree corruption is forbidden (this is the exact failure
this spec exists to prevent).
R5 — Exactly-one-winner serialization (optimistic, bounded retry)
The durable ref store provides a single-statement CAS linearization point (refs→kdb, FLOW-148). The advance loop:
loop (bounded N):
base ← read branch tip # linearization read
merged ← merge(base, incoming) # R3, pure
if CAS(branch tip: base → merged): done # exactly-one-winner
else continue # another writer won the race; re-read + re-merge
Because objects are CAS (R1) and merge is pure (R3), retry is idempotent and converges (each iteration starts from a newer tip). The retry bound exists only as a livelock backstop; exceeding it is an operational alarm, not a client error. No lock is held across the merge (kdb#769/#790/#791/#792 off-lock pipelining is the data-plane enabler).
Throughput realization (KVS-233/234, default): N-way optimistic retry is correct but O(N²) under high single-branch contention (every loser re-merges). The host therefore serializes one branch's advances through a single per-branch merge worker that is the branch's sole writer → each incoming is merged exactly once (O(N), no retry storm), while distinct branches and repos stay fully parallel (the fleet is never globally serialized). This is the optimistic model collapsed to its one-winner outcome without the wasted re-merges — not a held lock across the merge. (Impl note: an out-of-process merge worker must import the push's quarantined objects into the shared store before merging — KVS-234.)
R6 — Provenance on every merge (auditability for the AI fleet)
Every server-side merge records which principal/agent contributed which side
(KVS#202 provenance trailers + git notes) — human or agent (namespace-tree R-AI).
A conflict object records the provenance of each conflicting region's sides. This
makes "which agent's concurrent edit caused this conflict" answerable (D8
observability), essential when most writers are agents.
R7 — Fast-forward & force semantics
- Fast-forward stays fast-forward: if incoming strictly descends tip, advance with no merge (cheapest path; the common single-writer case).
- Force-advance (replace tip with a non-descendant, discarding commits) is an explicit, permission-gated, audited operation — never the default of a normal push, and never how a concurrent race is resolved.
R8 — Submit protocol: the refs/for/<branch> pseudo-ref (added 2026-06-10, KVS-232)
git's receive-pack applies the fast-forward check to real branch refs
(refs/heads/*) regardless of any proc-receive config — so a divergent push to a
branch is rejected before any server merge can run. The mergeable-ref property
(R2) is therefore delivered over a pseudo-ref submit namespace: clients push to
refs/for/<branch> (Gerrit-style — a pseudo-ref is always a "create", so git
applies no ff-check), the proc-receive hook runs the engine (R3/R5) to advance
the real refs/heads/<branch>, and reports the mapping back (option refname refs/heads/<branch> + the merged new-oid). A direct push to refs/heads/<branch>
keeps git's normal fast-forward-only semantics (the single-writer fast path, R7).
The push refspec (HEAD:refs/for/<branch>) is set transparently by KVS tooling /
the agent MCP path and by a server-provided remote.origin.push in the clone.
Lesson (KVS-227 false-positive): a concurrency test that merely fires N parallel pushes can pass by serializing into fast-forwards, never exercising divergence. Tests MUST force divergence (clone two at the same base, let one land, then push the other → assert it MERGES, not rejects) — see T1.
Tests (normative)
- T1 — N concurrent pushes to one branch all land, 0 non-ff rejections; final tip is a merge reachable from every pushed commit. AND a deterministic divergence case (two clones at the same base; one lands; the other's push MUST auto-merge, not reject) — the parallel-only form can false-pass via serialized FFs.
- T2 — unresolvable overlap → a conflict object +
needs-resolutionstate; the push is accepted (not rejected) and no side is silently lost. - T3 — disjoint structural edits to the same file (AST driver) auto-merge with no conflict where textual three-way would conflict.
- T4 — optimistic retry converges under a hot race (M writers, bounded iterations, idempotent result independent of arrival order — the kdb#790 apply- order-independence property).
- T5 — fast-forward path takes no merge; force-advance requires the gated permission and emits an audit record.
- T6 — provenance: a merged tip / conflict object reports the contributing principals per side.
Anti-patterns
- Per-repo or per-ref locking (serializes the fleet — the regime we reject).
- Non-ff rejection as the concurrency answer (the thundering-herd retry pain).
- Silent overwrite / last-writer-wins on the ref (data loss; the corruption this spec forbids).
- Holding any lock across the merge computation (kills throughput).
- A non-deterministic merge driver (breaks retry convergence + replay).
Referências
rfcs/kvs-RFC-002-concurrent-ai-first-forge-on-kdb.kmdrfcs/stack-RFC-006-object-storage-plane-and-trust-tiering.kmdproducts/dev/flow/engine#148 (refs→kdb single-statement CAS primitive)KVS#179 (conflict-as-data) / KVS#180 (virtual-branch) / KVS#202 (provenance trailers)