@openclaw/discord 2026.7.2-beta.7 → 2026.8.1-beta.3

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.
Files changed (114) hide show
  1. package/dist/{account-inspect-SXiCBCgP.js → account-inspect-D5xmUpnj.js} +49 -43
  2. package/dist/account-inspect-api.js +1 -1
  3. package/dist/accounts-DNl-oCEw.js +371 -0
  4. package/dist/action-runtime-api.js +1 -1
  5. package/dist/{active-turn-thread-route-MlKWp8RD.js → active-turn-thread-route-fmHl1HNd.js} +9 -11
  6. package/dist/{activities-api-DTKsuDxf.js → activities-api-KCBAmWxD.js} +133 -175
  7. package/dist/activities-api.js +1 -1
  8. package/dist/{allow-list-DzKrtfah.js → allow-list-BDMimASD.js} +2 -3
  9. package/dist/{api-DnrK9gML.js → api-DNVbJyEy.js} +7 -2
  10. package/dist/api.js +18 -18
  11. package/dist/{approval-handler.runtime-HymvFTYn.js → approval-handler.runtime-B12RCYr8.js} +6 -6
  12. package/dist/{approval-message-safety-DEyKVsAw.js → approval-message-safety-XGFPbDoI.js} +89 -3
  13. package/dist/{audit-CjsrabP8.js → audit-Yc7eZDd6.js} +5 -5
  14. package/dist/{channel-actions-uXFTJX3o.js → channel-actions-Dr2QI8Bf.js} +11 -7
  15. package/dist/{channel-actions.runtime-CIYU90sl.js → channel-actions.runtime-CODTvo7c.js} +99 -97
  16. package/dist/{channel-vKa-76xY.js → channel-cXYzGc4L.js} +102 -104
  17. package/dist/channel-config-api.js +1 -1
  18. package/dist/channel-plugin-api.js +1 -1
  19. package/dist/{channel.setup-CmcwZ_qd.js → channel.setup-CSiwnMgs.js} +16 -54
  20. package/dist/{outbound-session-route-H-HoyH5U.js → components-ezGKo_Yh.js} +69 -142
  21. package/dist/{config-schema-WHRQcY19.js → config-schema-DC5kDtQ5.js} +42 -11
  22. package/dist/conversation-binding-route-D6rCQ5S0.js +43 -0
  23. package/dist/{directory-config-D_LojtCe.js → directory-config-XyCfoqLg.js} +2 -2
  24. package/dist/directory-contract-api.js +1 -1
  25. package/dist/{directory-live-Bw5Ephzv.js → directory-live-Df0NbFJB.js} +7 -4
  26. package/dist/{doctor-Bc4S1qyd.js → doctor-DYY31GrO.js} +34 -13
  27. package/dist/{doctor-contract-CCFMIDk9.js → doctor-contract-DsWvKIXF.js} +2 -3
  28. package/dist/doctor-contract-api.js +147 -2
  29. package/dist/{handle-action.guild-admin-D6VRRW-C.js → handle-action.guild-admin-6Tvvinsa.js} +4 -7
  30. package/dist/{inbound-event-delivery-CEPlt2uz.js → inbound-event-delivery-De2qxTrP.js} +4 -25
  31. package/dist/index.js +5 -4
  32. package/dist/{message-handler-CeiSulIw.js → message-handler-B61q5MOF.js} +118 -33
  33. package/dist/{message-handler.preflight-DibgQFf2.js → message-handler.preflight-Bzg_Hawz.js} +75 -61
  34. package/dist/{message-handler.process-Dnc_oz9y.js → message-handler.process--4_712Ec.js} +236 -296
  35. package/dist/{message-utils-BgTwT_q7.js → message-utils-BXLLnFl6.js} +34 -35
  36. package/dist/{model-picker-preference-primitives-D25pnCOu.js → model-picker-preference-primitives-BHT1LkR_.js} +3 -3
  37. package/dist/{outbound-adapter-DtE-i2OJ.js → outbound-adapter-DxHTOqiN.js} +157 -68
  38. package/dist/{outbound-components-C7GVyfPh.js → outbound-components-B2PL0ooH.js} +3 -3
  39. package/dist/owner-access-Bg41VzAf.js +107 -0
  40. package/dist/{pluralkit-CD4B11JB.js → pluralkit-dUl4v1X3.js} +1 -1
  41. package/dist/{preflight-audio-BjnQZ5yS.js → preflight-audio-DrnqFA8U.js} +22 -28
  42. package/dist/{probe-BeWKhjqX.js → probe-BTdtVRwU.js} +37 -13
  43. package/dist/probe.runtime-Dq5-PRKT.js +2 -0
  44. package/dist/{provider-DfQJX3z5.js → provider-DlK_ynqt.js} +1131 -2021
  45. package/dist/{provider-session.runtime-Bo88k0pV.js → provider-session.runtime-CvvphHvs.js} +3 -3
  46. package/dist/provider.runtime-CzKGRiAF.js +2 -0
  47. package/dist/realtime-session.runtime-ysbMG8v7.js +1351 -0
  48. package/dist/{resolve-allowlist-common-DHERApxi.js → resolve-allowlist-common-DIVXbD1P.js} +3 -3
  49. package/dist/{resolve-channels-BjepAj8b.js → resolve-channels-CGeUoIqb.js} +4 -4
  50. package/dist/{resolve-users-Bc28htgt.js → resolve-users-BtKxLUZv.js} +3 -3
  51. package/dist/{chunk-DX4jpFQP.js → retry-DNsVTeTo.js} +308 -131
  52. package/dist/{runtime-BmW_j_hl.js → runtime-CQovMsH3.js} +12 -15
  53. package/dist/{runtime-_aRC6V2s.js → runtime-D71_VFLr.js} +49 -58
  54. package/dist/runtime-api.actions.js +2 -2
  55. package/dist/runtime-api.js +23 -23
  56. package/dist/runtime-api.lookup.js +5 -5
  57. package/dist/runtime-api.monitor-CzeTYF32.js +6 -0
  58. package/dist/runtime-api.monitor.js +6 -5
  59. package/dist/runtime-api.send.js +6 -6
  60. package/dist/runtime-api.threads.js +5 -6
  61. package/dist/sdk-runtime-37OU6_Yh.js +369 -0
  62. package/dist/{secret-config-contract-B1akrlyY.js → secret-config-contract-B3i6WxV5.js} +53 -25
  63. package/dist/secret-contract-api.js +1 -1
  64. package/dist/security-audit-contract-api.js +1 -1
  65. package/dist/{security-audit.runtime-CPN0xFYz.js → security-audit.runtime-BVKF9VtY.js} +1 -1
  66. package/dist/{send-CdkZ6zeW.js → send-rQIffSYu.js} +180 -91
  67. package/dist/{send.components-5uqwkjgE.js → send.components-ChK1wS7w.js} +32 -46
  68. package/dist/{send.outbound-D-80Ej-t.js → send.outbound-HIkgji_7.js} +57 -34
  69. package/dist/{send.reactions-CtlmGd_o.js → send.reactions-gmJ6yFPo.js} +14 -21
  70. package/dist/{send.receipt-CuwRrpFz.js → send.receipt-Cnhoautb.js} +29 -1
  71. package/dist/{send.shared-jpvAG_u6.js → send.shared-B3fpaLWJ.js} +112 -181
  72. package/dist/{sender-identity-mG_zsdox.js → sender-identity-BJtfyise.js} +1 -1
  73. package/dist/setup-entry.js +0 -5
  74. package/dist/setup-plugin-api.js +1 -1
  75. package/dist/{shared-CXF1tup1.js → shared-CaAaoksy.js} +27 -23
  76. package/dist/subagent-hooks-CNT2ZJTJ.js +49 -0
  77. package/dist/subagent-hooks-api.js +2 -6
  78. package/dist/subagent-progress-FPLqBY1s.js +110 -0
  79. package/dist/{system-events-DdUqf91B.js → system-events-Nuwoq_au.js} +2 -2
  80. package/dist/{target-resolver-C3s_2m0t.js → target-resolver-DIXShHaZ.js} +3 -3
  81. package/dist/targets-B2pf49zc.js +3 -0
  82. package/dist/{thread-bindings-C-M-ly7P.js → thread-bindings-VlymUnbz.js} +29 -28
  83. package/dist/{thread-bindings.manager-DYvSlrva.js → thread-bindings.manager-CngsgErq.js} +258 -29
  84. package/dist/{thread-bindings.session-updates-DAIlp7Px.js → thread-bindings.session-updates-CJb-BpPL.js} +1 -1
  85. package/dist/{thread-bindings.state-Cgi5uRn-.js → thread-bindings.state-D8OMdk4O.js} +3 -16
  86. package/dist/threading-DU6pgeGm.js +456 -0
  87. package/dist/transcripts-source-DeIMNbRh.js +265 -0
  88. package/dist/transcripts-source-api.js +7 -2
  89. package/dist/{typing--Mzo2kM7.js → typing-oLiS5Z13.js} +1 -1
  90. package/dist/voice-runtime-CjNFaynA.js +2549 -0
  91. package/openclaw.plugin.json +215 -25
  92. package/package.json +15 -11
  93. package/skills/discord/SKILL.md +22 -115
  94. package/dist/accounts-DJTf1LtA.js +0 -219
  95. package/dist/channel-api-CAJ0wMoV.js +0 -21
  96. package/dist/legacy-state-migrations-api.js +0 -138
  97. package/dist/manager.runtime-Bjav3pfC.js +0 -3655
  98. package/dist/preflight-audio.runtime-CoCXMM8r.js +0 -7
  99. package/dist/probe.runtime-DqFGFMbZ.js +0 -2
  100. package/dist/provider.runtime-Cy7XZmyh.js +0 -2
  101. package/dist/runtime-api.monitor-CCRubd3-.js +0 -5
  102. package/dist/session-binding-contract-api.js +0 -2
  103. package/dist/subagent-hooks-Dxy7Fmnq.js +0 -120
  104. package/dist/subagent-progress-DI3sYLLo.js +0 -664
  105. package/dist/targets-Dv_RN7vT.js +0 -3
  106. package/dist/thread-bindings.discord-api-BoDNqamb.js +0 -246
  107. package/dist/transcripts-source-DIKGsTGT.js +0 -123
  108. package/node_modules/p-map/index.d.ts +0 -155
  109. package/node_modules/p-map/index.js +0 -285
  110. package/node_modules/p-map/license +0 -9
  111. package/node_modules/p-map/package.json +0 -57
  112. package/node_modules/p-map/readme.md +0 -190
  113. package/dist/{audit-core-xwjIczO0.js → audit-core-BCX8UeFH.js} +1 -1
  114. package/dist/{security-audit-jGnJF__m.js → security-audit-BfJhAXZp.js} +1 -1
@@ -1,11 +1,9 @@
1
- import { $ as Section, B as Label, G as ChannelSelectMenu, H as RadioGroup, J as LinkButton, K as Container, Q as Row, U as TextInput, V as Modal, W as Button, X as MentionableSelectMenu, Y as MediaGallery, Yt as __exportAll, Z as RoleSelectMenu, et as Separator, it as UserSelectMenu, l as parseDiscordTarget, nt as TextDisplay, ot as parseCustomId, q as File, rt as Thumbnail, tt as StringSelectMenu, z as CheckboxGroup } from "./chunk-DX4jpFQP.js";
2
- import { buildOutboundBaseSessionKey } from "openclaw/plugin-sdk/routing";
3
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
1
+ import { $ as MediaGallery, G as Modal, J as Button, K as RadioGroup, Q as LinkButton, U as CheckboxGroup, W as Label, X as Container, Y as ChannelSelectMenu, Z as File, at as StringSelectMenu, ct as UserSelectMenu, en as __exportAll, et as MentionableSelectMenu, it as Separator, nt as Row, ot as TextDisplay, q as TextInput, rt as Section, st as Thumbnail, tt as RoleSelectMenu, ut as parseCustomId } from "./retry-DNsVTeTo.js";
4
2
  import { ButtonStyle, MessageFlags, TextInputStyle } from "discord-api-types/v10";
5
3
  import crypto from "node:crypto";
6
- import { reduceLegacyInteractiveReply, resolveMessagePresentationButtonAction, resolveMessagePresentationOptionAction } from "openclaw/plugin-sdk/interactive-runtime";
4
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
+ import { reduceLegacyInteractiveReply, resolveMessagePresentationActionValue, resolveMessagePresentationButtonAction, resolveMessagePresentationOptionAction } from "openclaw/plugin-sdk/interactive-runtime";
7
6
  import { buildApprovalResolutionRef } from "openclaw/plugin-sdk/approval-reference-runtime";
8
- import { buildThreadAwareOutboundSessionRoute } from "openclaw/plugin-sdk/channel-core";
9
7
  //#region extensions/discord/src/custom-id-codec.ts
10
8
  /**
11
9
  * URI-component codec for values embedded in `k=v;` custom-id grammars
@@ -136,19 +134,14 @@ function requireObject(value, label) {
136
134
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
137
135
  return value;
138
136
  }
139
- function readString(value, label, opts) {
137
+ function readRequiredString(value, label, opts) {
140
138
  if (typeof value !== "string") throw new Error(`${label} must be a string`);
141
139
  const trimmed = value.trim();
142
140
  if (!opts?.allowEmpty && !trimmed) throw new Error(`${label} cannot be empty`);
143
141
  return opts?.allowEmpty ? value : trimmed;
144
142
  }
145
- function readOptionalString(value) {
146
- if (typeof value !== "string") return;
147
- const trimmed = value.trim();
148
- return trimmed ? trimmed : void 0;
149
- }
150
143
  function readOptionalCallbackDataKind(value, label) {
151
- const kind = readOptionalString(value);
144
+ const kind = normalizeOptionalString(value);
152
145
  if (kind === void 0) return;
153
146
  if (kind === "command" || kind === "callback") return kind;
154
147
  throw new Error(`${label} must be one of command, callback`);
@@ -157,7 +150,7 @@ function readOptionalStringArray(value, label) {
157
150
  if (value === void 0) return;
158
151
  if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
159
152
  if (value.length === 0) return;
160
- return value.map((entry, index) => readString(entry, `${label}[${index}]`));
153
+ return value.map((entry, index) => readRequiredString(entry, `${label}[${index}]`));
161
154
  }
162
155
  function readOptionalInteger(value, label, bounds) {
163
156
  if (value == null) return;
@@ -170,8 +163,8 @@ function readOptionalEmoji(value, label) {
170
163
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
171
164
  const obj = value;
172
165
  return {
173
- name: readString(obj.name, `${label}.name`),
174
- id: readOptionalString(obj.id),
166
+ name: readRequiredString(obj.name, `${label}.name`),
167
+ id: normalizeOptionalString(obj.id),
175
168
  animated: typeof obj.animated === "boolean" ? obj.animated : void 0
176
169
  };
177
170
  }
@@ -216,9 +209,9 @@ function parseSelectOptions(raw, label) {
216
209
  return raw.map((entry, index) => {
217
210
  const obj = requireObject(entry, `${label}[${index}]`);
218
211
  return {
219
- label: readString(obj.label, `${label}[${index}].label`),
220
- value: readString(obj.value, `${label}[${index}].value`),
221
- description: readOptionalString(obj.description),
212
+ label: readRequiredString(obj.label, `${label}[${index}].label`),
213
+ value: readRequiredString(obj.value, `${label}[${index}].value`),
214
+ description: normalizeOptionalString(obj.description),
222
215
  emoji: readOptionalEmoji(obj.emoji, `${label}[${index}].emoji`),
223
216
  default: typeof obj.default === "boolean" ? obj.default : void 0
224
217
  };
@@ -226,14 +219,14 @@ function parseSelectOptions(raw, label) {
226
219
  }
227
220
  function parseButtonSpec(raw, label) {
228
221
  const obj = requireObject(raw, label);
229
- const style = readOptionalString(obj.style);
230
- const url = readOptionalString(obj.url);
222
+ const style = normalizeOptionalString(obj.style);
223
+ const url = normalizeOptionalString(obj.url);
231
224
  if ((style === "link" || url) && !url) throw new Error(`${label}.url is required for link buttons`);
232
225
  return {
233
- label: readString(obj.label, `${label}.label`),
226
+ label: readRequiredString(obj.label, `${label}.label`),
234
227
  style,
235
228
  url,
236
- callbackData: readOptionalString(obj.callbackData),
229
+ callbackData: normalizeOptionalString(obj.callbackData),
237
230
  callbackDataKind: readOptionalCallbackDataKind(obj.callbackDataKind, `${label}.callbackDataKind`),
238
231
  emoji: readOptionalEmoji(obj.emoji, `${label}.emoji`),
239
232
  disabled: typeof obj.disabled === "boolean" ? obj.disabled : void 0,
@@ -242,7 +235,7 @@ function parseButtonSpec(raw, label) {
242
235
  }
243
236
  function parseSelectSpec(raw, label) {
244
237
  const obj = requireObject(raw, label);
245
- const type = readOptionalString(obj.type);
238
+ const type = normalizeOptionalString(obj.type);
246
239
  const allowedTypes = [
247
240
  "string",
248
241
  "user",
@@ -253,9 +246,9 @@ function parseSelectSpec(raw, label) {
253
246
  if (type && !allowedTypes.includes(type)) throw new Error(`${label}.type must be one of ${allowedTypes.join(", ")}`);
254
247
  return {
255
248
  type,
256
- callbackData: readOptionalString(obj.callbackData),
249
+ callbackData: normalizeOptionalString(obj.callbackData),
257
250
  callbackDataKind: readOptionalCallbackDataKind(obj.callbackDataKind, `${label}.callbackDataKind`),
258
- placeholder: readOptionalString(obj.placeholder),
251
+ placeholder: normalizeOptionalString(obj.placeholder),
259
252
  minValues: readOptionalInteger(obj.minValues, `${label}.minValues`, {
260
253
  min: 0,
261
254
  max: 25
@@ -270,7 +263,7 @@ function parseSelectSpec(raw, label) {
270
263
  }
271
264
  function parseModalField(raw, label, index) {
272
265
  const obj = requireObject(raw, label);
273
- const type = normalizeLowercaseStringOrEmpty(readString(obj.type, `${label}.type`));
266
+ const type = normalizeLowercaseStringOrEmpty(readRequiredString(obj.type, `${label}.type`));
274
267
  const supported = [
275
268
  "text",
276
269
  "checkbox",
@@ -291,10 +284,10 @@ function parseModalField(raw, label, index) {
291
284
  const maxValues = type === "checkbox" ? 10 : 25;
292
285
  return {
293
286
  type,
294
- name: normalizeModalFieldName(readOptionalString(obj.name), index),
295
- label: readString(obj.label, `${label}.label`),
296
- description: readOptionalString(obj.description),
297
- placeholder: readOptionalString(obj.placeholder),
287
+ name: normalizeModalFieldName(normalizeOptionalString(obj.name), index),
288
+ label: readRequiredString(obj.label, `${label}.label`),
289
+ description: normalizeOptionalString(obj.description),
290
+ placeholder: normalizeOptionalString(obj.placeholder),
298
291
  required,
299
292
  options,
300
293
  minValues: readOptionalInteger(obj.minValues, `${label}.minValues`, {
@@ -313,28 +306,28 @@ function parseModalField(raw, label, index) {
313
306
  min: 1,
314
307
  max: 4e3
315
308
  }),
316
- style: readOptionalString(obj.style)
309
+ style: normalizeOptionalString(obj.style)
317
310
  };
318
311
  }
319
312
  function parseComponentBlock(raw, label) {
320
313
  const obj = requireObject(raw, label);
321
- switch (normalizeBlockType(normalizeLowercaseStringOrEmpty(readString(obj.type, `${label}.type`)))) {
314
+ switch (normalizeBlockType(normalizeLowercaseStringOrEmpty(readRequiredString(obj.type, `${label}.type`)))) {
322
315
  case "text": return {
323
316
  type: "text",
324
- text: readString(obj.text, `${label}.text`)
317
+ text: readRequiredString(obj.text, `${label}.text`)
325
318
  };
326
319
  case "section": {
327
- const text = readOptionalString(obj.text);
320
+ const text = normalizeOptionalString(obj.text);
328
321
  const textsRaw = obj.texts;
329
- const texts = Array.isArray(textsRaw) ? textsRaw.map((entry, idx) => readString(entry, `${label}.texts[${idx}]`)) : void 0;
322
+ const texts = Array.isArray(textsRaw) ? textsRaw.map((entry, idx) => readRequiredString(entry, `${label}.texts[${idx}]`)) : void 0;
330
323
  if (!text && (!texts || texts.length === 0)) throw new Error(`${label}.text or ${label}.texts is required for section blocks`);
331
324
  let accessory;
332
325
  if (obj.accessory !== void 0) {
333
326
  const accessoryObj = requireObject(obj.accessory, `${label}.accessory`);
334
- const accessoryType = normalizeLowercaseStringOrEmpty(readString(accessoryObj.type, `${label}.accessory.type`));
327
+ const accessoryType = normalizeLowercaseStringOrEmpty(readRequiredString(accessoryObj.type, `${label}.accessory.type`));
335
328
  if (accessoryType === "thumbnail") accessory = {
336
329
  type: "thumbnail",
337
- url: readString(accessoryObj.url, `${label}.accessory.url`)
330
+ url: readRequiredString(accessoryObj.url, `${label}.accessory.url`)
338
331
  };
339
332
  else if (accessoryType === "button") accessory = {
340
333
  type: "button",
@@ -382,8 +375,8 @@ function parseComponentBlock(raw, label) {
382
375
  items: itemsRaw.map((entry, idx) => {
383
376
  const itemObj = requireObject(entry, `${label}.items[${idx}]`);
384
377
  return {
385
- url: readString(itemObj.url, `${label}.items[${idx}].url`),
386
- description: readOptionalString(itemObj.description),
378
+ url: readRequiredString(itemObj.url, `${label}.items[${idx}].url`),
379
+ description: normalizeOptionalString(itemObj.description),
387
380
  spoiler: typeof itemObj.spoiler === "boolean" ? itemObj.spoiler : void 0
388
381
  };
389
382
  })
@@ -391,7 +384,7 @@ function parseComponentBlock(raw, label) {
391
384
  }
392
385
  case "file": return {
393
386
  type: "file",
394
- file: normalizeAttachmentRef(readString(obj.file, `${label}.file`), `${label}.file`),
387
+ file: normalizeAttachmentRef(readRequiredString(obj.file, `${label}.file`), `${label}.file`),
395
388
  spoiler: typeof obj.spoiler === "boolean" ? obj.spoiler : void 0
396
389
  };
397
390
  default: throw new Error(`${label}.type must be a supported component block`);
@@ -412,16 +405,16 @@ function readDiscordComponentSpec(raw) {
412
405
  if (fieldsRaw.length > 5) throw new Error("components.modal.fields supports up to 5 inputs");
413
406
  const fields = fieldsRaw.map((entry, idx) => parseModalField(entry, `components.modal.fields[${idx}]`, idx));
414
407
  modal = {
415
- title: readString(modalObj.title, "components.modal.title"),
416
- callbackData: readOptionalString(modalObj.callbackData),
417
- triggerLabel: readOptionalString(modalObj.triggerLabel),
418
- triggerStyle: readOptionalString(modalObj.triggerStyle),
408
+ title: readRequiredString(modalObj.title, "components.modal.title"),
409
+ callbackData: normalizeOptionalString(modalObj.callbackData),
410
+ triggerLabel: normalizeOptionalString(modalObj.triggerLabel),
411
+ triggerStyle: normalizeOptionalString(modalObj.triggerStyle),
419
412
  allowedUsers: readOptionalStringArray(modalObj.allowedUsers, "components.modal.allowedUsers"),
420
413
  fields
421
414
  };
422
415
  }
423
416
  return {
424
- text: readOptionalString(obj.text),
417
+ text: normalizeOptionalString(obj.text),
425
418
  reusable,
426
419
  container: typeof obj.container === "object" && obj.container && !Array.isArray(obj.container) ? {
427
420
  accentColor: obj.container.accentColor,
@@ -916,14 +909,13 @@ function resolveDiscordInteractiveButtonStyle(style) {
916
909
  return style ?? "secondary";
917
910
  }
918
911
  function resolveDiscordSelectOptionValue(option) {
919
- const action = resolveMessagePresentationOptionAction(option);
920
- if (action?.type === "command") return action.command;
921
- if (action?.type === "callback") return action.value;
912
+ return resolveMessagePresentationActionValue(resolveMessagePresentationOptionAction(option));
922
913
  }
923
914
  function resolveDiscordSelectCallbackDataKind(options) {
924
915
  const renderableOptions = options.filter((option) => resolveDiscordSelectOptionValue(option));
925
- if (renderableOptions.length > 0 && renderableOptions.every((option) => option.action?.type === "command")) return "command";
926
- if (renderableOptions.length > 0 && renderableOptions.every((option) => option.action?.type === "callback")) return "callback";
916
+ if (renderableOptions.length === 0) return;
917
+ if (renderableOptions.every((option) => option.action?.type === "command")) return "command";
918
+ if (renderableOptions.every((option) => option.action?.type === "callback")) return "callback";
927
919
  if (renderableOptions.some((option) => option.action)) return "mixed";
928
920
  }
929
921
  const DISCORD_INTERACTIVE_BUTTON_ROW_SIZE = 5;
@@ -980,6 +972,24 @@ function appendDiscordButtonBlocks(blocks, buttons) {
980
972
  buttons: components.slice(index, index + DISCORD_INTERACTIVE_BUTTON_ROW_SIZE)
981
973
  });
982
974
  }
975
+ function appendDiscordSelectBlock(blocks, block) {
976
+ const options = block.options.map((option) => ({
977
+ label: option.label,
978
+ value: resolveDiscordSelectOptionValue(option)
979
+ })).filter((option) => Boolean(option.value));
980
+ if (options.length === 0) return;
981
+ const callbackDataKind = resolveDiscordSelectCallbackDataKind(block.options);
982
+ if (callbackDataKind === "mixed") return;
983
+ blocks.push({
984
+ type: "actions",
985
+ select: {
986
+ type: "string",
987
+ placeholder: block.placeholder,
988
+ options,
989
+ callbackDataKind
990
+ }
991
+ });
992
+ }
983
993
  /**
984
994
  * @deprecated Use buildDiscordPresentationComponents with MessagePresentation.
985
995
  */
@@ -997,77 +1007,38 @@ function buildDiscordInteractiveComponents(interactive) {
997
1007
  appendDiscordButtonBlocks(state, block.buttons);
998
1008
  return state;
999
1009
  }
1000
- if (block.type === "select" && block.options.length > 0) {
1001
- const options = block.options.map((option) => ({
1002
- label: option.label,
1003
- value: resolveDiscordSelectOptionValue(option)
1004
- })).filter((option) => Boolean(option.value));
1005
- if (options.length === 0) return state;
1006
- const callbackDataKind = resolveDiscordSelectCallbackDataKind(block.options);
1007
- if (callbackDataKind === "mixed") return state;
1008
- state.push({
1009
- type: "actions",
1010
- select: {
1011
- type: "string",
1012
- placeholder: block.placeholder,
1013
- options,
1014
- callbackDataKind
1015
- }
1016
- });
1017
- }
1010
+ if (block.type === "select") appendDiscordSelectBlock(state, block);
1018
1011
  return state;
1019
1012
  });
1020
1013
  return blocks.length > 0 ? { blocks } : void 0;
1021
1014
  }
1022
1015
  function buildDiscordPresentationComponents(presentation) {
1023
1016
  if (!presentation) return;
1024
- const spec = { blocks: [] };
1025
- if (presentation.title) spec.blocks?.push({
1017
+ const blocks = [];
1018
+ if (presentation.title) blocks.push({
1026
1019
  type: "text",
1027
1020
  text: presentation.title
1028
1021
  });
1029
1022
  for (const block of presentation.blocks) {
1030
1023
  if (block.type === "text" || block.type === "context") {
1031
1024
  const text = block.text.trim();
1032
- if (text) spec.blocks?.push({
1025
+ if (text) blocks.push({
1033
1026
  type: "text",
1034
1027
  text: block.type === "context" ? `-# ${text}` : text
1035
1028
  });
1036
1029
  continue;
1037
1030
  }
1038
1031
  if (block.type === "divider") {
1039
- spec.blocks?.push({ type: "separator" });
1032
+ blocks.push({ type: "separator" });
1040
1033
  continue;
1041
1034
  }
1042
- }
1043
- for (const block of presentation.blocks) {
1044
1035
  if (block.type === "buttons") {
1045
- appendDiscordPresentationButtonBlocks(spec, block.buttons);
1036
+ appendDiscordButtonBlocks(blocks, block.buttons);
1046
1037
  continue;
1047
1038
  }
1048
- if (block.type === "select" && block.options.length > 0) {
1049
- const options = block.options.map((option) => ({
1050
- label: option.label,
1051
- value: resolveDiscordSelectOptionValue(option)
1052
- })).filter((option) => Boolean(option.value));
1053
- if (options.length === 0) continue;
1054
- const callbackDataKind = resolveDiscordSelectCallbackDataKind(block.options);
1055
- if (callbackDataKind === "mixed") continue;
1056
- spec.blocks?.push({
1057
- type: "actions",
1058
- select: {
1059
- type: "string",
1060
- placeholder: block.placeholder,
1061
- options,
1062
- callbackDataKind
1063
- }
1064
- });
1065
- }
1039
+ if (block.type === "select") appendDiscordSelectBlock(blocks, block);
1066
1040
  }
1067
- return spec.blocks?.length ? spec : void 0;
1068
- }
1069
- function appendDiscordPresentationButtonBlocks(spec, buttons) {
1070
- if (spec.blocks) appendDiscordButtonBlocks(spec.blocks, buttons);
1041
+ return blocks.length ? { blocks } : void 0;
1071
1042
  }
1072
1043
  //#endregion
1073
1044
  //#region extensions/discord/src/components.ts
@@ -1098,48 +1069,4 @@ function formatDiscordComponentEventText(params) {
1098
1069
  return `Selected ${values.join(", ")} from "${params.label}".`;
1099
1070
  }
1100
1071
  //#endregion
1101
- //#region extensions/discord/src/outbound-session-route.ts
1102
- function resolveDiscordOutboundSessionRoute(params) {
1103
- const parsed = parseDiscordTarget(params.target, { defaultKind: resolveDiscordOutboundTargetKindHint(params) });
1104
- if (!parsed) return null;
1105
- const explicitThreadId = params.threadId == null ? void 0 : String(params.threadId).trim();
1106
- const peerId = explicitThreadId || parsed.id;
1107
- const isDm = parsed.kind === "user" && !explicitThreadId;
1108
- const recipientSessionExact = /^\d+$/.test(peerId);
1109
- const peer = {
1110
- kind: isDm ? "direct" : "channel",
1111
- id: peerId
1112
- };
1113
- const baseSessionKey = buildOutboundBaseSessionKey({
1114
- cfg: params.cfg,
1115
- agentId: params.agentId,
1116
- channel: "discord",
1117
- accountId: params.accountId,
1118
- peer
1119
- });
1120
- return buildThreadAwareOutboundSessionRoute({
1121
- route: {
1122
- sessionKey: baseSessionKey,
1123
- baseSessionKey,
1124
- recipientSessionExact,
1125
- peer,
1126
- chatType: isDm ? "direct" : "channel",
1127
- from: isDm ? `discord:${peerId}` : `discord:channel:${peerId}`,
1128
- to: isDm ? `user:${peerId}` : `channel:${peerId}`
1129
- },
1130
- threadId: params.threadId,
1131
- precedence: ["threadId"],
1132
- useSuffix: false
1133
- });
1134
- }
1135
- function resolveDiscordOutboundTargetKindHint(params) {
1136
- const resolvedKind = params.resolvedTarget?.kind;
1137
- if (resolvedKind === "user") return "user";
1138
- if (resolvedKind === "group" || resolvedKind === "channel") return "channel";
1139
- const target = params.target.trim();
1140
- if (/^channel:/i.test(target)) return "channel";
1141
- if (/^(user:|discord:|@|<@!?)/i.test(target)) return "user";
1142
- return "channel";
1143
- }
1144
- //#endregion
1145
- export { parseDiscordActivityCustomIdForInteraction as C, parseDiscordModalCustomIdForInteraction as D, parseDiscordModalCustomId as E, decodeCustomIdComponent as O, parseDiscordActivityCustomId as S, parseDiscordComponentCustomIdForInteraction as T, DISCORD_COMPONENT_CUSTOM_ID_KEY as _, buildDiscordPresentationComponents as a, buildDiscordComponentCustomId as b, buildExecApprovalCustomId as c, createDiscordFormModal as d, buildDiscordComponentMessage as f, resolveDiscordComponentAttachmentName as g, readDiscordComponentSpec as h, buildDiscordInteractiveComponents as i, encodeCustomIdComponent as k, parseExecApprovalData as l, DISCORD_COMPONENT_ATTACHMENT_PREFIX as m, components_exports as n, shared_interactive_exports as o, buildDiscordComponentMessageFlags as p, formatDiscordComponentEventText as r, parseDiscordQuestionData as s, resolveDiscordOutboundSessionRoute as t, DiscordFormModal as u, DISCORD_MODAL_CUSTOM_ID_KEY as v, parseDiscordComponentCustomId as w, buildDiscordModalCustomId as x, buildDiscordActivityCustomId as y };
1072
+ export { parseDiscordComponentCustomId as C, decodeCustomIdComponent as D, parseDiscordModalCustomIdForInteraction as E, encodeCustomIdComponent as O, parseDiscordActivityCustomIdForInteraction as S, parseDiscordModalCustomId as T, DISCORD_MODAL_CUSTOM_ID_KEY as _, shared_interactive_exports as a, buildDiscordModalCustomId as b, parseExecApprovalData as c, buildDiscordComponentMessage as d, buildDiscordComponentMessageFlags as f, DISCORD_COMPONENT_CUSTOM_ID_KEY as g, resolveDiscordComponentAttachmentName as h, buildDiscordPresentationComponents as i, DiscordFormModal as l, readDiscordComponentSpec as m, formatDiscordComponentEventText as n, parseDiscordQuestionData as o, DISCORD_COMPONENT_ATTACHMENT_PREFIX as p, buildDiscordInteractiveComponents as r, buildExecApprovalCustomId as s, components_exports as t, createDiscordFormModal as u, buildDiscordActivityCustomId as v, parseDiscordComponentCustomIdForInteraction as w, parseDiscordActivityCustomId as x, buildDiscordComponentCustomId as y };
@@ -1,6 +1,8 @@
1
+ import { normalizeLegacyDmAliases } from "openclaw/plugin-sdk/channel-config-helpers";
1
2
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
2
- import { buildSecretInputSchema, registerSensitiveConfigSchema } from "openclaw/plugin-sdk/secret-input";
3
3
  import { ChannelBotLoopProtectionSchema, ChannelDangerouslyAllowNameMatchingSchema, ChannelPreviewStreamingConfigSchema, ChannelStreamingProgressSchema, ProviderCommandsSchema, TtsConfigSchema, buildChannelAllowBotsSchema, buildChannelConfigSchema, buildChannelExecApprovalsSchema, buildChannelReactionShape, buildCommonChannelAccountShape, buildGroupEntrySchema, requireAllowlistAllowFrom, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
4
+ import { asObjectRecord } from "openclaw/plugin-sdk/runtime-doctor-migrations";
5
+ import { buildSecretInputSchema, registerSensitiveConfigSchema } from "openclaw/plugin-sdk/secret-input";
4
6
  import { z } from "zod";
5
7
  import { createChannelConfigUiHints } from "openclaw/plugin-sdk/channel-core";
6
8
  //#region extensions/discord/src/config-ui-hints.ts
@@ -20,16 +22,16 @@ const discordChannelConfigUiHints = {
20
22
  },
21
23
  nativeCommands: true,
22
24
  streaming: {
23
- "": "Unified Discord stream preview mode: \"off\" | \"partial\" | \"block\" | \"progress\". \"progress\" keeps a single editable progress draft until final delivery. Legacy boolean/streamMode keys are auto-mapped.",
24
- mode: "Canonical Discord preview mode: \"off\" | \"partial\" | \"block\" | \"progress\".",
25
+ "": "Discord preview streaming is off by default. Set mode to \"partial\", \"block\", or \"progress\" to opt in. Run openclaw doctor --fix to migrate legacy keys.",
26
+ mode: "Discord preview mode: \"off\" | \"partial\" | \"block\" | \"progress\". Default: \"off\".",
25
27
  chunkMode: "Chunking mode for outbound Discord text delivery: \"length\" (default) or \"newline\".",
26
- "block.enabled": "Enable chunked block-style Discord preview delivery when channels.discord.streaming.mode=\"block\".",
28
+ "block.enabled": "Enable normal Discord block replies. This takes precedence over editable preview delivery.",
27
29
  "block.coalesce": "Merge streamed Discord block replies before final delivery.",
28
30
  "preview.chunk.minChars": "Minimum chars before emitting a Discord stream preview update when channels.discord.streaming.mode=\"block\" (default: 200).",
29
31
  "preview.chunk.maxChars": "Target max size for a Discord stream preview chunk when channels.discord.streaming.mode=\"block\" (default: 800; clamped to channels.discord.textChunkLimit).",
30
32
  "preview.chunk.breakPreference": "Preferred breakpoints for Discord draft chunks (paragraph | newline | sentence). Default: paragraph.",
31
33
  "preview.toolProgress": "Show tool/progress activity in the live draft preview message (default: true). Set false to hide interim tool updates while the draft preview stays active.",
32
- "preview.commandText": "Command/exec detail in preview tool-progress lines: \"raw\" preserves released behavior; \"status\" shows only the tool label."
34
+ "preview.commandText": "Command/exec detail in preview tool-progress lines: \"status\" is the safe default; \"raw\" opts into command text."
33
35
  },
34
36
  progress: { includeCommentary: true }
35
37
  }),
@@ -73,6 +75,10 @@ const discordChannelConfigUiHints = {
73
75
  label: "Discord Component TTL (ms)",
74
76
  help: "How long sent Discord component callbacks remain registered. Default is 1800000 (30 minutes); maximum is 86400000 (24 hours)."
75
77
  },
78
+ "intents.messageContent": {
79
+ label: "Discord Message Content Intent",
80
+ help: "Request the privileged Message Content intent (default: true). Set false only for mention-only guild operation when Discord cannot grant the intent; DMs and explicit mentions still include message content."
81
+ },
76
82
  "intents.presence": {
77
83
  label: "Discord Presence Intent",
78
84
  help: "Enable the Guild Presences privileged intent. Must also be enabled in the Discord Developer Portal. Allows tracking user activities (e.g. Spotify). Default: false."
@@ -164,7 +170,11 @@ const discordChannelConfigUiHints = {
164
170
  },
165
171
  "voice.autoJoin": {
166
172
  label: "Discord Voice Auto-Join",
167
- help: "Voice channels to auto-join on startup (list of guildId/channelId entries)."
173
+ help: "Voice channels to auto-join (list of guildId/channelId entries). Set whenOccupied on an entry to connect only while humans are present."
174
+ },
175
+ "voice.autoJoin.*.whenOccupied": {
176
+ label: "Discord Voice Auto-Join When Occupied",
177
+ help: "Join and remain in this auto-managed voice channel only while at least one human is present. The OpenClaw bot and other bots do not count. Default: false."
168
178
  },
169
179
  "voice.allowedChannels": {
170
180
  label: "Discord Voice Allowed Channels",
@@ -297,7 +307,7 @@ const discordChannelConfigUiHints = {
297
307
  },
298
308
  activities: {
299
309
  label: "Discord Activities",
300
- help: "Enable Discord Activity widgets for this account. Routes, the agent tool, and the launch handler remain disabled when this block is absent."
310
+ help: "Enable the Discord Activity presenter for the core show_widget tool on this account. Activity routes and the launch handler remain disabled when this block is absent."
301
311
  },
302
312
  "activities.clientSecret": {
303
313
  label: "Discord Activities Client Secret",
@@ -385,7 +395,8 @@ const DiscordGuildSchema = buildGroupEntrySchema({
385
395
  ] });
386
396
  const DiscordVoiceAutoJoinSchema = z.object({
387
397
  guildId: z.string().min(1),
388
- channelId: z.string().min(1)
398
+ channelId: z.string().min(1),
399
+ whenOccupied: z.boolean().optional()
389
400
  }).strict();
390
401
  const DiscordVoiceAllowedChannelSchema = z.object({
391
402
  guildId: z.string().min(1),
@@ -450,7 +461,7 @@ const DiscordVoiceSchema = z.object({
450
461
  captureSilenceGraceMs: z.number().int().positive().max(3e4).optional(),
451
462
  tts: TtsConfigSchema.optional()
452
463
  }).strict().optional();
453
- const DiscordAccountSchema = z.object({
464
+ const DiscordAccountSchemaBase = z.object({
454
465
  ...buildCommonChannelAccountShape({
455
466
  omit: ["groupAllowFrom"],
456
467
  groupPolicyDefault: true,
@@ -509,6 +520,7 @@ const DiscordAccountSchema = z.object({
509
520
  defaultSpawnContext: z.enum(["isolated", "fork"]).optional()
510
521
  }).strict().optional(),
511
522
  intents: z.object({
523
+ messageContent: z.boolean().optional(),
512
524
  presence: z.boolean().optional(),
513
525
  guildMembers: z.boolean().optional(),
514
526
  voiceStates: z.boolean().optional()
@@ -578,7 +590,25 @@ const DiscordAccountSchema = z.object({
578
590
  path: ["autoPresence", "minUpdateIntervalMs"]
579
591
  });
580
592
  });
581
- const DiscordChannelConfigSchema = buildChannelConfigSchema(DiscordAccountSchema.extend({
593
+ function normalizeShippedDiscordDmAliases(value) {
594
+ const entry = asObjectRecord(value);
595
+ if (!entry) return value;
596
+ const updated = normalizeLegacyDmAliases({
597
+ entry,
598
+ pathPrefix: "channels.discord",
599
+ changes: []
600
+ }).entry;
601
+ const dm = asObjectRecord(updated.dm);
602
+ if (!dm || dm.policy === void 0 && dm.allowFrom === void 0) return updated;
603
+ const { policy: _policy, allowFrom: _allowFrom, ...retainedDm } = dm;
604
+ const { dm: _dm, ...rest } = updated;
605
+ return Object.keys(retainedDm).length > 0 ? {
606
+ ...rest,
607
+ dm: retainedDm
608
+ } : rest;
609
+ }
610
+ const DiscordAccountSchema = z.preprocess(normalizeShippedDiscordDmAliases, DiscordAccountSchemaBase);
611
+ const DiscordConfigSchemaBase = DiscordAccountSchemaBase.extend({
582
612
  accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
583
613
  defaultAccount: z.string().optional()
584
614
  }).superRefine((value, ctx) => {
@@ -626,6 +656,7 @@ const DiscordChannelConfigSchema = buildChannelConfigSchema(DiscordAccountSchema
626
656
  message: "channels.discord.accounts.*.dmPolicy=\"allowlist\" requires channels.discord.accounts.*.allowFrom (or channels.discord.allowFrom) to contain at least one sender ID"
627
657
  });
628
658
  }
629
- }), { uiHints: discordChannelConfigUiHints });
659
+ });
660
+ const DiscordChannelConfigSchema = buildChannelConfigSchema(z.preprocess(normalizeShippedDiscordDmAliases, DiscordConfigSchemaBase), { uiHints: discordChannelConfigUiHints });
630
661
  //#endregion
631
662
  export { DiscordChannelConfigSchema as t };
@@ -0,0 +1,43 @@
1
+ import { c as shouldIgnoreStaleDiscordRouteBinding } from "./approval-message-safety-XGFPbDoI.js";
2
+ import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
3
+ import { resolveConfiguredBindingRoute, resolveRuntimeConversationBindingRoute } from "openclaw/plugin-sdk/conversation-binding-runtime";
4
+ //#region extensions/discord/src/monitor/conversation-binding-route.ts
5
+ function resolveDiscordConversationBindingRoute(params) {
6
+ let runtimeRoute = resolveRuntimeConversationBindingRoute({
7
+ route: params.route,
8
+ touchBinding: params.touchBinding,
9
+ conversation: {
10
+ channel: "discord",
11
+ accountId: params.accountId,
12
+ conversationId: params.runtimeConversationId,
13
+ parentConversationId: params.parentConversationId
14
+ }
15
+ });
16
+ if (shouldIgnoreStaleDiscordRouteBinding({
17
+ bindingRecord: runtimeRoute.bindingRecord,
18
+ route: params.route
19
+ })) {
20
+ logVerbose(`discord: ignoring stale route binding for conversation ${params.runtimeConversationId} (${runtimeRoute.bindingRecord?.targetSessionKey} -> ${params.route.sessionKey})`);
21
+ runtimeRoute = {
22
+ bindingOwnerAvailable: true,
23
+ bindingRecord: null,
24
+ route: params.route
25
+ };
26
+ }
27
+ const configuredRoute = runtimeRoute.bindingRecord ? null : resolveConfiguredBindingRoute({
28
+ cfg: params.cfg,
29
+ route: params.route,
30
+ conversation: {
31
+ channel: "discord",
32
+ accountId: params.accountId,
33
+ conversationId: params.configuredConversationId,
34
+ parentConversationId: params.parentConversationId
35
+ }
36
+ });
37
+ return {
38
+ runtimeRoute,
39
+ configuredRoute
40
+ };
41
+ }
42
+ //#endregion
43
+ export { resolveDiscordConversationBindingRoute as t };
@@ -1,5 +1,5 @@
1
- import { Yt as __exportAll } from "./chunk-DX4jpFQP.js";
2
- import { a as mergeDiscordAccountConfig, c as resolveDiscordAccountAllowFrom, o as resolveDefaultDiscordAccountId } from "./accounts-DJTf1LtA.js";
1
+ import { en as __exportAll } from "./retry-DNsVTeTo.js";
2
+ import { l as resolveDiscordAccountAllowFrom, o as mergeDiscordAccountConfig, s as resolveDefaultDiscordAccountId } from "./accounts-DNl-oCEw.js";
3
3
  import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
4
4
  import { createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-config-runtime";
5
5
  //#region extensions/discord/src/directory-config.ts
@@ -1,4 +1,4 @@
1
- import { n as listDiscordDirectoryGroupsFromConfig, r as listDiscordDirectoryPeersFromConfig } from "./directory-config-D_LojtCe.js";
1
+ import { n as listDiscordDirectoryGroupsFromConfig, r as listDiscordDirectoryPeersFromConfig } from "./directory-config-XyCfoqLg.js";
2
2
  //#region extensions/discord/directory-contract-api.ts
3
3
  const discordDirectoryContractPlugin = {
4
4
  id: "discord",
@@ -1,7 +1,7 @@
1
- import { Yt as __exportAll } from "./chunk-DX4jpFQP.js";
2
- import { p as normalizeDiscordToken, s as resolveDiscordAccount } from "./accounts-DJTf1LtA.js";
3
- import { r as fetchDiscord, t as DISCORD_DIRECTORY_LOOKUP_TIMEOUT_MS } from "./api-DnrK9gML.js";
4
- import { a as normalizeDiscordSlug } from "./allow-list-DzKrtfah.js";
1
+ import { en as __exportAll } from "./retry-DNsVTeTo.js";
2
+ import { c as resolveDiscordAccount, m as normalizeDiscordToken } from "./accounts-DNl-oCEw.js";
3
+ import { r as fetchDiscord, t as DISCORD_DIRECTORY_LOOKUP_TIMEOUT_MS } from "./api-DNVbJyEy.js";
4
+ import { a as normalizeDiscordSlug } from "./allow-list-BDMimASD.js";
5
5
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/routing";
6
6
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  //#region extensions/discord/src/directory-cache-state.ts
@@ -123,6 +123,7 @@ async function listDiscordDirectoryPeersLive(params) {
123
123
  if (!query) return [];
124
124
  const guilds = await listDiscordGuilds(token);
125
125
  const rows = [];
126
+ const seenUserIds = /* @__PURE__ */ new Set();
126
127
  const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 25;
127
128
  for (const guild of guilds) {
128
129
  const paramsObj = new URLSearchParams({
@@ -143,6 +144,8 @@ async function listDiscordDirectoryPeersLive(params) {
143
144
  user.username ? `@${user.username}` : null
144
145
  ]
145
146
  });
147
+ if (seenUserIds.has(user.id)) continue;
148
+ seenUserIds.add(user.id);
146
149
  const name = member.nick?.trim() || user.global_name?.trim() || user.username?.trim();
147
150
  rows.push({
148
151
  kind: "user",