libp2p-mesh 2026.6.19 → 2026.6.21

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,8 +1,12 @@
1
1
  import type {
2
2
  DeliveryTargetResult,
3
+ InstancePeerRecord,
3
4
  InstanceRouter,
5
+ LocalPeerLabelAttribute,
4
6
  MeshNetwork,
7
+ PeerLabelStore,
5
8
  UserAttributeMatch,
9
+ UserAttributeMatchScope,
6
10
  UserAttributeMessageDeliveryResult,
7
11
  UserAttributeMessageTarget,
8
12
  UserPublicAttribute,
@@ -28,7 +32,7 @@ function formatDeliveryResults(
28
32
  return [heading, ...lines].join("\n");
29
33
  }
30
34
 
31
- function attributeLabel(attribute: UserPublicAttribute): string {
35
+ function attributeLabel(attribute: UserPublicAttribute | LocalPeerLabelAttribute): string {
32
36
  if (attribute.kind === "tag") {
33
37
  return `tag:${attribute.value}`;
34
38
  }
@@ -42,7 +46,7 @@ function instanceTargetLabel(target: Pick<UserAttributeMessageTarget, "instanceI
42
46
  }
43
47
 
44
48
  function formatUserAttributeTargets(targets: UserAttributeMessageTarget[]) {
45
- return targets.map((target) => `${instanceTargetLabel(target)} [${attributeLabel(target.matchedAttribute)}]`);
49
+ return targets.map((target) => `${instanceTargetLabel(target)} [${target.matchSource}:${attributeLabel(target.matchedAttribute)}]`);
46
50
  }
47
51
 
48
52
  function formatUserAttributeResults(results: UserAttributeMessageDeliveryResult[]) {
@@ -58,6 +62,82 @@ function formatUserAttributeResults(results: UserAttributeMessageDeliveryResult[
58
62
  });
59
63
  }
60
64
 
65
+ type ListInstanceRow = InstancePeerRecord & {
66
+ connected: boolean;
67
+ localLabels: LocalPeerLabelAttribute[];
68
+ };
69
+
70
+ type BuildP2PToolsOptions = {
71
+ peerLabelStore?: Pick<PeerLabelStore, "listLabels">;
72
+ };
73
+
74
+ function formatPublicAttribute(attribute: UserPublicAttribute): string[] {
75
+ if (attribute.kind === "tag") {
76
+ return [
77
+ " - kind: tag",
78
+ ` value: ${attribute.value}`,
79
+ ` label: ${attribute.label}`,
80
+ ` source: ${attribute.source}`,
81
+ ];
82
+ }
83
+
84
+ return [
85
+ " - kind: structured",
86
+ ` key: ${attribute.key}`,
87
+ ` value: ${attribute.value}`,
88
+ ` label: ${attribute.label}`,
89
+ ` source: ${attribute.source}`,
90
+ ];
91
+ }
92
+
93
+ function formatLocalLabel(label: LocalPeerLabelAttribute): string[] {
94
+ return [
95
+ " - kind: structured",
96
+ ` key: ${label.key}`,
97
+ ` value: ${label.value}`,
98
+ ` label: ${label.label}`,
99
+ ` source: ${label.source}`,
100
+ ];
101
+ }
102
+
103
+ function formatInstanceList(rows: ListInstanceRow[]): string {
104
+ if (rows.length === 0) {
105
+ return "No OpenClaw instances discovered yet.";
106
+ }
107
+
108
+ const lines = [`Discovered OpenClaw instances: ${rows.length}`];
109
+
110
+ rows.forEach((entry, index) => {
111
+ lines.push(
112
+ "",
113
+ `${index + 1}. ${entry.instanceId}`,
114
+ ` peerId: ${entry.peerId}`,
115
+ ` instanceName: ${entry.instanceName ?? "(none)"}`,
116
+ ` connected: ${entry.connected}`,
117
+ );
118
+
119
+ if ((entry.userPublicAttributes ?? []).length === 0) {
120
+ lines.push(" userPublicAttributes: none");
121
+ } else {
122
+ lines.push(" userPublicAttributes:");
123
+ for (const attribute of entry.userPublicAttributes ?? []) {
124
+ lines.push(...formatPublicAttribute(attribute));
125
+ }
126
+ }
127
+
128
+ if (entry.localLabels.length === 0) {
129
+ lines.push(" localLabels: none");
130
+ } else {
131
+ lines.push(" localLabels:");
132
+ for (const label of entry.localLabels) {
133
+ lines.push(...formatLocalLabel(label));
134
+ }
135
+ }
136
+ });
137
+
138
+ return lines.join("\n");
139
+ }
140
+
61
141
  type SendUserAttributeToolParams = {
62
142
  selector?: unknown;
63
143
  match?: {
@@ -67,8 +147,34 @@ type SendUserAttributeToolParams = {
67
147
  };
68
148
  message?: unknown;
69
149
  dryRun?: unknown;
150
+ scope?: unknown;
70
151
  };
71
152
 
153
+ function normalizeUserAttributeScope(scope: unknown): UserAttributeMatchScope | undefined {
154
+ if (scope === undefined) {
155
+ return undefined;
156
+ }
157
+
158
+ if (scope === "public" || scope === "local" || scope === "all") {
159
+ return scope;
160
+ }
161
+
162
+ return undefined;
163
+ }
164
+
165
+ function validateUserAttributeScope(scope: unknown): string | undefined {
166
+ if (
167
+ scope === undefined ||
168
+ scope === "public" ||
169
+ scope === "local" ||
170
+ scope === "all"
171
+ ) {
172
+ return undefined;
173
+ }
174
+
175
+ return 'scope must be "public", "local", or "all".';
176
+ }
177
+
72
178
  function normalizeUserAttributeSelector(selector: unknown): UserAttributeMatch | string {
73
179
  const value = typeof selector === "string" ? selector.trim() : "";
74
180
  if (!value) {
@@ -124,7 +230,11 @@ function normalizeUserAttributeMatch(params: SendUserAttributeToolParams): UserA
124
230
  return 'match.kind must be "tag" or "structured".';
125
231
  }
126
232
 
127
- export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
233
+ export function buildP2PTools(
234
+ mesh: MeshNetwork,
235
+ router?: InstanceRouter,
236
+ options: BuildP2PToolsOptions = {},
237
+ ) {
128
238
  return [
129
239
  {
130
240
  name: "p2p_send_message",
@@ -334,19 +444,17 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
334
444
  try {
335
445
  const instances = await router.listInstances();
336
446
  const connected = new Set(mesh.getConnectedPeers());
337
- const rows = instances.map((entry) => ({
338
- ...entry,
339
- connected: connected.has(entry.peerId),
340
- }));
341
- const text =
342
- rows.length === 0
343
- ? "No OpenClaw instances discovered yet."
344
- : rows
345
- .map(
346
- (entry) =>
347
- `${entry.instanceId} -> ${entry.peerId}${entry.connected ? " (connected)" : ""}`,
348
- )
349
- .join("\n");
447
+ const rows = await Promise.all(
448
+ instances.map(async (entry): Promise<ListInstanceRow> => ({
449
+ ...entry,
450
+ userPublicAttributes: entry.userPublicAttributes ?? [],
451
+ connected: connected.has(entry.peerId),
452
+ localLabels: options.peerLabelStore
453
+ ? await options.peerLabelStore.listLabels(entry.instanceId)
454
+ : [],
455
+ })),
456
+ );
457
+ const text = formatInstanceList(rows);
350
458
  return {
351
459
  content: [{ type: "text" as const, text }],
352
460
  details: { instances: rows, count: rows.length },
@@ -486,7 +594,7 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
486
594
  name: "p2p_send_user_attribute_message",
487
595
  label: "P2P Send User Attribute Message",
488
596
  description:
489
- 'Send a user message to discovered OpenClaw instances matching a public user attribute selector. Use selectors like "group=实验室", "project=小龙虾", "tag:P2P", or "#P2P". First run a dry run with dryRun=true to preview targets; if targets match, call again immediately with dryRun=false and the same selector.',
597
+ 'Send a user message to discovered OpenClaw instances matching an attribute selector. scope controls the source: "public" for announced user attributes, "local" for local peer labels, or "all" for both; defaults to public. Use selectors like "group=实验室", "project=小龙虾", "tag:P2P", or "#P2P". First run a dry run with dryRun=true to preview targets; if targets match, call again immediately with dryRun=false and the same selector, scope, and message.',
490
598
  parameters: {
491
599
  type: "object" as const,
492
600
  properties: {
@@ -539,8 +647,15 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
539
647
  type: "boolean" as const,
540
648
  description: "Preview matching instances without sending. Run this before group sending.",
541
649
  },
650
+ scope: {
651
+ type: "string" as const,
652
+ enum: ["public", "local", "all"],
653
+ description:
654
+ "Attribute source to match: public announced attributes, local peer labels, or both. Defaults to public.",
655
+ },
542
656
  },
543
- required: ["selector", "message"],
657
+ required: ["message"],
658
+ anyOf: [{ required: ["selector"] }, { required: ["match"] }],
544
659
  },
545
660
  async execute(_toolCallId: string, params: SendUserAttributeToolParams) {
546
661
  if (!router) {
@@ -552,9 +667,16 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
552
667
  }
553
668
 
554
669
  const match = normalizeUserAttributeMatch(params);
670
+ const scopeError = validateUserAttributeScope(params.scope);
671
+ const scope = normalizeUserAttributeScope(params.scope);
555
672
  const message = typeof params.message === "string" ? params.message.trim() : "";
556
- if (typeof match === "string" || !message) {
557
- const error = typeof match === "string" ? match : "message is required.";
673
+ if (typeof match === "string" || scopeError || !message) {
674
+ const error =
675
+ typeof match === "string"
676
+ ? match
677
+ : scopeError
678
+ ? scopeError
679
+ : "message is required.";
558
680
  return {
559
681
  content: [{ type: "text" as const, text: error }],
560
682
  details: { error },
@@ -563,7 +685,8 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
563
685
  }
564
686
 
565
687
  const dryRun = params.dryRun === true;
566
- const result = await router.sendUserAttributeMessage(match, message, { dryRun });
688
+ const options = scope ? { dryRun, scope } : { dryRun };
689
+ const result = await router.sendUserAttributeMessage(match, message, options);
567
690
  if (result.error) {
568
691
  return {
569
692
  content: [{ type: "text" as const, text: result.error }],
@@ -66,13 +66,15 @@ function sameRecord(
66
66
 
67
67
  const recordAttributes = normalizeUserPublicAttributes(record.userPublicAttributes);
68
68
  const payloadAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
69
+ const payloadIncludesAttributes = "userPublicAttributes" in payload;
69
70
 
70
71
  return (
71
72
  record.peerId === payload.peerId &&
72
73
  record.instanceName === payload.instanceName &&
73
74
  record.pubkey === payload.pubkey &&
74
75
  sameStringArray(record.multiaddrs, payload.multiaddrs) &&
75
- sameUserPublicAttributes(recordAttributes, payloadAttributes) &&
76
+ (!payloadIncludesAttributes ||
77
+ sameUserPublicAttributes(recordAttributes, payloadAttributes)) &&
76
78
  record.lastAnnouncedAt === payload.announcedAt
77
79
  );
78
80
  }
@@ -187,7 +189,10 @@ export function createInstancePeerStore(options?: {
187
189
  return runMutation(async () => {
188
190
  const table = await load();
189
191
  const existing = table.instances[payload.instanceId];
190
- const userPublicAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
192
+ const userPublicAttributes =
193
+ "userPublicAttributes" in payload
194
+ ? normalizeUserPublicAttributes(payload.userPublicAttributes)
195
+ : normalizeUserPublicAttributes(existing?.userPublicAttributes);
191
196
  const changed = !sameRecord(existing, payload);
192
197
  const record: InstancePeerRecord = {
193
198
  instanceId: payload.instanceId,
@@ -6,14 +6,23 @@ import type {
6
6
  InstanceAnnouncePayload,
7
7
  InstanceRouter,
8
8
  InstanceRouterOptions,
9
+ LocalPeerLabelAttribute,
9
10
  MeshConfig,
10
11
  P2PMessage,
11
12
  UserAttributeMatch,
13
+ UserAttributeMessageOptions,
14
+ UserAttributeMatchScope,
15
+ UserAttributeMatchSource,
12
16
  UserAttributeMessageTarget,
13
17
  UserPublicAttribute,
14
18
  UserMessagePayload,
15
19
  } from "./types.js";
16
- import { matchesUserAttribute, mergeUserPublicAttributes } from "./user-attributes.js";
20
+ import {
21
+ matchesUserAttribute,
22
+ mergeUserPublicAttributes,
23
+ normalizeAttributeKey,
24
+ normalizeAttributeValue,
25
+ } from "./user-attributes.js";
17
26
 
18
27
  export type RouterLogger = {
19
28
  info?: (message: string) => void;
@@ -77,6 +86,40 @@ function describeAttributeMatch(match: UserAttributeMatch): string {
77
86
  return `structured attribute ${match.key}=${match.value}`;
78
87
  }
79
88
 
89
+ function effectiveAttributeMatchScope(
90
+ scope: UserAttributeMatchScope | undefined,
91
+ ): UserAttributeMatchScope {
92
+ return scope ?? "public";
93
+ }
94
+
95
+ function matchesLocalPeerLabel(
96
+ attribute: LocalPeerLabelAttribute,
97
+ match: UserAttributeMatch,
98
+ ): boolean {
99
+ if (match.kind !== "structured") {
100
+ return false;
101
+ }
102
+
103
+ return (
104
+ normalizeAttributeKey(attribute.key) === normalizeAttributeKey(match.key) &&
105
+ normalizeAttributeValue(attribute.value) === normalizeAttributeValue(match.value)
106
+ );
107
+ }
108
+
109
+ function buildUserAttributeTarget(
110
+ record: { instanceId: string; instanceName?: string; peerId: string },
111
+ matchedAttribute: UserPublicAttribute | LocalPeerLabelAttribute,
112
+ matchSource: UserAttributeMatchSource,
113
+ ): UserAttributeMessageTarget {
114
+ return {
115
+ instanceId: record.instanceId,
116
+ ...(record.instanceName ? { instanceName: record.instanceName } : {}),
117
+ peerId: record.peerId,
118
+ matchedAttribute,
119
+ matchSource,
120
+ };
121
+ }
122
+
80
123
  type EffectiveInboundTarget = {
81
124
  id?: string;
82
125
  channel: string;
@@ -174,6 +217,8 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
174
217
  const pendingAcks = new Map<string, PendingAck>();
175
218
  const deliveryCache = new Map<string, DeliveryCacheEntry>();
176
219
  const unsubs: Array<() => void> = [];
220
+ const pendingAttributePeers = new Set<string>();
221
+ let attributeRefreshPromise: Promise<void> | undefined;
177
222
 
178
223
  function localInstanceId(): string {
179
224
  const identity = mesh.getInstanceIdentity();
@@ -183,26 +228,35 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
183
228
  return identity.id;
184
229
  }
185
230
 
186
- async function loadUserPublicAttributes(): Promise<UserPublicAttribute[]> {
187
- const [userMdTags, profileAttributes] = await Promise.all([
188
- options.userAttributeSource?.loadTags().catch((error) => {
231
+ async function loadUserPublicAttributes(loadOptions?: {
232
+ refreshUserMd?: boolean;
233
+ }): Promise<UserPublicAttribute[]> {
234
+ const userMdPromise = loadOptions?.refreshUserMd && options.userAttributeSource?.refreshTags
235
+ ? options.userAttributeSource.refreshTags()
236
+ : options.userAttributeSource?.loadTags() ?? Promise.resolve([]);
237
+
238
+ const profilePromise =
239
+ options.userProfileStore?.listAttributes().catch((error) => {
189
240
  logger?.warn?.(
190
- `[libp2p-mesh] Failed to load USER.md public attributes: ${summarizeError(error)}`,
241
+ `[libp2p-mesh] Failed to load profile public attributes: ${summarizeError(error)}`,
191
242
  );
192
243
  return [];
193
- }) ?? Promise.resolve([]),
194
- options.userProfileStore?.listAttributes().catch((error) => {
244
+ }) ?? Promise.resolve([]);
245
+
246
+ const [userMdTags, profileAttributes] = await Promise.all([
247
+ userMdPromise.catch((error) => {
195
248
  logger?.warn?.(
196
- `[libp2p-mesh] Failed to load profile public attributes: ${summarizeError(error)}`,
249
+ `[libp2p-mesh] Failed to load USER.md public attributes: ${summarizeError(error)}`,
197
250
  );
198
251
  return [];
199
- }) ?? Promise.resolve([]),
252
+ }),
253
+ profilePromise,
200
254
  ]);
201
255
 
202
256
  return mergeUserPublicAttributes(userMdTags, profileAttributes);
203
257
  }
204
258
 
205
- async function buildAnnouncePayload(): Promise<InstanceAnnouncePayload> {
259
+ function buildBaseAnnouncePayload(): InstanceAnnouncePayload {
206
260
  const identity = mesh.getInstanceIdentity();
207
261
  if (!identity) {
208
262
  throw new Error("Local instance identity is not initialized");
@@ -214,17 +268,102 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
214
268
  instanceName: identity.name,
215
269
  multiaddrs: mesh.getMultiaddrs(),
216
270
  pubkey: identity.pubkey,
217
- userPublicAttributes: await loadUserPublicAttributes(),
218
271
  announcedAt: Date.now(),
219
272
  };
220
273
  }
221
274
 
275
+ async function buildAttributeAnnouncePayload(): Promise<InstanceAnnouncePayload> {
276
+ return {
277
+ ...buildBaseAnnouncePayload(),
278
+ userPublicAttributes: await loadUserPublicAttributes({ refreshUserMd: true }),
279
+ };
280
+ }
281
+
282
+ function attributeAnnouncePeers(extraPeerId?: string): string[] {
283
+ const peers = new Set<string>();
284
+ for (const peerId of mesh.getConnectedPeers()) {
285
+ if (isNonEmptyString(peerId) && peerId !== mesh.getLocalPeerId()) {
286
+ peers.add(peerId);
287
+ }
288
+ }
289
+ for (const peerId of announcedPeers) {
290
+ if (isNonEmptyString(peerId) && peerId !== mesh.getLocalPeerId()) {
291
+ peers.add(peerId);
292
+ }
293
+ }
294
+ if (extraPeerId && extraPeerId !== mesh.getLocalPeerId()) {
295
+ peers.add(extraPeerId);
296
+ }
297
+ return [...peers];
298
+ }
299
+
300
+ async function sendAttributeAnnounceToPeers(peers: string[]): Promise<void> {
301
+ if (peers.length === 0) {
302
+ return;
303
+ }
304
+
305
+ const payload = await buildAttributeAnnouncePayload();
306
+ for (const peerId of peers) {
307
+ try {
308
+ await mesh.sendStructuredMessage(peerId, {
309
+ id: crypto.randomUUID(),
310
+ type: "instance-announce",
311
+ to: peerId,
312
+ payload: JSON.stringify(payload),
313
+ });
314
+ logAnnounce("Sent", peerId, payload);
315
+ } catch (error) {
316
+ logger?.warn?.(
317
+ `[libp2p-mesh] Failed to send attribute announce to ${peerId}: ${summarizeError(error)}`,
318
+ );
319
+ }
320
+ }
321
+ }
322
+
323
+ function queueAttributeRefresh(extraPeerId?: string): void {
324
+ const peers =
325
+ extraPeerId === undefined
326
+ ? attributeAnnouncePeers()
327
+ : isNonEmptyString(extraPeerId) && extraPeerId !== mesh.getLocalPeerId()
328
+ ? [extraPeerId]
329
+ : [];
330
+ for (const peerId of peers) {
331
+ pendingAttributePeers.add(peerId);
332
+ }
333
+
334
+ if (pendingAttributePeers.size === 0 && !attributeRefreshPromise) {
335
+ return;
336
+ }
337
+
338
+ drainAttributeRefresh();
339
+ }
340
+
341
+ function drainAttributeRefresh(): Promise<void> {
342
+ if (!attributeRefreshPromise) {
343
+ attributeRefreshPromise = (async () => {
344
+ while (pendingAttributePeers.size > 0) {
345
+ const peers = [...pendingAttributePeers];
346
+ pendingAttributePeers.clear();
347
+ await sendAttributeAnnounceToPeers(peers).catch((error) => {
348
+ logger?.warn?.(
349
+ `[libp2p-mesh] Failed to refresh public attributes: ${summarizeError(error)}`,
350
+ );
351
+ });
352
+ }
353
+ })().finally(() => {
354
+ attributeRefreshPromise = undefined;
355
+ });
356
+ }
357
+
358
+ return attributeRefreshPromise;
359
+ }
360
+
222
361
  async function announceToPeer(peerId: string): Promise<void> {
223
362
  if (!isNonEmptyString(peerId) || peerId === mesh.getLocalPeerId()) {
224
363
  return;
225
364
  }
226
365
 
227
- const payload = await buildAnnouncePayload();
366
+ const payload = buildBaseAnnouncePayload();
228
367
  await mesh.sendStructuredMessage(peerId, {
229
368
  id: crypto.randomUUID(),
230
369
  type: "instance-announce",
@@ -233,6 +372,15 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
233
372
  });
234
373
  announcedPeers.add(peerId);
235
374
  logAnnounce("Sent", peerId, payload);
375
+ queueAttributeRefresh(peerId);
376
+ }
377
+
378
+ async function refreshPublicAttributes(): Promise<void> {
379
+ for (const peerId of attributeAnnouncePeers()) {
380
+ pendingAttributePeers.add(peerId);
381
+ }
382
+
383
+ await drainAttributeRefresh();
236
384
  }
237
385
 
238
386
  function announceSummary(
@@ -638,24 +786,38 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
638
786
 
639
787
  async function resolveUserAttributeTargets(
640
788
  match: UserAttributeMatch,
789
+ scope: UserAttributeMatchScope,
641
790
  ): Promise<UserAttributeMessageTarget[]> {
642
791
  const records = await store.list();
643
792
  const targets: UserAttributeMessageTarget[] = [];
644
793
 
645
794
  for (const record of records) {
646
- const matchedAttribute = record.userPublicAttributes?.find((attribute) =>
647
- matchesUserAttribute(attribute, match),
648
- );
649
- if (!matchedAttribute) {
795
+ const publicAttribute =
796
+ scope === "local"
797
+ ? undefined
798
+ : record.userPublicAttributes?.find((attribute) =>
799
+ matchesUserAttribute(attribute, match),
800
+ );
801
+ const localAttribute =
802
+ scope === "public"
803
+ ? undefined
804
+ : (await options.peerLabelStore?.listLabels(record.instanceId))?.find((attribute) =>
805
+ matchesLocalPeerLabel(attribute, match),
806
+ );
807
+
808
+ if (publicAttribute && localAttribute && scope === "all") {
809
+ targets.push(buildUserAttributeTarget(record, publicAttribute, "all"));
650
810
  continue;
651
811
  }
652
812
 
653
- targets.push({
654
- instanceId: record.instanceId,
655
- instanceName: record.instanceName,
656
- peerId: record.peerId,
657
- matchedAttribute,
658
- });
813
+ if (publicAttribute) {
814
+ targets.push(buildUserAttributeTarget(record, publicAttribute, "public"));
815
+ continue;
816
+ }
817
+
818
+ if (localAttribute) {
819
+ targets.push(buildUserAttributeTarget(record, localAttribute, "local"));
820
+ }
659
821
  }
660
822
 
661
823
  return targets;
@@ -664,11 +826,16 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
664
826
  async function sendUserAttributeMessage(
665
827
  match: UserAttributeMatch,
666
828
  message: string,
667
- sendOptions: { dryRun?: boolean } = {},
829
+ sendOptions: UserAttributeMessageOptions = {},
668
830
  ) {
669
- const targets = await resolveUserAttributeTargets(match);
831
+ const scope = effectiveAttributeMatchScope(sendOptions.scope);
832
+ const targets = await resolveUserAttributeTargets(
833
+ match,
834
+ scope,
835
+ );
670
836
  if (targets.length === 0) {
671
837
  return {
838
+ scope,
672
839
  matched: 0,
673
840
  sent: 0,
674
841
  delivered: 0,
@@ -679,6 +846,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
679
846
 
680
847
  if (sendOptions.dryRun === true) {
681
848
  return {
849
+ scope,
682
850
  matched: targets.length,
683
851
  sent: 0,
684
852
  delivered: 0,
@@ -712,6 +880,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
712
880
  }
713
881
 
714
882
  return {
883
+ scope,
715
884
  matched: targets.length,
716
885
  sent: results.filter((result) => result.sent).length,
717
886
  delivered: results.filter((result) => result.delivered).length,
@@ -727,6 +896,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
727
896
  stop,
728
897
  handleMessage,
729
898
  announceToPeer,
899
+ refreshPublicAttributes,
730
900
  listInstances,
731
901
  resolveInstance,
732
902
  sendInstanceMessage,