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.
@@ -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
- peerId: string;
28
- payload: DeliveryAckPayload;
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
- await mesh.sendStructuredMessage(peerId, {
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 cached = deliveryCache.get(payload.messageId);
497
+ const cacheKey = deliveryCacheKey(payload, msg);
498
+ const cached = deliveryCache.get(cacheKey);
202
499
  if (cached) {
203
- await sendAck(cached.peerId, cached.payload);
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
- let ack: DeliveryAckPayload;
208
- if (!config.inboundChannel || !config.inboundTarget) {
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: result.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
- deliveryCache.set(payload.messageId, { peerId: msg.from, payload: ack });
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 trimDeliveryCache(): void {
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 oldestKey = deliveryCache.keys().next().value as string | undefined;
250
- if (!oldestKey) return;
251
- deliveryCache.delete(oldestKey);
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 { createOpenClawCliInboundDelivery } from "./inbound-delivery.js";
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 = createOpenClawCliInboundDelivery({ logger: api.logger });
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" || msg.type === "agent-sync") {
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: createLibp2pMeshChannel(mesh) as ChannelPlugin,
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