libp2p-mesh 2026.5.17 → 2026.5.19

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.
@@ -14,6 +14,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
14
14
  type: "string";
15
15
  description: string;
16
16
  };
17
+ instanceId?: undefined;
17
18
  topic?: undefined;
18
19
  };
19
20
  required: string[];
@@ -44,6 +45,78 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
44
45
  };
45
46
  isError: boolean;
46
47
  }>;
48
+ } | {
49
+ name: string;
50
+ label: string;
51
+ description: string;
52
+ parameters: {
53
+ type: "object";
54
+ properties: {
55
+ instanceId: {
56
+ type: "string";
57
+ description: string;
58
+ };
59
+ message: {
60
+ type: "string";
61
+ description: string;
62
+ };
63
+ peerId?: undefined;
64
+ topic?: undefined;
65
+ };
66
+ required: string[];
67
+ };
68
+ execute(_toolCallId: string, params: {
69
+ instanceId: string;
70
+ message: string;
71
+ }, _ctx: any): Promise<{
72
+ content: {
73
+ type: "text";
74
+ text: string;
75
+ }[];
76
+ details: {
77
+ sent: boolean;
78
+ error: string;
79
+ instanceId?: undefined;
80
+ peerId?: undefined;
81
+ };
82
+ isError: boolean;
83
+ } | {
84
+ content: {
85
+ type: "text";
86
+ text: string;
87
+ }[];
88
+ details: {
89
+ sent: boolean;
90
+ instanceId: string;
91
+ error?: undefined;
92
+ peerId?: undefined;
93
+ };
94
+ isError: boolean;
95
+ } | {
96
+ content: {
97
+ type: "text";
98
+ text: string;
99
+ }[];
100
+ details: {
101
+ sent: boolean;
102
+ instanceId: string;
103
+ peerId: any;
104
+ error?: undefined;
105
+ };
106
+ isError?: undefined;
107
+ } | {
108
+ content: {
109
+ type: "text";
110
+ text: string;
111
+ }[];
112
+ details: {
113
+ sent: boolean;
114
+ instanceId: string;
115
+ error: string;
116
+ peerId?: undefined;
117
+ };
118
+ isError: boolean;
119
+ }>;
47
120
  } | {
48
121
  name: string;
49
122
  label: string;
@@ -60,6 +133,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
60
133
  description: string;
61
134
  };
62
135
  peerId?: undefined;
136
+ instanceId?: undefined;
63
137
  };
64
138
  required: string[];
65
139
  };
@@ -98,19 +172,21 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
98
172
  properties: {
99
173
  peerId?: undefined;
100
174
  message?: undefined;
175
+ instanceId?: undefined;
101
176
  topic?: undefined;
102
177
  };
103
178
  required?: undefined;
104
179
  };
105
- execute(_toolCallId: string): Promise<{
180
+ execute(_toolCallId: string, _params: any, ctx: any): Promise<{
106
181
  content: {
107
182
  type: "text";
108
183
  text: string;
109
184
  }[];
110
185
  details: {
111
- localPeerId: string;
112
- connectedPeers: string[];
113
- count: number;
186
+ peers: {
187
+ instanceId: any;
188
+ channel: any;
189
+ }[];
114
190
  error?: undefined;
115
191
  };
116
192
  isError?: undefined;
@@ -121,9 +197,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
121
197
  }[];
122
198
  details: {
123
199
  error: string;
124
- localPeerId?: undefined;
125
- connectedPeers?: undefined;
126
- count?: undefined;
200
+ peers?: undefined;
127
201
  };
128
202
  isError: boolean;
129
203
  }>;
@@ -136,6 +210,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
136
210
  properties: {
137
211
  peerId?: undefined;
138
212
  message?: undefined;
213
+ instanceId?: undefined;
139
214
  topic?: undefined;
140
215
  };
141
216
  required?: undefined;
@@ -183,6 +258,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
183
258
  properties: {
184
259
  peerId?: undefined;
185
260
  message?: undefined;
261
+ instanceId?: undefined;
186
262
  topic?: undefined;
187
263
  };
188
264
  required?: undefined;
@@ -223,6 +299,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
223
299
  properties: {
224
300
  peerId?: undefined;
225
301
  message?: undefined;
302
+ instanceId?: undefined;
226
303
  topic?: undefined;
227
304
  };
228
305
  required?: undefined;
@@ -233,12 +310,11 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
233
310
  text: string;
234
311
  }[];
235
312
  details: {
236
- localPeerId: string;
237
- connectedPeers: string[];
238
- knownIdentities: {
239
- peerId: string;
240
- identity: any;
241
- }[];
313
+ knownIdentities: ({
314
+ instanceId: any;
315
+ agentId: any;
316
+ channel: any;
317
+ } | null)[];
242
318
  };
243
319
  }>;
244
320
  })[];
@@ -3,7 +3,7 @@ export function buildP2PTools(mesh) {
3
3
  {
4
4
  name: "p2p_send_message",
5
5
  label: "P2P Send Message",
6
- description: "Send a direct message to another agent via the P2P mesh network.",
6
+ description: "Send a direct message to another agent via the P2P mesh network by Peer ID.",
7
7
  parameters: {
8
8
  type: "object",
9
9
  properties: {
@@ -40,6 +40,72 @@ export function buildP2PTools(mesh) {
40
40
  }
41
41
  },
42
42
  },
43
+ {
44
+ name: "p2p_send_to_instance",
45
+ label: "P2P Send to Instance",
46
+ description: "Send a direct message to a peer by its human-readable Instance ID (e.g. alice-mac).",
47
+ parameters: {
48
+ type: "object",
49
+ properties: {
50
+ instanceId: {
51
+ type: "string",
52
+ description: "Target Instance ID (e.g. alice-mac@AQIDBAU.7a3f9e2b)",
53
+ },
54
+ message: {
55
+ type: "string",
56
+ description: "Message content to send",
57
+ },
58
+ },
59
+ required: ["instanceId", "message"],
60
+ },
61
+ async execute(_toolCallId, params, _ctx) {
62
+ const peerIdentityMap = _ctx?.peerIdentityMap;
63
+ if (!peerIdentityMap) {
64
+ return {
65
+ content: [{ type: "text", text: "Internal error: peer identity map not available." }],
66
+ details: { sent: false, error: "peerIdentityMap missing" },
67
+ isError: true,
68
+ };
69
+ }
70
+ const lookup = peerIdentityMap.resolveByInstanceId(params.instanceId);
71
+ if (!lookup) {
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: `Unknown Instance ID: "${params.instanceId}". Use p2p_list_peers to see available users.`,
77
+ },
78
+ ],
79
+ details: { sent: false, instanceId: params.instanceId },
80
+ isError: true,
81
+ };
82
+ }
83
+ try {
84
+ await mesh.sendToPeer(lookup.peerId, params.message);
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: `Message sent to ${params.instanceId}`,
90
+ },
91
+ ],
92
+ details: { sent: true, instanceId: params.instanceId, peerId: lookup.peerId },
93
+ };
94
+ }
95
+ catch (err) {
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: `Failed to send message to ${params.instanceId}: ${String(err)}`,
101
+ },
102
+ ],
103
+ details: { sent: false, instanceId: params.instanceId, error: String(err) },
104
+ isError: true,
105
+ };
106
+ }
107
+ },
108
+ },
43
109
  {
44
110
  name: "p2p_broadcast",
45
111
  label: "P2P Broadcast",
@@ -83,24 +149,42 @@ export function buildP2PTools(mesh) {
83
149
  {
84
150
  name: "p2p_list_peers",
85
151
  label: "P2P List Peers",
86
- description: "List currently connected peers in the P2P mesh network.",
152
+ description: "List currently connected peers with their Instance IDs and channels.",
87
153
  parameters: {
88
154
  type: "object",
89
155
  properties: {},
90
156
  },
91
- async execute(_toolCallId) {
157
+ async execute(_toolCallId, _params, ctx) {
92
158
  try {
159
+ const peerIdentityMap = ctx?.peerIdentityMap;
93
160
  const peers = mesh.getConnectedPeers();
94
- const text = peers.length === 0
95
- ? "No peers currently connected."
96
- : `Connected peers (${peers.length}): ${peers.join(", ")}`;
161
+ if (peers.length === 0) {
162
+ return {
163
+ content: [{ type: "text", text: "No peers currently connected." }],
164
+ details: { peers: [] },
165
+ };
166
+ }
167
+ const userList = peers
168
+ .map((p) => {
169
+ const identity = peerIdentityMap?.resolve(p);
170
+ if (identity?.instanceId) {
171
+ const ch = identity.channel ? ` (${identity.channel})` : "";
172
+ return ` - ${identity.instanceId}${ch}`;
173
+ }
174
+ return ` - ${p.slice(0, 12)}... (identity unknown)`;
175
+ })
176
+ .join("\n");
177
+ const text = `在线用户 (${peers.length}):\n${userList}`;
178
+ const safePeers = peers.map((p) => {
179
+ const identity = peerIdentityMap?.resolve(p);
180
+ return {
181
+ instanceId: identity?.instanceId || p.slice(0, 12) + "...",
182
+ channel: identity?.channel || "unknown",
183
+ };
184
+ });
97
185
  return {
98
186
  content: [{ type: "text", text }],
99
- details: {
100
- localPeerId: mesh.getLocalPeerId(),
101
- connectedPeers: peers,
102
- count: peers.length,
103
- },
187
+ details: { peers: safePeers },
104
188
  };
105
189
  }
106
190
  catch (err) {
@@ -201,12 +285,21 @@ export function buildP2PTools(mesh) {
201
285
  const localPeerId = mesh.getLocalPeerId();
202
286
  const connectedPeers = mesh.getConnectedPeers();
203
287
  const knownIdentities = connectedPeers
204
- .map((p) => ({ peerId: p, identity: peerIdentityMap?.resolve(p) }))
205
- .filter((entry) => entry.identity !== undefined);
288
+ .map((p) => {
289
+ const identity = peerIdentityMap?.resolve(p);
290
+ if (!identity)
291
+ return null;
292
+ return {
293
+ instanceId: identity.instanceId || p.slice(0, 12) + "...",
294
+ agentId: identity.agentId,
295
+ channel: identity.channel,
296
+ };
297
+ })
298
+ .filter(Boolean);
206
299
  const text = `P2P Mesh Status:\nLocal Peer ID: ${localPeerId}\nConnected Peers: ${connectedPeers.length}\nKnown Identities: ${knownIdentities.length}`;
207
300
  return {
208
301
  content: [{ type: "text", text }],
209
- details: { localPeerId, connectedPeers, knownIdentities },
302
+ details: { knownIdentities },
210
303
  };
211
304
  },
212
305
  },
@@ -6,7 +6,8 @@ export interface IdentityExchangeDeps {
6
6
  localAgentId: string;
7
7
  localChannel: string;
8
8
  localAccountId: string;
9
+ localInstanceId?: string;
9
10
  send: (peerId: string, message: P2PMessage) => Promise<void>;
10
11
  }
11
- export declare function buildIdentityMessage(peerId: string, agentId: string, channel: string, accountId: string): P2PMessage;
12
+ export declare function buildIdentityMessage(peerId: string, agentId: string, channel: string, accountId: string, instanceId?: string): P2PMessage;
12
13
  export declare function handleIdentityMessage(msg: P2PMessage, deps: IdentityExchangeDeps): Promise<void>;
@@ -1,9 +1,9 @@
1
- export function buildIdentityMessage(peerId, agentId, channel, accountId) {
1
+ export function buildIdentityMessage(peerId, agentId, channel, accountId, instanceId) {
2
2
  return {
3
3
  id: crypto.randomUUID(),
4
4
  type: "identity",
5
5
  from: peerId,
6
- payload: JSON.stringify({ agentId, channel, accountId }),
6
+ payload: JSON.stringify({ agentId, channel, accountId, instanceId }),
7
7
  timestamp: Date.now(),
8
8
  };
9
9
  }
@@ -17,7 +17,7 @@ export async function handleIdentityMessage(msg, deps) {
17
17
  // Malformed payload — skip silently
18
18
  return;
19
19
  }
20
- const { agentId, channel, accountId } = parsed;
20
+ const { agentId, channel, accountId, instanceId } = parsed;
21
21
  // Only register if all required fields are present
22
22
  if (agentId && channel && accountId) {
23
23
  const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
@@ -27,9 +27,10 @@ export async function handleIdentityMessage(msg, deps) {
27
27
  channel,
28
28
  accountId,
29
29
  sessionKey,
30
+ instanceId,
30
31
  });
31
32
  }
32
33
  // Send local identity back to the remote peer
33
- const localIdentity = buildIdentityMessage(deps.localPeerId, deps.localAgentId, deps.localChannel, deps.localAccountId);
34
+ const localIdentity = buildIdentityMessage(deps.localPeerId, deps.localAgentId, deps.localChannel, deps.localAccountId, deps.localInstanceId);
34
35
  await deps.send(msg.from, localIdentity);
35
36
  }
@@ -46,7 +46,7 @@ export function handleP2PInbound(msg, deps) {
46
46
  // Malformed payload — skip silently
47
47
  return;
48
48
  }
49
- const { agentId, channel, accountId } = parsed;
49
+ const { agentId, channel, accountId, instanceId } = parsed;
50
50
  if (agentId && channel && accountId) {
51
51
  const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
52
52
  deps.peerIdentityMap.register(msg.from, {
@@ -54,6 +54,7 @@ export function handleP2PInbound(msg, deps) {
54
54
  channel,
55
55
  accountId,
56
56
  sessionKey,
57
+ instanceId,
57
58
  });
58
59
  }
59
60
  }
package/dist/src/mesh.js CHANGED
@@ -77,6 +77,8 @@ export function createMeshNetwork(options) {
77
77
  const seenMessages = new Set();
78
78
  const messageHandlers = new Set();
79
79
  const topicHandlers = new Map();
80
+ const peerConnectHandlers = new Set();
81
+ const peerDisconnectHandlers = new Set();
80
82
  function getDHTService() {
81
83
  return state.node?.services?.dht;
82
84
  }
@@ -215,10 +217,22 @@ export function createMeshNetwork(options) {
215
217
  state.node.addEventListener("peer:connect", (evt) => {
216
218
  const peerIdStr = evt.detail.toString();
217
219
  logger?.info?.(`[libp2p-mesh] Peer connected: ${peerIdStr}`);
220
+ for (const handler of peerConnectHandlers) {
221
+ try {
222
+ handler(peerIdStr);
223
+ }
224
+ catch { }
225
+ }
218
226
  });
219
227
  state.node.addEventListener("peer:disconnect", (evt) => {
220
228
  const peerIdStr = evt.detail.toString();
221
229
  logger?.info?.(`[libp2p-mesh] Peer disconnected: ${peerIdStr}`);
230
+ for (const handler of peerDisconnectHandlers) {
231
+ try {
232
+ handler(peerIdStr);
233
+ }
234
+ catch { }
235
+ }
222
236
  });
223
237
  await state.node.handle(PROTOCOL, async ({ stream, connection }) => {
224
238
  try {
@@ -580,6 +594,14 @@ export function createMeshNetwork(options) {
580
594
  stop,
581
595
  sendToPeer,
582
596
  onMessage,
597
+ onPeerConnect(handler) {
598
+ peerConnectHandlers.add(handler);
599
+ return () => { peerConnectHandlers.delete(handler); };
600
+ },
601
+ onPeerDisconnect(handler) {
602
+ peerDisconnectHandlers.add(handler);
603
+ return () => { peerDisconnectHandlers.delete(handler); };
604
+ },
583
605
  publishToTopic,
584
606
  subscribeToTopic,
585
607
  getLocalPeerId,
@@ -3,11 +3,17 @@ export interface PeerIdentity {
3
3
  channel: string;
4
4
  accountId: string;
5
5
  sessionKey: string;
6
+ /** Human-readable Instance ID (e.g. "alice-mac@AQIDBAU.7a3f9e2b") */
7
+ instanceId?: string;
6
8
  }
7
9
  export interface PeerIdentityMap {
8
10
  register(peerId: string, identity: PeerIdentity): void;
9
11
  resolve(peerId: string): PeerIdentity | undefined;
10
12
  resolveSessionKey(peerId: string): string | undefined;
13
+ resolveByInstanceId(instanceId: string): {
14
+ peerId: string;
15
+ identity: PeerIdentity;
16
+ } | undefined;
11
17
  unregister(peerId: string): void;
12
18
  setLocalIdentity(peerId: string, identity: PeerIdentity): void;
13
19
  getLocalIdentity(): PeerIdentity | undefined;
@@ -1,10 +1,14 @@
1
1
  export function createPeerIdentityMap() {
2
2
  const peers = new Map();
3
+ const byInstanceId = new Map();
3
4
  let localPeerId;
4
5
  let localIdentity;
5
6
  return {
6
7
  register(peerId, identity) {
7
8
  peers.set(peerId, identity);
9
+ if (identity.instanceId) {
10
+ byInstanceId.set(identity.instanceId, { peerId, identity });
11
+ }
8
12
  },
9
13
  resolve(peerId) {
10
14
  return peers.get(peerId);
@@ -12,13 +16,23 @@ export function createPeerIdentityMap() {
12
16
  resolveSessionKey(peerId) {
13
17
  return peers.get(peerId)?.sessionKey;
14
18
  },
19
+ resolveByInstanceId(instanceId) {
20
+ return byInstanceId.get(instanceId);
21
+ },
15
22
  unregister(peerId) {
23
+ const identity = peers.get(peerId);
24
+ if (identity?.instanceId) {
25
+ byInstanceId.delete(identity.instanceId);
26
+ }
16
27
  peers.delete(peerId);
17
28
  },
18
29
  setLocalIdentity(peerId, identity) {
19
30
  localPeerId = peerId;
20
31
  localIdentity = identity;
21
32
  peers.set(peerId, identity);
33
+ if (identity.instanceId) {
34
+ byInstanceId.set(identity.instanceId, { peerId, identity });
35
+ }
22
36
  },
23
37
  getLocalIdentity() {
24
38
  return localIdentity;
@@ -24,12 +24,15 @@ export function registerLibp2pMesh(api) {
24
24
  id: "libp2p-mesh",
25
25
  start: async () => {
26
26
  await mesh.start();
27
- // Register local identity so remote peers can route messages back to us
27
+ // Gather local relay identity info
28
28
  const config = api.pluginConfig;
29
29
  const relayChannel = config?.relayChannel;
30
30
  const relayAccountId = config?.relayAccountId;
31
+ const instanceIdentity = mesh.getInstanceIdentity();
32
+ const localInstanceId = instanceIdentity?.id;
33
+ const localPeerId = mesh.getLocalPeerId();
34
+ // Register local identity so remote peers can route messages back to us
31
35
  if (relayChannel && relayAccountId) {
32
- const localPeerId = mesh.getLocalPeerId();
33
36
  const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
34
37
  const sessionKey = buildAgentSessionKey({
35
38
  agentId: api.name,
@@ -41,12 +44,12 @@ export function registerLibp2pMesh(api) {
41
44
  channel: relayChannel,
42
45
  accountId: relayAccountId,
43
46
  sessionKey,
47
+ instanceId: localInstanceId,
44
48
  });
45
- api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`);
49
+ api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`);
46
50
  // Announce identity to all currently-connected peers
47
- const identityMsg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId);
48
- const connectedPeers = mesh.getConnectedPeers();
49
- for (const peerId of connectedPeers) {
51
+ const identityMsg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
52
+ for (const peerId of mesh.getConnectedPeers()) {
50
53
  try {
51
54
  await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
52
55
  }
@@ -54,13 +57,24 @@ export function registerLibp2pMesh(api) {
54
57
  // Best-effort; peer may be stale in the connection list
55
58
  }
56
59
  }
60
+ // When a new peer connects, send our identity to them
61
+ mesh.onPeerConnect((peerId) => {
62
+ const msg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
63
+ mesh.sendToPeer(peerId, JSON.stringify(msg)).catch(() => {
64
+ // Best-effort
65
+ });
66
+ });
67
+ // When a peer disconnects, clean up the identity map
68
+ mesh.onPeerDisconnect((peerId) => {
69
+ peerIdentityMap.unregister(peerId);
70
+ });
57
71
  }
58
72
  // Wire up relay-aware message handler
59
73
  mesh.onMessage((msg) => {
60
74
  handleP2PInbound(msg, buildInboundDeps());
61
75
  });
62
76
  const identity = mesh.getInstanceIdentity();
63
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
77
+ api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
64
78
  if (identity) {
65
79
  api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
66
80
  }
@@ -106,6 +106,8 @@ export interface MeshNetwork {
106
106
  stop(): Promise<void>;
107
107
  sendToPeer(peerId: string, message: string): Promise<void>;
108
108
  onMessage(handler: (msg: P2PMessage) => void): () => void;
109
+ onPeerConnect(handler: (peerId: string) => void): () => void;
110
+ onPeerDisconnect(handler: (peerId: string) => void): () => void;
109
111
  publishToTopic(topic: string, message: string): Promise<void>;
110
112
  subscribeToTopic(topic: string, handler: (msg: string) => void): Promise<void>;
111
113
  getLocalPeerId(): string;
@@ -116,6 +116,7 @@
116
116
  "contracts": {
117
117
  "tools": [
118
118
  "p2p_send_message",
119
+ "p2p_send_to_instance",
119
120
  "p2p_broadcast",
120
121
  "p2p_list_peers",
121
122
  "p2p_get_instance_identity",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.5.17",
3
+ "version": "2026.5.19",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,11 +1,13 @@
1
1
  import type { MeshNetwork } from "./types.js";
2
+ import type { PeerIdentity } from "./peer-identity.ts";
3
+ import type { PeerIdentityMap } from "./peer-identity.ts";
2
4
 
3
5
  export function buildP2PTools(mesh: MeshNetwork) {
4
6
  return [
5
7
  {
6
8
  name: "p2p_send_message",
7
9
  label: "P2P Send Message",
8
- description: "Send a direct message to another agent via the P2P mesh network.",
10
+ description: "Send a direct message to another agent via the P2P mesh network by Peer ID.",
9
11
  parameters: {
10
12
  type: "object" as const,
11
13
  properties: {
@@ -41,6 +43,71 @@ export function buildP2PTools(mesh: MeshNetwork) {
41
43
  }
42
44
  },
43
45
  },
46
+ {
47
+ name: "p2p_send_to_instance",
48
+ label: "P2P Send to Instance",
49
+ description: "Send a direct message to a peer by its human-readable Instance ID (e.g. alice-mac).",
50
+ parameters: {
51
+ type: "object" as const,
52
+ properties: {
53
+ instanceId: {
54
+ type: "string" as const,
55
+ description: "Target Instance ID (e.g. alice-mac@AQIDBAU.7a3f9e2b)",
56
+ },
57
+ message: {
58
+ type: "string" as const,
59
+ description: "Message content to send",
60
+ },
61
+ },
62
+ required: ["instanceId", "message"],
63
+ },
64
+ async execute(_toolCallId: string, params: { instanceId: string; message: string }, _ctx: any) {
65
+ const peerIdentityMap = (_ctx as any)?.peerIdentityMap;
66
+ if (!peerIdentityMap) {
67
+ return {
68
+ content: [{ type: "text" as const, text: "Internal error: peer identity map not available." }],
69
+ details: { sent: false, error: "peerIdentityMap missing" },
70
+ isError: true,
71
+ };
72
+ }
73
+ const lookup = peerIdentityMap.resolveByInstanceId(params.instanceId);
74
+ if (!lookup) {
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text" as const,
79
+ text: `Unknown Instance ID: "${params.instanceId}". Use p2p_list_peers to see available users.`,
80
+ },
81
+ ],
82
+ details: { sent: false, instanceId: params.instanceId },
83
+ isError: true,
84
+ };
85
+ }
86
+ try {
87
+ await mesh.sendToPeer(lookup.peerId, params.message);
88
+ return {
89
+ content: [
90
+ {
91
+ type: "text" as const,
92
+ text: `Message sent to ${params.instanceId}`,
93
+ },
94
+ ],
95
+ details: { sent: true, instanceId: params.instanceId, peerId: lookup.peerId },
96
+ };
97
+ } catch (err) {
98
+ return {
99
+ content: [
100
+ {
101
+ type: "text" as const,
102
+ text: `Failed to send message to ${params.instanceId}: ${String(err)}`,
103
+ },
104
+ ],
105
+ details: { sent: false, instanceId: params.instanceId, error: String(err) },
106
+ isError: true,
107
+ };
108
+ }
109
+ },
110
+ },
44
111
  {
45
112
  name: "p2p_broadcast",
46
113
  label: "P2P Broadcast",
@@ -83,25 +150,42 @@ export function buildP2PTools(mesh: MeshNetwork) {
83
150
  {
84
151
  name: "p2p_list_peers",
85
152
  label: "P2P List Peers",
86
- description: "List currently connected peers in the P2P mesh network.",
153
+ description: "List currently connected peers with their Instance IDs and channels.",
87
154
  parameters: {
88
155
  type: "object" as const,
89
156
  properties: {},
90
157
  },
91
- async execute(_toolCallId: string) {
158
+ async execute(_toolCallId: string, _params: any, ctx: any) {
92
159
  try {
160
+ const peerIdentityMap = ctx?.peerIdentityMap;
93
161
  const peers = mesh.getConnectedPeers();
94
- const text =
95
- peers.length === 0
96
- ? "No peers currently connected."
97
- : `Connected peers (${peers.length}): ${peers.join(", ")}`;
162
+ if (peers.length === 0) {
163
+ return {
164
+ content: [{ type: "text" as const, text: "No peers currently connected." }],
165
+ details: { peers: [] },
166
+ };
167
+ }
168
+ const userList = peers
169
+ .map((p) => {
170
+ const identity = peerIdentityMap?.resolve(p);
171
+ if (identity?.instanceId) {
172
+ const ch = identity.channel ? ` (${identity.channel})` : "";
173
+ return ` - ${identity.instanceId}${ch}`;
174
+ }
175
+ return ` - ${p.slice(0, 12)}... (identity unknown)`;
176
+ })
177
+ .join("\n");
178
+ const text = `在线用户 (${peers.length}):\n${userList}`;
179
+ const safePeers = peers.map((p) => {
180
+ const identity = peerIdentityMap?.resolve(p);
181
+ return {
182
+ instanceId: identity?.instanceId || p.slice(0, 12) + "...",
183
+ channel: identity?.channel || "unknown",
184
+ };
185
+ });
98
186
  return {
99
187
  content: [{ type: "text" as const, text }],
100
- details: {
101
- localPeerId: mesh.getLocalPeerId(),
102
- connectedPeers: peers,
103
- count: peers.length,
104
- },
188
+ details: { peers: safePeers },
105
189
  };
106
190
  } catch (err) {
107
191
  return {
@@ -201,12 +285,20 @@ export function buildP2PTools(mesh: MeshNetwork) {
201
285
  const localPeerId = mesh.getLocalPeerId();
202
286
  const connectedPeers = mesh.getConnectedPeers();
203
287
  const knownIdentities = connectedPeers
204
- .map((p) => ({ peerId: p, identity: peerIdentityMap?.resolve(p) }))
205
- .filter((entry) => entry.identity !== undefined);
288
+ .map((p) => {
289
+ const identity = peerIdentityMap?.resolve(p);
290
+ if (!identity) return null;
291
+ return {
292
+ instanceId: identity.instanceId || p.slice(0, 12) + "...",
293
+ agentId: identity.agentId,
294
+ channel: identity.channel,
295
+ };
296
+ })
297
+ .filter(Boolean);
206
298
  const text = `P2P Mesh Status:\nLocal Peer ID: ${localPeerId}\nConnected Peers: ${connectedPeers.length}\nKnown Identities: ${knownIdentities.length}`;
207
299
  return {
208
300
  content: [{ type: "text" as const, text }],
209
- details: { localPeerId, connectedPeers, knownIdentities },
301
+ details: { knownIdentities },
210
302
  };
211
303
  },
212
304
  },
@@ -7,6 +7,7 @@ export interface IdentityExchangeDeps {
7
7
  localAgentId: string;
8
8
  localChannel: string;
9
9
  localAccountId: string;
10
+ localInstanceId?: string;
10
11
  send: (peerId: string, message: P2PMessage) => Promise<void>;
11
12
  }
12
13
 
@@ -15,12 +16,13 @@ export function buildIdentityMessage(
15
16
  agentId: string,
16
17
  channel: string,
17
18
  accountId: string,
19
+ instanceId?: string,
18
20
  ): P2PMessage {
19
21
  return {
20
22
  id: crypto.randomUUID(),
21
23
  type: "identity",
22
24
  from: peerId,
23
- payload: JSON.stringify({ agentId, channel, accountId }),
25
+ payload: JSON.stringify({ agentId, channel, accountId, instanceId }),
24
26
  timestamp: Date.now(),
25
27
  };
26
28
  }
@@ -30,7 +32,7 @@ export async function handleIdentityMessage(
30
32
  deps: IdentityExchangeDeps,
31
33
  ): Promise<void> {
32
34
  // Parse remote identity payload
33
- let parsed: { agentId?: string; channel?: string; accountId?: string } = {};
35
+ let parsed: { agentId?: string; channel?: string; accountId?: string; instanceId?: string } = {};
34
36
  try {
35
37
  parsed = JSON.parse(msg.payload);
36
38
  } catch {
@@ -38,7 +40,7 @@ export async function handleIdentityMessage(
38
40
  return;
39
41
  }
40
42
 
41
- const { agentId, channel, accountId } = parsed;
43
+ const { agentId, channel, accountId, instanceId } = parsed;
42
44
 
43
45
  // Only register if all required fields are present
44
46
  if (agentId && channel && accountId) {
@@ -49,6 +51,7 @@ export async function handleIdentityMessage(
49
51
  channel,
50
52
  accountId,
51
53
  sessionKey,
54
+ instanceId,
52
55
  });
53
56
  }
54
57
 
@@ -58,6 +61,7 @@ export async function handleIdentityMessage(
58
61
  deps.localAgentId,
59
62
  deps.localChannel,
60
63
  deps.localAccountId,
64
+ deps.localInstanceId,
61
65
  );
62
66
  await deps.send(msg.from, localIdentity);
63
67
  }
package/src/inbound.ts CHANGED
@@ -57,7 +57,7 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
57
57
  if (deps?.peerIdentityMap && deps?.enqueueNextTurnInjection) {
58
58
  if (msg.type === "identity") {
59
59
  // Parse remote identity payload and register synchronously
60
- let parsed: { agentId?: string; channel?: string; accountId?: string } = {};
60
+ let parsed: { agentId?: string; channel?: string; accountId?: string; instanceId?: string } = {};
61
61
  try {
62
62
  parsed = JSON.parse(msg.payload);
63
63
  } catch {
@@ -65,7 +65,7 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
65
65
  return;
66
66
  }
67
67
 
68
- const { agentId, channel, accountId } = parsed;
68
+ const { agentId, channel, accountId, instanceId } = parsed;
69
69
 
70
70
  if (agentId && channel && accountId) {
71
71
  const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
@@ -74,6 +74,7 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
74
74
  channel,
75
75
  accountId,
76
76
  sessionKey,
77
+ instanceId,
77
78
  });
78
79
  }
79
80
  } else if (msg.type === "direct") {
package/src/mesh.ts CHANGED
@@ -94,6 +94,8 @@ export function createMeshNetwork(options: {
94
94
  const seenMessages = new Set<string>();
95
95
  const messageHandlers = new Set<(msg: P2PMessage) => void>();
96
96
  const topicHandlers = new Map<string, Set<(msg: string) => void>>();
97
+ const peerConnectHandlers = new Set<(peerId: string) => void>();
98
+ const peerDisconnectHandlers = new Set<(peerId: string) => void>();
97
99
 
98
100
  function getDHTService(): ReturnType<typeof kadDHT> extends (components: infer C) => infer R ? R : never | undefined {
99
101
  return (state.node as any)?.services?.dht;
@@ -258,11 +260,17 @@ export function createMeshNetwork(options: {
258
260
  state.node.addEventListener("peer:connect", (evt) => {
259
261
  const peerIdStr = evt.detail.toString();
260
262
  logger?.info?.(`[libp2p-mesh] Peer connected: ${peerIdStr}`);
263
+ for (const handler of peerConnectHandlers) {
264
+ try { handler(peerIdStr); } catch {}
265
+ }
261
266
  });
262
267
 
263
268
  state.node.addEventListener("peer:disconnect", (evt) => {
264
269
  const peerIdStr = evt.detail.toString();
265
270
  logger?.info?.(`[libp2p-mesh] Peer disconnected: ${peerIdStr}`);
271
+ for (const handler of peerDisconnectHandlers) {
272
+ try { handler(peerIdStr); } catch {}
273
+ }
266
274
  });
267
275
 
268
276
  await state.node.handle(
@@ -667,6 +675,14 @@ export function createMeshNetwork(options: {
667
675
  stop,
668
676
  sendToPeer,
669
677
  onMessage,
678
+ onPeerConnect(handler: (peerId: string) => void): () => void {
679
+ peerConnectHandlers.add(handler);
680
+ return () => { peerConnectHandlers.delete(handler); };
681
+ },
682
+ onPeerDisconnect(handler: (peerId: string) => void): () => void {
683
+ peerDisconnectHandlers.add(handler);
684
+ return () => { peerDisconnectHandlers.delete(handler); };
685
+ },
670
686
  publishToTopic,
671
687
  subscribeToTopic,
672
688
  getLocalPeerId,
@@ -3,12 +3,15 @@ export interface PeerIdentity {
3
3
  channel: string;
4
4
  accountId: string;
5
5
  sessionKey: string;
6
+ /** Human-readable Instance ID (e.g. "alice-mac@AQIDBAU.7a3f9e2b") */
7
+ instanceId?: string;
6
8
  }
7
9
 
8
10
  export interface PeerIdentityMap {
9
11
  register(peerId: string, identity: PeerIdentity): void;
10
12
  resolve(peerId: string): PeerIdentity | undefined;
11
13
  resolveSessionKey(peerId: string): string | undefined;
14
+ resolveByInstanceId(instanceId: string): { peerId: string; identity: PeerIdentity } | undefined;
12
15
  unregister(peerId: string): void;
13
16
  setLocalIdentity(peerId: string, identity: PeerIdentity): void;
14
17
  getLocalIdentity(): PeerIdentity | undefined;
@@ -18,12 +21,16 @@ export interface PeerIdentityMap {
18
21
 
19
22
  export function createPeerIdentityMap(): PeerIdentityMap {
20
23
  const peers = new Map<string, PeerIdentity>();
24
+ const byInstanceId = new Map<string, { peerId: string; identity: PeerIdentity }>();
21
25
  let localPeerId: string | undefined;
22
26
  let localIdentity: PeerIdentity | undefined;
23
27
 
24
28
  return {
25
29
  register(peerId: string, identity: PeerIdentity): void {
26
30
  peers.set(peerId, identity);
31
+ if (identity.instanceId) {
32
+ byInstanceId.set(identity.instanceId, { peerId, identity });
33
+ }
27
34
  },
28
35
 
29
36
  resolve(peerId: string): PeerIdentity | undefined {
@@ -34,7 +41,15 @@ export function createPeerIdentityMap(): PeerIdentityMap {
34
41
  return peers.get(peerId)?.sessionKey;
35
42
  },
36
43
 
44
+ resolveByInstanceId(instanceId: string): { peerId: string; identity: PeerIdentity } | undefined {
45
+ return byInstanceId.get(instanceId);
46
+ },
47
+
37
48
  unregister(peerId: string): void {
49
+ const identity = peers.get(peerId);
50
+ if (identity?.instanceId) {
51
+ byInstanceId.delete(identity.instanceId);
52
+ }
38
53
  peers.delete(peerId);
39
54
  },
40
55
 
@@ -42,6 +57,9 @@ export function createPeerIdentityMap(): PeerIdentityMap {
42
57
  localPeerId = peerId;
43
58
  localIdentity = identity;
44
59
  peers.set(peerId, identity);
60
+ if (identity.instanceId) {
61
+ byInstanceId.set(identity.instanceId, { peerId, identity });
62
+ }
45
63
  },
46
64
 
47
65
  getLocalIdentity(): PeerIdentity | undefined {
package/src/plugin.ts CHANGED
@@ -33,12 +33,16 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
33
33
  start: async () => {
34
34
  await mesh.start();
35
35
 
36
- // Register local identity so remote peers can route messages back to us
36
+ // Gather local relay identity info
37
37
  const config = api.pluginConfig as MeshConfig | undefined;
38
38
  const relayChannel = config?.relayChannel;
39
39
  const relayAccountId = config?.relayAccountId;
40
+ const instanceIdentity = mesh.getInstanceIdentity();
41
+ const localInstanceId = instanceIdentity?.id;
42
+ const localPeerId = mesh.getLocalPeerId();
43
+
44
+ // Register local identity so remote peers can route messages back to us
40
45
  if (relayChannel && relayAccountId) {
41
- const localPeerId = mesh.getLocalPeerId();
42
46
  const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
43
47
  const sessionKey = buildAgentSessionKey({
44
48
  agentId: api.name,
@@ -50,9 +54,10 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
50
54
  channel: relayChannel,
51
55
  accountId: relayAccountId,
52
56
  sessionKey,
57
+ instanceId: localInstanceId,
53
58
  });
54
59
  api.logger.info?.(
55
- `[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`,
60
+ `[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`,
56
61
  );
57
62
 
58
63
  // Announce identity to all currently-connected peers
@@ -61,15 +66,34 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
61
66
  api.name,
62
67
  relayChannel,
63
68
  relayAccountId,
69
+ localInstanceId,
64
70
  );
65
- const connectedPeers = mesh.getConnectedPeers();
66
- for (const peerId of connectedPeers) {
71
+ for (const peerId of mesh.getConnectedPeers()) {
67
72
  try {
68
73
  await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
69
74
  } catch {
70
75
  // Best-effort; peer may be stale in the connection list
71
76
  }
72
77
  }
78
+
79
+ // When a new peer connects, send our identity to them
80
+ mesh.onPeerConnect((peerId: string) => {
81
+ const msg = buildIdentityMessage(
82
+ localPeerId,
83
+ api.name,
84
+ relayChannel,
85
+ relayAccountId,
86
+ localInstanceId,
87
+ );
88
+ mesh.sendToPeer(peerId, JSON.stringify(msg)).catch(() => {
89
+ // Best-effort
90
+ });
91
+ });
92
+
93
+ // When a peer disconnects, clean up the identity map
94
+ mesh.onPeerDisconnect((peerId: string) => {
95
+ peerIdentityMap.unregister(peerId);
96
+ });
73
97
  }
74
98
 
75
99
  // Wire up relay-aware message handler
@@ -78,7 +102,7 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
78
102
  });
79
103
 
80
104
  const identity = mesh.getInstanceIdentity();
81
- api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
105
+ api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
82
106
  if (identity) {
83
107
  api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
84
108
  }
package/src/types.ts CHANGED
@@ -116,6 +116,8 @@ export interface MeshNetwork {
116
116
  stop(): Promise<void>;
117
117
  sendToPeer(peerId: string, message: string): Promise<void>;
118
118
  onMessage(handler: (msg: P2PMessage) => void): () => void;
119
+ onPeerConnect(handler: (peerId: string) => void): () => void;
120
+ onPeerDisconnect(handler: (peerId: string) => void): () => void;
119
121
  publishToTopic(topic: string, message: string): Promise<void>;
120
122
  subscribeToTopic(topic: string, handler: (msg: string) => void): Promise<void>;
121
123
  getLocalPeerId(): string;