libp2p-mesh 2026.5.30 → 2026.5.32
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.
- package/dist/src/identity-exchange.js +4 -0
- package/dist/src/inbound.js +1 -0
- package/dist/src/mesh.js +9 -2
- package/dist/src/plugin.js +51 -7
- package/package.json +1 -1
- package/src/identity-exchange.ts +4 -0
- package/src/inbound.ts +1 -0
- package/src/mesh.ts +13 -4
- package/src/plugin.ts +54 -7
|
@@ -9,6 +9,7 @@ export function buildIdentityMessage(peerId, agentId, channel, accountId, instan
|
|
|
9
9
|
}
|
|
10
10
|
export async function handleIdentityMessage(msg, deps) {
|
|
11
11
|
const logger = deps.logger;
|
|
12
|
+
logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: from=${msg.from}, type=${msg.type}, payload_prefix=${msg.payload.slice(0, 60)}`);
|
|
12
13
|
// Parse remote identity payload
|
|
13
14
|
let parsed = {};
|
|
14
15
|
try {
|
|
@@ -31,6 +32,7 @@ export async function handleIdentityMessage(msg, deps) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
const { agentId, channel, accountId, instanceId } = parsed;
|
|
35
|
+
logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: parsed agentId=${agentId}, channel=${channel}, accountId=${accountId}, instanceId=${instanceId}`);
|
|
34
36
|
// Only register if all required fields are present
|
|
35
37
|
if (agentId && channel && accountId) {
|
|
36
38
|
const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
|
|
@@ -46,6 +48,7 @@ export async function handleIdentityMessage(msg, deps) {
|
|
|
46
48
|
// process exits before the next save cycle.
|
|
47
49
|
await deps.peerIdentityMap.saveNow();
|
|
48
50
|
logger?.info?.(`[libp2p-mesh] Registered peer identity: ${msg.from} (agent=${agentId}, channel=${channel}, instanceId=${instanceId ?? "n/a"})`);
|
|
51
|
+
logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: successfully registered ${msg.from}, now sending reply to ${msg.from}`);
|
|
49
52
|
}
|
|
50
53
|
// Send local identity back to the remote peer.
|
|
51
54
|
// We intentionally do NOT await this — the send callback (and any
|
|
@@ -54,6 +57,7 @@ export async function handleIdentityMessage(msg, deps) {
|
|
|
54
57
|
// blocks the caller; the recipient's handler will persist the reply on
|
|
55
58
|
// its own via saveNow().
|
|
56
59
|
const localIdentity = buildIdentityMessage(deps.localPeerId, deps.localAgentId, deps.localChannel, deps.localAccountId, deps.localInstanceId);
|
|
60
|
+
logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: sending identity reply to ${msg.from}`);
|
|
57
61
|
deps.send(msg.from, localIdentity).catch((sendErr) => {
|
|
58
62
|
logger?.warn?.(`[libp2p-mesh] Failed to send identity reply to ${msg.from}: ${String(sendErr)}`);
|
|
59
63
|
});
|
package/dist/src/inbound.js
CHANGED
|
@@ -41,6 +41,7 @@ export async function handleP2PInbound(msg, deps) {
|
|
|
41
41
|
// Delegate full identity exchange (register + send reply) to the
|
|
42
42
|
// dedicated handler. This ensures bidirectional identity propagation,
|
|
43
43
|
// forced persistence via saveNow(), and proper logging.
|
|
44
|
+
logger?.debug?.(`[libp2p-mesh] handleP2PInbound: processing identity from ${msg.from}`);
|
|
44
45
|
try {
|
|
45
46
|
await handleIdentityMessage(msg, {
|
|
46
47
|
peerIdentityMap: deps.peerIdentityMap,
|
package/dist/src/mesh.js
CHANGED
|
@@ -333,7 +333,9 @@ export function createMeshNetwork(options) {
|
|
|
333
333
|
runOnTransientConnection: true,
|
|
334
334
|
});
|
|
335
335
|
await state.node.start();
|
|
336
|
-
// Wait for DHT routing table to populate before registering pubkey
|
|
336
|
+
// Wait for DHT routing table to populate before registering pubkey.
|
|
337
|
+
// Non-blocking: we don't await the pubkey registration here — it runs
|
|
338
|
+
// in the background so the node can start serving messages immediately.
|
|
337
339
|
if (enableDHT) {
|
|
338
340
|
const dht = getDHTService();
|
|
339
341
|
if (dht) {
|
|
@@ -353,7 +355,12 @@ export function createMeshNetwork(options) {
|
|
|
353
355
|
logger?.warn?.(`[libp2p-mesh] DHT routing table still empty after ${maxAttempts}s; continuing anyway`);
|
|
354
356
|
}
|
|
355
357
|
if (state.instanceIdentity) {
|
|
356
|
-
|
|
358
|
+
// Fire-and-forget: pubkey registration is best-effort. In LAN
|
|
359
|
+
// scenarios with mDNS, peers connect directly and don't need DHT
|
|
360
|
+
// pubkey lookup to exchange identities. The registration will
|
|
361
|
+
// succeed once the DHT routing table populates (e.g. when a
|
|
362
|
+
// bootstrap peer is discovered or another DHT node joins).
|
|
363
|
+
registerPubkey(dht, state.instanceIdentity.id, state.instanceIdentity.pubkey, logger).catch(() => {
|
|
357
364
|
// Already logged inside registerPubkey
|
|
358
365
|
});
|
|
359
366
|
}
|
package/dist/src/plugin.js
CHANGED
|
@@ -26,6 +26,12 @@ export function registerLibp2pMesh(api) {
|
|
|
26
26
|
// peers connect during mesh.start() (mDNS can fire peer:connect
|
|
27
27
|
// before this function's sequential code reaches onPeerConnect).
|
|
28
28
|
await peerIdentityMap.load();
|
|
29
|
+
// Queue identity sends during mesh.start() so they are flushed only
|
|
30
|
+
// after the node is fully ready (protocol handler registered, DHT
|
|
31
|
+
// initialized, etc.). Sending inside onPeerConnect during startup
|
|
32
|
+
// races with node initialization and can cause the remote peer's
|
|
33
|
+
// reply to be lost or unprocessed.
|
|
34
|
+
const pendingIdentityPeers = new Set();
|
|
29
35
|
await mesh.start();
|
|
30
36
|
const instanceIdentity = mesh.getInstanceIdentity();
|
|
31
37
|
const localInstanceId = instanceIdentity?.id;
|
|
@@ -79,15 +85,21 @@ export function registerLibp2pMesh(api) {
|
|
|
79
85
|
mesh.onMessage((msg) => {
|
|
80
86
|
handleP2PInbound(msg, buildInboundDeps());
|
|
81
87
|
});
|
|
82
|
-
// Always send our identity when a peer connects.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
+
// Always send our identity when a peer connects. During mesh.start(),
|
|
89
|
+
// mDNS can fire peer:connect before the node is fully ready (protocol
|
|
90
|
+
// handler just registered, DHT still initializing). Sending identity
|
|
91
|
+
// messages at that point can cause the remote peer's reply to arrive
|
|
92
|
+
// before we're ready to process it, breaking the identity exchange.
|
|
93
|
+
// Queue during startup; flush after start() + send directly thereafter.
|
|
94
|
+
let startupComplete = false;
|
|
88
95
|
mesh.onPeerConnect((peerId) => {
|
|
96
|
+
if (!startupComplete) {
|
|
97
|
+
// During startup: queue, don't send yet
|
|
98
|
+
pendingIdentityPeers.add(peerId);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// After startup: send directly
|
|
89
102
|
api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
|
|
90
|
-
// Only send identity if relay config is set
|
|
91
103
|
if (!relayChannel || !relayAccountId)
|
|
92
104
|
return;
|
|
93
105
|
const msg = JSON.stringify({
|
|
@@ -141,6 +153,38 @@ export function registerLibp2pMesh(api) {
|
|
|
141
153
|
if (nat.reservedRelays.length > 0) {
|
|
142
154
|
api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
|
|
143
155
|
}
|
|
156
|
+
// Flush identity messages that were queued during mesh.start().
|
|
157
|
+
// Peers that connected during startup (mDNS-triggered peer:connect
|
|
158
|
+
// inside start()) are in pendingIdentityPeers. Send now that the
|
|
159
|
+
// node is fully ready.
|
|
160
|
+
startupComplete = true;
|
|
161
|
+
const flushPendingIdentities = () => {
|
|
162
|
+
if (!relayChannel || !relayAccountId)
|
|
163
|
+
return;
|
|
164
|
+
const identityMsg = JSON.stringify({
|
|
165
|
+
id: crypto.randomUUID(),
|
|
166
|
+
type: "identity",
|
|
167
|
+
from: mesh.getLocalPeerId(),
|
|
168
|
+
payload: JSON.stringify({
|
|
169
|
+
agentId: api.name,
|
|
170
|
+
channel: relayChannel,
|
|
171
|
+
accountId: relayAccountId,
|
|
172
|
+
instanceId: localInstanceId,
|
|
173
|
+
}),
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
});
|
|
176
|
+
for (const peerId of pendingIdentityPeers) {
|
|
177
|
+
if (peerIdentityMap.hasIdentity(peerId)) {
|
|
178
|
+
api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known (loaded from disk), skipping queued identity`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
mesh.sendToPeer(peerId, identityMsg).catch(() => { });
|
|
182
|
+
}
|
|
183
|
+
pendingIdentityPeers.clear();
|
|
184
|
+
};
|
|
185
|
+
if (pendingIdentityPeers.size > 0) {
|
|
186
|
+
flushPendingIdentities();
|
|
187
|
+
}
|
|
144
188
|
},
|
|
145
189
|
stop: async () => {
|
|
146
190
|
await mesh.stop();
|
package/package.json
CHANGED
package/src/identity-exchange.ts
CHANGED
|
@@ -38,6 +38,7 @@ export async function handleIdentityMessage(
|
|
|
38
38
|
deps: IdentityExchangeDeps,
|
|
39
39
|
): Promise<void> {
|
|
40
40
|
const logger = deps.logger;
|
|
41
|
+
logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: from=${msg.from}, type=${msg.type}, payload_prefix=${msg.payload.slice(0, 60)}`);
|
|
41
42
|
|
|
42
43
|
// Parse remote identity payload
|
|
43
44
|
let parsed: Record<string, unknown> = {};
|
|
@@ -66,6 +67,7 @@ export async function handleIdentityMessage(
|
|
|
66
67
|
accountId?: string;
|
|
67
68
|
instanceId?: string;
|
|
68
69
|
};
|
|
70
|
+
logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: parsed agentId=${agentId}, channel=${channel}, accountId=${accountId}, instanceId=${instanceId}`);
|
|
69
71
|
|
|
70
72
|
// Only register if all required fields are present
|
|
71
73
|
if (agentId && channel && accountId) {
|
|
@@ -84,6 +86,7 @@ export async function handleIdentityMessage(
|
|
|
84
86
|
logger?.info?.(
|
|
85
87
|
`[libp2p-mesh] Registered peer identity: ${msg.from} (agent=${agentId}, channel=${channel}, instanceId=${instanceId ?? "n/a"})`,
|
|
86
88
|
);
|
|
89
|
+
logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: successfully registered ${msg.from}, now sending reply to ${msg.from}`);
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
// Send local identity back to the remote peer.
|
|
@@ -99,6 +102,7 @@ export async function handleIdentityMessage(
|
|
|
99
102
|
deps.localAccountId,
|
|
100
103
|
deps.localInstanceId,
|
|
101
104
|
);
|
|
105
|
+
logger?.debug?.(`[libp2p-mesh] handleIdentityMessage: sending identity reply to ${msg.from}`);
|
|
102
106
|
deps.send(msg.from, localIdentity).catch((sendErr) => {
|
|
103
107
|
logger?.warn?.(
|
|
104
108
|
`[libp2p-mesh] Failed to send identity reply to ${msg.from}: ${String(sendErr)}`,
|
package/src/inbound.ts
CHANGED
|
@@ -60,6 +60,7 @@ export async function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDep
|
|
|
60
60
|
// Delegate full identity exchange (register + send reply) to the
|
|
61
61
|
// dedicated handler. This ensures bidirectional identity propagation,
|
|
62
62
|
// forced persistence via saveNow(), and proper logging.
|
|
63
|
+
logger?.debug?.(`[libp2p-mesh] handleP2PInbound: processing identity from ${msg.from}`);
|
|
63
64
|
try {
|
|
64
65
|
await handleIdentityMessage(msg, {
|
|
65
66
|
peerIdentityMap: deps.peerIdentityMap,
|
package/src/mesh.ts
CHANGED
|
@@ -388,7 +388,9 @@ export function createMeshNetwork(options: {
|
|
|
388
388
|
|
|
389
389
|
await state.node.start();
|
|
390
390
|
|
|
391
|
-
// Wait for DHT routing table to populate before registering pubkey
|
|
391
|
+
// Wait for DHT routing table to populate before registering pubkey.
|
|
392
|
+
// Non-blocking: we don't await the pubkey registration here — it runs
|
|
393
|
+
// in the background so the node can start serving messages immediately.
|
|
392
394
|
if (enableDHT) {
|
|
393
395
|
const dht = getDHTService();
|
|
394
396
|
if (dht) {
|
|
@@ -409,9 +411,16 @@ export function createMeshNetwork(options: {
|
|
|
409
411
|
}
|
|
410
412
|
|
|
411
413
|
if (state.instanceIdentity) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
414
|
+
// Fire-and-forget: pubkey registration is best-effort. In LAN
|
|
415
|
+
// scenarios with mDNS, peers connect directly and don't need DHT
|
|
416
|
+
// pubkey lookup to exchange identities. The registration will
|
|
417
|
+
// succeed once the DHT routing table populates (e.g. when a
|
|
418
|
+
// bootstrap peer is discovered or another DHT node joins).
|
|
419
|
+
registerPubkey(dht, state.instanceIdentity.id, state.instanceIdentity.pubkey, logger).catch(
|
|
420
|
+
() => {
|
|
421
|
+
// Already logged inside registerPubkey
|
|
422
|
+
},
|
|
423
|
+
);
|
|
415
424
|
}
|
|
416
425
|
}
|
|
417
426
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -35,6 +35,13 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
35
35
|
// before this function's sequential code reaches onPeerConnect).
|
|
36
36
|
await peerIdentityMap.load();
|
|
37
37
|
|
|
38
|
+
// Queue identity sends during mesh.start() so they are flushed only
|
|
39
|
+
// after the node is fully ready (protocol handler registered, DHT
|
|
40
|
+
// initialized, etc.). Sending inside onPeerConnect during startup
|
|
41
|
+
// races with node initialization and can cause the remote peer's
|
|
42
|
+
// reply to be lost or unprocessed.
|
|
43
|
+
const pendingIdentityPeers = new Set<string>();
|
|
44
|
+
|
|
38
45
|
await mesh.start();
|
|
39
46
|
|
|
40
47
|
const instanceIdentity = mesh.getInstanceIdentity();
|
|
@@ -94,15 +101,22 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
94
101
|
handleP2PInbound(msg, buildInboundDeps());
|
|
95
102
|
});
|
|
96
103
|
|
|
97
|
-
// Always send our identity when a peer connects.
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
104
|
+
// Always send our identity when a peer connects. During mesh.start(),
|
|
105
|
+
// mDNS can fire peer:connect before the node is fully ready (protocol
|
|
106
|
+
// handler just registered, DHT still initializing). Sending identity
|
|
107
|
+
// messages at that point can cause the remote peer's reply to arrive
|
|
108
|
+
// before we're ready to process it, breaking the identity exchange.
|
|
109
|
+
// Queue during startup; flush after start() + send directly thereafter.
|
|
110
|
+
let startupComplete = false;
|
|
111
|
+
|
|
103
112
|
mesh.onPeerConnect((peerId: string) => {
|
|
113
|
+
if (!startupComplete) {
|
|
114
|
+
// During startup: queue, don't send yet
|
|
115
|
+
pendingIdentityPeers.add(peerId);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// After startup: send directly
|
|
104
119
|
api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
|
|
105
|
-
// Only send identity if relay config is set
|
|
106
120
|
if (!relayChannel || !relayAccountId) return;
|
|
107
121
|
const msg = JSON.stringify({
|
|
108
122
|
id: crypto.randomUUID(),
|
|
@@ -161,6 +175,39 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
161
175
|
`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`,
|
|
162
176
|
);
|
|
163
177
|
}
|
|
178
|
+
|
|
179
|
+
// Flush identity messages that were queued during mesh.start().
|
|
180
|
+
// Peers that connected during startup (mDNS-triggered peer:connect
|
|
181
|
+
// inside start()) are in pendingIdentityPeers. Send now that the
|
|
182
|
+
// node is fully ready.
|
|
183
|
+
startupComplete = true;
|
|
184
|
+
const flushPendingIdentities = () => {
|
|
185
|
+
if (!relayChannel || !relayAccountId) return;
|
|
186
|
+
const identityMsg = JSON.stringify({
|
|
187
|
+
id: crypto.randomUUID(),
|
|
188
|
+
type: "identity",
|
|
189
|
+
from: mesh.getLocalPeerId(),
|
|
190
|
+
payload: JSON.stringify({
|
|
191
|
+
agentId: api.name,
|
|
192
|
+
channel: relayChannel,
|
|
193
|
+
accountId: relayAccountId,
|
|
194
|
+
instanceId: localInstanceId,
|
|
195
|
+
}),
|
|
196
|
+
timestamp: Date.now(),
|
|
197
|
+
});
|
|
198
|
+
for (const peerId of pendingIdentityPeers) {
|
|
199
|
+
if (peerIdentityMap.hasIdentity(peerId)) {
|
|
200
|
+
api.logger.debug?.(`[libp2p-mesh] Peer ${peerId} already known (loaded from disk), skipping queued identity`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
mesh.sendToPeer(peerId, identityMsg).catch(() => {});
|
|
204
|
+
}
|
|
205
|
+
pendingIdentityPeers.clear();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (pendingIdentityPeers.size > 0) {
|
|
209
|
+
flushPendingIdentities();
|
|
210
|
+
}
|
|
164
211
|
},
|
|
165
212
|
stop: async () => {
|
|
166
213
|
await mesh.stop();
|