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
- Propose only from the primary: subscribe to 'primary.elected' (or check engine.isLeader()) and route proposals accordingly.
- Forward the value to the current primary over the transport when this node is a follower.
- Ensure election/view setup has completed before the first propose.
- 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
- Route all proposals through a primary-aware dispatcher; never propose from every replica.
- Cache the current primary from the 'primary.elected' event and re-check before each proposal.
- Expect view changes: on leadership loss, re-check and retry or forward.
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
- Only leader can propose values
- Unknown consensus algorithm: ${this.config.algorithm}
- Consensus engine not initialized
- Proposal ${proposalId} not found
- LocalTransport: peer ${target.nodeId} is closed
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/6686f175a56c1d1c.
Report an issue: GitHub.