ruvnet/ruflo · error · Error
LocalTransport: unreachable peer ${to}
Error message
LocalTransport: unreachable peer ${to} What it means
send() looks the recipient up in the LocalTransportRegistry and this error means no transport is registered under that nodeId — the peer never joined this registry, already left (close() unregisters), or the sender is looking in a different registry entirely.
Source
Thrown at v3/@claude-flow/swarm/src/consensus/transport.ts:259
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)),
]);
}
async broadcast(msg: Omit<ConsensusMessage, 'from'>): Promise<void> {
if (this.closed) throw new Error('LocalTransport: closed');
const stamped = this.stamp(msg);
await Promise.allSettled(
this.registry.peerIds(this.nodeId).map(id => {
const target = this.registry.get(id);
return target ? this.deliver(target, stamped).catch(() => {}) : Promise.resolve();
}),
);
}View on GitHub (pinned to fa13ee4ad6)
Solutions
- Construct the target LocalTransport (it self-registers in its constructor) before sending, or wait for a join event
- Ensure every node shares ONE registry instance (pass the same registry option to all constructors)
- Check spelling and casing of the nodeId; registry keys are exact strings
- For a rejoining node, construct a new transport; do not send to an id whose transport closed
- Remember LocalTransport is in-process only — use FederationTransport for real cross-node messaging
Example fix
// before
const registry = new LocalTransportRegistry();
const a = new LocalTransport('a', { registry });
const b = new LocalTransport('b'); // oops: joined defaultLocalRegistry
await a.send('b', msg); // unreachable peer b
// after
const registry = new LocalTransportRegistry();
const a = new LocalTransport('a', { registry });
const b = new LocalTransport('b', { registry }); // same registry
await a.send('b', msg); // delivered Defensive patterns
Strategy: validation
Validate before calling
// Verify peer is registered before sending:
const reachable = transport.peers(); // excludes self, includes only registered, non-closed peers
if (!reachable.includes(targetId)) {
await waitForPeerJoin(registry, targetId); // or fail fast with a clear message
} Try / catch
try { await transport.send(to, msg); }
catch (e) {
if (e instanceof Error && e.message.startsWith('LocalTransport: unreachable peer')) {
// retry with backoff until peer joins, or proceed with remaining quorum
return retryAfterJoin(to, msg);
}
throw e;
} Prevention
- Pass the SAME registry instance to every LocalTransport constructor
- Construct (and await) all peer transports before starting consensus traffic
- Match nodeId strings exactly — case-sensitive keys in the registry
- Remember LocalTransport is in-process only; cross-process messaging needs FederationTransport
When it happens
Trigger: (1) Sending before the peer transport was constructed (registration happens in the constructor). (2) The peer called close(), which unregisters it. (3) Sender and target were given different registries — e.g., a test passes a fresh registry to some nodes but not others (default is the process-wide defaultLocalRegistry). (4) Typo/case mismatch in the nodeId string. (5) Duplicate nodeId: a later construction overwrote the map entry and then closed.
Common situations: Test isolation: creating nodes with `registry: new LocalTransportRegistry()` for some but not all participants Nodes joining/leaving dynamically while Raft leaders cache the last-known peer set Copying nodeIds across services where the registry is per-process (LocalTransport only reaches peers in the same process — cross-process needs FederationTransport) A node restarted with the same id after its old transport was closed and unregistered
Related errors
- LocalTransport: peer ${target.nodeId} is closed
- LocalTransport: signature verification failed for message fr
- LocalTransport: replayed/out-of-order seq from ${msg.from} (
- LocalTransport: closed
- Provider ${name} already registered
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/28748a05833b7398.
Report an issue: GitHub.