fix: prevent discord voice self-feedback · openclaw/openclaw@1c28325
steipete
·
2026-05-07
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
6 | 6 | |
7 | 7 | ### Changes |
8 | 8 | |
| 9 | +- Discord/voice: keep TTS playback running when another user starts speaking, ignore new capture during playback to avoid feedback loops, and downgrade expected receive-stream aborts to verbose diagnostics. |
9 | 10 | - Telegram: treat successful same-chat `message` tool outbound sends during an inbound telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback (#78685). Thanks @neeravmakwana. |
10 | 11 | - Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared even when a child session row remains, and apply the default bounded reload deferral timeout to channel hot reloads so stale task records cannot block Discord/Slack/Telegram reloads forever. |
11 | 12 | - Discord/voice: make `openclaw channels capabilities --channel discord --target channel:<id>` and `channels status --probe` audit voice-channel permissions, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before `/vc join`. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1205,8 +1205,10 @@ Notes:
|
1205 | 1205 | - `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset. |
1206 | 1206 | - `voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts. Default: `30000`. |
1207 | 1207 | - `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`. |
| 1208 | +- Voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn. |
1208 | 1209 | - OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window. |
1209 | 1210 | - If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419. |
| 1211 | +- `The operation was aborted` receive events are expected when OpenClaw finalizes a captured speaker segment; they are verbose diagnostics, not warnings. |
1210 | 1212 | |
1211 | 1213 | Voice channel pipeline: |
1212 | 1214 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -351,6 +351,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
351 | 351 | - `channels.discord.voice.daveEncryption` and `channels.discord.voice.decryptionFailureTolerance` pass through to `@discordjs/voice` DAVE options (`true` and `24` by default). |
352 | 352 | - `channels.discord.voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts (`30000` by default). |
353 | 353 | - `channels.discord.voice.reconnectGraceMs` controls how long a disconnected voice session may take to enter reconnect signalling before OpenClaw destroys it (`15000` by default). |
| 354 | +- Discord voice playback is not interrupted by another user's speaking-start event. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing. |
354 | 355 | - OpenClaw additionally attempts voice receive recovery by leaving/rejoining a voice session after repeated decrypt failures. |
355 | 356 | - `channels.discord.streaming` is the canonical stream mode key. Discord defaults to `streaming.mode: "progress"` so tool/work progress appears in one edited preview message; set `streaming.mode: "off"` to disable it. Legacy `streamMode` and boolean `streaming` values remain runtime aliases; run `openclaw doctor --fix` to rewrite persisted config. |
356 | 357 | - `channels.discord.autoPresence` maps runtime availability to bot presence (healthy => online, degraded => idle, exhausted => dnd) and allows optional status text overrides. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -393,6 +393,29 @@ describe("DiscordVoiceManager", () => {
|
393 | 393 | expect(player.off).toHaveBeenCalledWith("error", expect.any(Function)); |
394 | 394 | }); |
395 | 395 | |
| 396 | +it("ignores new capture while playback is running", async () => { |
| 397 | +const connection = createConnectionMock(); |
| 398 | +joinVoiceChannelMock.mockReturnValueOnce(connection); |
| 399 | +const manager = createManager(); |
| 400 | + |
| 401 | +await manager.join({ guildId: "g1", channelId: "1001" }); |
| 402 | + |
| 403 | +const player = createAudioPlayerMock.mock.results.at(-1)?.value; |
| 404 | +const entry = (manager as unknown as { sessions: Map<string, unknown> }).sessions.get("g1"); |
| 405 | +expect(entry).toBeDefined(); |
| 406 | +expect(player).toBeDefined(); |
| 407 | +player.state.status = "playing"; |
| 408 | + |
| 409 | +await ( |
| 410 | +manager as unknown as { |
| 411 | +handleSpeakingStart: (entry: unknown, userId: string) => Promise<void>; |
| 412 | +} |
| 413 | +).handleSpeakingStart(entry, "u1"); |
| 414 | + |
| 415 | +expect(player.stop).not.toHaveBeenCalled(); |
| 416 | +expect(connection.receiver.subscribe).not.toHaveBeenCalled(); |
| 417 | +}); |
| 418 | + |
396 | 419 | it("passes DAVE options to joinVoiceChannel", async () => { |
397 | 420 | const manager = createManager({ |
398 | 421 | voice: { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -496,15 +496,17 @@ export class DiscordVoiceManager {
|
496 | 496 | `capture start: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`, |
497 | 497 | ); |
498 | 498 | const voiceSdk = loadDiscordVoiceSdk(); |
| 499 | +if (entry.player.state.status === voiceSdk.AudioPlayerStatus.Playing) { |
| 500 | +logVoiceVerbose( |
| 501 | +`capture ignored during playback: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`, |
| 502 | +); |
| 503 | +return; |
| 504 | +} |
499 | 505 | this.enableDaveReceivePassthrough( |
500 | 506 | entry, |
501 | 507 | `speaker ${userId} start`, |
502 | 508 | DAVE_RECEIVE_PASSTHROUGH_REARM_EXPIRY_SECONDS, |
503 | 509 | ); |
504 | | -if (entry.player.state.status === voiceSdk.AudioPlayerStatus.Playing) { |
505 | | -entry.player.stop(true); |
506 | | -} |
507 | | - |
508 | 510 | const stream = entry.connection.receiver.subscribe(userId, { |
509 | 511 | end: { |
510 | 512 | behavior: voiceSdk.EndBehaviorType.Manual, |
@@ -575,6 +577,10 @@ export class DiscordVoiceManager {
|
575 | 577 | |
576 | 578 | private handleReceiveError(entry: VoiceSessionEntry, err: unknown) { |
577 | 579 | const analysis = analyzeVoiceReceiveError(err); |
| 580 | +if (analysis.isAbortLike && !analysis.countsAsDecryptFailure) { |
| 581 | +logVoiceVerbose(`receive stream ended: ${analysis.message}`); |
| 582 | +return; |
| 583 | +} |
578 | 584 | logger.warn(`discord voice: receive error: ${analysis.message}`); |
579 | 585 | if (analysis.shouldAttemptPassthrough) { |
580 | 586 | this.enableDaveReceivePassthrough( |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。