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

  1. Construct the target LocalTransport (it self-registers in its constructor) before sending, or wait for a join event
  2. Ensure every node shares ONE registry instance (pass the same registry option to all constructors)
  3. Check spelling and casing of the nodeId; registry keys are exact strings
  4. For a rejoining node, construct a new transport; do not send to an id whose transport closed
  5. 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

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


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