libp2p-mesh 2026.6.3 → 2026.6.4

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.
@@ -1,6 +1,6 @@
1
1
  import { verifyInstanceSignature } from "./instance-id.js";
2
2
  export function handleP2PInbound(msg, deps) {
3
- const { logger } = deps;
3
+ const { logger, sendToChannel } = deps;
4
4
  const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
5
5
  const signedTag = msg.signature ? " [signed]" : "";
6
6
  // Verify signature if present
@@ -28,8 +28,15 @@ export function handleP2PInbound(msg, deps) {
28
28
  }
29
29
  if (msg.type === "broadcast") {
30
30
  logger?.info?.(`[libp2p-mesh] Broadcast from ${msg.from}${instanceTag}${signedTag} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`);
31
+ return;
31
32
  }
32
- else {
33
- logger?.info?.(`[libp2p-mesh] Direct message from ${msg.from}${instanceTag}${signedTag}: ${msg.payload}`);
33
+ // Direct message — log and forward to local channel
34
+ logger?.info?.(`[libp2p-mesh] Direct message from ${msg.from}${instanceTag}${signedTag}: ${msg.payload}`);
35
+ if (!sendToChannel || !msg.payload) {
36
+ return;
34
37
  }
38
+ const text = `[来自 ${msg.from}]\n${msg.payload}`;
39
+ sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
40
+ logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
41
+ });
35
42
  }
@@ -13,6 +13,67 @@ function isNonEmptyString(value) {
13
13
  function summarizeError(error) {
14
14
  return error instanceof Error ? error.message : String(error);
15
15
  }
16
+ const INVALID_INBOUND_TARGET_ERROR = "inbound target channel and target are required";
17
+ function invalidInboundTarget() {
18
+ return {
19
+ channel: "",
20
+ target: "",
21
+ valid: false,
22
+ error: INVALID_INBOUND_TARGET_ERROR,
23
+ };
24
+ }
25
+ function displayTargetId(target) {
26
+ return typeof target.id === "string" ? target.id.trim() || undefined : undefined;
27
+ }
28
+ function normalizeConfiguredTarget(target) {
29
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
30
+ return invalidInboundTarget();
31
+ }
32
+ const configuredTarget = target;
33
+ const channel = typeof configuredTarget.channel === "string" ? configuredTarget.channel.trim() : "";
34
+ const destination = typeof configuredTarget.target === "string" ? configuredTarget.target.trim() : "";
35
+ const normalized = {
36
+ id: displayTargetId(configuredTarget),
37
+ channel,
38
+ target: destination,
39
+ valid: Boolean(channel && destination),
40
+ };
41
+ if (!normalized.valid) {
42
+ normalized.error = INVALID_INBOUND_TARGET_ERROR;
43
+ }
44
+ return normalized;
45
+ }
46
+ function effectiveInboundTargets(config) {
47
+ if (Array.isArray(config.inboundTargets)) {
48
+ const seen = new Set();
49
+ const targets = [];
50
+ for (const target of config.inboundTargets) {
51
+ const normalized = normalizeConfiguredTarget(target);
52
+ const key = `${normalized.channel}\0${normalized.target}`;
53
+ if (normalized.valid && seen.has(key)) {
54
+ continue;
55
+ }
56
+ if (normalized.valid) {
57
+ seen.add(key);
58
+ }
59
+ targets.push(normalized);
60
+ }
61
+ return targets;
62
+ }
63
+ if (!config.inboundChannel || !config.inboundTarget) {
64
+ return [];
65
+ }
66
+ return [
67
+ {
68
+ channel: config.inboundChannel,
69
+ target: config.inboundTarget,
70
+ valid: true,
71
+ },
72
+ ];
73
+ }
74
+ function firstAttemptedResult(results) {
75
+ return results.find((result) => result.ok) ?? results[0];
76
+ }
16
77
  export function createInstanceRouter(options) {
17
78
  const { mesh, store, delivery } = options;
18
79
  const config = options.config ?? {};
@@ -21,7 +82,10 @@ export function createInstanceRouter(options) {
21
82
  const announcedPeers = new Set();
22
83
  const pendingAcks = new Map();
23
84
  const deliveryCache = new Map();
85
+ const inboundTimeoutControllers = new Set();
86
+ const activeAckSends = new Set();
24
87
  const unsubs = [];
88
+ let stopped = false;
25
89
  function localInstanceId() {
26
90
  const identity = mesh.getInstanceIdentity();
27
91
  if (!identity) {
@@ -95,14 +159,191 @@ export function createInstanceRouter(options) {
95
159
  }
96
160
  }
97
161
  async function sendAck(peerId, ack) {
98
- await mesh.sendStructuredMessage(peerId, {
162
+ if (stopped) {
163
+ return;
164
+ }
165
+ const send = mesh.sendStructuredMessage(peerId, {
99
166
  id: crypto.randomUUID(),
100
167
  type: "delivery-ack",
101
168
  to: peerId,
102
169
  payload: JSON.stringify(ack),
103
170
  });
171
+ activeAckSends.add(send);
172
+ try {
173
+ await send;
174
+ }
175
+ finally {
176
+ activeAckSends.delete(send);
177
+ }
178
+ }
179
+ function deliveryCacheKey(payload, msg) {
180
+ return [
181
+ payload.fromInstanceId,
182
+ msg.from,
183
+ payload.toInstanceId,
184
+ payload.messageId,
185
+ ].join("\0");
186
+ }
187
+ function buildAckFromDeliveryState(messageId, state) {
188
+ const selected = firstAttemptedResult(state.results);
189
+ return {
190
+ ackFor: messageId,
191
+ ok: state.results.some((result) => result.ok),
192
+ inboundChannel: selected?.channel,
193
+ inboundTarget: selected?.target,
194
+ deliveredAt: Date.now(),
195
+ error: state.results.every((result) => !result.ok)
196
+ ? state.results.map((result) => result.error).filter(Boolean).join("; ") ||
197
+ "inbound delivery failed"
198
+ : undefined,
199
+ results: state.results,
200
+ };
201
+ }
202
+ async function deliverAndBuildAck(payload, msg, state) {
203
+ const targets = effectiveInboundTargets(config);
204
+ if (targets.length === 0) {
205
+ return {
206
+ ackFor: payload.messageId,
207
+ ok: false,
208
+ deliveredAt: Date.now(),
209
+ error: "inbound delivery is not configured",
210
+ results: [],
211
+ };
212
+ }
213
+ const metadata = payload.metadata;
214
+ for (const target of targets) {
215
+ if (state.timedOut) {
216
+ break;
217
+ }
218
+ if (!target.valid) {
219
+ state.results.push({
220
+ id: target.id,
221
+ channel: target.channel,
222
+ target: target.target,
223
+ ok: false,
224
+ error: target.error ?? "inbound target channel and target are required",
225
+ });
226
+ continue;
227
+ }
228
+ state.currentTarget = target;
229
+ try {
230
+ const result = await delivery.deliver({
231
+ channel: target.channel,
232
+ target: target.target,
233
+ text: payload.text,
234
+ metadata: {
235
+ fromInstanceId: payload.fromInstanceId,
236
+ fromPeerId: msg.from,
237
+ p2pMessageId: payload.messageId,
238
+ allowAgentAutoReply: metadata?.allowAgentAutoReply === true,
239
+ replyToInstanceId: payload.fromInstanceId,
240
+ replyTool: "p2p_send_instance_message",
241
+ },
242
+ });
243
+ if (state.timedOut) {
244
+ break;
245
+ }
246
+ state.results.push({
247
+ id: target.id,
248
+ channel: result.channel,
249
+ target: result.target,
250
+ ok: result.ok,
251
+ error: result.error,
252
+ });
253
+ }
254
+ catch (error) {
255
+ if (state.timedOut) {
256
+ break;
257
+ }
258
+ state.results.push({
259
+ id: target.id,
260
+ channel: target.channel,
261
+ target: target.target,
262
+ ok: false,
263
+ error: summarizeError(error),
264
+ });
265
+ }
266
+ finally {
267
+ if (state.currentTarget === target) {
268
+ state.currentTarget = undefined;
269
+ }
270
+ }
271
+ }
272
+ return buildAckFromDeliveryState(payload.messageId, state);
273
+ }
274
+ function buildInboundTimeoutAck(messageId, state) {
275
+ const error = `inbound delivery timeout after ${ackTimeoutMs}ms`;
276
+ const results = state.results.slice();
277
+ const currentTarget = state.currentTarget;
278
+ if (currentTarget) {
279
+ results.push({
280
+ id: currentTarget.id,
281
+ channel: currentTarget.channel,
282
+ target: currentTarget.target,
283
+ ok: false,
284
+ error: currentTarget.valid
285
+ ? error
286
+ : currentTarget.error ?? "inbound target channel and target are required",
287
+ });
288
+ }
289
+ const selected = firstAttemptedResult(results);
290
+ return {
291
+ ackFor: messageId,
292
+ ok: results.some((result) => result.ok),
293
+ inboundChannel: selected?.channel,
294
+ inboundTarget: selected?.target,
295
+ deliveredAt: Date.now(),
296
+ error,
297
+ results,
298
+ };
299
+ }
300
+ function withInboundDeliveryTimeout(promise, messageId, state) {
301
+ return new Promise((resolve) => {
302
+ let settled = false;
303
+ const settle = (ack) => {
304
+ if (settled)
305
+ return;
306
+ settled = true;
307
+ clearTimeout(timer);
308
+ inboundTimeoutControllers.delete(controller);
309
+ resolve(ack);
310
+ };
311
+ const timer = setTimeout(() => {
312
+ state.timedOut = true;
313
+ settle(buildInboundTimeoutAck(messageId, state));
314
+ }, ackTimeoutMs);
315
+ const controller = {
316
+ stop() {
317
+ state.timedOut = true;
318
+ settle({
319
+ ackFor: messageId,
320
+ ok: false,
321
+ deliveredAt: Date.now(),
322
+ error: "instance router stopped",
323
+ results: state.results,
324
+ });
325
+ },
326
+ };
327
+ inboundTimeoutControllers.add(controller);
328
+ promise
329
+ .then((ack) => {
330
+ settle(ack);
331
+ })
332
+ .catch((error) => {
333
+ settle({
334
+ ackFor: messageId,
335
+ ok: false,
336
+ deliveredAt: Date.now(),
337
+ error: summarizeError(error),
338
+ results: [],
339
+ });
340
+ });
341
+ });
104
342
  }
105
343
  async function handleUserMessage(msg) {
344
+ if (stopped) {
345
+ return;
346
+ }
106
347
  const payload = parsePayload(msg);
107
348
  if (!payload ||
108
349
  !isNonEmptyString(payload.messageId) ||
@@ -117,6 +358,9 @@ export function createInstanceRouter(options) {
117
358
  return;
118
359
  }
119
360
  const senderRoute = await store.resolve(payload.fromInstanceId);
361
+ if (stopped) {
362
+ return;
363
+ }
120
364
  if (!senderRoute || senderRoute.peerId !== msg.from) {
121
365
  logger?.warn?.(`[libp2p-mesh] Ignoring user-message from ${msg.from}; instance ${payload.fromInstanceId} is not routed to that peer`);
122
366
  return;
@@ -126,56 +370,78 @@ export function createInstanceRouter(options) {
126
370
  logger?.warn?.(`[libp2p-mesh] Ignoring user-message for ${payload.toInstanceId}; local instance is ${localId}`);
127
371
  return;
128
372
  }
129
- const cached = deliveryCache.get(payload.messageId);
373
+ const cacheKey = deliveryCacheKey(payload, msg);
374
+ const cached = deliveryCache.get(cacheKey);
130
375
  if (cached) {
131
- await sendAck(cached.peerId, cached.payload);
376
+ const ack = cached.payload ?? (await cached.promise);
377
+ if (!ack) {
378
+ throw new Error(`delivery cache entry for ${payload.messageId} has no ACK payload`);
379
+ }
380
+ if (stopped) {
381
+ return;
382
+ }
383
+ await sendAck(msg.from, ack);
132
384
  return;
133
385
  }
134
- let ack;
135
- if (!config.inboundChannel || !config.inboundTarget) {
136
- ack = {
386
+ if (pendingDeliveryCount() >= MAX_DELIVERY_CACHE_ENTRIES) {
387
+ const ack = {
137
388
  ackFor: payload.messageId,
138
389
  ok: false,
139
- inboundChannel: config.inboundChannel,
140
- inboundTarget: config.inboundTarget,
141
390
  deliveredAt: Date.now(),
142
- error: "inbound delivery is not configured",
391
+ error: `too many pending inbound deliveries (${MAX_DELIVERY_CACHE_ENTRIES})`,
392
+ results: [],
143
393
  };
394
+ deliveryCache.set(cacheKey, { payload: ack });
395
+ trimDeliveryCache(cacheKey);
396
+ if (stopped) {
397
+ return;
398
+ }
399
+ await sendAck(msg.from, ack);
400
+ return;
144
401
  }
145
- else {
146
- const metadata = payload.metadata;
147
- const result = await delivery.deliver({
148
- channel: config.inboundChannel,
149
- target: config.inboundTarget,
150
- text: payload.text,
151
- metadata: {
152
- fromInstanceId: payload.fromInstanceId,
153
- fromPeerId: msg.from,
154
- p2pMessageId: payload.messageId,
155
- allowAgentAutoReply: metadata?.allowAgentAutoReply === true,
156
- replyToInstanceId: payload.fromInstanceId,
157
- replyTool: "p2p_send_instance_message",
158
- },
159
- });
160
- ack = {
161
- ackFor: payload.messageId,
162
- ok: result.ok,
163
- inboundChannel: result.channel,
164
- inboundTarget: result.target,
165
- deliveredAt: Date.now(),
166
- error: result.error,
167
- };
402
+ const deliveryState = {
403
+ timedOut: false,
404
+ results: [],
405
+ };
406
+ const deliveryPromise = Promise.resolve()
407
+ .then(() => deliverAndBuildAck(payload, msg, deliveryState))
408
+ .finally(() => {
409
+ const entry = deliveryCache.get(cacheKey);
410
+ if (entry) {
411
+ entry.inFlight = false;
412
+ }
413
+ });
414
+ const ackPromise = withInboundDeliveryTimeout(deliveryPromise, payload.messageId, deliveryState);
415
+ const cacheEntry = { inFlight: true, promise: ackPromise };
416
+ deliveryCache.set(cacheKey, cacheEntry);
417
+ trimDeliveryCache();
418
+ const ack = await ackPromise;
419
+ if (stopped) {
420
+ return;
168
421
  }
169
- deliveryCache.set(payload.messageId, { peerId: msg.from, payload: ack });
422
+ cacheEntry.payload = ack;
170
423
  trimDeliveryCache();
424
+ if (stopped) {
425
+ return;
426
+ }
171
427
  await sendAck(msg.from, ack);
172
428
  }
173
- function trimDeliveryCache() {
429
+ function pendingDeliveryCount() {
430
+ let count = 0;
431
+ for (const entry of deliveryCache.values()) {
432
+ if (entry.inFlight)
433
+ count += 1;
434
+ }
435
+ return count;
436
+ }
437
+ function trimDeliveryCache(protectedKey) {
174
438
  while (deliveryCache.size > MAX_DELIVERY_CACHE_ENTRIES) {
175
- const oldestKey = deliveryCache.keys().next().value;
176
- if (!oldestKey)
439
+ const settledKeys = Array.from(deliveryCache)
440
+ .filter(([key, entry]) => key !== protectedKey && entry.payload && !entry.inFlight)
441
+ .map(([key]) => key);
442
+ if (settledKeys.length === 0)
177
443
  return;
178
- deliveryCache.delete(oldestKey);
444
+ deliveryCache.delete(settledKeys[0]);
179
445
  }
180
446
  }
181
447
  function handleAck(msg) {
@@ -211,6 +477,7 @@ export function createInstanceRouter(options) {
211
477
  }
212
478
  }
213
479
  async function start() {
480
+ stopped = false;
214
481
  unsubs.push(mesh.onMessage((msg) => {
215
482
  handleMessage(msg).catch((error) => {
216
483
  logger?.error?.(`[libp2p-mesh] Instance router message error: ${summarizeError(error)}`);
@@ -224,9 +491,14 @@ export function createInstanceRouter(options) {
224
491
  await announceToConnectedPeers();
225
492
  }
226
493
  async function stop() {
494
+ stopped = true;
227
495
  for (const unsub of unsubs.splice(0)) {
228
496
  unsub();
229
497
  }
498
+ for (const controller of Array.from(inboundTimeoutControllers)) {
499
+ controller.stop();
500
+ }
501
+ inboundTimeoutControllers.clear();
230
502
  for (const [messageId, pending] of pendingAcks) {
231
503
  clearTimeout(pending.timer);
232
504
  pending.resolve({
@@ -238,6 +510,8 @@ export function createInstanceRouter(options) {
238
510
  }
239
511
  pendingAcks.clear();
240
512
  deliveryCache.clear();
513
+ await Promise.allSettled([...activeAckSends]);
514
+ activeAckSends.clear();
241
515
  }
242
516
  async function listInstances() {
243
517
  return store.list();
@@ -311,6 +585,8 @@ export function createInstanceRouter(options) {
311
585
  toPeerId: route.peerId,
312
586
  ackMessageId: ack.ackFor,
313
587
  inboundChannel: ack.inboundChannel,
588
+ inboundTarget: ack.inboundTarget,
589
+ deliveryResults: ack.results,
314
590
  error: ack.error,
315
591
  };
316
592
  }
@@ -26,6 +26,7 @@ export function registerLibp2pMesh(api) {
26
26
  config,
27
27
  logger: api.logger,
28
28
  });
29
+ const channel = createLibp2pMeshChannel(mesh);
29
30
  // 1. Register Service (manages libp2p node lifecycle)
30
31
  api.registerService({
31
32
  id: "libp2p-mesh",
@@ -37,7 +38,32 @@ export function registerLibp2pMesh(api) {
37
38
  await mesh.start();
38
39
  await router.start();
39
40
  unsubscribeInbound = mesh.onMessage((msg) => {
40
- if (msg.type === "direct" || msg.type === "broadcast" || msg.type === "agent-sync") {
41
+ if (msg.type === "direct" || msg.type === "broadcast") {
42
+ const sendToChannel = async (_channelId, _target, text) => {
43
+ if (!config?.inboundChannel || !config?.inboundTarget) {
44
+ api.logger.warn?.("[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.");
45
+ return;
46
+ }
47
+ const result = await delivery.deliver({
48
+ channel: config.inboundChannel,
49
+ target: config.inboundTarget,
50
+ text,
51
+ metadata: {
52
+ fromInstanceId: msg.instanceId ?? msg.from,
53
+ fromPeerId: msg.from,
54
+ p2pMessageId: msg.id,
55
+ allowAgentAutoReply: false,
56
+ replyToInstanceId: msg.instanceId ?? msg.from,
57
+ replyTool: "p2p_send_instance_message",
58
+ },
59
+ });
60
+ if (!result.ok) {
61
+ api.logger.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`);
62
+ }
63
+ };
64
+ handleP2PInbound(msg, { logger: api.logger, sendToChannel });
65
+ }
66
+ else if (msg.type === "agent-sync") {
41
67
  handleP2PInbound(msg, { logger: api.logger });
42
68
  }
43
69
  });
@@ -69,7 +95,7 @@ export function registerLibp2pMesh(api) {
69
95
  });
70
96
  // 2. Register Channel (lightweight debugging surface)
71
97
  api.registerChannel({
72
- plugin: createLibp2pMeshChannel(mesh),
98
+ plugin: channel,
73
99
  });
74
100
  // 3. Register Agent Tools
75
101
  const tools = buildP2PTools(mesh, router);
@@ -51,6 +51,18 @@ export interface UserMessagePayload {
51
51
  replyTool: "p2p_send_instance_message";
52
52
  };
53
53
  }
54
+ export interface InboundTargetConfig {
55
+ id?: string;
56
+ channel: string;
57
+ target: string;
58
+ }
59
+ export interface DeliveryTargetResult {
60
+ id?: string;
61
+ channel: string;
62
+ target: string;
63
+ ok: boolean;
64
+ error?: string;
65
+ }
54
66
  export interface DeliveryAckPayload {
55
67
  ackFor: string;
56
68
  ok: boolean;
@@ -58,6 +70,7 @@ export interface DeliveryAckPayload {
58
70
  inboundTarget?: string;
59
71
  deliveredAt: number;
60
72
  error?: string;
73
+ results?: DeliveryTargetResult[];
61
74
  }
62
75
  export interface InstancePeerRecord {
63
76
  instanceId: string;
@@ -120,6 +133,8 @@ export interface InstanceRouter {
120
133
  toPeerId: string;
121
134
  ackMessageId?: string;
122
135
  inboundChannel?: string;
136
+ inboundTarget?: string;
137
+ deliveryResults?: DeliveryTargetResult[];
123
138
  error?: string;
124
139
  }>;
125
140
  }
@@ -165,7 +180,8 @@ export interface MeshConfig {
165
180
  /**
166
181
  * Deprecated pre-2026.6 config keys kept so existing OpenClaw configs keep
167
182
  * validating after upgrade. Relay selection is now configured with
168
- * `relayList`; inbound display uses `inboundChannel`/`inboundTarget`.
183
+ * `relayList`; inbound display uses `inboundChannel`/`inboundTarget`, or
184
+ * `inboundTargets` when multi-target inbound delivery is enabled.
169
185
  */
170
186
  relayChannel?: string;
171
187
  relayAccountId?: string;
@@ -182,6 +198,7 @@ export interface MeshConfig {
182
198
  announceAddrs?: string[];
183
199
  inboundChannel?: string;
184
200
  inboundTarget?: string;
201
+ inboundTargets?: InboundTargetConfig[];
185
202
  deliveryAckTimeoutMs?: number;
186
203
  }
187
204
  export interface NATTraversalStatus {
@@ -0,0 +1,22 @@
1
+ export interface PromptChoice {
2
+ label: string;
3
+ value: string;
4
+ hint?: string;
5
+ }
6
+ export interface WizardPrompter {
7
+ question(prompt: string, defaultValue?: string): Promise<string>;
8
+ confirm(prompt: string, defaultValue?: boolean): Promise<boolean>;
9
+ select(prompt: string, choices: PromptChoice[]): Promise<string>;
10
+ multiline(prompt: string, helpText?: string): Promise<string[]>;
11
+ displayBox(title: string, lines: string[]): void;
12
+ displaySuccess(message: string): void;
13
+ displayError(message: string): void;
14
+ displayWarning(message: string): void;
15
+ close(): void;
16
+ }
17
+ export declare function validateMultiaddr(raw: string): string | null;
18
+ export declare function createReadlinePrompter(): WizardPrompter;
19
+ export declare function runSetupWizard(prompter: WizardPrompter, currentConfig: Record<string, unknown>, availableChannels: string[]): Promise<Record<string, unknown>>;
20
+ export declare class WizardCancelledError extends Error {
21
+ constructor();
22
+ }