libp2p-mesh 2026.5.22 → 2026.5.24

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.
@@ -58,6 +58,29 @@ export async function handleP2PInbound(msg, deps) {
58
58
  }
59
59
  }
60
60
  else if (msg.type === "direct") {
61
+ // onPeerConnect 通过 sendToPeer 发送 identity 时会被包装成
62
+ // type=direct 的外壳,真正的 identity 消息藏在 payload 里。
63
+ // 先检测并处理这种情况,确保双向身份交换能完成。
64
+ try {
65
+ const raw = JSON.parse(msg.payload);
66
+ if (raw && raw.type === "identity") {
67
+ await handleIdentityMessage(msg, {
68
+ peerIdentityMap: deps.peerIdentityMap,
69
+ localPeerId: deps.peerIdentityMap.getLocalIdentity()?.sessionKey ?? "",
70
+ localAgentId: "",
71
+ localChannel: "",
72
+ localAccountId: "",
73
+ localInstanceId: deps.peerIdentityMap.getLocalIdentity()?.instanceId,
74
+ send: async () => { }, // no-op; 双方都通过 onPeerConnect 主动发送身份
75
+ logger,
76
+ });
77
+ // identity 处理完毕,不再走 direct 消息路由
78
+ return;
79
+ }
80
+ }
81
+ catch {
82
+ // payload 不是 JSON,正常走 direct 消息逻辑
83
+ }
61
84
  if (!msg.to) {
62
85
  logger?.warn?.("[libp2p-mesh] Direct message missing 'to' field");
63
86
  }
@@ -11,26 +11,52 @@ 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 {
54
+ handleP2PInbound(msg, buildInboundDeps());
55
+ }
56
+ });
57
+ await mesh.start();
58
+ // Load persisted peer identities from disk
59
+ await peerIdentityMap.load();
34
60
  const instanceIdentity = mesh.getInstanceIdentity();
35
61
  const localInstanceId = instanceIdentity?.id;
36
62
  const localPeerId = mesh.getLocalPeerId();
@@ -42,10 +68,6 @@ export function registerLibp2pMesh(api) {
42
68
  channel: relayChannel,
43
69
  accountId: relayAccountId,
44
70
  });
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
71
  peerIdentityMap.setLocalIdentity(localPeerId, {
50
72
  agentId: api.name,
51
73
  channel: relayChannel,
@@ -67,52 +89,48 @@ export function registerLibp2pMesh(api) {
67
89
  try {
68
90
  await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
69
91
  }
70
- catch {
71
- // Best-effort; peer may be stale in the connection list
92
+ catch (err) {
93
+ api.logger.warn?.(`[libp2p-mesh] Failed to send initial identity to ${peerId}: ${String(err)}`);
72
94
  }
73
95
  }
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
96
  }
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
- }
97
+ // Register peer lifecycle handlers AFTER mesh.start() so that
98
+ // sendToPeer can establish streams reliably. Without this deferral,
99
+ // onPeerConnect can fire during mDNS discovery (before node.start()
100
+ // completes), causing dialProtocol to fail because the protocol
101
+ // handler isn't fully wired yet and the .catch(() => {}) swallows
102
+ // the error silently, breaking identity exchange entirely.
103
+ mesh.onPeerConnect((peerId) => {
104
+ api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
105
+ // Only send identity if relay config is set (otherwise there's no
106
+ // identity to announce).
107
+ if (!relayChannel || !relayAccountId)
108
+ return;
109
+ const msg = buildIdentityMessage(localPeerId ?? "", api.name, relayChannel, relayAccountId, localInstanceId);
110
+ // Retry up to 3 times with 500ms间隔, log failures
111
+ const maxRetries = 3;
112
+ let attempt = 0;
113
+ const sendWithRetry = async () => {
114
+ try {
115
+ await mesh.sendToPeer(peerId, JSON.stringify(msg));
116
+ }
117
+ catch (err) {
118
+ attempt++;
119
+ if (attempt < maxRetries) {
120
+ api.logger.debug?.(`[libp2p-mesh] Identity send to ${peerId} failed (attempt ${attempt}/${maxRetries}), retrying...`);
121
+ setTimeout(sendWithRetry, 500);
122
+ }
123
+ else {
124
+ api.logger.warn?.(`[libp2p-mesh] Failed to send identity to ${peerId} after ${maxRetries} attempts: ${String(err)}`);
125
+ }
126
+ }
127
+ };
128
+ sendWithRetry();
129
+ });
130
+ mesh.onPeerDisconnect((peerId) => {
131
+ api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
132
+ peerIdentityMap.unregister(peerId);
133
+ peerIdentityMap.saveNow().catch(() => { });
116
134
  });
117
135
  const identity = mesh.getInstanceIdentity();
118
136
  api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.5.22",
3
+ "version": "2026.5.24",
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/inbound.ts CHANGED
@@ -75,6 +75,29 @@ export async function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDep
75
75
  logger?.error?.(`[libp2p-mesh] Identity message handler error: ${String(err)}`);
76
76
  }
77
77
  } else if (msg.type === "direct") {
78
+ // onPeerConnect 通过 sendToPeer 发送 identity 时会被包装成
79
+ // type=direct 的外壳,真正的 identity 消息藏在 payload 里。
80
+ // 先检测并处理这种情况,确保双向身份交换能完成。
81
+ try {
82
+ const raw = JSON.parse(msg.payload);
83
+ if (raw && raw.type === "identity") {
84
+ await handleIdentityMessage(msg, {
85
+ peerIdentityMap: deps.peerIdentityMap,
86
+ localPeerId: deps.peerIdentityMap.getLocalIdentity()?.sessionKey ?? "",
87
+ localAgentId: "",
88
+ localChannel: "",
89
+ localAccountId: "",
90
+ localInstanceId: deps.peerIdentityMap.getLocalIdentity()?.instanceId,
91
+ send: async () => {}, // no-op; 双方都通过 onPeerConnect 主动发送身份
92
+ logger,
93
+ });
94
+ // identity 处理完毕,不再走 direct 消息路由
95
+ return;
96
+ }
97
+ } catch {
98
+ // payload 不是 JSON,正常走 direct 消息逻辑
99
+ }
100
+
78
101
  if (!msg.to) {
79
102
  logger?.warn?.("[libp2p-mesh] Direct message missing 'to' field");
80
103
  } else {
package/src/plugin.ts CHANGED
@@ -16,31 +16,58 @@ 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 {
62
+ handleP2PInbound(msg, buildInboundDeps());
63
+ }
64
+ });
65
+
35
66
  await mesh.start();
36
67
 
37
68
  // Load persisted peer identities from disk
38
69
  await peerIdentityMap.load();
39
70
 
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
71
  const instanceIdentity = mesh.getInstanceIdentity();
45
72
  const localInstanceId = instanceIdentity?.id;
46
73
  const localPeerId = mesh.getLocalPeerId();
@@ -53,10 +80,6 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
53
80
  channel: relayChannel,
54
81
  accountId: relayAccountId,
55
82
  });
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
83
  peerIdentityMap.setLocalIdentity(localPeerId, {
61
84
  agentId: api.name,
62
85
  channel: relayChannel,
@@ -86,60 +109,60 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
86
109
  for (const peerId of mesh.getConnectedPeers()) {
87
110
  try {
88
111
  await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
89
- } catch {
90
- // Best-effort; peer may be stale in the connection list
112
+ } catch (err) {
113
+ api.logger.warn?.(
114
+ `[libp2p-mesh] Failed to send initial identity to ${peerId}: ${String(err)}`,
115
+ );
91
116
  }
92
117
  }
118
+ }
93
119
 
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
- });
120
+ // Register peer lifecycle handlers AFTER mesh.start() so that
121
+ // sendToPeer can establish streams reliably. Without this deferral,
122
+ // onPeerConnect can fire during mDNS discovery (before node.start()
123
+ // completes), causing dialProtocol to fail because the protocol
124
+ // handler isn't fully wired yet — and the .catch(() => {}) swallows
125
+ // the error silently, breaking identity exchange entirely.
108
126
 
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
- }
127
+ mesh.onPeerConnect((peerId: string) => {
128
+ api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} sending identity`);
129
+ // Only send identity if relay config is set (otherwise there's no
130
+ // identity to announce).
131
+ if (!relayChannel || !relayAccountId) return;
132
+ const msg = buildIdentityMessage(
133
+ localPeerId ?? "",
134
+ api.name,
135
+ relayChannel,
136
+ relayAccountId,
137
+ localInstanceId,
138
+ );
139
+ // Retry up to 3 times with 500ms间隔, log failures
140
+ const maxRetries = 3;
141
+ let attempt = 0;
142
+ const sendWithRetry = async (): Promise<void> => {
143
+ try {
144
+ await mesh.sendToPeer(peerId, JSON.stringify(msg));
145
+ } catch (err) {
146
+ attempt++;
147
+ if (attempt < maxRetries) {
148
+ api.logger.debug?.(
149
+ `[libp2p-mesh] Identity send to ${peerId} failed (attempt ${attempt}/${maxRetries}), retrying...`,
150
+ );
151
+ setTimeout(sendWithRetry, 500);
152
+ } else {
153
+ api.logger.warn?.(
154
+ `[libp2p-mesh] Failed to send identity to ${peerId} after ${maxRetries} attempts: ${String(err)}`,
155
+ );
156
+ }
157
+ }
158
+ };
159
+ sendWithRetry();
160
+ });
116
161
 
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
- }
162
+ mesh.onPeerDisconnect((peerId: string) => {
163
+ api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
164
+ peerIdentityMap.unregister(peerId);
165
+ peerIdentityMap.saveNow().catch(() => {});
143
166
  });
144
167
 
145
168
  const identity = mesh.getInstanceIdentity();