From 487687a6f00deb75b767048244a53159060ec3b9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 22:59:09 -0400 Subject: [PATCH] build(discord): skip native opus builds by default (#80071) --- CHANGELOG.md | 1 + docs/channels/discord.md | 1 + docs/help/testing.md | 5 ++++ extensions/discord/src/voice/audio.test.ts | 19 ++++++++++++++ extensions/discord/src/voice/audio.ts | 20 +++++++-------- package.json | 2 +- pnpm-workspace.yaml | 2 +- test/package-manager-config.test.ts | 30 ++++++++++++++++++++++ 8 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 extensions/discord/src/voice/audio.test.ts create mode 100644 test/package-manager-config.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f7ede26d7..68acd1cff75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - Talk: add `talk.realtime.instructions` so operators can append realtime voice style instructions while preserving OpenClaw's built-in agent-consult guidance. (#79081) Thanks @VACInc. +- Discord/voice: default test and source installs to the pure-JS `opusscript` decoder by ignoring optional native `@discordjs/opus` builds, avoiding slow native addon compiles outside dedicated voice-performance lanes. - Gateway/skills: add an opt-in private skill archive upload install path gated by `skills.install.allowUploadedArchives`, so trusted Gateway clients can stage and install zip-backed skills only when operators explicitly enable the code-install surface. (#74430) Thanks @samzong. - Dependencies: refresh workspace pins and patch targets, including ACPX `@agentclientprotocol/claude-agent-acp` `0.33.1`, Codex ACP `0.14.0`, Baileys `7.0.0-rc10`, Google GenAI `2.0.1`, OpenAI `6.37.0`, AWS SDK `3.1045.0`, Kysely `0.29.0`, Tlon skill `0.3.6`, Aimock `1.19.5`, and tsdown `0.22.0`. - Agents/compaction: preserve scoped background exec/process session references across embedded compaction and after-turn runtime contexts without exposing sessions from unrelated scopes. Fixes #79284. (#79307) Thanks @TurboTheTurtle. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 36212e4055d..1613a2534a1 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -1213,6 +1213,7 @@ Notes: - If `voice.autoJoin` has multiple entries for the same guild, OpenClaw joins the last configured channel for that guild. - `voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options. - `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset. +- OpenClaw defaults to the pure-JS `opusscript` decoder for Discord voice receive. The optional native `@discordjs/opus` package is ignored by the repo pnpm install policy so normal installs and tests do not compile a native addon; only opt into a native opus build in a dedicated voice-performance or live-lane environment. - `voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts. Default: `30000`. - `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`. - In `stt-tts` mode, 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. Realtime modes forward speaker starts as barge-in signals to the realtime provider. diff --git a/docs/help/testing.md b/docs/help/testing.md index 75c0a5cd8b6..3b6692f5691 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -443,6 +443,11 @@ Think of the suites as "increasing realism" (and increasing flakiness/cost): real bundled plugin source APIs. Real plugin API loads belong in plugin-owned contract/integration suites. +Native dependency policy: + +- Default test installs skip optional native Discord opus builds. Discord voice receive uses the pure-JS `opusscript` decoder, and `@discordjs/opus` stays in `ignoredBuiltDependencies` so local tests and Testbox lanes do not compile the native addon. +- Use a dedicated Discord voice performance or live lane if you intentionally need to compare a native opus build. Do not add `@discordjs/opus` back to the default `onlyBuiltDependencies`; that makes unrelated install/test loops compile native code. + diff --git a/extensions/discord/src/voice/audio.test.ts b/extensions/discord/src/voice/audio.test.ts new file mode 100644 index 00000000000..1c90498ce8b --- /dev/null +++ b/extensions/discord/src/voice/audio.test.ts @@ -0,0 +1,19 @@ +import { Readable } from "node:stream"; +import { describe, expect, it } from "vitest"; +import { decodeOpusStream } from "./audio.js"; + +describe("discord voice opus decoder selection", () => { + it("prefers the pure-JS opusscript decoder over optional native opus", async () => { + const verbose: string[] = []; + const warnings: string[] = []; + + const decoded = await decodeOpusStream(Readable.from([]), { + onVerbose: (message) => verbose.push(message), + onWarn: (message) => warnings.push(message), + }); + + expect(decoded.length).toBe(0); + expect(verbose).toContain("opus decoder: opusscript"); + expect(warnings).toEqual([]); + }); +}); diff --git a/extensions/discord/src/voice/audio.ts b/extensions/discord/src/voice/audio.ts index 1273382e1d9..2459c5216bf 100644 --- a/extensions/discord/src/voice/audio.ts +++ b/extensions/discord/src/voice/audio.ts @@ -48,6 +48,16 @@ function resolveOpusDecoderFactory(params: { onWarn: (message: string) => void; }): OpusDecoderFactory | null { const factories: OpusDecoderFactory[] = [ + { + name: "opusscript", + load: () => { + const OpusScript = require("opusscript") as { + new (sampleRate: number, channels: number, application: number): OpusDecoder; + Application: { AUDIO: number }; + }; + return new OpusScript(SAMPLE_RATE, CHANNELS, OpusScript.Application.AUDIO); + }, + }, { name: "@discordjs/opus", load: () => { @@ -62,16 +72,6 @@ function resolveOpusDecoderFactory(params: { return new DiscordOpus.OpusEncoder(SAMPLE_RATE, CHANNELS); }, }, - { - name: "opusscript", - load: () => { - const OpusScript = require("opusscript") as { - new (sampleRate: number, channels: number, application: number): OpusDecoder; - Application: { AUDIO: number }; - }; - return new OpusScript(SAMPLE_RATE, CHANNELS, OpusScript.Application.AUDIO); - }, - }, ]; const failures: string[] = []; diff --git a/package.json b/package.json index 8847f6223c9..cd23af09f3d 100644 --- a/package.json +++ b/package.json @@ -1820,7 +1820,6 @@ }, "onlyBuiltDependencies": [ "@openclaw/fs-safe", - "@discordjs/opus", "@google/genai", "@lydell/node-pty", "@matrix-org/matrix-sdk-crypto-nodejs", @@ -1835,6 +1834,7 @@ "sharp" ], "ignoredBuiltDependencies": [ + "@discordjs/opus", "koffi", "tree-sitter-bash" ], diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 50933fa10fb..4886d78f991 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -34,7 +34,6 @@ minimumReleaseAgeExclude: onlyBuiltDependencies: - "@openclaw/fs-safe" - - "@discordjs/opus" - "@google/genai" - "@lydell/node-pty" - "@matrix-org/matrix-sdk-crypto-nodejs" @@ -49,5 +48,6 @@ onlyBuiltDependencies: - sharp ignoredBuiltDependencies: + - "@discordjs/opus" - koffi - tree-sitter-bash diff --git a/test/package-manager-config.test.ts b/test/package-manager-config.test.ts new file mode 100644 index 00000000000..d632e1e8eae --- /dev/null +++ b/test/package-manager-config.test.ts @@ -0,0 +1,30 @@ +import fs from "node:fs"; +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; + +type PnpmBuildConfig = { + ignoredBuiltDependencies?: string[]; + onlyBuiltDependencies?: string[]; +}; + +type RootPackageJson = { + pnpm?: PnpmBuildConfig; +}; + +type WorkspaceConfig = PnpmBuildConfig; + +function readJson(filePath: string): T { + return JSON.parse(fs.readFileSync(filePath, "utf8")) as T; +} + +describe("package manager build policy", () => { + it("keeps optional native Discord opus builds disabled by default", () => { + const packageJson = readJson("package.json"); + const workspace = parse(fs.readFileSync("pnpm-workspace.yaml", "utf8")) as WorkspaceConfig; + + for (const config of [packageJson.pnpm, workspace]) { + expect(config?.ignoredBuiltDependencies ?? []).toContain("@discordjs/opus"); + expect(config?.onlyBuiltDependencies ?? []).not.toContain("@discordjs/opus"); + } + }); +});