libp2p-mesh 2026.5.26 → 2026.5.28

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.
@@ -18,6 +18,18 @@ export async function handleIdentityMessage(msg, deps) {
18
18
  // Malformed payload — skip silently
19
19
  return;
20
20
  }
21
+ // sendToPeer 包装了一层 direct 外壳后,payload 里可能嵌套了 identity 消息:
22
+ // outer: {"type":"identity","from":"A","payload":"{\"agentId\":\"A\",\"channel\":\"feishu\",...}"}
23
+ // 先解析拿到外层,再检测 type 并递归解析内层 payload。
24
+ if (parsed.type === "identity" && typeof parsed.payload === "string") {
25
+ try {
26
+ parsed = JSON.parse(parsed.payload);
27
+ }
28
+ catch {
29
+ // Inner payload malformed — skip silently
30
+ return;
31
+ }
32
+ }
21
33
  const { agentId, channel, accountId, instanceId } = parsed;
22
34
  // Only register if all required fields are present
23
35
  if (agentId && channel && accountId) {
@@ -120,25 +120,8 @@ export async function handleP2PInbound(msg, deps) {
120
120
  }
121
121
  }
122
122
  }
123
- // If the sender is not yet registered, register them synchronously
124
- // (in-memory only) so that subsequent message routing works correctly.
125
- // This handles NAT/relay scenarios where peer:connect may not fire
126
- // bidirectionally, causing the identity exchange to be one-sided.
127
- // Persistence (saveNow) is fire-and-forget to avoid blocking the
128
- // message handler.
129
123
  if (!deps.peerIdentityMap.hasIdentity(msg.from)) {
130
- const localIdentity = deps.peerIdentityMap.getLocalIdentity();
131
- if (localIdentity) {
132
- deps.peerIdentityMap.registerSync(msg.from, {
133
- agentId: localIdentity.agentId,
134
- channel: localIdentity.channel,
135
- accountId: localIdentity.accountId,
136
- sessionKey: "",
137
- instanceId: msg.instanceId,
138
- });
139
- deps.peerIdentityMap.saveNow().catch(() => { });
140
- logger?.debug?.(`[libp2p-mesh] Auto-registered unknown sender ${msg.from} from direct message`);
141
- }
124
+ logger?.debug?.(`[libp2p-mesh] Unknown sender ${msg.from}, waiting for identity exchange`);
142
125
  }
143
126
  }
144
127
  else if (msg.type === "broadcast") {
@@ -171,20 +154,8 @@ export async function handleP2PInbound(msg, deps) {
171
154
  });
172
155
  deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
173
156
  }
174
- // If the sender is not yet registered, register them synchronously
175
157
  if (!deps.peerIdentityMap.hasIdentity(msg.from)) {
176
- const localIdentity = deps.peerIdentityMap.getLocalIdentity();
177
- if (localIdentity) {
178
- deps.peerIdentityMap.registerSync(msg.from, {
179
- agentId: localIdentity.agentId,
180
- channel: localIdentity.channel,
181
- accountId: localIdentity.accountId,
182
- sessionKey: "",
183
- instanceId: msg.instanceId,
184
- });
185
- deps.peerIdentityMap.saveNow().catch(() => { });
186
- logger?.debug?.(`[libp2p-mesh] Auto-registered unknown sender ${msg.from} from broadcast`);
187
- }
158
+ logger?.debug?.(`[libp2p-mesh] Unknown sender ${msg.from}, waiting for identity exchange`);
188
159
  }
189
160
  }
190
161
  }
@@ -1,99 +1,36 @@
1
1
  import { createLibp2pMeshChannel } from "./channel.js";
2
2
  import { handleP2PInbound } from "./inbound.js";
3
- import { handleIdentityMessage } from "./identity-exchange.js";
4
3
  import { createMeshNetwork } from "./mesh.js";
5
4
  import { buildP2PTools } from "./agent-tools.js";
6
5
  import { createPeerIdentityMap } from "./peer-identity.js";
7
- import { buildIdentityMessage } from "./identity-exchange.js";
8
6
  export function registerLibp2pMesh(api) {
9
7
  const mesh = createMeshNetwork({
10
8
  config: api.pluginConfig,
11
9
  logger: api.logger,
12
10
  });
13
11
  // 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.
16
12
  const peerIdentityMap = createPeerIdentityMap();
13
+ // Helper: build the deps object for the relay-aware inbound handler
14
+ function buildInboundDeps() {
15
+ return {
16
+ peerIdentityMap,
17
+ enqueueNextTurnInjection: (injection) => api.session.workflow.enqueueNextTurnInjection(injection),
18
+ logger: api.logger,
19
+ };
20
+ }
17
21
  // 1. Register Service (manages libp2p node lifecycle)
18
22
  api.registerService({
19
23
  id: "libp2p-mesh",
20
24
  start: async () => {
21
- // Gather config before starting (needed for onMessage handler)
22
- const config = api.pluginConfig;
23
- const relayChannel = config?.relayChannel;
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
25
  await mesh.start();
90
- // Load persisted peer identities from disk
26
+ // Load persisted peer identities so dedup checks work on restart
91
27
  await peerIdentityMap.load();
92
- const instanceIdentity = mesh.getInstanceIdentity();
93
- const localInstanceId = instanceIdentity?.id;
94
- const localPeerId = mesh.getLocalPeerId();
95
28
  // Register local identity so remote peers can route messages back to us
29
+ const config = api.pluginConfig;
30
+ const relayChannel = config?.relayChannel;
31
+ const relayAccountId = config?.relayAccountId;
96
32
  if (relayChannel && relayAccountId) {
33
+ const localPeerId = mesh.getLocalPeerId();
97
34
  const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
98
35
  const sessionKey = buildAgentSessionKey({
99
36
  agentId: api.name,
@@ -105,83 +42,80 @@ export function registerLibp2pMesh(api) {
105
42
  channel: relayChannel,
106
43
  accountId: relayAccountId,
107
44
  sessionKey,
108
- instanceId: localInstanceId,
109
45
  });
110
- await peerIdentityMap.register(localPeerId, {
111
- agentId: api.name,
112
- channel: relayChannel,
113
- accountId: relayAccountId,
114
- sessionKey,
115
- instanceId: localInstanceId,
116
- });
117
- api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`);
46
+ api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`);
118
47
  // Announce identity to all currently-connected peers
119
- const identityMsg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
120
- for (const peerId of mesh.getConnectedPeers()) {
48
+ const identityMsg = JSON.stringify({
49
+ id: crypto.randomUUID(),
50
+ type: "identity",
51
+ from: localPeerId,
52
+ payload: JSON.stringify({
53
+ agentId: api.name,
54
+ channel: relayChannel,
55
+ accountId: relayAccountId,
56
+ }),
57
+ timestamp: Date.now(),
58
+ });
59
+ const connectedPeers = mesh.getConnectedPeers();
60
+ for (const peerId of connectedPeers) {
121
61
  try {
122
- await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
62
+ await mesh.sendToPeer(peerId, identityMsg);
123
63
  }
124
- catch (err) {
125
- api.logger.warn?.(`[libp2p-mesh] Failed to send initial identity to ${peerId}: ${String(err)}`);
64
+ catch {
65
+ // Best-effort; peer may be stale in the connection list
126
66
  }
127
67
  }
128
68
  }
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.
69
+ // Wire up relay-aware message handler
70
+ mesh.onMessage((msg) => {
71
+ handleP2PInbound(msg, buildInboundDeps());
72
+ });
73
+ // Dedup: skip identity send if peer is already known
135
74
  mesh.onPeerConnect((peerId) => {
75
+ if (peerIdentityMap.hasIdentity(peerId)) {
76
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping identity send`);
77
+ return;
78
+ }
136
79
  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).
80
+ // Only send identity if relay config is set
139
81
  if (!relayChannel || !relayAccountId)
140
82
  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(() => { });
83
+ const msg = JSON.stringify({
84
+ id: crypto.randomUUID(),
85
+ type: "identity",
86
+ from: mesh.getLocalPeerId(),
87
+ payload: JSON.stringify({
88
+ agentId: api.name,
89
+ channel: relayChannel,
90
+ accountId: relayAccountId,
91
+ }),
92
+ timestamp: Date.now(),
93
+ });
94
+ mesh.sendToPeer(peerId, msg).catch(() => { });
166
95
  });
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.
96
+ // Dedup: skip initial identity announce for known peers
171
97
  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
- });
98
+ const identityMsg = JSON.stringify({
99
+ id: crypto.randomUUID(),
100
+ type: "identity",
101
+ from: mesh.getLocalPeerId(),
102
+ payload: JSON.stringify({
103
+ agentId: api.name,
104
+ channel: relayChannel,
105
+ accountId: relayAccountId,
106
+ }),
107
+ timestamp: Date.now(),
108
+ });
109
+ for (const peerId of mesh.getConnectedPeers()) {
110
+ if (peerIdentityMap.hasIdentity(peerId)) {
111
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping initial identity`);
112
+ continue;
180
113
  }
114
+ mesh.sendToPeer(peerId, identityMsg).catch(() => { });
181
115
  }
182
116
  }
183
117
  const identity = mesh.getInstanceIdentity();
184
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
118
+ api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
185
119
  if (identity) {
186
120
  api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
187
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.5.26",
3
+ "version": "2026.5.28",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,7 +40,7 @@ export async function handleIdentityMessage(
40
40
  const logger = deps.logger;
41
41
 
42
42
  // Parse remote identity payload
43
- let parsed: { agentId?: string; channel?: string; accountId?: string; instanceId?: string } = {};
43
+ let parsed: Record<string, unknown> = {};
44
44
  try {
45
45
  parsed = JSON.parse(msg.payload);
46
46
  } catch {
@@ -48,7 +48,24 @@ export async function handleIdentityMessage(
48
48
  return;
49
49
  }
50
50
 
51
- const { agentId, channel, accountId, instanceId } = parsed;
51
+ // sendToPeer 包装了一层 direct 外壳后,payload 里可能嵌套了 identity 消息:
52
+ // outer: {"type":"identity","from":"A","payload":"{\"agentId\":\"A\",\"channel\":\"feishu\",...}"}
53
+ // 先解析拿到外层,再检测 type 并递归解析内层 payload。
54
+ if (parsed.type === "identity" && typeof parsed.payload === "string") {
55
+ try {
56
+ parsed = JSON.parse(parsed.payload);
57
+ } catch {
58
+ // Inner payload malformed — skip silently
59
+ return;
60
+ }
61
+ }
62
+
63
+ const { agentId, channel, accountId, instanceId } = parsed as {
64
+ agentId?: string;
65
+ channel?: string;
66
+ accountId?: string;
67
+ instanceId?: string;
68
+ };
52
69
 
53
70
  // Only register if all required fields are present
54
71
  if (agentId && channel && accountId) {
package/src/inbound.ts CHANGED
@@ -137,27 +137,10 @@ export async function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDep
137
137
  }
138
138
  }
139
139
 
140
- // If the sender is not yet registered, register them synchronously
141
- // (in-memory only) so that subsequent message routing works correctly.
142
- // This handles NAT/relay scenarios where peer:connect may not fire
143
- // bidirectionally, causing the identity exchange to be one-sided.
144
- // Persistence (saveNow) is fire-and-forget to avoid blocking the
145
- // message handler.
146
140
  if (!deps.peerIdentityMap.hasIdentity(msg.from)) {
147
- const localIdentity = deps.peerIdentityMap.getLocalIdentity();
148
- if (localIdentity) {
149
- deps.peerIdentityMap.registerSync(msg.from, {
150
- agentId: localIdentity.agentId,
151
- channel: localIdentity.channel,
152
- accountId: localIdentity.accountId,
153
- sessionKey: "",
154
- instanceId: msg.instanceId,
155
- });
156
- deps.peerIdentityMap.saveNow().catch(() => {});
157
- logger?.debug?.(
158
- `[libp2p-mesh] Auto-registered unknown sender ${msg.from} from direct message`,
159
- );
160
- }
141
+ logger?.debug?.(
142
+ `[libp2p-mesh] Unknown sender ${msg.from}, waiting for identity exchange`,
143
+ );
161
144
  }
162
145
  } else if (msg.type === "broadcast") {
163
146
  const topic = msg.topic || "(none)";
@@ -191,22 +174,10 @@ export async function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDep
191
174
  deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
192
175
  }
193
176
 
194
- // If the sender is not yet registered, register them synchronously
195
177
  if (!deps.peerIdentityMap.hasIdentity(msg.from)) {
196
- const localIdentity = deps.peerIdentityMap.getLocalIdentity();
197
- if (localIdentity) {
198
- deps.peerIdentityMap.registerSync(msg.from, {
199
- agentId: localIdentity.agentId,
200
- channel: localIdentity.channel,
201
- accountId: localIdentity.accountId,
202
- sessionKey: "",
203
- instanceId: msg.instanceId,
204
- });
205
- deps.peerIdentityMap.saveNow().catch(() => {});
206
- logger?.debug?.(
207
- `[libp2p-mesh] Auto-registered unknown sender ${msg.from} from broadcast`,
208
- );
209
- }
178
+ logger?.debug?.(
179
+ `[libp2p-mesh] Unknown sender ${msg.from}, waiting for identity exchange`,
180
+ );
210
181
  }
211
182
  }
212
183
  }
package/src/plugin.ts CHANGED
@@ -1,11 +1,9 @@
1
- import type { OpenClawPluginApi, PluginNextTurnInjection } from "openclaw/plugin-sdk/core";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
2
  import { createLibp2pMeshChannel } from "./channel.js";
3
3
  import { handleP2PInbound } from "./inbound.js";
4
- import { handleIdentityMessage } from "./identity-exchange.js";
5
4
  import { createMeshNetwork } from "./mesh.js";
6
5
  import { buildP2PTools } from "./agent-tools.js";
7
6
  import { createPeerIdentityMap } from "./peer-identity.js";
8
- import { buildIdentityMessage } from "./identity-exchange.js";
9
7
  import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
10
8
  import type { MeshConfig } from "./types.js";
11
9
 
@@ -16,94 +14,33 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
16
14
  });
17
15
 
18
16
  // 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.
21
17
  const peerIdentityMap = createPeerIdentityMap();
22
18
 
19
+ // Helper: build the deps object for the relay-aware inbound handler
20
+ function buildInboundDeps() {
21
+ return {
22
+ peerIdentityMap,
23
+ enqueueNextTurnInjection: (injection: any) =>
24
+ api.session.workflow.enqueueNextTurnInjection(injection),
25
+ logger: api.logger,
26
+ };
27
+ }
28
+
23
29
  // 1. Register Service (manages libp2p node lifecycle)
24
30
  api.registerService({
25
31
  id: "libp2p-mesh",
26
32
  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
-
96
33
  await mesh.start();
97
34
 
98
- // Load persisted peer identities from disk
35
+ // Load persisted peer identities so dedup checks work on restart
99
36
  await peerIdentityMap.load();
100
37
 
101
- const instanceIdentity = mesh.getInstanceIdentity();
102
- const localInstanceId = instanceIdentity?.id;
103
- const localPeerId = mesh.getLocalPeerId();
104
-
105
38
  // Register local identity so remote peers can route messages back to us
39
+ const config = api.pluginConfig as MeshConfig | undefined;
40
+ const relayChannel = config?.relayChannel;
41
+ const relayAccountId = config?.relayAccountId;
106
42
  if (relayChannel && relayAccountId) {
43
+ const localPeerId = mesh.getLocalPeerId();
107
44
  const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
108
45
  const sessionKey = buildAgentSessionKey({
109
46
  agentId: api.name,
@@ -115,115 +52,85 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
115
52
  channel: relayChannel,
116
53
  accountId: relayAccountId,
117
54
  sessionKey,
118
- instanceId: localInstanceId,
119
- });
120
- await peerIdentityMap.register(localPeerId, {
121
- agentId: api.name,
122
- channel: relayChannel,
123
- accountId: relayAccountId,
124
- sessionKey,
125
- instanceId: localInstanceId,
126
55
  });
127
56
  api.logger.info?.(
128
- `[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`,
57
+ `[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`,
129
58
  );
130
59
 
131
60
  // Announce identity to all currently-connected peers
132
- const identityMsg = buildIdentityMessage(
133
- localPeerId,
134
- api.name,
135
- relayChannel,
136
- relayAccountId,
137
- localInstanceId,
138
- );
139
- for (const peerId of mesh.getConnectedPeers()) {
61
+ const identityMsg = JSON.stringify({
62
+ id: crypto.randomUUID(),
63
+ type: "identity",
64
+ from: localPeerId,
65
+ payload: JSON.stringify({
66
+ agentId: api.name,
67
+ channel: relayChannel,
68
+ accountId: relayAccountId,
69
+ }),
70
+ timestamp: Date.now(),
71
+ });
72
+ const connectedPeers = mesh.getConnectedPeers();
73
+ for (const peerId of connectedPeers) {
140
74
  try {
141
- await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
142
- } catch (err) {
143
- api.logger.warn?.(
144
- `[libp2p-mesh] Failed to send initial identity to ${peerId}: ${String(err)}`,
145
- );
75
+ await mesh.sendToPeer(peerId, identityMsg);
76
+ } catch {
77
+ // Best-effort; peer may be stale in the connection list
146
78
  }
147
79
  }
148
80
  }
149
81
 
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.
82
+ // Wire up relay-aware message handler
83
+ mesh.onMessage((msg) => {
84
+ handleP2PInbound(msg, buildInboundDeps());
85
+ });
156
86
 
87
+ // Dedup: skip identity send if peer is already known
157
88
  mesh.onPeerConnect((peerId: string) => {
89
+ if (peerIdentityMap.hasIdentity(peerId)) {
90
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping identity send`);
91
+ return;
92
+ }
158
93
  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).
94
+ // Only send identity if relay config is set
161
95
  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
- });
191
-
192
- mesh.onPeerDisconnect((peerId: string) => {
193
- api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
194
- peerIdentityMap.unregister(peerId);
195
- peerIdentityMap.saveNow().catch(() => {});
96
+ const msg = JSON.stringify({
97
+ id: crypto.randomUUID(),
98
+ type: "identity",
99
+ from: mesh.getLocalPeerId(),
100
+ payload: JSON.stringify({
101
+ agentId: api.name,
102
+ channel: relayChannel,
103
+ accountId: relayAccountId,
104
+ }),
105
+ timestamp: Date.now(),
106
+ });
107
+ mesh.sendToPeer(peerId, msg).catch(() => {});
196
108
  });
197
109
 
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.
110
+ // Dedup: skip initial identity announce for known peers
202
111
  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
- });
112
+ const identityMsg = JSON.stringify({
113
+ id: crypto.randomUUID(),
114
+ type: "identity",
115
+ from: mesh.getLocalPeerId(),
116
+ payload: JSON.stringify({
117
+ agentId: api.name,
118
+ channel: relayChannel,
119
+ accountId: relayAccountId,
120
+ }),
121
+ timestamp: Date.now(),
122
+ });
123
+ for (const peerId of mesh.getConnectedPeers()) {
124
+ if (peerIdentityMap.hasIdentity(peerId)) {
125
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping initial identity`);
126
+ continue;
221
127
  }
128
+ mesh.sendToPeer(peerId, identityMsg).catch(() => {});
222
129
  }
223
130
  }
224
131
 
225
132
  const identity = mesh.getInstanceIdentity();
226
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
133
+ api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
227
134
  if (identity) {
228
135
  api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
229
136
  }