ruvnet/ruflo · error · Error

Only leader can propose values

Error message

Only leader can propose values

What it means

RaftConsensus.propose() requires this.node.state === 'leader'; followers and candidates throw. Leadership comes only from Raft election, and the ConsensusEngine facade forwards the 'leader.elected' event and exposes isLeader()/getLeaderId() so callers can check before proposing.

Source

Thrown at v3/@claude-flow/swarm/src/consensus/raft.ts:181

  addPeer(peerId: string): void {
    this.peers.set(peerId, {
      id: peerId,
      state: 'follower',
      currentTerm: 0,
      log: [],
      commitIndex: 0,
      lastApplied: 0,
    });
  }

  removePeer(peerId: string): void {
    this.peers.delete(peerId);
  }

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

    this.proposalCounter++;
    const proposalId = `raft_${this.node.id}_${this.proposalCounter}`;

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

    // Add to local log
    const logEntry: RaftLogEntry = {
      term: this.node.currentTerm,

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Wait for the 'leader.elected' event, then propose only from the leader (check engine.isLeader()).
  2. Forward values to the current leader (engine.getLeaderId()) when this node is a follower.
  3. Retry the proposal after an election timeout — leadership may have moved to this node.
  4. In tests, drive the election explicitly before asserting propose() succeeds.

Example fix

// before — propose from any node
await raft.propose(value); // throws unless this node is leader

// after — gate on leadership
let leaderId: string | undefined;
engine.on('leader.elected', ({ leaderId: id }) => { leaderId = id; });
if (engine.isLeader()) {
  await raft.propose(value);
} else {
  await forwardToLeader(leaderId, value);
}
Defensive patterns

Strategy: validation

Validate before calling

// the facade exposes leadership explicitly
if (!engine.isLeader()) {
  const leaderId = engine.getLeaderId();
  if (!leaderId) throw new Error('no leader elected yet');
  await forwardToLeader(leaderId, value);
} else {
  await engine.propose(value);
}

Try / catch

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

Prevention

When it happens

Trigger: Proposing before the election has finished (node is 'follower' or 'candidate'); proposing after this node lost leadership through a term change or partition; every node in a test cluster proposing simultaneously instead of only the elected leader.

Common situations: Symmetric deployments where each replica proposes on a timer; leadership churn under network partitions; single-node tests where election was never triggered; proposals routed to a stale leader id.

Related errors


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