Case Study: Automatic CRDT Fork Resolution with Fork Agent
Problem: When two replicas of cks-mcp concurrently modify the same knowledge object (e.g. via gossip), a CRDT conflict (fork) arises. Without automatic resolution, the conflict remains unresolved, leaving the graph in an inconsistent state and requiring manual intervention.
CKS solution: CRDT adapter (ADR-013) Stages 2–3: MV-Register,
causality tracking, fork detection events, and a dedicated
ForkResolutionAgent that autonomously claims and resolves
crdt_fork tasks from the persistent outbox.
Scenario
Section titled “Scenario”We run two instances of cks-mcp (on ports 8765 and 8766) sharing
knowledge via gossip, backed by SQLite databases /tmp/cks-a.db and
/tmp/cks-b.db. A session with a single object concept-1 is created
on instance A. Then a conflicting update is applied on instance B.
Gossip synchronises the sessions, and the CRDT layer detects a fork in
the MV-Register.
Tools Used
Section titled “Tools Used”validate_knowledge/evolve_knowledge– creating and modifying the session.cks-fork-agent– standalone process that resolves CRDT forks.- SQLite introspection – verifying MV-Register state.
What Happened
Section titled “What Happened”- Session creation – Instance A received a
concept-1with description “Initial version”. - Conflicting update – Instance B independently updated the same object to “Version from instance B” without coordinating with A.
- Gossip synchronisation – After gossip rounds, both replicas contained both versions of the object.
- CRDT fork detection – The CRDT layer detected two concurrent
pointers for
concept-1incks_mv_registerand escalated aCRDTForkDetectedevent. - Outbox task creation – A
crdt_forktask was written to the persistent outbox (cks_outbox_tasks). - Fork Agent resolution –
cks-fork-agent(polling every 10 s) claimed the task, applied its resolution policy (causality → most‑recent → alphabetical tie‑break), and calledresolve_pointer, collapsing the MV-Register to a single winner. - Verification –
SELECT * FROM cks_mv_register WHERE pointer_key = 'concept-1'returned exactly one row; the outbox task status wasCOMPLETED.
Log excerpt:
[cks-fork-agent] resolved crdt_fork task_id=3 pointer_key=concept-1Key Takeaways
Section titled “Key Takeaways”- CRDT fork resolution works end‑to‑end. The agent correctly consumes outbox tasks, resolves forks, and updates the MV-Register.
- Fully autonomous. No human intervention was required after the initial session creation and conflicting update.
- Persistent outbox ensures reliability. If the agent crashes mid‑resolution, another instance can claim the task.
- Integration with gossip is functional (verified in a separate setup). The CRDT quarantine, Merkle tree, and causality checks all performed as designed.
Current Limitations
Section titled “Current Limitations”OutboxEmbeddingWorkerraises'AddObject' object has no attribute 'object_id'in certain background tasks – tracked as a known bug (see issue #…).- CRDT objects are only populated during gossip exchange, not on
local
evolve_knowledgecalls – this is by design (ADR-013 Stage 2) but means the fork agent cannot resolve conflicts that originate purely within a single node. - Gossip synchronisation itself requires careful ordering of startup and occasional manual restart – stability improvements are planned.
Reproduce It Yourself
Section titled “Reproduce It Yourself”- Start two cks-mcp instances with gossip enabled on different ports
and separate SQLite databases (see
scripts/folder for helper scripts). - Create a session on instance A with
validate_knowledge. - Apply a conflicting update on instance B with
evolve_knowledge. - Wait for gossip rounds (or manually trigger sync).
- Inspect
cks_mv_register– two rows should appear. - Start
cks-fork-agent --poll-interval 10pointing at one of the databases. - Verify that after ~10–20 seconds only one row remains and the
outbox task is marked
COMPLETED.