libp2p-mesh 2026.5.29 → 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,14 +85,21 @@ export function registerLibp2pMesh(api) {
79
85
  mesh.onMessage((msg) => {
80
86
  handleP2PInbound(msg, buildInboundDeps());
81
87
  });
82
- // Dedup: skip identity send if peer is already known
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;
83
95
  mesh.onPeerConnect((peerId) => {
84
- if (peerIdentityMap.hasIdentity(peerId)) {
85
- api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping identity send`);
96
+ if (!startupComplete) {
97
+ // During startup: queue, don't send yet
98
+ pendingIdentityPeers.add(peerId);
86
99
  return;
87
100
  }
101
+ // After startup: send directly
88
102
  api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
89
- // Only send identity if relay config is set
90
103
  if (!relayChannel || !relayAccountId)
91
104
  return;
92
105
  const msg = JSON.stringify({
@@ -140,6 +153,38 @@ export function registerLibp2pMesh(api) {
140
153
  if (nat.reservedRelays.length > 0) {
141
154
  api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
142
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
+ }
143
188
  },
144
189
  stop: async () => {
145
190
  await mesh.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.5.29",
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,14 +101,22 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
94
101
  handleP2PInbound(msg, buildInboundDeps());
95
102
  });
96
103
 
97
- // Dedup: skip identity send if peer is already known
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
+
98
112
  mesh.onPeerConnect((peerId: string) => {
99
- if (peerIdentityMap.hasIdentity(peerId)) {
100
- api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping identity send`);
113
+ if (!startupComplete) {
114
+ // During startup: queue, don't send yet
115
+ pendingIdentityPeers.add(peerId);
101
116
  return;
102
117
  }
118
+ // After startup: send directly
103
119
  api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
104
- // Only send identity if relay config is set
105
120
  if (!relayChannel || !relayAccountId) return;
106
121
  const msg = JSON.stringify({
107
122
  id: crypto.randomUUID(),
@@ -160,6 +175,39 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
160
175
  `[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`,
161
176
  );
162
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
+ }
163
211
  },
164
212
  stop: async () => {
165
213
  await mesh.stop();