ruvnet/ruflo · error · Error

Only primary can propose values

Error message

Only primary can propose values

What it means

ByzantineConsensus is primary-based (PBFT-style): only the node currently elected primary may propose values. electPrimary() sets node.isPrimary on exactly one node and emits 'primary.elected'; propose() on any other node throws before a proposal is created. The ConsensusEngine facade exposes this via isLeader(), which delegates to isPrimary() for the byzantine implementation.

Source

Thrown at v3/@claude-flow/swarm/src/consensus/byzantine.ts:184

  electPrimary(): string {
    const nodeIds = [this.node.id, ...Array.from(this.nodes.keys())];
    const primaryIndex = this.node.viewNumber % nodeIds.length;
    const primaryId = nodeIds[primaryIndex];

    this.node.isPrimary = primaryId === this.node.id;

    for (const [id, node] of this.nodes) {
      node.isPrimary = id === primaryId;
    }

    this.emit('primary.elected', { primaryId, viewNumber: this.node.viewNumber });

    return primaryId;
  }

  async propose(value: unknown): Promise<ConsensusProposal> {
    if (!this.node.isPrimary) {
      throw new Error('Only primary can propose values');
    }

    this.proposalCounter++;
    const sequenceNumber = ++this.node.sequenceNumber;
    const digest = this.computeDigest(value);
    const proposalId = `bft_${this.node.viewNumber}_${sequenceNumber}`;

    const proposal: ConsensusProposal = {
      id: proposalId,
      proposerId: this.node.id,
      value,
      term: this.node.viewNumber,
      timestamp: new Date(),
      votes: new Map(),
      status: 'pending',
    };

    this.proposals.set(proposalId, proposal);

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Propose only from the primary: subscribe to 'primary.elected' (or check engine.isLeader()) and route proposals accordingly.
  2. Forward the value to the current primary over the transport when this node is a follower.
  3. Ensure election/view setup has completed before the first propose.
  4. On view change, re-check primary status and retry the proposal.

Example fix

// before — every replica proposes
await byzantine.propose(value); // followers throw

// after — only the current primary proposes
let currentPrimary: string | null = null;
byzantine.on('primary.elected', ({ primaryId }) => { currentPrimary = primaryId; });
if (currentPrimary === thisNodeId) {
  await byzantine.propose(value);
} else if (currentPrimary) {
  await transport.send(currentPrimary, { type: 'propose', value });
}
Defensive patterns

Strategy: validation

Validate before calling

// ConsensusEngine.isLeader() delegates to isPrimary() for the byzantine implementation
if (!engine.isLeader()) {
  throw new Error('not primary; forward the proposal to the current primary');
}
await engine.propose(value);

Type guard

function canPropose(engine: ConsensusEngine): boolean {
  return engine.isLeader(); // true only for the byzantine primary / raft leader
}

Try / catch

try {
  await byzantine.propose(value);
} catch (e) {
  if (e instanceof Error && e.message === 'Only primary can propose values') {
    // wait for the next 'primary.elected' event, or forward the value to the primary
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling propose() from a follower node; proposing before electPrimary()/initial election has run; proposing after a view change where this node lost primary status; symmetric deployments where every replica unconditionally runs the same propose code regardless of role.

Common situations: Fleet-wide code proposing on a timer from every node; view-change races during primary failure; single-node tests that never ran an election; proposals routed to the wrong node after membership churn.

Related errors


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