libp2p-mesh 2026.5.23 → 2026.5.25

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.
@@ -11,26 +11,84 @@ export function registerLibp2pMesh(api) {
11
11
  logger: api.logger,
12
12
  });
13
13
  // Singleton: maps peerId -> { agentId, channel, accountId, sessionKey }
14
+ // Declared here (not inside start) so tool registration below can attach
15
+ // it to tool objects via closure.
14
16
  const peerIdentityMap = createPeerIdentityMap();
15
- // Helper: build the deps object for the relay-aware inbound handler
16
- function buildInboundDeps() {
17
- return {
18
- peerIdentityMap,
19
- enqueueNextTurnInjection: (injection) => api.session.workflow.enqueueNextTurnInjection(injection),
20
- logger: api.logger,
21
- };
22
- }
23
17
  // 1. Register Service (manages libp2p node lifecycle)
24
18
  api.registerService({
25
19
  id: "libp2p-mesh",
26
20
  start: async () => {
27
- await mesh.start();
28
- // Load persisted peer identities from disk
29
- await peerIdentityMap.load();
30
- // Gather local relay identity info
21
+ // Gather config before starting (needed for onMessage handler)
31
22
  const config = api.pluginConfig;
32
23
  const relayChannel = config?.relayChannel;
33
24
  const relayAccountId = config?.relayAccountId;
25
+ function buildInboundDeps() {
26
+ return {
27
+ peerIdentityMap,
28
+ enqueueNextTurnInjection: (injection) => api.session.workflow.enqueueNextTurnInjection(injection),
29
+ logger: api.logger,
30
+ };
31
+ }
32
+ // Wire up relay-aware message handler BEFORE mesh.start() so that
33
+ // any identity messages arriving during startup are queued by libp2p
34
+ // and processed as soon as the protocol handler is ready.
35
+ mesh.onMessage((msg) => {
36
+ if (msg.type === "identity") {
37
+ const localIdentity = peerIdentityMap.getLocalIdentity();
38
+ handleIdentityMessage(msg, {
39
+ peerIdentityMap,
40
+ localPeerId: localIdentity?.sessionKey ?? "",
41
+ localAgentId: api.name,
42
+ localChannel: relayChannel ?? "",
43
+ localAccountId: relayAccountId ?? "",
44
+ localInstanceId: localIdentity?.instanceId,
45
+ send: async (targetPeerId, replyMsg) => {
46
+ mesh.sendToPeer(targetPeerId, JSON.stringify(replyMsg)).catch(() => { });
47
+ },
48
+ logger: api.logger,
49
+ }).catch((err) => {
50
+ api.logger.error?.(`[libp2p-mesh] Identity exchange error: ${String(err)}`);
51
+ });
52
+ }
53
+ else if (msg.type === "direct") {
54
+ // sendToPeer 包装了一层 direct 外壳,原始 identity 消息藏在 payload 里。
55
+ // 检测并处理:注册对方 + 发送自己的身份回复。
56
+ let handled = false;
57
+ try {
58
+ const raw = JSON.parse(msg.payload);
59
+ if (raw && raw.type === "identity") {
60
+ const localIdentity = peerIdentityMap.getLocalIdentity();
61
+ handleIdentityMessage(msg, {
62
+ peerIdentityMap,
63
+ localPeerId: localIdentity?.sessionKey ?? "",
64
+ localAgentId: api.name,
65
+ localChannel: relayChannel ?? "",
66
+ localAccountId: relayAccountId ?? "",
67
+ localInstanceId: localIdentity?.instanceId,
68
+ send: async (targetPeerId, replyMsg) => {
69
+ mesh.sendToPeer(targetPeerId, JSON.stringify(replyMsg)).catch(() => { });
70
+ },
71
+ logger: api.logger,
72
+ }).catch((err) => {
73
+ api.logger.error?.(`[libp2p-mesh] Identity exchange error: ${String(err)}`);
74
+ });
75
+ handled = true;
76
+ }
77
+ }
78
+ catch {
79
+ // payload 不是 JSON,正常走 direct 消息逻辑
80
+ }
81
+ if (!handled) {
82
+ handleP2PInbound(msg, buildInboundDeps());
83
+ }
84
+ }
85
+ else {
86
+ handleP2PInbound(msg, buildInboundDeps());
87
+ }
88
+ });
89
+ await mesh.start();
90
+ // Load persisted peer identities from disk
91
+ await peerIdentityMap.load();
34
92
  const instanceIdentity = mesh.getInstanceIdentity();
35
93
  const localInstanceId = instanceIdentity?.id;
36
94
  const localPeerId = mesh.getLocalPeerId();
@@ -42,10 +100,6 @@ export function registerLibp2pMesh(api) {
42
100
  channel: relayChannel,
43
101
  accountId: relayAccountId,
44
102
  });
45
- // setLocalIdentity stores the local identity separately (not in peers
46
- // Map) to avoid duplication. We also register in peers Map so
47
- // direct messages addressed to us (msg.to === localPeerId) can be
48
- // resolved by deps.peerIdentityMap.resolve() in handleP2PInbound.
49
103
  peerIdentityMap.setLocalIdentity(localPeerId, {
50
104
  agentId: api.name,
51
105
  channel: relayChannel,
@@ -67,53 +121,65 @@ export function registerLibp2pMesh(api) {
67
121
  try {
68
122
  await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
69
123
  }
70
- catch {
71
- // Best-effort; peer may be stale in the connection list
124
+ catch (err) {
125
+ api.logger.warn?.(`[libp2p-mesh] Failed to send initial identity to ${peerId}: ${String(err)}`);
72
126
  }
73
127
  }
74
- // When a new peer connects, send our identity to them
75
- mesh.onPeerConnect((peerId) => {
76
- api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
77
- const msg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
78
- mesh.sendToPeer(peerId, JSON.stringify(msg)).catch(() => {
79
- // Best-effort
80
- });
81
- });
82
- // When a peer disconnects, clean up the identity map
83
- mesh.onPeerDisconnect((peerId) => {
84
- api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
85
- peerIdentityMap.unregister(peerId);
86
- peerIdentityMap.saveNow().catch(() => { });
87
- });
88
128
  }
89
- // Wire up relay-aware message handler.
90
- // Identity messages get special treatment: they register the remote
91
- // peer's identity, force immediate persistence via saveNow(), and
92
- // automatically send our identity back ensuring bidirectional
93
- // identity exchange even when peer:connect events are one-sided
94
- // (e.g. NAT/relay scenarios).
95
- mesh.onMessage((msg) => {
96
- if (msg.type === "identity") {
97
- const localIdentity = peerIdentityMap.getLocalIdentity();
98
- handleIdentityMessage(msg, {
99
- peerIdentityMap,
100
- localPeerId,
101
- localAgentId: api.name,
102
- localChannel: relayChannel ?? "",
103
- localAccountId: relayAccountId ?? "",
104
- localInstanceId,
105
- send: async (targetPeerId, replyMsg) => {
106
- mesh.sendToPeer(targetPeerId, JSON.stringify(replyMsg)).catch(() => { });
107
- },
108
- logger: api.logger,
109
- }).catch((err) => {
110
- api.logger.error?.(`[libp2p-mesh] Identity exchange error: ${String(err)}`);
111
- });
112
- }
113
- else {
114
- handleP2PInbound(msg, buildInboundDeps());
115
- }
129
+ // Register peer lifecycle handlers AFTER mesh.start() so that
130
+ // sendToPeer can establish streams reliably. Without this deferral,
131
+ // onPeerConnect can fire during mDNS discovery (before node.start()
132
+ // completes), causing dialProtocol to fail because the protocol
133
+ // handler isn't fully wired yet and the .catch(() => {}) swallows
134
+ // the error silently, breaking identity exchange entirely.
135
+ mesh.onPeerConnect((peerId) => {
136
+ api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
137
+ // Only send identity if relay config is set (otherwise there's no
138
+ // identity to announce).
139
+ if (!relayChannel || !relayAccountId)
140
+ return;
141
+ const msg = buildIdentityMessage(localPeerId ?? "", api.name, relayChannel, relayAccountId, localInstanceId);
142
+ // Retry up to 3 times with 500ms间隔, log failures
143
+ const maxRetries = 3;
144
+ let attempt = 0;
145
+ const sendWithRetry = async () => {
146
+ try {
147
+ await mesh.sendToPeer(peerId, JSON.stringify(msg));
148
+ }
149
+ catch (err) {
150
+ attempt++;
151
+ if (attempt < maxRetries) {
152
+ api.logger.debug?.(`[libp2p-mesh] Identity send to ${peerId} failed (attempt ${attempt}/${maxRetries}), retrying...`);
153
+ setTimeout(sendWithRetry, 500);
154
+ }
155
+ else {
156
+ api.logger.warn?.(`[libp2p-mesh] Failed to send identity to ${peerId} after ${maxRetries} attempts: ${String(err)}`);
157
+ }
158
+ }
159
+ };
160
+ sendWithRetry();
161
+ });
162
+ mesh.onPeerDisconnect((peerId) => {
163
+ api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
164
+ peerIdentityMap.unregister(peerId);
165
+ peerIdentityMap.saveNow().catch(() => { });
116
166
  });
167
+ // Actively announce identity to all currently-connected peers.
168
+ // This covers the race where mDNS discovery fires peer:connect during
169
+ // node.start() before onPeerConnect was registered — those peers are
170
+ // already connected but never received our identity.
171
+ if (relayChannel && relayAccountId) {
172
+ const identityMsg = buildIdentityMessage(localPeerId ?? "", api.name, relayChannel, relayAccountId, localInstanceId);
173
+ const connectedNow = mesh.getConnectedPeers();
174
+ if (connectedNow.length > 0) {
175
+ api.logger.info?.(`[libp2p-mesh] Sending initial identity to ${connectedNow.length} connected peer(s)`);
176
+ for (const peerId of connectedNow) {
177
+ mesh.sendToPeer(peerId, JSON.stringify(identityMsg)).catch((err) => {
178
+ api.logger.debug?.(`[libp2p-mesh] Initial identity send to ${peerId} failed: ${String(err)}`);
179
+ });
180
+ }
181
+ }
182
+ }
117
183
  const identity = mesh.getInstanceIdentity();
118
184
  api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
119
185
  if (identity) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.5.23",
3
+ "version": "2026.5.25",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/plugin.ts CHANGED
@@ -16,31 +16,88 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
16
16
  });
17
17
 
18
18
  // Singleton: maps peerId -> { agentId, channel, accountId, sessionKey }
19
+ // Declared here (not inside start) so tool registration below can attach
20
+ // it to tool objects via closure.
19
21
  const peerIdentityMap = createPeerIdentityMap();
20
22
 
21
- // Helper: build the deps object for the relay-aware inbound handler
22
- function buildInboundDeps() {
23
- return {
24
- peerIdentityMap,
25
- enqueueNextTurnInjection: (injection: PluginNextTurnInjection) =>
26
- api.session.workflow.enqueueNextTurnInjection(injection),
27
- logger: api.logger,
28
- };
29
- }
30
-
31
23
  // 1. Register Service (manages libp2p node lifecycle)
32
24
  api.registerService({
33
25
  id: "libp2p-mesh",
34
26
  start: async () => {
27
+ // Gather config before starting (needed for onMessage handler)
28
+ const config = api.pluginConfig as MeshConfig | undefined;
29
+ const relayChannel = config?.relayChannel;
30
+ const relayAccountId = config?.relayAccountId;
31
+
32
+ function buildInboundDeps() {
33
+ return {
34
+ peerIdentityMap,
35
+ enqueueNextTurnInjection: (injection: PluginNextTurnInjection) =>
36
+ api.session.workflow.enqueueNextTurnInjection(injection),
37
+ logger: api.logger,
38
+ };
39
+ }
40
+
41
+ // Wire up relay-aware message handler BEFORE mesh.start() so that
42
+ // any identity messages arriving during startup are queued by libp2p
43
+ // and processed as soon as the protocol handler is ready.
44
+ mesh.onMessage((msg) => {
45
+ if (msg.type === "identity") {
46
+ const localIdentity = peerIdentityMap.getLocalIdentity();
47
+ handleIdentityMessage(msg, {
48
+ peerIdentityMap,
49
+ localPeerId: localIdentity?.sessionKey ?? "",
50
+ localAgentId: api.name,
51
+ localChannel: relayChannel ?? "",
52
+ localAccountId: relayAccountId ?? "",
53
+ localInstanceId: localIdentity?.instanceId,
54
+ send: async (targetPeerId: string, replyMsg: any) => {
55
+ mesh.sendToPeer(targetPeerId, JSON.stringify(replyMsg)).catch(() => {});
56
+ },
57
+ logger: api.logger,
58
+ }).catch((err) => {
59
+ api.logger.error?.(`[libp2p-mesh] Identity exchange error: ${String(err)}`);
60
+ });
61
+ } else if (msg.type === "direct") {
62
+ // sendToPeer 包装了一层 direct 外壳,原始 identity 消息藏在 payload 里。
63
+ // 检测并处理:注册对方 + 发送自己的身份回复。
64
+ let handled = false;
65
+ try {
66
+ const raw = JSON.parse(msg.payload);
67
+ if (raw && raw.type === "identity") {
68
+ const localIdentity = peerIdentityMap.getLocalIdentity();
69
+ handleIdentityMessage(msg, {
70
+ peerIdentityMap,
71
+ localPeerId: localIdentity?.sessionKey ?? "",
72
+ localAgentId: api.name,
73
+ localChannel: relayChannel ?? "",
74
+ localAccountId: relayAccountId ?? "",
75
+ localInstanceId: localIdentity?.instanceId,
76
+ send: async (targetPeerId: string, replyMsg: any) => {
77
+ mesh.sendToPeer(targetPeerId, JSON.stringify(replyMsg)).catch(() => {});
78
+ },
79
+ logger: api.logger,
80
+ }).catch((err) => {
81
+ api.logger.error?.(`[libp2p-mesh] Identity exchange error: ${String(err)}`);
82
+ });
83
+ handled = true;
84
+ }
85
+ } catch {
86
+ // payload 不是 JSON,正常走 direct 消息逻辑
87
+ }
88
+ if (!handled) {
89
+ handleP2PInbound(msg, buildInboundDeps());
90
+ }
91
+ } else {
92
+ handleP2PInbound(msg, buildInboundDeps());
93
+ }
94
+ });
95
+
35
96
  await mesh.start();
36
97
 
37
98
  // Load persisted peer identities from disk
38
99
  await peerIdentityMap.load();
39
100
 
40
- // Gather local relay identity info
41
- const config = api.pluginConfig as MeshConfig | undefined;
42
- const relayChannel = config?.relayChannel;
43
- const relayAccountId = config?.relayAccountId;
44
101
  const instanceIdentity = mesh.getInstanceIdentity();
45
102
  const localInstanceId = instanceIdentity?.id;
46
103
  const localPeerId = mesh.getLocalPeerId();
@@ -53,10 +110,6 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
53
110
  channel: relayChannel,
54
111
  accountId: relayAccountId,
55
112
  });
56
- // setLocalIdentity stores the local identity separately (not in peers
57
- // Map) to avoid duplication. We also register in peers Map so
58
- // direct messages addressed to us (msg.to === localPeerId) can be
59
- // resolved by deps.peerIdentityMap.resolve() in handleP2PInbound.
60
113
  peerIdentityMap.setLocalIdentity(localPeerId, {
61
114
  agentId: api.name,
62
115
  channel: relayChannel,
@@ -86,62 +139,89 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
86
139
  for (const peerId of mesh.getConnectedPeers()) {
87
140
  try {
88
141
  await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
89
- } catch {
90
- // Best-effort; peer may be stale in the connection list
142
+ } catch (err) {
143
+ api.logger.warn?.(
144
+ `[libp2p-mesh] Failed to send initial identity to ${peerId}: ${String(err)}`,
145
+ );
91
146
  }
92
147
  }
148
+ }
93
149
 
94
- // When a new peer connects, send our identity to them
95
- mesh.onPeerConnect((peerId: string) => {
96
- api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} sending identity`);
97
- const msg = buildIdentityMessage(
98
- localPeerId,
99
- api.name,
100
- relayChannel,
101
- relayAccountId,
102
- localInstanceId,
103
- );
104
- mesh.sendToPeer(peerId, JSON.stringify(msg)).catch(() => {
105
- // Best-effort
106
- });
107
- });
150
+ // Register peer lifecycle handlers AFTER mesh.start() so that
151
+ // sendToPeer can establish streams reliably. Without this deferral,
152
+ // onPeerConnect can fire during mDNS discovery (before node.start()
153
+ // completes), causing dialProtocol to fail because the protocol
154
+ // handler isn't fully wired yet — and the .catch(() => {}) swallows
155
+ // the error silently, breaking identity exchange entirely.
108
156
 
109
- // When a peer disconnects, clean up the identity map
110
- mesh.onPeerDisconnect((peerId: string) => {
111
- api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
112
- peerIdentityMap.unregister(peerId);
113
- peerIdentityMap.saveNow().catch(() => {});
114
- });
115
- }
157
+ mesh.onPeerConnect((peerId: string) => {
158
+ api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} sending identity`);
159
+ // Only send identity if relay config is set (otherwise there's no
160
+ // identity to announce).
161
+ if (!relayChannel || !relayAccountId) return;
162
+ const msg = buildIdentityMessage(
163
+ localPeerId ?? "",
164
+ api.name,
165
+ relayChannel,
166
+ relayAccountId,
167
+ localInstanceId,
168
+ );
169
+ // Retry up to 3 times with 500ms间隔, log failures
170
+ const maxRetries = 3;
171
+ let attempt = 0;
172
+ const sendWithRetry = async (): Promise<void> => {
173
+ try {
174
+ await mesh.sendToPeer(peerId, JSON.stringify(msg));
175
+ } catch (err) {
176
+ attempt++;
177
+ if (attempt < maxRetries) {
178
+ api.logger.debug?.(
179
+ `[libp2p-mesh] Identity send to ${peerId} failed (attempt ${attempt}/${maxRetries}), retrying...`,
180
+ );
181
+ setTimeout(sendWithRetry, 500);
182
+ } else {
183
+ api.logger.warn?.(
184
+ `[libp2p-mesh] Failed to send identity to ${peerId} after ${maxRetries} attempts: ${String(err)}`,
185
+ );
186
+ }
187
+ }
188
+ };
189
+ sendWithRetry();
190
+ });
116
191
 
117
- // Wire up relay-aware message handler.
118
- // Identity messages get special treatment: they register the remote
119
- // peer's identity, force immediate persistence via saveNow(), and
120
- // automatically send our identity back — ensuring bidirectional
121
- // identity exchange even when peer:connect events are one-sided
122
- // (e.g. NAT/relay scenarios).
123
- mesh.onMessage((msg) => {
124
- if (msg.type === "identity") {
125
- const localIdentity = peerIdentityMap.getLocalIdentity();
126
- handleIdentityMessage(msg, {
127
- peerIdentityMap,
128
- localPeerId,
129
- localAgentId: api.name,
130
- localChannel: relayChannel ?? "",
131
- localAccountId: relayAccountId ?? "",
132
- localInstanceId,
133
- send: async (targetPeerId: string, replyMsg: any) => {
134
- mesh.sendToPeer(targetPeerId, JSON.stringify(replyMsg)).catch(() => {});
135
- },
136
- logger: api.logger,
137
- }).catch((err) => {
138
- api.logger.error?.(`[libp2p-mesh] Identity exchange error: ${String(err)}`);
139
- });
140
- } else {
141
- handleP2PInbound(msg, buildInboundDeps());
142
- }
192
+ mesh.onPeerDisconnect((peerId: string) => {
193
+ api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
194
+ peerIdentityMap.unregister(peerId);
195
+ peerIdentityMap.saveNow().catch(() => {});
143
196
  });
144
197
 
198
+ // Actively announce identity to all currently-connected peers.
199
+ // This covers the race where mDNS discovery fires peer:connect during
200
+ // node.start() before onPeerConnect was registered — those peers are
201
+ // already connected but never received our identity.
202
+ if (relayChannel && relayAccountId) {
203
+ const identityMsg = buildIdentityMessage(
204
+ localPeerId ?? "",
205
+ api.name,
206
+ relayChannel,
207
+ relayAccountId,
208
+ localInstanceId,
209
+ );
210
+ const connectedNow = mesh.getConnectedPeers();
211
+ if (connectedNow.length > 0) {
212
+ api.logger.info?.(
213
+ `[libp2p-mesh] Sending initial identity to ${connectedNow.length} connected peer(s)`,
214
+ );
215
+ for (const peerId of connectedNow) {
216
+ mesh.sendToPeer(peerId, JSON.stringify(identityMsg)).catch((err) => {
217
+ api.logger.debug?.(
218
+ `[libp2p-mesh] Initial identity send to ${peerId} failed: ${String(err)}`,
219
+ );
220
+ });
221
+ }
222
+ }
223
+ }
224
+
145
225
  const identity = mesh.getInstanceIdentity();
146
226
  api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
147
227
  if (identity) {