ruvnet/ruflo · error · Error
LocalTransport: replayed/out-of-order seq from ${msg.from} (
Error message
LocalTransport: replayed/out-of-order seq from ${msg.from} (${msg.seq} <= ${last}) What it means
Part of LocalTransport's replay defense: when the target verifies signed messages, it tracks the highest seq seen per sender and requires strictly increasing values. This error means msg.seq is <= the last seq recorded from that sender — either a literal replay of an earlier message or an out-of-order/duplicate delivery.
Source
Thrown at v3/@claude-flow/swarm/src/consensus/transport.ts:247
if (this.keyPair) {
return { ...base, signature: signMessage(base, this.keyPair.privateKeyPem) };
}
return base;
}
/** Deliver an inbound message to a target's handler, with optional sig + replay checks. */
private async deliver(target: LocalTransport, msg: ConsensusMessage): Promise<ConsensusReply> {
if (target.closed) throw new Error(`LocalTransport: peer ${target.nodeId} is closed`);
// Verification path — only when the *target* expects signed messages.
if (target.keyPair && target.resolvePeerPublicKey) {
const pub = target.resolvePeerPublicKey(msg.from);
if (!pub || !verifyMessage(msg, pub)) {
throw new Error(`LocalTransport: signature verification failed for message from ${msg.from}`);
}
// Replay defense: seq must be strictly increasing per sender.
if (typeof msg.seq === 'number') {
const last = target.lastSeenSeq.get(msg.from) ?? 0;
if (msg.seq <= last) throw new Error(`LocalTransport: replayed/out-of-order seq from ${msg.from} (${msg.seq} <= ${last})`);
target.lastSeenSeq.set(msg.from, msg.seq);
}
}
if (!target.handler) return null;
const reply = await target.handler(msg);
return (reply ?? null) as ConsensusReply;
}
async send(to: string, msg: Omit<ConsensusMessage, 'from'>, timeoutMs?: number): Promise<ConsensusReply> {
if (this.closed) throw new Error('LocalTransport: closed');
const target = this.registry.get(to);
if (!target) throw new Error(`LocalTransport: unreachable peer ${to}`);
const stamped = this.stamp(msg);
const t = timeoutMs ?? this.defaultTimeoutMs;
return Promise.race([
this.deliver(target, stamped),
new Promise<ConsensusReply>((_, rej) => setTimeout(() => rej(new Error(`LocalTransport: send to ${to} timed out (${t}ms)`)), t)),
]);View on GitHub (pinned to fa13ee4ad6)
Solutions
- Always let the transport stamp fresh messages (construct a new message object per send; never cache the stamped result for retry)
- When re-creating a sender node with the same nodeId, use a fresh LocalTransportRegistry or a new nodeId so per-sender seq state resets together
- If you must set seq manually on unsigned senders, keep your own monotonic counter per sender
- On this error, drop the duplicate message rather than crashing the handler — it indicates dedup working as designed
Example fix
// before (retry replays the stamped message -> seq replay)
const stamped = { type: 'gossip', seq: 7, payload: {...}, from: 'a' };
await tx.send('b', stamped).catch(() => tx.send('b', stamped)); // second call: 7 <= 7
// after (retry re-stamps; transport's seqCounter keeps increasing)
const build = () => ({ type: 'gossip', payload: {...} }); // no manual seq
try { await tx.send('b', build()); }
catch { await tx.send('b', build()); } // new seq each attempt Defensive patterns
Strategy: try-catch
Validate before calling
// Do not hand-craft seq; let the transport stamp it. If you must pre-check duplicates: const digest = messageDigest(msg); // stable sha256 of content if (seenDigests.has(digest)) skip(); else seenDigests.add(digest);
Try / catch
try { await transport.send(to, msg); }
catch (e) {
if (e instanceof Error && /replayed\/out-of-order seq/.test(e.message)) {
return; // duplicate delivery — drop silently, this is dedup working
}
throw e;
} Prevention
- Never cache/retry with an already-stamped message object; construct a fresh one per attempt
- If re-creating a sender with the same nodeId, reset the whole registry (or change the nodeId) so seq state resets together
- Keep one monotonic seq source per sender; don't mix manual seq with keyPair-signed transports (stamp() overrides seq when signing)
When it happens
Trigger: (1) Re-delivering the exact same stamped message object twice (e.g., application-level retry that caches the stamped message instead of re-stamping). (2) Manually supplying msg.seq with a repeated or lower number — note stamp() only overrides seq when the sender has a keyPair, so manual seq values survive from unsigned senders. (3) Sequential restarts reusing a sender nodeId without resetting the target's lastSeenSeq (fresh sender seqCounter starts at 1 while target remembers a higher value).
Common situations: Application retry logic around transport.send() that captures and replays the outbound message Test suites reusing a registry across cases while constructing new transports with the same nodeIds Protocols that attach their own sequence numbers colliding with the transport's replay counter Reconstructing a 'resumed' node with the same identity but a reset seqCounter
Related errors
- LocalTransport: signature verification failed for message fr
- LocalTransport: peer ${target.nodeId} is closed
- LocalTransport: closed
- LocalTransport: unreachable peer ${to}
- Validation failed: ${result.error}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/621d9d5d54bdddfb.
Report an issue: GitHub.