libp2p-mesh 2026.5.23 → 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.
- package/dist/src/plugin.js +77 -59
- package/package.json +1 -1
- package/src/plugin.ts +90 -67
package/dist/src/plugin.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
mesh.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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();
|