libp2p-mesh 2026.5.30 → 2026.5.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ export function buildIdentityMessage(peerId, agentId, channel, accountId, instan
9
9
  }
10
10
  export async function handleIdentityMessage(msg, deps) {
11
11
  const logger = deps.logger;
12
+ logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: from=${msg.from}, type=${msg.type}, payload_prefix=${msg.payload.slice(0, 60)}`);
12
13
  // Parse remote identity payload
13
14
  let parsed = {};
14
15
  try {
@@ -31,6 +32,7 @@ export async function handleIdentityMessage(msg, deps) {
31
32
  }
32
33
  }
33
34
  const { agentId, channel, accountId, instanceId } = parsed;
35
+ logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: parsed agentId=${agentId}, channel=${channel}, accountId=${accountId}, instanceId=${instanceId}`);
34
36
  // Only register if all required fields are present
35
37
  if (agentId && channel && accountId) {
36
38
  const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
@@ -46,6 +48,7 @@ export async function handleIdentityMessage(msg, deps) {
46
48
  // process exits before the next save cycle.
47
49
  await deps.peerIdentityMap.saveNow();
48
50
  logger?.info?.(`[libp2p-mesh] Registered peer identity: ${msg.from} (agent=${agentId}, channel=${channel}, instanceId=${instanceId ?? "n/a"})`);
51
+ logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: successfully registered ${msg.from}, now sending reply to ${msg.from}`);
49
52
  }
50
53
  // Send local identity back to the remote peer.
51
54
  // We intentionally do NOT await this — the send callback (and any
@@ -54,6 +57,7 @@ export async function handleIdentityMessage(msg, deps) {
54
57
  // blocks the caller; the recipient's handler will persist the reply on
55
58
  // its own via saveNow().
56
59
  const localIdentity = buildIdentityMessage(deps.localPeerId, deps.localAgentId, deps.localChannel, deps.localAccountId, deps.localInstanceId);
60
+ logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: sending identity reply to ${msg.from}`);
57
61
  deps.send(msg.from, localIdentity).catch((sendErr) => {
58
62
  logger?.warn?.(`[libp2p-mesh] Failed to send identity reply to ${msg.from}: ${String(sendErr)}`);
59
63
  });
@@ -41,6 +41,7 @@ export async function handleP2PInbound(msg, deps) {
41
41
  // Delegate full identity exchange (register + send reply) to the
42
42
  // dedicated handler. This ensures bidirectional identity propagation,
43
43
  // forced persistence via saveNow(), and proper logging.
44
+ logger?.debug?.(`[libp2p-mesh] handleP2PInbound: processing identity from ${msg.from}`);
44
45
  try {
45
46
  await handleIdentityMessage(msg, {
46
47
  peerIdentityMap: deps.peerIdentityMap,
@@ -26,6 +26,12 @@ export function registerLibp2pMesh(api) {
26
26
  // peers connect during mesh.start() (mDNS can fire peer:connect
27
27
  // before this function's sequential code reaches onPeerConnect).
28
28
  await peerIdentityMap.load();
29
+ // Queue identity sends during mesh.start() so they are flushed only
30
+ // after the node is fully ready (protocol handler registered, DHT
31
+ // initialized, etc.). Sending inside onPeerConnect during startup
32
+ // races with node initialization and can cause the remote peer's
33
+ // reply to be lost or unprocessed.
34
+ const pendingIdentityPeers = new Set();
29
35
  await mesh.start();
30
36
  const instanceIdentity = mesh.getInstanceIdentity();
31
37
  const localInstanceId = instanceIdentity?.id;
@@ -79,15 +85,21 @@ export function registerLibp2pMesh(api) {
79
85
  mesh.onMessage((msg) => {
80
86
  handleP2PInbound(msg, buildInboundDeps());
81
87
  });
82
- // Always send our identity when a peer connects. Identity exchange is
83
- // bidirectional: even if we already know this peer, they may not know
84
- // our current identity (or theirs may be stale). The receiving side
85
- // handles dedup via handleIdentityMessage (register only if new or
86
- // updated). Removing the send-side dedup ensures both sides always
87
- // have each other's current identity.
88
+ // Always send our identity when a peer connects. During mesh.start(),
89
+ // mDNS can fire peer:connect before the node is fully ready (protocol
90
+ // handler just registered, DHT still initializing). Sending identity
91
+ // messages at that point can cause the remote peer's reply to arrive
92
+ // before we're ready to process it, breaking the identity exchange.
93
+ // Queue during startup; flush after start() + send directly thereafter.
94
+ let startupComplete = false;
88
95
  mesh.onPeerConnect((peerId) => {
96
+ if (!startupComplete) {
97
+ // During startup: queue, don't send yet
98
+ pendingIdentityPeers.add(peerId);
99
+ return;
100
+ }
101
+ // After startup: send directly
89
102
  api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
90
- // Only send identity if relay config is set
91
103
  if (!relayChannel || !relayAccountId)
92
104
  return;
93
105
  const msg = JSON.stringify({
@@ -141,6 +153,38 @@ export function registerLibp2pMesh(api) {
141
153
  if (nat.reservedRelays.length > 0) {
142
154
  api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
143
155
  }
156
+ // Flush identity messages that were queued during mesh.start().
157
+ // Peers that connected during startup (mDNS-triggered peer:connect
158
+ // inside start()) are in pendingIdentityPeers. Send now that the
159
+ // node is fully ready.
160
+ startupComplete = true;
161
+ const flushPendingIdentities = () => {
162
+ if (!relayChannel || !relayAccountId)
163
+ return;
164
+ const identityMsg = JSON.stringify({
165
+ id: crypto.randomUUID(),
166
+ type: "identity",
167
+ from: mesh.getLocalPeerId(),
168
+ payload: JSON.stringify({
169
+ agentId: api.name,
170
+ channel: relayChannel,
171
+ accountId: relayAccountId,
172
+ instanceId: localInstanceId,
173
+ }),
174
+ timestamp: Date.now(),
175
+ });
176
+ for (const peerId of pendingIdentityPeers) {
177
+ if (peerIdentityMap.hasIdentity(peerId)) {
178
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known (loaded from disk), skipping queued identity`);
179
+ continue;
180
+ }
181
+ mesh.sendToPeer(peerId, identityMsg).catch(() => { });
182
+ }
183
+ pendingIdentityPeers.clear();
184
+ };
185
+ if (pendingIdentityPeers.size > 0) {
186
+ flushPendingIdentities();
187
+ }
144
188
  },
145
189
  stop: async () => {
146
190
  await mesh.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.5.30",
3
+ "version": "2026.5.31",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,6 +38,7 @@ export async function handleIdentityMessage(
38
38
  deps: IdentityExchangeDeps,
39
39
  ): Promise<void> {
40
40
  const logger = deps.logger;
41
+ logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: from=${msg.from}, type=${msg.type}, payload_prefix=${msg.payload.slice(0, 60)}`);
41
42
 
42
43
  // Parse remote identity payload
43
44
  let parsed: Record<string, unknown> = {};
@@ -66,6 +67,7 @@ export async function handleIdentityMessage(
66
67
  accountId?: string;
67
68
  instanceId?: string;
68
69
  };
70
+ logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: parsed agentId=${agentId}, channel=${channel}, accountId=${accountId}, instanceId=${instanceId}`);
69
71
 
70
72
  // Only register if all required fields are present
71
73
  if (agentId && channel && accountId) {
@@ -84,6 +86,7 @@ export async function handleIdentityMessage(
84
86
  logger?.info?.(
85
87
  `[libp2p-mesh] Registered peer identity: ${msg.from} (agent=${agentId}, channel=${channel}, instanceId=${instanceId ?? "n/a"})`,
86
88
  );
89
+ logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: successfully registered ${msg.from}, now sending reply to ${msg.from}`);
87
90
  }
88
91
 
89
92
  // Send local identity back to the remote peer.
@@ -99,6 +102,7 @@ export async function handleIdentityMessage(
99
102
  deps.localAccountId,
100
103
  deps.localInstanceId,
101
104
  );
105
+ logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: sending identity reply to ${msg.from}`);
102
106
  deps.send(msg.from, localIdentity).catch((sendErr) => {
103
107
  logger?.warn?.(
104
108
  `[libp2p-mesh] Failed to send identity reply to ${msg.from}: ${String(sendErr)}`,
package/src/inbound.ts CHANGED
@@ -60,6 +60,7 @@ export async function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDep
60
60
  // Delegate full identity exchange (register + send reply) to the
61
61
  // dedicated handler. This ensures bidirectional identity propagation,
62
62
  // forced persistence via saveNow(), and proper logging.
63
+ logger?.debug?.(`[libp2p-mesh] handleP2PInbound: processing identity from ${msg.from}`);
63
64
  try {
64
65
  await handleIdentityMessage(msg, {
65
66
  peerIdentityMap: deps.peerIdentityMap,
package/src/plugin.ts CHANGED
@@ -35,6 +35,13 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
35
35
  // before this function's sequential code reaches onPeerConnect).
36
36
  await peerIdentityMap.load();
37
37
 
38
+ // Queue identity sends during mesh.start() so they are flushed only
39
+ // after the node is fully ready (protocol handler registered, DHT
40
+ // initialized, etc.). Sending inside onPeerConnect during startup
41
+ // races with node initialization and can cause the remote peer's
42
+ // reply to be lost or unprocessed.
43
+ const pendingIdentityPeers = new Set<string>();
44
+
38
45
  await mesh.start();
39
46
 
40
47
  const instanceIdentity = mesh.getInstanceIdentity();
@@ -94,15 +101,22 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
94
101
  handleP2PInbound(msg, buildInboundDeps());
95
102
  });
96
103
 
97
- // Always send our identity when a peer connects. Identity exchange is
98
- // bidirectional: even if we already know this peer, they may not know
99
- // our current identity (or theirs may be stale). The receiving side
100
- // handles dedup via handleIdentityMessage (register only if new or
101
- // updated). Removing the send-side dedup ensures both sides always
102
- // have each other's current identity.
104
+ // Always send our identity when a peer connects. During mesh.start(),
105
+ // mDNS can fire peer:connect before the node is fully ready (protocol
106
+ // handler just registered, DHT still initializing). Sending identity
107
+ // messages at that point can cause the remote peer's reply to arrive
108
+ // before we're ready to process it, breaking the identity exchange.
109
+ // Queue during startup; flush after start() + send directly thereafter.
110
+ let startupComplete = false;
111
+
103
112
  mesh.onPeerConnect((peerId: string) => {
113
+ if (!startupComplete) {
114
+ // During startup: queue, don't send yet
115
+ pendingIdentityPeers.add(peerId);
116
+ return;
117
+ }
118
+ // After startup: send directly
104
119
  api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
105
- // Only send identity if relay config is set
106
120
  if (!relayChannel || !relayAccountId) return;
107
121
  const msg = JSON.stringify({
108
122
  id: crypto.randomUUID(),
@@ -161,6 +175,39 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
161
175
  `[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`,
162
176
  );
163
177
  }
178
+
179
+ // Flush identity messages that were queued during mesh.start().
180
+ // Peers that connected during startup (mDNS-triggered peer:connect
181
+ // inside start()) are in pendingIdentityPeers. Send now that the
182
+ // node is fully ready.
183
+ startupComplete = true;
184
+ const flushPendingIdentities = () => {
185
+ if (!relayChannel || !relayAccountId) return;
186
+ const identityMsg = JSON.stringify({
187
+ id: crypto.randomUUID(),
188
+ type: "identity",
189
+ from: mesh.getLocalPeerId(),
190
+ payload: JSON.stringify({
191
+ agentId: api.name,
192
+ channel: relayChannel,
193
+ accountId: relayAccountId,
194
+ instanceId: localInstanceId,
195
+ }),
196
+ timestamp: Date.now(),
197
+ });
198
+ for (const peerId of pendingIdentityPeers) {
199
+ if (peerIdentityMap.hasIdentity(peerId)) {
200
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known (loaded from disk), skipping queued identity`);
201
+ continue;
202
+ }
203
+ mesh.sendToPeer(peerId, identityMsg).catch(() => {});
204
+ }
205
+ pendingIdentityPeers.clear();
206
+ };
207
+
208
+ if (pendingIdentityPeers.size > 0) {
209
+ flushPendingIdentities();
210
+ }
164
211
  },
165
212
  stop: async () => {
166
213
  await mesh.stop();