ruvnet/ruflo · critical · ConcurrentWriteError

Concurrent write detected on aggregate '${aggregateId}'. Res

Error message

Concurrent write detected on aggregate '${aggregateId}'. Resolve via contest mechanism.

What it means

FederatedEventStore.applyRemoteEvent compares the remote event's vector clock against the local vclock for the aggregate; when compareVectorClocks returns 'concurrent' (neither dominates), both nodes wrote events independently and the store refuses to silently pick a winner. It throws the typed ConcurrentWriteError and expects the conflict to be settled out-of-band through the claims contest mechanism (contestSteal/resolveContest).

Source

Thrown at v3/@claude-flow/claims/src/infrastructure/federated-event-store.ts:226

   * Apply an event received from a federation peer.
   * Throws `ConcurrentWriteError` if the remote event is concurrent with our
   * latest known state for the aggregate; the caller should surface this as
   * a contest.
   */
  async applyRemoteEvent(
    event: ClaimDomainEvent,
    remoteVclock: VectorClock,
    remoteHlc: HlcTimestamp,
    envelopeSignature?: string,
  ): Promise<void> {
    const state = this.aggregates.get(event.aggregateId) ?? {
      vclock: zeroVectorClock(),
    };

    // Concurrency check
    const order = compareVectorClocks(state.vclock, remoteVclock);
    if (order === 'concurrent') {
      throw new ConcurrentWriteError(event.aggregateId, state.vclock, remoteVclock);
    }
    if (order === 'after' || order === 'equal') {
      // We've already seen this or a later event — drop silently (idempotent).
      return;
    }

    // Update our HLC from the remote (may throw HlcSkewError).
    const mergedHlc = this.hlc.update(remoteHlc);
    const mergedVclock = mergeVectorClocks(state.vclock, remoteVclock);

    const stamped = writeFederationMetadata(event, {
      hlc: mergedHlc,
      vclock: mergedVclock,
      originNodeId: readFederationMetadata(event)?.originNodeId ?? 'unknown',
      arrivedFromFederation: true,
      envelopeSignature,
    });

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Catch ConcurrentWriteError and route the aggregate into the contest workflow: let contestSteal/resolveContest pick the winner, then re-apply
  2. Serialize writes per aggregate: only the current claim owner (or lease holder) emits events; everyone else syncs read-only
  3. Before a rejoined node writes again, replay/sync bidirectionally so one vclock dominates
  4. Deduplicate identical events before applying so re-delivery never produces concurrent clocks

Example fix

// before
await store.applyRemoteEvent(ev, vclock, hlc);

// after
import { ConcurrentWriteError } from '../infrastructure/federated-event-store.js';
try {
  await store.applyRemoteEvent(ev, vclock, hlc);
} catch (e) {
  if (e instanceof ConcurrentWriteError) {
    await workStealing.resolveContest(ev.aggregateId, localClaimant, 'concurrent-write');
    return;
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Try / catch

import { ConcurrentWriteError } from './federated-event-store.js';
try { await store.applyRemoteEvent(ev, vclock, hlc); }
catch (e) {
  if (e instanceof ConcurrentWriteError) {
    await workStealing.contestSteal(ev.aggregateId, localClaimant, 'concurrent write');
    await workStealing.resolveContest(ev.aggregateId, decidedWinner, 'queen decision');
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Two nodes apply events to the same claim aggregate without syncing first — both steal or mutate the same issue during a network partition, or a node replays buffered outbound events after reconnect while the peer already advanced the aggregate.

Common situations: Multi-writer topologies without per-aggregate ownership or a lease; partition healing; restarting a node from an old snapshot while others progressed; test harnesses driving two stores with the same events in different orders.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/5ca9f413815e8505. Report an issue: GitHub.