libp2p-mesh 2026.6.2 → 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.
- package/README.md +81 -19
- package/api.ts +2 -0
- package/dist/api.d.ts +1 -1
- package/dist/index.js +31 -1
- package/dist/src/agent-tools.d.ts +6 -2
- package/dist/src/agent-tools.js +29 -0
- package/dist/src/cli.d.ts +15 -0
- package/dist/src/cli.js +196 -0
- package/dist/src/config-io.d.ts +9 -0
- package/dist/src/config-io.js +173 -0
- package/dist/src/inbound-delivery.d.ts +5 -3
- package/dist/src/inbound-delivery.js +35 -77
- package/dist/src/inbound.d.ts +1 -0
- package/dist/src/inbound.js +10 -3
- package/dist/src/instance-router.js +313 -37
- package/dist/src/plugin.js +34 -4
- package/dist/src/types.d.ts +18 -1
- package/dist/src/wizard.d.ts +22 -0
- package/dist/src/wizard.js +276 -0
- package/index.ts +31 -1
- package/openclaw.plugin.json +71 -1
- package/package.json +6 -5
- package/src/agent-tools.ts +36 -1
- package/src/cli.ts +226 -0
- package/src/config-io.ts +204 -0
- package/src/inbound-delivery.ts +47 -91
- package/src/inbound.ts +17 -5
- package/src/instance-router.ts +370 -39
- package/src/plugin.ts +40 -5
- package/src/types.ts +20 -1
- package/src/wizard.ts +332 -0
package/src/instance-router.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DeliveryAckPayload,
|
|
3
|
+
DeliveryTargetResult,
|
|
3
4
|
InboundDeliveryAdapter,
|
|
5
|
+
InboundTargetConfig,
|
|
4
6
|
InstanceAnnouncePayload,
|
|
5
7
|
InstancePeerStore,
|
|
6
8
|
InstanceRouter,
|
|
@@ -24,8 +26,19 @@ type PendingAck = {
|
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
type DeliveryCacheEntry = {
|
|
27
|
-
|
|
28
|
-
payload
|
|
29
|
+
inFlight?: boolean;
|
|
30
|
+
payload?: DeliveryAckPayload;
|
|
31
|
+
promise?: Promise<DeliveryAckPayload>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type InboundDeliveryState = {
|
|
35
|
+
timedOut: boolean;
|
|
36
|
+
currentTarget?: EffectiveInboundTarget;
|
|
37
|
+
results: DeliveryTargetResult[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type InboundTimeoutController = {
|
|
41
|
+
stop: () => void;
|
|
29
42
|
};
|
|
30
43
|
|
|
31
44
|
const MAX_DELIVERY_CACHE_ENTRIES = 1000;
|
|
@@ -46,6 +59,85 @@ function summarizeError(error: unknown): string {
|
|
|
46
59
|
return error instanceof Error ? error.message : String(error);
|
|
47
60
|
}
|
|
48
61
|
|
|
62
|
+
type EffectiveInboundTarget = {
|
|
63
|
+
id?: string;
|
|
64
|
+
channel: string;
|
|
65
|
+
target: string;
|
|
66
|
+
valid: boolean;
|
|
67
|
+
error?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const INVALID_INBOUND_TARGET_ERROR = "inbound target channel and target are required";
|
|
71
|
+
|
|
72
|
+
function invalidInboundTarget(): EffectiveInboundTarget {
|
|
73
|
+
return {
|
|
74
|
+
channel: "",
|
|
75
|
+
target: "",
|
|
76
|
+
valid: false,
|
|
77
|
+
error: INVALID_INBOUND_TARGET_ERROR,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function displayTargetId(target: { id?: unknown }): string | undefined {
|
|
82
|
+
return typeof target.id === "string" ? target.id.trim() || undefined : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeConfiguredTarget(target: unknown): EffectiveInboundTarget {
|
|
86
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
|
87
|
+
return invalidInboundTarget();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const configuredTarget = target as Partial<InboundTargetConfig>;
|
|
91
|
+
const channel =
|
|
92
|
+
typeof configuredTarget.channel === "string" ? configuredTarget.channel.trim() : "";
|
|
93
|
+
const destination =
|
|
94
|
+
typeof configuredTarget.target === "string" ? configuredTarget.target.trim() : "";
|
|
95
|
+
const normalized: EffectiveInboundTarget = {
|
|
96
|
+
id: displayTargetId(configuredTarget),
|
|
97
|
+
channel,
|
|
98
|
+
target: destination,
|
|
99
|
+
valid: Boolean(channel && destination),
|
|
100
|
+
};
|
|
101
|
+
if (!normalized.valid) {
|
|
102
|
+
normalized.error = INVALID_INBOUND_TARGET_ERROR;
|
|
103
|
+
}
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function effectiveInboundTargets(config: MeshConfig): EffectiveInboundTarget[] {
|
|
108
|
+
if (Array.isArray(config.inboundTargets)) {
|
|
109
|
+
const seen = new Set<string>();
|
|
110
|
+
const targets: EffectiveInboundTarget[] = [];
|
|
111
|
+
for (const target of config.inboundTargets) {
|
|
112
|
+
const normalized = normalizeConfiguredTarget(target);
|
|
113
|
+
const key = `${normalized.channel}\0${normalized.target}`;
|
|
114
|
+
if (normalized.valid && seen.has(key)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (normalized.valid) {
|
|
118
|
+
seen.add(key);
|
|
119
|
+
}
|
|
120
|
+
targets.push(normalized);
|
|
121
|
+
}
|
|
122
|
+
return targets;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!config.inboundChannel || !config.inboundTarget) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
channel: config.inboundChannel,
|
|
131
|
+
target: config.inboundTarget,
|
|
132
|
+
valid: true,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function firstAttemptedResult(results: DeliveryTargetResult[]): DeliveryTargetResult | undefined {
|
|
138
|
+
return results.find((result) => result.ok) ?? results[0];
|
|
139
|
+
}
|
|
140
|
+
|
|
49
141
|
export function createInstanceRouter(options: {
|
|
50
142
|
mesh: MeshNetwork;
|
|
51
143
|
store: InstancePeerStore;
|
|
@@ -60,7 +152,10 @@ export function createInstanceRouter(options: {
|
|
|
60
152
|
const announcedPeers = new Set<string>();
|
|
61
153
|
const pendingAcks = new Map<string, PendingAck>();
|
|
62
154
|
const deliveryCache = new Map<string, DeliveryCacheEntry>();
|
|
155
|
+
const inboundTimeoutControllers = new Set<InboundTimeoutController>();
|
|
156
|
+
const activeAckSends = new Set<Promise<void>>();
|
|
63
157
|
const unsubs: Array<() => void> = [];
|
|
158
|
+
let stopped = false;
|
|
64
159
|
|
|
65
160
|
function localInstanceId(): string {
|
|
66
161
|
const identity = mesh.getInstanceIdentity();
|
|
@@ -156,15 +251,213 @@ export function createInstanceRouter(options: {
|
|
|
156
251
|
}
|
|
157
252
|
|
|
158
253
|
async function sendAck(peerId: string, ack: DeliveryAckPayload): Promise<void> {
|
|
159
|
-
|
|
254
|
+
if (stopped) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const send = mesh.sendStructuredMessage(peerId, {
|
|
160
259
|
id: crypto.randomUUID(),
|
|
161
260
|
type: "delivery-ack",
|
|
162
261
|
to: peerId,
|
|
163
262
|
payload: JSON.stringify(ack),
|
|
164
263
|
});
|
|
264
|
+
activeAckSends.add(send);
|
|
265
|
+
try {
|
|
266
|
+
await send;
|
|
267
|
+
} finally {
|
|
268
|
+
activeAckSends.delete(send);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function deliveryCacheKey(payload: UserMessagePayload, msg: P2PMessage): string {
|
|
273
|
+
return [
|
|
274
|
+
payload.fromInstanceId,
|
|
275
|
+
msg.from,
|
|
276
|
+
payload.toInstanceId,
|
|
277
|
+
payload.messageId,
|
|
278
|
+
].join("\0");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildAckFromDeliveryState(
|
|
282
|
+
messageId: string,
|
|
283
|
+
state: Pick<InboundDeliveryState, "results">,
|
|
284
|
+
): DeliveryAckPayload {
|
|
285
|
+
const selected = firstAttemptedResult(state.results);
|
|
286
|
+
return {
|
|
287
|
+
ackFor: messageId,
|
|
288
|
+
ok: state.results.some((result) => result.ok),
|
|
289
|
+
inboundChannel: selected?.channel,
|
|
290
|
+
inboundTarget: selected?.target,
|
|
291
|
+
deliveredAt: Date.now(),
|
|
292
|
+
error: state.results.every((result) => !result.ok)
|
|
293
|
+
? state.results.map((result) => result.error).filter(Boolean).join("; ") ||
|
|
294
|
+
"inbound delivery failed"
|
|
295
|
+
: undefined,
|
|
296
|
+
results: state.results,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function deliverAndBuildAck(
|
|
301
|
+
payload: UserMessagePayload,
|
|
302
|
+
msg: P2PMessage,
|
|
303
|
+
state: InboundDeliveryState,
|
|
304
|
+
): Promise<DeliveryAckPayload> {
|
|
305
|
+
const targets = effectiveInboundTargets(config);
|
|
306
|
+
if (targets.length === 0) {
|
|
307
|
+
return {
|
|
308
|
+
ackFor: payload.messageId,
|
|
309
|
+
ok: false,
|
|
310
|
+
deliveredAt: Date.now(),
|
|
311
|
+
error: "inbound delivery is not configured",
|
|
312
|
+
results: [],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const metadata = payload.metadata;
|
|
317
|
+
for (const target of targets) {
|
|
318
|
+
if (state.timedOut) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!target.valid) {
|
|
323
|
+
state.results.push({
|
|
324
|
+
id: target.id,
|
|
325
|
+
channel: target.channel,
|
|
326
|
+
target: target.target,
|
|
327
|
+
ok: false,
|
|
328
|
+
error: target.error ?? "inbound target channel and target are required",
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
state.currentTarget = target;
|
|
334
|
+
try {
|
|
335
|
+
const result = await delivery.deliver({
|
|
336
|
+
channel: target.channel,
|
|
337
|
+
target: target.target,
|
|
338
|
+
text: payload.text,
|
|
339
|
+
metadata: {
|
|
340
|
+
fromInstanceId: payload.fromInstanceId,
|
|
341
|
+
fromPeerId: msg.from,
|
|
342
|
+
p2pMessageId: payload.messageId,
|
|
343
|
+
allowAgentAutoReply: metadata?.allowAgentAutoReply === true,
|
|
344
|
+
replyToInstanceId: payload.fromInstanceId,
|
|
345
|
+
replyTool: "p2p_send_instance_message",
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
if (state.timedOut) {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
state.results.push({
|
|
352
|
+
id: target.id,
|
|
353
|
+
channel: result.channel,
|
|
354
|
+
target: result.target,
|
|
355
|
+
ok: result.ok,
|
|
356
|
+
error: result.error,
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (state.timedOut) {
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
state.results.push({
|
|
363
|
+
id: target.id,
|
|
364
|
+
channel: target.channel,
|
|
365
|
+
target: target.target,
|
|
366
|
+
ok: false,
|
|
367
|
+
error: summarizeError(error),
|
|
368
|
+
});
|
|
369
|
+
} finally {
|
|
370
|
+
if (state.currentTarget === target) {
|
|
371
|
+
state.currentTarget = undefined;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return buildAckFromDeliveryState(payload.messageId, state);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function buildInboundTimeoutAck(
|
|
380
|
+
messageId: string,
|
|
381
|
+
state: InboundDeliveryState,
|
|
382
|
+
): DeliveryAckPayload {
|
|
383
|
+
const error = `inbound delivery timeout after ${ackTimeoutMs}ms`;
|
|
384
|
+
const results = state.results.slice();
|
|
385
|
+
const currentTarget = state.currentTarget;
|
|
386
|
+
if (currentTarget) {
|
|
387
|
+
results.push({
|
|
388
|
+
id: currentTarget.id,
|
|
389
|
+
channel: currentTarget.channel,
|
|
390
|
+
target: currentTarget.target,
|
|
391
|
+
ok: false,
|
|
392
|
+
error: currentTarget.valid
|
|
393
|
+
? error
|
|
394
|
+
: currentTarget.error ?? "inbound target channel and target are required",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const selected = firstAttemptedResult(results);
|
|
398
|
+
return {
|
|
399
|
+
ackFor: messageId,
|
|
400
|
+
ok: results.some((result) => result.ok),
|
|
401
|
+
inboundChannel: selected?.channel,
|
|
402
|
+
inboundTarget: selected?.target,
|
|
403
|
+
deliveredAt: Date.now(),
|
|
404
|
+
error,
|
|
405
|
+
results,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function withInboundDeliveryTimeout(
|
|
410
|
+
promise: Promise<DeliveryAckPayload>,
|
|
411
|
+
messageId: string,
|
|
412
|
+
state: InboundDeliveryState,
|
|
413
|
+
): Promise<DeliveryAckPayload> {
|
|
414
|
+
return new Promise((resolve) => {
|
|
415
|
+
let settled = false;
|
|
416
|
+
const settle = (ack: DeliveryAckPayload): void => {
|
|
417
|
+
if (settled) return;
|
|
418
|
+
settled = true;
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
inboundTimeoutControllers.delete(controller);
|
|
421
|
+
resolve(ack);
|
|
422
|
+
};
|
|
423
|
+
const timer = setTimeout(() => {
|
|
424
|
+
state.timedOut = true;
|
|
425
|
+
settle(buildInboundTimeoutAck(messageId, state));
|
|
426
|
+
}, ackTimeoutMs);
|
|
427
|
+
const controller: InboundTimeoutController = {
|
|
428
|
+
stop() {
|
|
429
|
+
state.timedOut = true;
|
|
430
|
+
settle({
|
|
431
|
+
ackFor: messageId,
|
|
432
|
+
ok: false,
|
|
433
|
+
deliveredAt: Date.now(),
|
|
434
|
+
error: "instance router stopped",
|
|
435
|
+
results: state.results,
|
|
436
|
+
});
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
inboundTimeoutControllers.add(controller);
|
|
440
|
+
|
|
441
|
+
promise
|
|
442
|
+
.then((ack) => {
|
|
443
|
+
settle(ack);
|
|
444
|
+
})
|
|
445
|
+
.catch((error) => {
|
|
446
|
+
settle({
|
|
447
|
+
ackFor: messageId,
|
|
448
|
+
ok: false,
|
|
449
|
+
deliveredAt: Date.now(),
|
|
450
|
+
error: summarizeError(error),
|
|
451
|
+
results: [],
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
165
455
|
}
|
|
166
456
|
|
|
167
457
|
async function handleUserMessage(msg: P2PMessage): Promise<void> {
|
|
458
|
+
if (stopped) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
168
461
|
const payload = parsePayload<UserMessagePayload>(msg);
|
|
169
462
|
if (
|
|
170
463
|
!payload ||
|
|
@@ -183,6 +476,9 @@ export function createInstanceRouter(options: {
|
|
|
183
476
|
return;
|
|
184
477
|
}
|
|
185
478
|
const senderRoute = await store.resolve(payload.fromInstanceId);
|
|
479
|
+
if (stopped) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
186
482
|
if (!senderRoute || senderRoute.peerId !== msg.from) {
|
|
187
483
|
logger?.warn?.(
|
|
188
484
|
`[libp2p-mesh] Ignoring user-message from ${msg.from}; instance ${payload.fromInstanceId} is not routed to that peer`,
|
|
@@ -198,57 +494,80 @@ export function createInstanceRouter(options: {
|
|
|
198
494
|
return;
|
|
199
495
|
}
|
|
200
496
|
|
|
201
|
-
const
|
|
497
|
+
const cacheKey = deliveryCacheKey(payload, msg);
|
|
498
|
+
const cached = deliveryCache.get(cacheKey);
|
|
202
499
|
if (cached) {
|
|
203
|
-
|
|
500
|
+
const ack = cached.payload ?? (await cached.promise);
|
|
501
|
+
if (!ack) {
|
|
502
|
+
throw new Error(`delivery cache entry for ${payload.messageId} has no ACK payload`);
|
|
503
|
+
}
|
|
504
|
+
if (stopped) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
await sendAck(msg.from, ack);
|
|
204
508
|
return;
|
|
205
509
|
}
|
|
206
510
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
ack = {
|
|
511
|
+
if (pendingDeliveryCount() >= MAX_DELIVERY_CACHE_ENTRIES) {
|
|
512
|
+
const ack: DeliveryAckPayload = {
|
|
210
513
|
ackFor: payload.messageId,
|
|
211
514
|
ok: false,
|
|
212
|
-
inboundChannel: config.inboundChannel,
|
|
213
|
-
inboundTarget: config.inboundTarget,
|
|
214
|
-
deliveredAt: Date.now(),
|
|
215
|
-
error: "inbound delivery is not configured",
|
|
216
|
-
};
|
|
217
|
-
} else {
|
|
218
|
-
const metadata = payload.metadata;
|
|
219
|
-
const result = await delivery.deliver({
|
|
220
|
-
channel: config.inboundChannel,
|
|
221
|
-
target: config.inboundTarget,
|
|
222
|
-
text: payload.text,
|
|
223
|
-
metadata: {
|
|
224
|
-
fromInstanceId: payload.fromInstanceId,
|
|
225
|
-
fromPeerId: msg.from,
|
|
226
|
-
p2pMessageId: payload.messageId,
|
|
227
|
-
allowAgentAutoReply: metadata?.allowAgentAutoReply === true,
|
|
228
|
-
replyToInstanceId: payload.fromInstanceId,
|
|
229
|
-
replyTool: "p2p_send_instance_message",
|
|
230
|
-
},
|
|
231
|
-
});
|
|
232
|
-
ack = {
|
|
233
|
-
ackFor: payload.messageId,
|
|
234
|
-
ok: result.ok,
|
|
235
|
-
inboundChannel: result.channel,
|
|
236
|
-
inboundTarget: result.target,
|
|
237
515
|
deliveredAt: Date.now(),
|
|
238
|
-
error:
|
|
516
|
+
error: `too many pending inbound deliveries (${MAX_DELIVERY_CACHE_ENTRIES})`,
|
|
517
|
+
results: [],
|
|
239
518
|
};
|
|
519
|
+
deliveryCache.set(cacheKey, { payload: ack });
|
|
520
|
+
trimDeliveryCache(cacheKey);
|
|
521
|
+
if (stopped) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
await sendAck(msg.from, ack);
|
|
525
|
+
return;
|
|
240
526
|
}
|
|
241
527
|
|
|
242
|
-
|
|
528
|
+
const deliveryState: InboundDeliveryState = {
|
|
529
|
+
timedOut: false,
|
|
530
|
+
results: [],
|
|
531
|
+
};
|
|
532
|
+
const deliveryPromise = Promise.resolve()
|
|
533
|
+
.then(() => deliverAndBuildAck(payload, msg, deliveryState))
|
|
534
|
+
.finally(() => {
|
|
535
|
+
const entry = deliveryCache.get(cacheKey);
|
|
536
|
+
if (entry) {
|
|
537
|
+
entry.inFlight = false;
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
const ackPromise = withInboundDeliveryTimeout(deliveryPromise, payload.messageId, deliveryState);
|
|
541
|
+
const cacheEntry: DeliveryCacheEntry = { inFlight: true, promise: ackPromise };
|
|
542
|
+
deliveryCache.set(cacheKey, cacheEntry);
|
|
243
543
|
trimDeliveryCache();
|
|
544
|
+
const ack = await ackPromise;
|
|
545
|
+
if (stopped) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
cacheEntry.payload = ack;
|
|
549
|
+
trimDeliveryCache();
|
|
550
|
+
if (stopped) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
244
553
|
await sendAck(msg.from, ack);
|
|
245
554
|
}
|
|
246
555
|
|
|
247
|
-
function
|
|
556
|
+
function pendingDeliveryCount(): number {
|
|
557
|
+
let count = 0;
|
|
558
|
+
for (const entry of deliveryCache.values()) {
|
|
559
|
+
if (entry.inFlight) count += 1;
|
|
560
|
+
}
|
|
561
|
+
return count;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function trimDeliveryCache(protectedKey?: string): void {
|
|
248
565
|
while (deliveryCache.size > MAX_DELIVERY_CACHE_ENTRIES) {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
566
|
+
const settledKeys = Array.from(deliveryCache)
|
|
567
|
+
.filter(([key, entry]) => key !== protectedKey && entry.payload && !entry.inFlight)
|
|
568
|
+
.map(([key]) => key);
|
|
569
|
+
if (settledKeys.length === 0) return;
|
|
570
|
+
deliveryCache.delete(settledKeys[0]!);
|
|
252
571
|
}
|
|
253
572
|
}
|
|
254
573
|
|
|
@@ -293,6 +612,7 @@ export function createInstanceRouter(options: {
|
|
|
293
612
|
}
|
|
294
613
|
|
|
295
614
|
async function start(): Promise<void> {
|
|
615
|
+
stopped = false;
|
|
296
616
|
unsubs.push(
|
|
297
617
|
mesh.onMessage((msg) => {
|
|
298
618
|
handleMessage(msg).catch((error) => {
|
|
@@ -316,10 +636,16 @@ export function createInstanceRouter(options: {
|
|
|
316
636
|
}
|
|
317
637
|
|
|
318
638
|
async function stop(): Promise<void> {
|
|
639
|
+
stopped = true;
|
|
319
640
|
for (const unsub of unsubs.splice(0)) {
|
|
320
641
|
unsub();
|
|
321
642
|
}
|
|
322
643
|
|
|
644
|
+
for (const controller of Array.from(inboundTimeoutControllers)) {
|
|
645
|
+
controller.stop();
|
|
646
|
+
}
|
|
647
|
+
inboundTimeoutControllers.clear();
|
|
648
|
+
|
|
323
649
|
for (const [messageId, pending] of pendingAcks) {
|
|
324
650
|
clearTimeout(pending.timer);
|
|
325
651
|
pending.resolve({
|
|
@@ -331,6 +657,9 @@ export function createInstanceRouter(options: {
|
|
|
331
657
|
}
|
|
332
658
|
pendingAcks.clear();
|
|
333
659
|
deliveryCache.clear();
|
|
660
|
+
|
|
661
|
+
await Promise.allSettled([...activeAckSends]);
|
|
662
|
+
activeAckSends.clear();
|
|
334
663
|
}
|
|
335
664
|
|
|
336
665
|
async function listInstances() {
|
|
@@ -410,6 +739,8 @@ export function createInstanceRouter(options: {
|
|
|
410
739
|
toPeerId: route.peerId,
|
|
411
740
|
ackMessageId: ack.ackFor,
|
|
412
741
|
inboundChannel: ack.inboundChannel,
|
|
742
|
+
inboundTarget: ack.inboundTarget,
|
|
743
|
+
deliveryResults: ack.results,
|
|
413
744
|
error: ack.error,
|
|
414
745
|
};
|
|
415
746
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { createLibp2pMeshChannel } from "./channel.js";
|
|
3
|
-
import { handleP2PInbound } from "./inbound.js";
|
|
4
|
-
import {
|
|
3
|
+
import { handleP2PInbound, type InboundHandlerDeps } from "./inbound.js";
|
|
4
|
+
import { createOpenClawRuntimeInboundDelivery } from "./inbound-delivery.js";
|
|
5
5
|
import { createInstancePeerStore } from "./instance-peer-store.js";
|
|
6
6
|
import { createInstanceRouter } from "./instance-router.js";
|
|
7
7
|
import { createMeshNetwork } from "./mesh.js";
|
|
@@ -18,7 +18,11 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
18
18
|
logger: api.logger,
|
|
19
19
|
});
|
|
20
20
|
const store = createInstancePeerStore({ logger: api.logger });
|
|
21
|
-
const delivery =
|
|
21
|
+
const delivery = createOpenClawRuntimeInboundDelivery({
|
|
22
|
+
config: api.config,
|
|
23
|
+
loadAdapter: api.runtime.channel.outbound.loadAdapter,
|
|
24
|
+
logger: api.logger,
|
|
25
|
+
});
|
|
22
26
|
const router = createInstanceRouter({
|
|
23
27
|
mesh,
|
|
24
28
|
store,
|
|
@@ -27,6 +31,8 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
27
31
|
logger: api.logger,
|
|
28
32
|
});
|
|
29
33
|
|
|
34
|
+
const channel = createLibp2pMeshChannel(mesh);
|
|
35
|
+
|
|
30
36
|
// 1. Register Service (manages libp2p node lifecycle)
|
|
31
37
|
api.registerService({
|
|
32
38
|
id: "libp2p-mesh",
|
|
@@ -38,7 +44,36 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
38
44
|
await mesh.start();
|
|
39
45
|
await router.start();
|
|
40
46
|
unsubscribeInbound = mesh.onMessage((msg) => {
|
|
41
|
-
if (msg.type === "direct" || msg.type === "broadcast"
|
|
47
|
+
if (msg.type === "direct" || msg.type === "broadcast") {
|
|
48
|
+
const sendToChannel: InboundHandlerDeps["sendToChannel"] = async (_channelId, _target, text) => {
|
|
49
|
+
if (!config?.inboundChannel || !config?.inboundTarget) {
|
|
50
|
+
api.logger.warn?.(
|
|
51
|
+
"[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.",
|
|
52
|
+
);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result = await delivery.deliver({
|
|
57
|
+
channel: config.inboundChannel,
|
|
58
|
+
target: config.inboundTarget,
|
|
59
|
+
text,
|
|
60
|
+
metadata: {
|
|
61
|
+
fromInstanceId: msg.instanceId ?? msg.from,
|
|
62
|
+
fromPeerId: msg.from,
|
|
63
|
+
p2pMessageId: msg.id,
|
|
64
|
+
allowAgentAutoReply: false,
|
|
65
|
+
replyToInstanceId: msg.instanceId ?? msg.from,
|
|
66
|
+
replyTool: "p2p_send_instance_message",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
api.logger.error?.(
|
|
71
|
+
`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
handleP2PInbound(msg, { logger: api.logger, sendToChannel });
|
|
76
|
+
} else if (msg.type === "agent-sync") {
|
|
42
77
|
handleP2PInbound(msg, { logger: api.logger });
|
|
43
78
|
}
|
|
44
79
|
});
|
|
@@ -75,7 +110,7 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
75
110
|
|
|
76
111
|
// 2. Register Channel (lightweight debugging surface)
|
|
77
112
|
api.registerChannel({
|
|
78
|
-
plugin:
|
|
113
|
+
plugin: channel as ChannelPlugin,
|
|
79
114
|
});
|
|
80
115
|
|
|
81
116
|
// 3. Register Agent Tools
|
package/src/types.ts
CHANGED
|
@@ -62,6 +62,20 @@ export interface UserMessagePayload {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export interface InboundTargetConfig {
|
|
66
|
+
id?: string;
|
|
67
|
+
channel: string;
|
|
68
|
+
target: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DeliveryTargetResult {
|
|
72
|
+
id?: string;
|
|
73
|
+
channel: string;
|
|
74
|
+
target: string;
|
|
75
|
+
ok: boolean;
|
|
76
|
+
error?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
export interface DeliveryAckPayload {
|
|
66
80
|
ackFor: string;
|
|
67
81
|
ok: boolean;
|
|
@@ -69,6 +83,7 @@ export interface DeliveryAckPayload {
|
|
|
69
83
|
inboundTarget?: string;
|
|
70
84
|
deliveredAt: number;
|
|
71
85
|
error?: string;
|
|
86
|
+
results?: DeliveryTargetResult[];
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
export interface InstancePeerRecord {
|
|
@@ -138,6 +153,8 @@ export interface InstanceRouter {
|
|
|
138
153
|
toPeerId: string;
|
|
139
154
|
ackMessageId?: string;
|
|
140
155
|
inboundChannel?: string;
|
|
156
|
+
inboundTarget?: string;
|
|
157
|
+
deliveryResults?: DeliveryTargetResult[];
|
|
141
158
|
error?: string;
|
|
142
159
|
}>;
|
|
143
160
|
}
|
|
@@ -190,7 +207,8 @@ export interface MeshConfig {
|
|
|
190
207
|
/**
|
|
191
208
|
* Deprecated pre-2026.6 config keys kept so existing OpenClaw configs keep
|
|
192
209
|
* validating after upgrade. Relay selection is now configured with
|
|
193
|
-
* `relayList`; inbound display uses `inboundChannel`/`inboundTarget
|
|
210
|
+
* `relayList`; inbound display uses `inboundChannel`/`inboundTarget`, or
|
|
211
|
+
* `inboundTargets` when multi-target inbound delivery is enabled.
|
|
194
212
|
*/
|
|
195
213
|
relayChannel?: string;
|
|
196
214
|
relayAccountId?: string;
|
|
@@ -207,6 +225,7 @@ export interface MeshConfig {
|
|
|
207
225
|
announceAddrs?: string[];
|
|
208
226
|
inboundChannel?: string;
|
|
209
227
|
inboundTarget?: string;
|
|
228
|
+
inboundTargets?: InboundTargetConfig[];
|
|
210
229
|
deliveryAckTimeoutMs?: number;
|
|
211
230
|
}
|
|
212
231
|
|