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
- Catch ConcurrentWriteError and route the aggregate into the contest workflow: let contestSteal/resolveContest pick the winner, then re-apply
- Serialize writes per aggregate: only the current claim owner (or lease holder) emits events; everyone else syncs read-only
- Before a rejoined node writes again, replay/sync bidirectionally so one vclock dominates
- 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
- Enforce single-writer per aggregate: only the claim owner/lease holder emits events
- Sync bidirectionally before a rejoined node starts writing again
- Deduplicate identical replayed events so clocks never become concurrent from re-delivery
- Monitor for ConcurrentWriteError frequency — a spike means ownership is not being respected
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
- HLC skew exceeded: received physicalMs=${receivedPhysicalMs}
- FederationBridge.decode: expected type 'claim-event', got '$
- FederationBridge.decode: payload missing or non-object
- FederationBridge.decode: payload missing required fields
- Unsupported federation signature mode: ${String(signatureMod
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/5ca9f413815e8505.
Report an issue: GitHub.