libp2p-mesh 2026.5.27 → 2026.5.29

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.
@@ -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,40 @@
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
- await mesh.start();
90
- // Load persisted peer identities from disk
25
+ // Load persisted peer identities FIRST so dedup checks work when
26
+ // peers connect during mesh.start() (mDNS can fire peer:connect
27
+ // before this function's sequential code reaches onPeerConnect).
91
28
  await peerIdentityMap.load();
29
+ await mesh.start();
92
30
  const instanceIdentity = mesh.getInstanceIdentity();
93
31
  const localInstanceId = instanceIdentity?.id;
94
- const localPeerId = mesh.getLocalPeerId();
95
32
  // Register local identity so remote peers can route messages back to us
33
+ const config = api.pluginConfig;
34
+ const relayChannel = config?.relayChannel;
35
+ const relayAccountId = config?.relayAccountId;
96
36
  if (relayChannel && relayAccountId) {
37
+ const localPeerId = mesh.getLocalPeerId();
97
38
  const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
98
39
  const sessionKey = buildAgentSessionKey({
99
40
  agentId: api.name,
@@ -107,81 +48,85 @@ export function registerLibp2pMesh(api) {
107
48
  sessionKey,
108
49
  instanceId: localInstanceId,
109
50
  });
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}`);
51
+ api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`);
118
52
  // Announce identity to all currently-connected peers
119
- const identityMsg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
120
- for (const peerId of mesh.getConnectedPeers()) {
53
+ // setLocalIdentity is called BEFORE this block, and localInstanceId
54
+ // is available from mesh.start(), so the identity message includes
55
+ // the instanceId.
56
+ const identityMsg = JSON.stringify({
57
+ id: crypto.randomUUID(),
58
+ type: "identity",
59
+ from: localPeerId,
60
+ payload: JSON.stringify({
61
+ agentId: api.name,
62
+ channel: relayChannel,
63
+ accountId: relayAccountId,
64
+ instanceId: localInstanceId,
65
+ }),
66
+ timestamp: Date.now(),
67
+ });
68
+ const connectedPeers = mesh.getConnectedPeers();
69
+ for (const peerId of connectedPeers) {
121
70
  try {
122
- await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
71
+ await mesh.sendToPeer(peerId, identityMsg);
123
72
  }
124
- catch (err) {
125
- api.logger.warn?.(`[libp2p-mesh] Failed to send initial identity to ${peerId}: ${String(err)}`);
73
+ catch {
74
+ // Best-effort; peer may be stale in the connection list
126
75
  }
127
76
  }
128
77
  }
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.
78
+ // Wire up relay-aware message handler
79
+ mesh.onMessage((msg) => {
80
+ handleP2PInbound(msg, buildInboundDeps());
81
+ });
82
+ // Dedup: skip identity send if peer is already known
135
83
  mesh.onPeerConnect((peerId) => {
84
+ if (peerIdentityMap.hasIdentity(peerId)) {
85
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping identity send`);
86
+ return;
87
+ }
136
88
  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).
89
+ // Only send identity if relay config is set
139
90
  if (!relayChannel || !relayAccountId)
140
91
  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(() => { });
92
+ const msg = JSON.stringify({
93
+ id: crypto.randomUUID(),
94
+ type: "identity",
95
+ from: mesh.getLocalPeerId(),
96
+ payload: JSON.stringify({
97
+ agentId: api.name,
98
+ channel: relayChannel,
99
+ accountId: relayAccountId,
100
+ instanceId: localInstanceId,
101
+ }),
102
+ timestamp: Date.now(),
103
+ });
104
+ mesh.sendToPeer(peerId, msg).catch(() => { });
166
105
  });
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.
106
+ // Dedup: skip initial identity announce for known peers
171
107
  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
- });
108
+ const identityMsg = JSON.stringify({
109
+ id: crypto.randomUUID(),
110
+ type: "identity",
111
+ from: mesh.getLocalPeerId(),
112
+ payload: JSON.stringify({
113
+ agentId: api.name,
114
+ channel: relayChannel,
115
+ accountId: relayAccountId,
116
+ instanceId: localInstanceId,
117
+ }),
118
+ timestamp: Date.now(),
119
+ });
120
+ for (const peerId of mesh.getConnectedPeers()) {
121
+ if (peerIdentityMap.hasIdentity(peerId)) {
122
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping initial identity`);
123
+ continue;
180
124
  }
125
+ mesh.sendToPeer(peerId, identityMsg).catch(() => { });
181
126
  }
182
127
  }
183
128
  const identity = mesh.getInstanceIdentity();
184
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
129
+ api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
185
130
  if (identity) {
186
131
  api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
187
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.5.27",
3
+ "version": "2026.5.29",
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
@@ -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,38 @@ 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
- });
33
+ // Load persisted peer identities FIRST so dedup checks work when
34
+ // peers connect during mesh.start() (mDNS can fire peer:connect
35
+ // before this function's sequential code reaches onPeerConnect).
36
+ await peerIdentityMap.load();
95
37
 
96
38
  await mesh.start();
97
39
 
98
- // Load persisted peer identities from disk
99
- await peerIdentityMap.load();
100
-
101
40
  const instanceIdentity = mesh.getInstanceIdentity();
102
41
  const localInstanceId = instanceIdentity?.id;
103
- const localPeerId = mesh.getLocalPeerId();
104
42
 
105
43
  // Register local identity so remote peers can route messages back to us
44
+ const config = api.pluginConfig as MeshConfig | undefined;
45
+ const relayChannel = config?.relayChannel;
46
+ const relayAccountId = config?.relayAccountId;
106
47
  if (relayChannel && relayAccountId) {
48
+ const localPeerId = mesh.getLocalPeerId();
107
49
  const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
108
50
  const sessionKey = buildAgentSessionKey({
109
51
  agentId: api.name,
@@ -117,113 +59,90 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
117
59
  sessionKey,
118
60
  instanceId: localInstanceId,
119
61
  });
120
- await peerIdentityMap.register(localPeerId, {
121
- agentId: api.name,
122
- channel: relayChannel,
123
- accountId: relayAccountId,
124
- sessionKey,
125
- instanceId: localInstanceId,
126
- });
127
62
  api.logger.info?.(
128
- `[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`,
63
+ `[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`,
129
64
  );
130
65
 
131
66
  // 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()) {
67
+ // setLocalIdentity is called BEFORE this block, and localInstanceId
68
+ // is available from mesh.start(), so the identity message includes
69
+ // the instanceId.
70
+ const identityMsg = JSON.stringify({
71
+ id: crypto.randomUUID(),
72
+ type: "identity",
73
+ from: localPeerId,
74
+ payload: JSON.stringify({
75
+ agentId: api.name,
76
+ channel: relayChannel,
77
+ accountId: relayAccountId,
78
+ instanceId: localInstanceId,
79
+ }),
80
+ timestamp: Date.now(),
81
+ });
82
+ const connectedPeers = mesh.getConnectedPeers();
83
+ for (const peerId of connectedPeers) {
140
84
  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
- );
85
+ await mesh.sendToPeer(peerId, identityMsg);
86
+ } catch {
87
+ // Best-effort; peer may be stale in the connection list
146
88
  }
147
89
  }
148
90
  }
149
91
 
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.
92
+ // Wire up relay-aware message handler
93
+ mesh.onMessage((msg) => {
94
+ handleP2PInbound(msg, buildInboundDeps());
95
+ });
156
96
 
97
+ // Dedup: skip identity send if peer is already known
157
98
  mesh.onPeerConnect((peerId: string) => {
99
+ if (peerIdentityMap.hasIdentity(peerId)) {
100
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping identity send`);
101
+ return;
102
+ }
158
103
  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).
104
+ // Only send identity if relay config is set
161
105
  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(() => {});
106
+ const msg = JSON.stringify({
107
+ id: crypto.randomUUID(),
108
+ type: "identity",
109
+ from: mesh.getLocalPeerId(),
110
+ payload: JSON.stringify({
111
+ agentId: api.name,
112
+ channel: relayChannel,
113
+ accountId: relayAccountId,
114
+ instanceId: localInstanceId,
115
+ }),
116
+ timestamp: Date.now(),
117
+ });
118
+ mesh.sendToPeer(peerId, msg).catch(() => {});
196
119
  });
197
120
 
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.
121
+ // Dedup: skip initial identity announce for known peers
202
122
  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
- });
123
+ const identityMsg = JSON.stringify({
124
+ id: crypto.randomUUID(),
125
+ type: "identity",
126
+ from: mesh.getLocalPeerId(),
127
+ payload: JSON.stringify({
128
+ agentId: api.name,
129
+ channel: relayChannel,
130
+ accountId: relayAccountId,
131
+ instanceId: localInstanceId,
132
+ }),
133
+ timestamp: Date.now(),
134
+ });
135
+ for (const peerId of mesh.getConnectedPeers()) {
136
+ if (peerIdentityMap.hasIdentity(peerId)) {
137
+ api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known, skipping initial identity`);
138
+ continue;
221
139
  }
140
+ mesh.sendToPeer(peerId, identityMsg).catch(() => {});
222
141
  }
223
142
  }
224
143
 
225
144
  const identity = mesh.getInstanceIdentity();
226
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
145
+ api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
227
146
  if (identity) {
228
147
  api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
229
148
  }