@openclaw/discord 2026.8.1-beta.2 → 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 (100) hide show
  1. package/dist/{account-inspect-CqzrH14k.js → account-inspect-D5xmUpnj.js} +1 -1
  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/{activities-api-DS7vpdtp.js → activities-api-KCBAmWxD.js} +133 -175
  6. package/dist/activities-api.js +1 -1
  7. package/dist/api.js +17 -17
  8. package/dist/{approval-handler.runtime-B6yEV79q.js → approval-handler.runtime-B12RCYr8.js} +6 -6
  9. package/dist/{approval-message-safety-AcIxvrf3.js → approval-message-safety-XGFPbDoI.js} +89 -3
  10. package/dist/{audit-Db-rlntl.js → audit-Yc7eZDd6.js} +5 -5
  11. package/dist/{channel-actions-5s_hjYKd.js → channel-actions-Dr2QI8Bf.js} +10 -6
  12. package/dist/{channel-actions.runtime-CLKrx6tr.js → channel-actions.runtime-CODTvo7c.js} +53 -68
  13. package/dist/{channel-CnnbJSt5.js → channel-cXYzGc4L.js} +74 -50
  14. package/dist/channel-config-api.js +1 -1
  15. package/dist/channel-plugin-api.js +1 -1
  16. package/dist/{channel.setup-BFAO-V1P.js → channel.setup-CSiwnMgs.js} +5 -5
  17. package/dist/{outbound-session-route-DxLkrgKQ.js → components-ezGKo_Yh.js} +3 -49
  18. package/dist/{config-schema-BgEskFAs.js → config-schema-DC5kDtQ5.js} +33 -7
  19. package/dist/conversation-binding-route-D6rCQ5S0.js +43 -0
  20. package/dist/{directory-config-BVX7Cflc.js → directory-config-XyCfoqLg.js} +2 -2
  21. package/dist/directory-contract-api.js +1 -1
  22. package/dist/{directory-live-DGIGxpIo.js → directory-live-Df0NbFJB.js} +3 -3
  23. package/dist/{doctor-BzC-TLnB.js → doctor-DYY31GrO.js} +33 -12
  24. package/dist/doctor-contract-api.js +3 -3
  25. package/dist/{handle-action.guild-admin-BNVTUISF.js → handle-action.guild-admin-6Tvvinsa.js} +2 -2
  26. package/dist/index.js +3 -4
  27. package/dist/{message-handler-Unk_f10F.js → message-handler-B61q5MOF.js} +82 -6
  28. package/dist/{message-handler.preflight-C9zecxR9.js → message-handler.preflight-Bzg_Hawz.js} +36 -46
  29. package/dist/{message-handler.process-DWQ6UHHu.js → message-handler.process--4_712Ec.js} +159 -183
  30. package/dist/{message-utils-BSCPVgy0.js → message-utils-BXLLnFl6.js} +31 -32
  31. package/dist/{outbound-adapter-D9nRivT1.js → outbound-adapter-DxHTOqiN.js} +83 -53
  32. package/dist/{outbound-components-cTzwBJ60.js → outbound-components-B2PL0ooH.js} +3 -3
  33. package/dist/owner-access-Bg41VzAf.js +107 -0
  34. package/dist/{pluralkit-DdiJ9KMK.js → pluralkit-dUl4v1X3.js} +1 -1
  35. package/dist/{probe-Bl-k5_Ku.js → probe-BTdtVRwU.js} +1 -1
  36. package/dist/{probe.runtime-M9fREKvp.js → probe.runtime-Dq5-PRKT.js} +1 -1
  37. package/dist/{provider-DvSNb-2v.js → provider-DlK_ynqt.js} +356 -967
  38. package/dist/{provider-session.runtime-BUEqD0rA.js → provider-session.runtime-CvvphHvs.js} +3 -3
  39. package/dist/provider.runtime-CzKGRiAF.js +2 -0
  40. package/dist/{realtime-session.runtime-C7wen-aF.js → realtime-session.runtime-ysbMG8v7.js} +24 -13
  41. package/dist/{resolve-allowlist-common-D04lpEzb.js → resolve-allowlist-common-DIVXbD1P.js} +2 -2
  42. package/dist/{resolve-channels-Dm7jEeg6.js → resolve-channels-CGeUoIqb.js} +3 -3
  43. package/dist/{resolve-users-bktrCUUU.js → resolve-users-BtKxLUZv.js} +2 -2
  44. package/dist/{retry-CLpJcXXf.js → retry-DNsVTeTo.js} +18 -64
  45. package/dist/{runtime-DoHjckYm.js → runtime-CQovMsH3.js} +9 -10
  46. package/dist/{runtime-DGsSyDTS.js → runtime-D71_VFLr.js} +29 -43
  47. package/dist/runtime-api.actions.js +2 -2
  48. package/dist/runtime-api.js +22 -21
  49. package/dist/runtime-api.lookup.js +5 -5
  50. package/dist/runtime-api.monitor-CzeTYF32.js +6 -0
  51. package/dist/runtime-api.monitor.js +6 -5
  52. package/dist/runtime-api.send.js +6 -6
  53. package/dist/runtime-api.threads.js +4 -4
  54. package/dist/{sdk-runtime-QpY8PvCh.js → sdk-runtime-37OU6_Yh.js} +3 -4
  55. package/dist/{secret-config-contract-B1akrlyY.js → secret-config-contract-B3i6WxV5.js} +53 -25
  56. package/dist/secret-contract-api.js +1 -1
  57. package/dist/security-audit-contract-api.js +1 -1
  58. package/dist/{security-audit.runtime-CPN0xFYz.js → security-audit.runtime-BVKF9VtY.js} +1 -1
  59. package/dist/{send-B7Q6Oc1I.js → send-rQIffSYu.js} +10 -10
  60. package/dist/{send.components-iJyFXw9L.js → send.components-ChK1wS7w.js} +14 -24
  61. package/dist/{send.outbound-DS5tHwfR.js → send.outbound-HIkgji_7.js} +26 -16
  62. package/dist/{send.reactions-BAn0iiVQ.js → send.reactions-gmJ6yFPo.js} +2 -2
  63. package/dist/{send.receipt-IPI1p-SP.js → send.receipt-Cnhoautb.js} +11 -1
  64. package/dist/{send.shared-BmwFjW6T.js → send.shared-B3fpaLWJ.js} +104 -104
  65. package/dist/{sender-identity-QtalnCYv.js → sender-identity-BJtfyise.js} +1 -1
  66. package/dist/setup-plugin-api.js +1 -1
  67. package/dist/{shared-BIXIX6uJ.js → shared-CaAaoksy.js} +15 -10
  68. package/dist/subagent-hooks-CNT2ZJTJ.js +49 -0
  69. package/dist/subagent-hooks-api.js +2 -2
  70. package/dist/{subagent-progress-DhaE_h8a.js → subagent-progress-FPLqBY1s.js} +5 -4
  71. package/dist/{system-events-CTqDf6_s.js → system-events-Nuwoq_au.js} +2 -2
  72. package/dist/{target-resolver-maOKSqO0.js → target-resolver-DIXShHaZ.js} +3 -3
  73. package/dist/targets-B2pf49zc.js +3 -0
  74. package/dist/{thread-bindings-3L8p82GV.js → thread-bindings-VlymUnbz.js} +29 -26
  75. package/dist/{thread-bindings.manager-DCIH6WDC.js → thread-bindings.manager-CngsgErq.js} +9 -8
  76. package/dist/{thread-bindings.session-updates-bJ2KQFY6.js → thread-bindings.session-updates-CJb-BpPL.js} +1 -1
  77. package/dist/{thread-bindings.state-aeV8HNgV.js → thread-bindings.state-D8OMdk4O.js} +1 -1
  78. package/dist/threading-DU6pgeGm.js +456 -0
  79. package/dist/transcripts-source-DeIMNbRh.js +265 -0
  80. package/dist/transcripts-source-api.js +7 -2
  81. package/dist/{typing-CS-nNVoe.js → typing-oLiS5Z13.js} +1 -1
  82. package/dist/{voice-runtime-BEdqNcST.js → voice-runtime-CjNFaynA.js} +181 -91
  83. package/openclaw.plugin.json +12 -6
  84. package/package.json +4 -6
  85. package/dist/accounts-DJTf1LtA.js +0 -219
  86. package/dist/channel-api-CAJ0wMoV.js +0 -21
  87. package/dist/provider.runtime-C9PnJZbZ.js +0 -2
  88. package/dist/runtime-api.monitor-CHxrFPnW.js +0 -5
  89. package/dist/subagent-hooks-CTt5KCJr.js +0 -120
  90. package/dist/targets-yUFTHoAx.js +0 -3
  91. package/dist/transcripts-source-CfFMexGt.js +0 -123
  92. package/node_modules/p-map/index.d.ts +0 -155
  93. package/node_modules/p-map/index.js +0 -285
  94. package/node_modules/p-map/license +0 -9
  95. package/node_modules/p-map/package.json +0 -57
  96. package/node_modules/p-map/readme.md +0 -190
  97. package/dist/{allow-list-DnxIlPP2.js → allow-list-BDMimASD.js} +1 -1
  98. package/dist/{audit-core-xwjIczO0.js → audit-core-BCX8UeFH.js} +1 -1
  99. package/dist/{doctor-contract-DLa-BTiU.js → doctor-contract-DsWvKIXF.js} +1 -1
  100. package/dist/{security-audit-jGnJF__m.js → security-audit-BfJhAXZp.js} +1 -1
@@ -1,18 +1,20 @@
1
- import { C as ReadyListener, D as VoiceStateUpdateListener, N as isUnknownDiscordVoiceStateError, Wt as getGuildVoiceState, h as discord_exports, p as parseDiscordTarget, w as ResumedListener } from "./retry-CLpJcXXf.js";
1
+ import { S as ResumedListener, T as VoiceStateUpdateListener, Ut as getGuildVoiceState, j as isUnknownDiscordVoiceStateError, p as discord_exports, x as ReadyListener } from "./retry-DNsVTeTo.js";
2
+ import { k as parseDiscordTarget } from "./accounts-DNl-oCEw.js";
2
3
  import { t as getDiscordRuntime } from "./runtime-DgnVQ7zW.js";
3
- import { a as normalizeDiscordSlug, h as resolveDiscordOwnerAccess, x as formatDiscordUserTag } from "./allow-list-DnxIlPP2.js";
4
- import { a as formatMention } from "./send.outbound-DS5tHwfR.js";
5
- import { g as buildDiscordGroupSystemPrompt, l as resolveDiscordVoiceAccess, m as resolveDiscordVoiceEnabled, u as authorizeDiscordVoiceIngress } from "./provider-DvSNb-2v.js";
6
- import { S as formatVoiceLogPreview, _ as decodeOpusStream, a as SPEAKING_READY_TIMEOUT_MS, b as formatVoiceIngressPrompt, c as isDiscordRealtimeVoiceMode, d as resolveDiscordVoiceMode, f as resolveVoiceTimeoutMs, g as createDiscordOpusPlaybackStream, i as PLAYBACK_READY_TIMEOUT_MS, l as isVoiceChannel, n as CAPTURE_FINALIZE_GRACE_MS, o as VOICE_CONNECT_READY_TIMEOUT_MS, s as VOICE_RECONNECT_GRACE_MS, t as loadDiscordVoiceSdk, u as logVoiceVerbose, v as decodeOpusStreamChunks, x as maybeControlDiscordVoiceAgentRun, y as writeVoiceWavFile } from "./sdk-runtime-QpY8PvCh.js";
7
- import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
8
- import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
4
+ import { a as normalizeDiscordSlug, h as resolveDiscordOwnerAccess, x as formatDiscordUserTag } from "./allow-list-BDMimASD.js";
5
+ import { a as formatMention } from "./send.outbound-HIkgji_7.js";
6
+ import { n as resolveDiscordVoiceEnabled, r as authorizeDiscordVoiceIngress, t as resolveDiscordVoiceAccess } from "./owner-access-Bg41VzAf.js";
7
+ import { p as buildDiscordGroupSystemPrompt, y as resolveFetchedDiscordThreadLikeChannelContext } from "./provider-DlK_ynqt.js";
8
+ import { S as formatVoiceLogPreview, _ as decodeOpusStream, a as SPEAKING_READY_TIMEOUT_MS, b as formatVoiceIngressPrompt, c as isDiscordRealtimeVoiceMode, d as resolveDiscordVoiceMode, f as resolveVoiceTimeoutMs, g as createDiscordOpusPlaybackStream, i as PLAYBACK_READY_TIMEOUT_MS, l as isVoiceChannel, n as CAPTURE_FINALIZE_GRACE_MS, o as VOICE_CONNECT_READY_TIMEOUT_MS, s as VOICE_RECONNECT_GRACE_MS, t as loadDiscordVoiceSdk, u as logVoiceVerbose, v as decodeOpusStreamChunks, x as maybeControlDiscordVoiceAgentRun, y as writeVoiceWavFile } from "./sdk-runtime-37OU6_Yh.js";
9
9
  import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
10
10
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
11
11
  import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
12
+ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
13
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
12
14
  import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
13
15
  import { escapeRegExp, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
14
16
  import { stripInlineDirectiveTagsForDisplay } from "openclaw/plugin-sdk/text-chunking";
15
- import path from "node:path";
17
+ import { unlinkIfExists } from "openclaw/plugin-sdk/media-runtime";
16
18
  import { agentCommandFromIngress, resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
17
19
  import { enqueueRoutedSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
18
20
  import { Readable } from "node:stream";
@@ -88,6 +90,7 @@ async function runDiscordVoiceAgentTurn(params) {
88
90
  agentId: params.entry.route.agentId,
89
91
  messageChannel: "discord",
90
92
  messageProvider: DISCORD_VOICE_MESSAGE_PROVIDER,
93
+ accountId: params.accountId,
91
94
  extraSystemPrompt: context.extraSystemPrompt,
92
95
  senderIsOwner: context.senderIsOwner,
93
96
  allowModelOverride: Boolean(voiceModel),
@@ -193,7 +196,7 @@ function countDiscordVoiceHumanParticipants(params) {
193
196
  const userId = state.user_id?.trim();
194
197
  if (!userId || userId === params.botUserId || knownUserIds.has(userId)) continue;
195
198
  knownUserIds.add(userId);
196
- if (state.member?.user?.bot !== true) count += 1;
199
+ if (state.member?.user && state.member.user.bot !== true) count += 1;
197
200
  }
198
201
  for (const rawUserId of params.additionalUserIds ?? []) {
199
202
  const userId = rawUserId.trim();
@@ -654,7 +657,8 @@ function normalizeVoiceChannelResidencies(entries) {
654
657
  const channelId = entry.channelId?.trim();
655
658
  if (guildId && channelId) normalized.push({
656
659
  guildId,
657
- channelId
660
+ channelId,
661
+ ...entry.whenOccupied === true ? { whenOccupied: true } : {}
658
662
  });
659
663
  }
660
664
  return normalized;
@@ -1017,6 +1021,7 @@ var DiscordVoiceFollowing = class {
1017
1021
  }
1018
1022
  resolveVoiceResidencyTarget(guildId) {
1019
1023
  const autoJoinTarget = this.params.autoJoinChannels.toReversed().find((entry) => entry.guildId === guildId);
1024
+ if (autoJoinTarget?.whenOccupied) return null;
1020
1025
  if (autoJoinTarget && this.params.isAllowedVoiceChannel(autoJoinTarget)) return autoJoinTarget;
1021
1026
  if (this.params.allowedChannels === null) return null;
1022
1027
  const guildAllowed = this.params.allowedChannels.filter((entry) => entry.guildId === guildId);
@@ -1142,6 +1147,7 @@ async function synthesizeVoiceReplyAudio(params) {
1142
1147
  release: streamResult.release,
1143
1148
  speakText
1144
1149
  };
1150
+ const streamFailure = streamResult && !streamResult.success ? streamResult.attempts?.findLast((attempt) => attempt.outcome === "failed") : void 0;
1145
1151
  const result = await runtime.tts.textToSpeech({
1146
1152
  text: speakText,
1147
1153
  cfg: prepared.cfg,
@@ -1156,7 +1162,11 @@ async function synthesizeVoiceReplyAudio(params) {
1156
1162
  status: "ok",
1157
1163
  mode: "file",
1158
1164
  audioPath: result.audioPath,
1159
- speakText
1165
+ speakText,
1166
+ ...streamFailure ? { streamFailure: {
1167
+ provider: streamFailure.provider,
1168
+ reasonCode: streamFailure.reasonCode
1169
+ } } : {}
1160
1170
  };
1161
1171
  }
1162
1172
  //#endregion
@@ -1219,10 +1229,12 @@ async function processDiscordVoiceSegment(params) {
1219
1229
  logger$4.info(`discord voice: active-run control handled mode=${control.result.mode} ok=${control.result.ok} active=${control.result.active} reason=${control.result.reason ?? "none"} session=${entry.route.sessionKey}`);
1220
1230
  replyText = control.speakText ?? "";
1221
1231
  } else {
1232
+ const prompt = formatVoiceIngressPrompt(transcript, ingress.speakerLabel);
1222
1233
  const turn = await runDiscordVoiceAgentTurn({
1223
1234
  entry,
1235
+ accountId: params.accountId,
1224
1236
  userId,
1225
- message: formatVoiceIngressPrompt(transcript, ingress.speakerLabel),
1237
+ message: prompt,
1226
1238
  cfg: params.cfg,
1227
1239
  discordConfig: params.discordConfig,
1228
1240
  runtime: params.runtime,
@@ -1256,26 +1268,30 @@ async function processDiscordVoiceSegment(params) {
1256
1268
  logger$4.warn(`discord voice: TTS failed: ${voiceReplyAudio.error ?? "unknown error"}`);
1257
1269
  return;
1258
1270
  }
1271
+ const streamFailure = voiceReplyAudio.mode === "file" ? voiceReplyAudio.streamFailure : void 0;
1272
+ if (streamFailure && !entry.ttsStreamFallbackWarned) {
1273
+ entry.ttsStreamFallbackWarned = true;
1274
+ logger$4.warn(`discord voice: streaming TTS failed provider=${streamFailure.provider} reasonCode=${streamFailure.reasonCode}; using file fallback`);
1275
+ }
1259
1276
  logVoiceVerbose(`tts ok (${voiceReplyAudio.speakText.length} chars): guild ${entry.guildId} channel ${entry.channelId}`);
1277
+ const releaseAudio = voiceReplyAudio.mode === "stream" ? voiceReplyAudio.release : () => unlinkIfExists(voiceReplyAudio.audioPath);
1278
+ if (entry.sessionLifecycle.status === "stopped") {
1279
+ await releaseAudio?.();
1280
+ return;
1281
+ }
1260
1282
  params.enqueuePlayback(entry, async () => {
1261
1283
  const voiceSdk = loadDiscordVoiceSdk();
1262
- const releaseAudioStream = voiceReplyAudio.mode === "stream" ? voiceReplyAudio.release : void 0;
1263
1284
  try {
1264
- if (voiceReplyAudio.mode === "stream") {
1265
- logVoiceVerbose(`playback start: guild ${entry.guildId} channel ${entry.channelId} stream`);
1266
- const nodeStream = Readable.fromWeb(voiceReplyAudio.audioStream);
1267
- const resource = voiceSdk.createAudioResource(createDiscordOpusPlaybackStream(nodeStream), { inputType: voiceSdk.StreamType.Opus });
1268
- entry.player.play(resource);
1269
- } else {
1270
- logVoiceVerbose(`playback start: guild ${entry.guildId} channel ${entry.channelId} file ${path.basename(voiceReplyAudio.audioPath)}`);
1271
- const resource = voiceSdk.createAudioResource(createDiscordOpusPlaybackStream(voiceReplyAudio.audioPath), { inputType: voiceSdk.StreamType.Opus });
1272
- entry.player.play(resource);
1273
- }
1285
+ if (entry.sessionLifecycle.status === "stopped") return;
1286
+ const input = voiceReplyAudio.mode === "stream" ? Readable.fromWeb(voiceReplyAudio.audioStream) : voiceReplyAudio.audioPath;
1287
+ logVoiceVerbose(`playback start: guild ${entry.guildId} channel ${entry.channelId} ${voiceReplyAudio.mode}`);
1288
+ const resource = voiceSdk.createAudioResource(createDiscordOpusPlaybackStream(input), { inputType: voiceSdk.StreamType.Opus });
1289
+ entry.player.play(resource);
1274
1290
  await voiceSdk.entersState(entry.player, voiceSdk.AudioPlayerStatus.Playing, PLAYBACK_READY_TIMEOUT_MS).catch(() => void 0);
1275
1291
  await voiceSdk.entersState(entry.player, voiceSdk.AudioPlayerStatus.Idle, SPEAKING_READY_TIMEOUT_MS).catch(() => void 0);
1276
1292
  logVoiceVerbose(`playback done: guild ${entry.guildId} channel ${entry.channelId}`);
1277
1293
  } finally {
1278
- await releaseAudioStream?.();
1294
+ await releaseAudio?.();
1279
1295
  }
1280
1296
  });
1281
1297
  }
@@ -1415,6 +1431,7 @@ var DiscordVoiceReceive = class {
1415
1431
  async processSegment(params) {
1416
1432
  await processDiscordVoiceSegment({
1417
1433
  ...params,
1434
+ accountId: this.params.accountId,
1418
1435
  cfg: this.params.cfg,
1419
1436
  discordConfig: this.params.discordConfig,
1420
1437
  admissionAllowFrom: this.params.admissionAllowFrom,
@@ -1515,6 +1532,7 @@ var DiscordVoiceReceive = class {
1515
1532
  logger$3.info(`discord voice: agent turn start guild=${entry.guildId} channel=${entry.channelId} voiceSession=${entry.voiceSessionKey} supervisorSession=${entry.route.sessionKey} agent=${entry.route.agentId} user=${userId} speaker=${context.speakerLabel} owner=${context.senderIsOwner} model=${this.params.discordConfig.voice?.model ?? "route-default"} message=${formatVoiceLogPreview(message)}`);
1516
1533
  const turn = await runDiscordVoiceAgentTurn({
1517
1534
  entry,
1535
+ accountId: this.params.accountId,
1518
1536
  userId,
1519
1537
  message,
1520
1538
  cfg: this.params.cfg,
@@ -1574,7 +1592,10 @@ var DiscordVoiceReceive = class {
1574
1592
  const result = await this.params.join({
1575
1593
  guildId: entry.guildId,
1576
1594
  channelId: entry.channelId
1577
- }, { preserveFollowState });
1595
+ }, {
1596
+ preserveFollowState,
1597
+ autoJoinWhenOccupied: entry.autoJoinWhenOccupied
1598
+ });
1578
1599
  if (!result.ok) logger$3.warn(`discord voice: rejoin after decrypt failures failed: ${result.message}`);
1579
1600
  }
1580
1601
  };
@@ -1660,8 +1681,15 @@ var DiscordVoiceSessions = class {
1660
1681
  const { guildId, channelId } = params;
1661
1682
  const voiceConfig = this.params.discordConfig.voice;
1662
1683
  const voiceMode = resolveDiscordVoiceMode(voiceConfig);
1684
+ const cancelledJoinResult = () => ({
1685
+ ok: false,
1686
+ message: "Discord voice join was cancelled.",
1687
+ guildId,
1688
+ channelId
1689
+ });
1663
1690
  const existing = this.params.sessions.get(guildId);
1664
1691
  if (existing && existing.channelId === channelId) {
1692
+ existing.autoJoinWhenOccupied = options?.autoJoinWhenOccupied === true;
1665
1693
  if (authority) existing.generation = authority.generation;
1666
1694
  if (options?.transcripts) existing.transcripts = options.transcripts;
1667
1695
  if (!options?.transcripts && isDiscordRealtimeVoiceMode(voiceMode) && existing.realtimeLifecycle.status !== "active" && existing.realtimeLifecycle.status !== "starting") {
@@ -1688,14 +1716,20 @@ var DiscordVoiceSessions = class {
1688
1716
  logVoiceVerbose(`join: replacing existing session for guild ${guildId}`);
1689
1717
  await this.leave({ guildId }, { preserveFollowState: options?.preserveFollowState });
1690
1718
  }
1691
- const channelInfo = await this.params.client.fetchChannel(channelId).catch(() => null);
1692
- if (authority && !authority.isCurrent()) return {
1693
- ok: false,
1694
- message: "Discord voice join was cancelled.",
1695
- guildId,
1696
- channelId
1697
- };
1698
- if (!channelInfo || "type" in channelInfo && !isVoiceChannel(channelInfo.type)) return {
1719
+ let channelInfo;
1720
+ try {
1721
+ channelInfo = await this.params.client.fetchChannel(channelId);
1722
+ } catch (err) {
1723
+ if (authority && !authority.isCurrent()) return cancelledJoinResult();
1724
+ return {
1725
+ ok: false,
1726
+ message: `Failed to resolve Discord channel ${channelId}: ${formatErrorMessage(err)}`,
1727
+ guildId,
1728
+ channelId
1729
+ };
1730
+ }
1731
+ if (authority && !authority.isCurrent()) return cancelledJoinResult();
1732
+ if (!isVoiceChannel(channelInfo.type)) return {
1699
1733
  ok: false,
1700
1734
  message: `Channel ${channelId} is not a voice channel.`
1701
1735
  };
@@ -1774,12 +1808,7 @@ var DiscordVoiceSessions = class {
1774
1808
  voiceSdk,
1775
1809
  reason: `cancelled join guild ${guildId} channel ${channelId}`
1776
1810
  });
1777
- return {
1778
- ok: false,
1779
- message: "Discord voice join was cancelled.",
1780
- guildId,
1781
- channelId
1782
- };
1811
+ return cancelledJoinResult();
1783
1812
  }
1784
1813
  if (this.params.destroyed()) {
1785
1814
  destroyVoiceConnectionSafely({
@@ -1855,6 +1884,7 @@ var DiscordVoiceSessions = class {
1855
1884
  };
1856
1885
  const entry = {
1857
1886
  generation: authority?.generation ?? 0,
1887
+ autoJoinWhenOccupied: options?.autoJoinWhenOccupied === true,
1858
1888
  sessionLifecycle: { status: "active" },
1859
1889
  guildId,
1860
1890
  guildName: channelInfo && "guild" in channelInfo && channelInfo.guild && typeof channelInfo.guild.name === "string" ? channelInfo.guild.name : void 0,
@@ -1867,6 +1897,7 @@ var DiscordVoiceSessions = class {
1867
1897
  player,
1868
1898
  playbackQueue: Promise.resolve(),
1869
1899
  processingQueue: Promise.resolve(),
1900
+ ttsStreamFallbackWarned: false,
1870
1901
  capture: createVoiceCaptureState(),
1871
1902
  transcripts: options?.transcripts,
1872
1903
  receiveRecovery: createVoiceReceiveRecoveryState(),
@@ -2011,8 +2042,9 @@ var DiscordVoiceSessions = class {
2011
2042
  ok: false,
2012
2043
  message: "Discord realtime voice session stopped before startup completed."
2013
2044
  };
2014
- const { DiscordRealtimeVoiceSession } = await import("./realtime-session.runtime-C7wen-aF.js");
2045
+ const { DiscordRealtimeVoiceSession } = await import("./realtime-session.runtime-ysbMG8v7.js");
2015
2046
  const realtime = new DiscordRealtimeVoiceSession({
2047
+ accountId: this.params.accountId,
2016
2048
  bootstrapContextInstructions,
2017
2049
  cfg: this.params.cfg,
2018
2050
  discordConfig: this.params.discordConfig,
@@ -2069,8 +2101,8 @@ var DiscordVoiceSessions = class {
2069
2101
  //#endregion
2070
2102
  //#region extensions/discord/src/voice/listeners.ts
2071
2103
  const logger$1 = createSubsystemLogger("discord/voice");
2072
- function startAutoJoin(manager) {
2073
- manager.autoJoin().catch((err) => logger$1.warn(`discord voice: autoJoin failed: ${formatErrorMessage(err)}`));
2104
+ function startAutoJoin(operation, context = "") {
2105
+ operation().catch((err) => logger$1.warn(`discord voice: autoJoin${context} failed: ${formatErrorMessage(err)}`));
2074
2106
  }
2075
2107
  var DiscordVoiceReadyListener = class extends ReadyListener {
2076
2108
  constructor(manager) {
@@ -2078,7 +2110,7 @@ var DiscordVoiceReadyListener = class extends ReadyListener {
2078
2110
  this.manager = manager;
2079
2111
  }
2080
2112
  async handle(_data, _client) {
2081
- startAutoJoin(this.manager);
2113
+ startAutoJoin(() => this.manager.autoJoin());
2082
2114
  }
2083
2115
  };
2084
2116
  var DiscordVoiceResumedListener = class extends ResumedListener {
@@ -2087,7 +2119,7 @@ var DiscordVoiceResumedListener = class extends ResumedListener {
2087
2119
  this.manager = manager;
2088
2120
  }
2089
2121
  async handle(_data, _client) {
2090
- startAutoJoin(this.manager);
2122
+ startAutoJoin(() => this.manager.autoJoin());
2091
2123
  }
2092
2124
  };
2093
2125
  var DiscordVoiceGuildCreateListener = class {
@@ -2096,7 +2128,10 @@ var DiscordVoiceGuildCreateListener = class {
2096
2128
  this.type = discord_exports.GatewayDispatchEvents.GuildCreate;
2097
2129
  }
2098
2130
  async handle(data, _client) {
2099
- if (!data.unavailable) this.manager.refreshGuildRoster(data.id);
2131
+ if (!data.unavailable) {
2132
+ this.manager.refreshGuildRoster(data.id);
2133
+ startAutoJoin(() => this.manager.reconcileAutoJoinGuild(data.id), ` occupancy reconciliation guild=${data.id}`);
2134
+ }
2100
2135
  }
2101
2136
  };
2102
2137
  var DiscordVoiceStateUpdateListener = class extends VoiceStateUpdateListener {
@@ -2137,9 +2172,10 @@ var DiscordVoiceManager = class {
2137
2172
  this.guildLifecycles = /* @__PURE__ */ new Map();
2138
2173
  this.nextGuildGeneration = 0;
2139
2174
  this.joinTasks = /* @__PURE__ */ new Map();
2140
- this.autoJoinTask = null;
2175
+ this.autoJoinTasks = /* @__PURE__ */ new Map();
2141
2176
  this.fatalAutoJoinFailures = /* @__PURE__ */ new Map();
2142
2177
  this.destroyed = false;
2178
+ this.client = params.client;
2143
2179
  this.botUserId = params.botUserId;
2144
2180
  this.voiceEnabled = resolveDiscordVoiceEnabled(params.discordConfig.voice);
2145
2181
  const voiceAccess = resolveDiscordVoiceAccess(params);
@@ -2153,6 +2189,7 @@ var DiscordVoiceManager = class {
2153
2189
  });
2154
2190
  this.membership = new DiscordVoiceMembershipTracker(params.client, this.speakerContext, params.accountId);
2155
2191
  this.receive = new DiscordVoiceReceive({
2192
+ accountId: params.accountId,
2156
2193
  admissionAllowFrom: this.admissionAllowFrom,
2157
2194
  botUserId: () => this.botUserId,
2158
2195
  cfg: params.cfg,
@@ -2218,51 +2255,24 @@ var DiscordVoiceManager = class {
2218
2255
  }
2219
2256
  async autoJoin() {
2220
2257
  if (!this.voiceEnabled || this.destroyed) return;
2221
- if (this.autoJoinTask) return this.autoJoinTask;
2222
- this.autoJoinTask = (async () => {
2223
- const entries = this.autoJoinChannels;
2224
- const entriesByGuild = /* @__PURE__ */ new Map();
2225
- const duplicateGuilds = /* @__PURE__ */ new Set();
2226
- for (const entry of entries) {
2227
- const guildId = entry.guildId.trim();
2228
- const channelId = entry.channelId.trim();
2229
- if (!guildId || !channelId) continue;
2230
- if (entriesByGuild.has(guildId)) duplicateGuilds.add(guildId);
2231
- entriesByGuild.set(guildId, {
2232
- guildId,
2233
- channelId
2234
- });
2235
- }
2236
- logVoiceVerbose(`autoJoin: ${entries.length} entries, ${entriesByGuild.size} guilds`);
2237
- for (const guildId of duplicateGuilds) {
2238
- const selected = entriesByGuild.get(guildId);
2239
- if (selected) logger.warn(`discord voice: autoJoin has multiple entries for guild ${guildId}; using channel ${selected.channelId}`);
2240
- }
2241
- for (const entry of entriesByGuild.values()) {
2242
- const failureKey = formatAutoJoinFailureKey(entry);
2243
- const fatalFailure = this.fatalAutoJoinFailures.get(failureKey);
2244
- if (fatalFailure) {
2245
- if (!fatalFailure.skipLogged) {
2246
- logger.warn(`discord voice: autoJoin suppressed guild=${entry.guildId} channel=${entry.channelId} after fatal startup failure; retry with /vc join or reload config after fixing credentials: ${fatalFailure.message}`);
2247
- fatalFailure.skipLogged = true;
2248
- }
2249
- continue;
2250
- }
2251
- logVoiceVerbose(`autoJoin: joining guild ${entry.guildId} channel ${entry.channelId}`);
2252
- const result = await this.join(entry);
2253
- if (!result.ok) {
2254
- logger.warn(`discord voice: autoJoin skipped guild=${entry.guildId} channel=${entry.channelId}: ${result.message}`);
2255
- if (isFatalAutoJoinFailure(result.message)) this.fatalAutoJoinFailures.set(failureKey, {
2256
- message: result.message,
2257
- skipLogged: false
2258
- });
2259
- }
2260
- }
2261
- await this.following.startReconciliation();
2262
- })().finally(() => {
2263
- this.autoJoinTask = null;
2264
- });
2265
- return this.autoJoinTask;
2258
+ const entriesByGuild = /* @__PURE__ */ new Map();
2259
+ const duplicateGuilds = /* @__PURE__ */ new Set();
2260
+ for (const entry of this.autoJoinChannels) {
2261
+ if (entriesByGuild.has(entry.guildId)) duplicateGuilds.add(entry.guildId);
2262
+ entriesByGuild.set(entry.guildId, entry);
2263
+ }
2264
+ logVoiceVerbose(`autoJoin: ${this.autoJoinChannels.length} entries, ${entriesByGuild.size} guilds`);
2265
+ for (const guildId of duplicateGuilds) {
2266
+ const selected = entriesByGuild.get(guildId);
2267
+ if (selected) logger.warn(`discord voice: autoJoin has multiple entries for guild ${guildId}; using channel ${selected.channelId}`);
2268
+ }
2269
+ for (const entry of entriesByGuild.values()) await this.enqueueAutoJoin(entry);
2270
+ await this.following.startReconciliation();
2271
+ }
2272
+ async reconcileAutoJoinGuild(guildId) {
2273
+ const entry = this.resolveAutoJoinTarget(guildId);
2274
+ if (!entry?.whenOccupied || !this.voiceEnabled || this.destroyed) return;
2275
+ await this.enqueueAutoJoin(entry);
2266
2276
  }
2267
2277
  status() {
2268
2278
  return Array.from(this.guildLifecycles.values()).filter((lifecycle) => lifecycle.status === "active").map(({ instance: session }) => ({
@@ -2279,6 +2289,24 @@ var DiscordVoiceManager = class {
2279
2289
  channelId: params.channelId.trim()
2280
2290
  });
2281
2291
  }
2292
+ async resolveAccessTarget(params) {
2293
+ const [guild, channel] = await Promise.all([this.client.fetchGuild(params.guildId).catch(() => null), this.client.fetchChannel(params.channelId).catch(() => null)]);
2294
+ if (!guild || !channel) return;
2295
+ const context = await resolveFetchedDiscordThreadLikeChannelContext({
2296
+ client: this.client,
2297
+ channel,
2298
+ channelIdFallback: params.channelId
2299
+ });
2300
+ return {
2301
+ guild,
2302
+ ...context.channelName ? { channelName: context.channelName } : {},
2303
+ channelSlug: context.channelSlug,
2304
+ ...context.parentId ? { parentId: context.parentId } : {},
2305
+ ...context.threadParentName ? { parentName: context.threadParentName } : {},
2306
+ ...context.threadParentSlug ? { parentSlug: context.threadParentSlug } : {},
2307
+ scope: context.isThreadChannel ? "thread" : "channel"
2308
+ };
2309
+ }
2282
2310
  async join(params, options) {
2283
2311
  if (this.destroyed) return {
2284
2312
  ok: false,
@@ -2429,6 +2457,7 @@ var DiscordVoiceManager = class {
2429
2457
  guildId,
2430
2458
  channelId
2431
2459
  });
2460
+ await this.reconcileAutoJoinGuild(guildId);
2432
2461
  return;
2433
2462
  }
2434
2463
  this.membership.track(this.sessions.get(guildId), data, previousVoiceState);
@@ -2437,6 +2466,8 @@ var DiscordVoiceManager = class {
2437
2466
  channelId,
2438
2467
  userId
2439
2468
  });
2469
+ const autoJoinTarget = this.resolveAutoJoinTarget(guildId);
2470
+ if (autoJoinTarget?.whenOccupied) await this.enqueueAutoJoin(autoJoinTarget);
2440
2471
  }
2441
2472
  async destroy() {
2442
2473
  this.destroyed = true;
@@ -2454,6 +2485,65 @@ var DiscordVoiceManager = class {
2454
2485
  const lifecycle = this.guildLifecycles.get(entry.guildId);
2455
2486
  return lifecycle?.status === "active" && lifecycle.generation === entry.generation && lifecycle.instance === entry && entry.sessionLifecycle.status === "active";
2456
2487
  }
2488
+ resolveAutoJoinTarget(guildId) {
2489
+ return this.autoJoinChannels.toReversed().find((entry) => entry.guildId === guildId.trim());
2490
+ }
2491
+ enqueueAutoJoin(entry) {
2492
+ const task = (this.autoJoinTasks.get(entry.guildId) ?? Promise.resolve()).catch(() => void 0).then(async () => await this.reconcileAutoJoinEntry(entry)).finally(() => {
2493
+ if (this.autoJoinTasks.get(entry.guildId) === task) this.autoJoinTasks.delete(entry.guildId);
2494
+ });
2495
+ this.autoJoinTasks.set(entry.guildId, task);
2496
+ return task;
2497
+ }
2498
+ async reconcileAutoJoinEntry(entry) {
2499
+ if (this.destroyed) return;
2500
+ const failureKey = formatAutoJoinFailureKey(entry);
2501
+ const fatalFailure = this.fatalAutoJoinFailures.get(failureKey);
2502
+ if (fatalFailure) {
2503
+ if (!fatalFailure.skipLogged) {
2504
+ logger.warn(`discord voice: autoJoin suppressed guild=${entry.guildId} channel=${entry.channelId} after fatal startup failure; retry with /vc join or reload config after fixing credentials: ${fatalFailure.message}`);
2505
+ fatalFailure.skipLogged = true;
2506
+ }
2507
+ return;
2508
+ }
2509
+ if (entry.whenOccupied) {
2510
+ const states = listDiscordVoiceParticipantStates({
2511
+ client: this.client,
2512
+ guildId: entry.guildId,
2513
+ channelId: entry.channelId
2514
+ });
2515
+ if (states === null) {
2516
+ logVoiceVerbose(`autoJoin waiting for guild voice snapshot guild=${entry.guildId} channel=${entry.channelId}`);
2517
+ return;
2518
+ }
2519
+ const humanCount = countDiscordVoiceHumanParticipants({
2520
+ states,
2521
+ botUserId: this.botUserId
2522
+ });
2523
+ const existing = this.sessions.get(entry.guildId);
2524
+ if (humanCount === 0) {
2525
+ if (!existing?.autoJoinWhenOccupied || existing.channelId !== entry.channelId) return;
2526
+ logger.info(`discord voice: occupied autoJoin leaving empty channel guild=${entry.guildId} channel=${entry.channelId}`);
2527
+ const result = await this.leave({
2528
+ guildId: entry.guildId,
2529
+ channelId: entry.channelId
2530
+ });
2531
+ if (!result.ok) logger.warn(`discord voice: occupied autoJoin failed to leave guild=${entry.guildId} channel=${entry.channelId}: ${result.message}`);
2532
+ return;
2533
+ }
2534
+ const lifecycle = this.guildLifecycles.get(entry.guildId);
2535
+ if (existing || lifecycle?.status === "starting" || lifecycle?.status === "active") return;
2536
+ logger.info(`discord voice: occupied autoJoin joining guild=${entry.guildId} channel=${entry.channelId} humans=${humanCount}`);
2537
+ } else logVoiceVerbose(`autoJoin: joining guild ${entry.guildId} channel ${entry.channelId}`);
2538
+ const result = await this.join(entry, { autoJoinWhenOccupied: entry.whenOccupied === true });
2539
+ if (!result.ok) {
2540
+ logger.warn(`discord voice: autoJoin skipped guild=${entry.guildId} channel=${entry.channelId}: ${result.message}`);
2541
+ if (isFatalAutoJoinFailure(result.message)) this.fatalAutoJoinFailures.set(failureKey, {
2542
+ message: result.message,
2543
+ skipLogged: false
2544
+ });
2545
+ }
2546
+ }
2457
2547
  };
2458
2548
  //#endregion
2459
2549
  export { DiscordVoiceGuildCreateListener, DiscordVoiceManager, DiscordVoiceReadyListener, DiscordVoiceResumedListener, DiscordVoiceStateUpdateListener };
@@ -17,10 +17,6 @@
17
17
  "discord"
18
18
  ],
19
19
  "contracts": {
20
- "tools": [
21
- "show_widget",
22
- "discord_widget"
23
- ],
24
20
  "transcriptSourceProviders": [
25
21
  "discord-voice"
26
22
  ]
@@ -1181,6 +1177,9 @@
1181
1177
  "channelId": {
1182
1178
  "type": "string",
1183
1179
  "minLength": 1
1180
+ },
1181
+ "whenOccupied": {
1182
+ "type": "boolean"
1184
1183
  }
1185
1184
  },
1186
1185
  "required": [
@@ -2989,6 +2988,9 @@
2989
2988
  "channelId": {
2990
2989
  "type": "string",
2991
2990
  "minLength": 1
2991
+ },
2992
+ "whenOccupied": {
2993
+ "type": "boolean"
2992
2994
  }
2993
2995
  },
2994
2996
  "required": [
@@ -3902,7 +3904,11 @@
3902
3904
  },
3903
3905
  "voice.autoJoin": {
3904
3906
  "label": "Discord Voice Auto-Join",
3905
- "help": "Voice channels to auto-join on startup (list of guildId/channelId entries)."
3907
+ "help": "Voice channels to auto-join (list of guildId/channelId entries). Set whenOccupied on an entry to connect only while humans are present."
3908
+ },
3909
+ "voice.autoJoin.*.whenOccupied": {
3910
+ "label": "Discord Voice Auto-Join When Occupied",
3911
+ "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."
3906
3912
  },
3907
3913
  "voice.allowedChannels": {
3908
3914
  "label": "Discord Voice Allowed Channels",
@@ -4035,7 +4041,7 @@
4035
4041
  },
4036
4042
  "activities": {
4037
4043
  "label": "Discord Activities",
4038
- "help": "Enable Discord Activity widgets for this account. Routes, the agent tool, and the launch handler remain disabled when this block is absent."
4044
+ "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."
4039
4045
  },
4040
4046
  "activities.clientSecret": {
4041
4047
  "label": "Discord Activities Client Secret",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/discord",
3
- "version": "2026.8.1-beta.2",
3
+ "version": "2026.8.1-beta.3",
4
4
  "description": "OpenClaw Discord channel plugin for channels, DMs, commands, and app events.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,14 +13,13 @@
13
13
  "discord-api-types": "0.38.52",
14
14
  "libopus-wasm": "0.2.0",
15
15
  "mdast-util-from-markdown": "2.0.3",
16
- "p-map": "7.0.6",
17
16
  "typebox": "1.3.6",
18
17
  "undici": "8.9.0",
19
18
  "ws": "8.21.1",
20
19
  "zod": "4.4.3"
21
20
  },
22
21
  "peerDependencies": {
23
- "openclaw": ">=2026.8.1-beta.2"
22
+ "openclaw": ">=2026.8.1-beta.3"
24
23
  },
25
24
  "peerDependenciesMeta": {
26
25
  "openclaw": {
@@ -95,10 +94,10 @@
95
94
  "allowInvalidConfigRecovery": true
96
95
  },
97
96
  "compat": {
98
- "pluginApi": ">=2026.8.1-beta.2"
97
+ "pluginApi": ">=2026.8.1-beta.3"
99
98
  },
100
99
  "build": {
101
- "openclawVersion": "2026.8.1-beta.2",
100
+ "openclawVersion": "2026.8.1-beta.3",
102
101
  "staticAssets": [
103
102
  {
104
103
  "source": "./assets/embedded-app-sdk.mjs",
@@ -130,7 +129,6 @@
130
129
  "discord-api-types",
131
130
  "libopus-wasm",
132
131
  "mdast-util-from-markdown",
133
- "p-map",
134
132
  "typebox",
135
133
  "undici",
136
134
  "ws",