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
|
@@ -1,83 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const logger = options
|
|
1
|
+
function summarizeError(error) {
|
|
2
|
+
return error instanceof Error ? error.message : String(error);
|
|
3
|
+
}
|
|
4
|
+
export function createOpenClawRuntimeInboundDelivery(options) {
|
|
5
|
+
const { config, loadAdapter, logger } = options;
|
|
6
6
|
return {
|
|
7
|
-
deliver(request) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
request.text,
|
|
17
|
-
];
|
|
18
|
-
logger?.debug?.(`[libp2p-mesh] Forwarding inbound delivery via CLI: ${command} ${args.join(" ")}`);
|
|
19
|
-
return new Promise((resolve) => {
|
|
20
|
-
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
21
|
-
const stdout = [];
|
|
22
|
-
const stderr = [];
|
|
23
|
-
let settled = false;
|
|
24
|
-
let timeout;
|
|
25
|
-
const finish = (result) => {
|
|
26
|
-
if (settled) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
settled = true;
|
|
30
|
-
if (timeout) {
|
|
31
|
-
clearTimeout(timeout);
|
|
32
|
-
}
|
|
33
|
-
resolve(result);
|
|
7
|
+
async deliver(request) {
|
|
8
|
+
logger?.debug?.(`[libp2p-mesh] Forwarding inbound delivery via runtime channel adapter: ${request.channel}/${request.target}`);
|
|
9
|
+
const adapter = await loadAdapter(request.channel);
|
|
10
|
+
if (!adapter?.sendText) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
channel: request.channel,
|
|
14
|
+
target: request.target,
|
|
15
|
+
error: `channel ${request.channel} does not expose runtime text delivery`,
|
|
34
16
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
target: request.target,
|
|
42
|
-
error: `openclaw message send timed out after ${timeoutMs}ms`,
|
|
43
|
-
});
|
|
44
|
-
}, timeoutMs);
|
|
45
|
-
child.stdout.on("data", (chunk) => {
|
|
46
|
-
stdout.push(chunk);
|
|
47
|
-
});
|
|
48
|
-
child.stderr.on("data", (chunk) => {
|
|
49
|
-
stderr.push(chunk);
|
|
50
|
-
});
|
|
51
|
-
child.on("error", (err) => {
|
|
52
|
-
finish({
|
|
53
|
-
ok: false,
|
|
54
|
-
channel: request.channel,
|
|
55
|
-
target: request.target,
|
|
56
|
-
error: String(err),
|
|
57
|
-
});
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
await adapter.sendText({
|
|
20
|
+
cfg: config,
|
|
21
|
+
to: request.target,
|
|
22
|
+
text: request.text,
|
|
58
23
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
target: request.target,
|
|
75
|
-
error: stderrText ||
|
|
76
|
-
stdoutText ||
|
|
77
|
-
`openclaw message send exited with code ${code}`,
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
channel: request.channel,
|
|
29
|
+
target: request.target,
|
|
30
|
+
error: summarizeError(error),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
logger?.info?.(`[libp2p-mesh] Delivered inbound message to ${request.channel}/${request.target}`);
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
channel: request.channel,
|
|
37
|
+
target: request.target,
|
|
38
|
+
};
|
|
81
39
|
},
|
|
82
40
|
};
|
|
83
41
|
}
|
package/dist/src/inbound.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ export type InboundHandlerDeps = {
|
|
|
6
6
|
warn?: (msg: string) => void;
|
|
7
7
|
error?: (msg: string) => void;
|
|
8
8
|
};
|
|
9
|
+
sendToChannel?: (channelId: string, target: string, text: string) => Promise<void>;
|
|
9
10
|
};
|
|
10
11
|
export declare function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void;
|
package/dist/src/inbound.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
373
|
+
const cacheKey = deliveryCacheKey(payload, msg);
|
|
374
|
+
const cached = deliveryCache.get(cacheKey);
|
|
130
375
|
if (cached) {
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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:
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
422
|
+
cacheEntry.payload = ack;
|
|
170
423
|
trimDeliveryCache();
|
|
424
|
+
if (stopped) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
171
427
|
await sendAck(msg.from, ack);
|
|
172
428
|
}
|
|
173
|
-
function
|
|
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
|
|
176
|
-
|
|
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(
|
|
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
|
}
|
package/dist/src/plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createLibp2pMeshChannel } from "./channel.js";
|
|
2
2
|
import { handleP2PInbound } from "./inbound.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createOpenClawRuntimeInboundDelivery } from "./inbound-delivery.js";
|
|
4
4
|
import { createInstancePeerStore } from "./instance-peer-store.js";
|
|
5
5
|
import { createInstanceRouter } from "./instance-router.js";
|
|
6
6
|
import { createMeshNetwork } from "./mesh.js";
|
|
@@ -14,7 +14,11 @@ export function registerLibp2pMesh(api) {
|
|
|
14
14
|
logger: api.logger,
|
|
15
15
|
});
|
|
16
16
|
const store = createInstancePeerStore({ logger: api.logger });
|
|
17
|
-
const delivery =
|
|
17
|
+
const delivery = createOpenClawRuntimeInboundDelivery({
|
|
18
|
+
config: api.config,
|
|
19
|
+
loadAdapter: api.runtime.channel.outbound.loadAdapter,
|
|
20
|
+
logger: api.logger,
|
|
21
|
+
});
|
|
18
22
|
const router = createInstanceRouter({
|
|
19
23
|
mesh,
|
|
20
24
|
store,
|
|
@@ -22,6 +26,7 @@ export function registerLibp2pMesh(api) {
|
|
|
22
26
|
config,
|
|
23
27
|
logger: api.logger,
|
|
24
28
|
});
|
|
29
|
+
const channel = createLibp2pMeshChannel(mesh);
|
|
25
30
|
// 1. Register Service (manages libp2p node lifecycle)
|
|
26
31
|
api.registerService({
|
|
27
32
|
id: "libp2p-mesh",
|
|
@@ -33,7 +38,32 @@ export function registerLibp2pMesh(api) {
|
|
|
33
38
|
await mesh.start();
|
|
34
39
|
await router.start();
|
|
35
40
|
unsubscribeInbound = mesh.onMessage((msg) => {
|
|
36
|
-
if (msg.type === "direct" || msg.type === "broadcast"
|
|
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") {
|
|
37
67
|
handleP2PInbound(msg, { logger: api.logger });
|
|
38
68
|
}
|
|
39
69
|
});
|
|
@@ -65,7 +95,7 @@ export function registerLibp2pMesh(api) {
|
|
|
65
95
|
});
|
|
66
96
|
// 2. Register Channel (lightweight debugging surface)
|
|
67
97
|
api.registerChannel({
|
|
68
|
-
plugin:
|
|
98
|
+
plugin: channel,
|
|
69
99
|
});
|
|
70
100
|
// 3. Register Agent Tools
|
|
71
101
|
const tools = buildP2PTools(mesh, router);
|