{"record":{"id":"ada554bebb512281","repo":"ruvnet/ruflo","slug":"localtransport-peer-target-nodeid-is-closed","errorCode":null,"errorMessage":"LocalTransport: peer ${target.nodeId} is closed","messagePattern":"LocalTransport: peer (.+?) is closed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/swarm/src/consensus/transport.ts","lineNumber":237,"sourceCode":"  peers(): readonly string[] {\n    return this.registry.peerIds(this.nodeId);\n  }\n\n  private stamp(msg: Omit<ConsensusMessage, 'from'>): ConsensusMessage {\n    const base: Omit<ConsensusMessage, 'signature'> = {\n      ...msg,\n      from: this.nodeId,\n      seq: this.keyPair ? ++this.seqCounter : msg.seq,\n    };\n    if (this.keyPair) {\n      return { ...base, signature: signMessage(base, this.keyPair.privateKeyPem) };\n    }\n    return base;\n  }\n\n  /** Deliver an inbound message to a target's handler, with optional sig + replay checks. */\n  private async deliver(target: LocalTransport, msg: ConsensusMessage): Promise<ConsensusReply> {\n    if (target.closed) throw new Error(`LocalTransport: peer ${target.nodeId} is closed`);\n    // Verification path — only when the *target* expects signed messages.\n    if (target.keyPair && target.resolvePeerPublicKey) {\n      const pub = target.resolvePeerPublicKey(msg.from);\n      if (!pub || !verifyMessage(msg, pub)) {\n        throw new Error(`LocalTransport: signature verification failed for message from ${msg.from}`);\n      }\n      // Replay defense: seq must be strictly increasing per sender.\n      if (typeof msg.seq === 'number') {\n        const last = target.lastSeenSeq.get(msg.from) ?? 0;\n        if (msg.seq <= last) throw new Error(`LocalTransport: replayed/out-of-order seq from ${msg.from} (${msg.seq} <= ${last})`);\n        target.lastSeenSeq.set(msg.from, msg.seq);\n      }\n    }\n    if (!target.handler) return null;\n    const reply = await target.handler(msg);\n    return (reply ?? null) as ConsensusReply;\n  }\n","sourceCodeStart":219,"sourceCodeEnd":255,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/swarm/src/consensus/transport.ts#L219-L255","documentation":"Thrown by LocalTransport.deliver() when a consensus message is being handed to a peer transport whose close() has already run (its internal closed flag is true). This is the in-process transport layer for raft/byzantine/gossip consensus (ADR-095 G2), so it means the recipient node in the same process was torn down while another node still had a message destined for it. The sender's send()/broadcast() promise rejects with this error.","triggerScenarios":"Calling transportA.send(nodeB, ...) concurrently with (or interleaved after) transportB.close() — send() resolved the target from the registry before unregister happened, or a broadcast's deliver() races a peer's close(). Also happens when application code holds stale transport references and sends to a node that was already shut down during swarm teardown.","commonSituations":"Swarm/coordinator shutdown sequences that close transports in arbitrary order while consensus ticks are still in flight; tests that close one node's transport then let a leader election continue; forgetting to stop a consensus protocol instance before closing its transport.","solutions":["Ensure consensus protocol instances are stopped before any transport is closed (stop election/heartbeat timers first, then close transports)","Sequence teardown so followers/close happens after in-flight sends resolve, or route sends through a supervisor that drains pending messages first","Wrap send()/broadcast() in try-catch and treat 'peer ... is closed' as a shutdown signal to stop retrying","In tests, await Promise.allSettled on in-flight consensus operations before calling close() on any node"],"exampleFix":"// before\nraftNode.stop(); // timers stopped, but an in-flight send is still pending\nawait transportA.send('nodeB', { type: 'append-entries', payload: {...} });\nawait transportB.close();\n\n// after\nawait raftNode.stop();\nawait drainPendingSends(transportA);            // settle in-flight messages first\nawait transportB.close();\ntry {\n  const reply = await transportA.send('nodeB', msg);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('LocalTransport: peer') && e.message.endsWith('is closed')) return; // peer gone during shutdown\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// In-process reachability check before sending:\nconst peers = transport.peers(); // close() unregisters, so closed peers drop out\nif (!peers.includes(targetNodeId)) throw new Error(`peer ${targetNodeId} not reachable`);","typeGuard":null,"tryCatchPattern":"try {\n  const reply = await transport.send(to, msg);\n} catch (e) {\n  if (e instanceof Error && /peer .+ is closed/.test(e.message)) {\n    // recipient tore down during shutdown — stop, don't retry\n    return shutdown();\n  }\n  throw e;\n}","preventionTips":["Stop consensus protocol timers before closing any transport","Treat 'peer is closed' as a terminal shutdown signal, never as a retryable error","Sequence swarm teardown: drain in-flight sends, then close transports in reverse join order"],"tags":["consensus","transport","lifecycle","distributed-systems","typescript"],"backgroundTag":"connection-closed-by-peer","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}