fix: update Discord voice to libopus-wasm 0.1.0

Updates Discord voice Opus callers to the published libopus-wasm 0.1.0 API, pins the Discord plugin dependency and lockfiles to that release, keeps the package freshness exception version-scoped, treats expected Discord receive-stream premature closes as normal stream ends, and includes routed OpenClaw transcript roots for local PR transcript discovery.\n\nProof: npm view libopus-wasm@0.1.0; pnpm install --lockfile-only --filter @openclaw/discord; Node encode/decode smoke with pkg 0.1.0 decoded=3840; node scripts/run-vitest.mjs extensions/discord/src/voice/audio.test.ts extensions/discord/src/voice/receive-recovery.test.ts; git diff --check; autoreview clean; live tmux gateway on e0fa3e3 joined Discord voice and processed realtime audio without decoder.decode or Premature close warning spam.
This commit is contained in:
Peter Steinberger
2026-05-26 16:17:53 +01:00
committed by GitHub
parent e89afa6afa
commit 0f605ee003
8 changed files with 54 additions and 18 deletions
@@ -56,10 +56,19 @@ function openClawSessionRoots() {
const agentsDir = path.join(stateDir, "agents");
if (!fs.existsSync(agentsDir)) return [];
try {
return fs
const roots = fs
.readdirSync(agentsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(agentsDir, entry.name, "sessions"));
.flatMap((entry) => {
const agentDir = path.join(agentsDir, entry.name);
return [
path.join(agentDir, "sessions"),
path.join(agentDir, "agent", "sessions"),
path.join(agentDir, "agent", "codex-home", "sessions"),
];
})
.filter((root) => fs.existsSync(root));
return [...new Set(roots)];
} catch {
return [];
}
+4 -4
View File
@@ -11,7 +11,7 @@
"@discordjs/voice": "0.19.2",
"discord-api-types": "0.38.48",
"https-proxy-agent": "9.0.0",
"libopus-wasm": "0.0.1",
"libopus-wasm": "0.1.0",
"prism-media": "1.3.5",
"typebox": "1.1.38",
"undici": "8.3.0",
@@ -424,9 +424,9 @@
}
},
"node_modules/libopus-wasm": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/libopus-wasm/-/libopus-wasm-0.0.1.tgz",
"integrity": "sha512-w//dxORfJfLl4quaA5nEA0xZlbbtS/Gba+NrZyCDPmAQh2THX4KI6aI600f+jpxf6v3p9ApGCx0/DCCe4Xftug==",
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/libopus-wasm/-/libopus-wasm-0.1.0.tgz",
"integrity": "sha512-/aurGcAVgy0GcBEUzFaX9pm9qv7zYcy8W5hBXFiK+cyqOXAX4lOS6rlFogkY9CcSIajhjnuXyixsbmziSHCDMQ==",
"license": "MIT",
"engines": {
"node": ">=20"
+1 -1
View File
@@ -11,7 +11,7 @@
"@discordjs/voice": "0.19.2",
"discord-api-types": "0.38.48",
"https-proxy-agent": "9.0.0",
"libopus-wasm": "0.0.1",
"libopus-wasm": "0.1.0",
"prism-media": "1.3.5",
"typebox": "1.1.38",
"undici": "8.3.0",
+22 -5
View File
@@ -4,8 +4,8 @@ import {
Application,
createDecoder as createLibopusDecoder,
createEncoder as createLibopusEncoder,
type OpusDecoder as LibopusDecoder,
type OpusEncoder as LibopusEncoder,
type OpusDecoderHandle as LibopusDecoder,
type OpusEncoderHandle as LibopusEncoder,
} from "libopus-wasm";
import { resamplePcm } from "openclaw/plugin-sdk/realtime-voice";
import { logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
@@ -80,7 +80,12 @@ async function createOpusDecoder(params: {
return {
name: "libopus-wasm",
decoder: {
decode: (buffer) => pcmInt16ToBuffer(decoder.decodeFrame(buffer, DISCORD_OPUS_FRAME_SIZE)),
decode: (buffer) =>
pcmInt16ToBuffer(
decoder.decode(buffer, {
maxFrameSize: DISCORD_OPUS_FRAME_SIZE,
}),
),
free: () => decoder.free(),
},
};
@@ -141,7 +146,13 @@ class DiscordOpusEncodeStream extends Transform {
while (this.#buffer.length >= DISCORD_OPUS_FRAME_BYTES) {
const frame = this.#buffer.subarray(0, DISCORD_OPUS_FRAME_BYTES);
this.#buffer = this.#buffer.subarray(DISCORD_OPUS_FRAME_BYTES);
this.push(Buffer.from(encoder.encodePcm16(frame, DISCORD_OPUS_FRAME_SIZE)));
this.push(
Buffer.from(
encoder.encode(frame, {
frameSize: DISCORD_OPUS_FRAME_SIZE,
}),
),
);
}
done();
} catch (err) {
@@ -156,7 +167,13 @@ class DiscordOpusEncodeStream extends Transform {
const frame = Buffer.alloc(DISCORD_OPUS_FRAME_BYTES);
this.#buffer.copy(frame);
this.#buffer = Buffer.alloc(0);
this.push(Buffer.from(encoder.encodePcm16(frame, DISCORD_OPUS_FRAME_SIZE)));
this.push(
Buffer.from(
encoder.encode(frame, {
frameSize: DISCORD_OPUS_FRAME_SIZE,
}),
),
);
}
this.#freeEncoder();
done();
@@ -29,6 +29,15 @@ describe("voice receive recovery", () => {
});
});
it("treats premature stream close as an expected receive end", () => {
expect(analyzeVoiceReceiveError(new Error("Premature close"))).toEqual({
message: "Premature close",
isAbortLike: true,
shouldAttemptPassthrough: false,
countsAsDecryptFailure: false,
});
});
it("gates recovery after repeated decrypt failures in the same window", () => {
const state = createVoiceReceiveRecoveryState();
@@ -74,6 +74,7 @@ function isAbortLikeReceiveError(err: unknown): boolean {
: "";
return (
name === "AbortError" ||
message === "Premature close" ||
message.includes("The operation was aborted") ||
message.includes("aborted")
);
+5 -5
View File
@@ -616,8 +616,8 @@ importers:
specifier: 9.0.0
version: 9.0.0
libopus-wasm:
specifier: 0.0.1
version: 0.0.1
specifier: 0.1.0
version: 0.1.0
prism-media:
specifier: 1.3.5
version: 1.3.5
@@ -5479,8 +5479,8 @@ packages:
resolution: {integrity: sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==}
engines: {node: '>=22.0.0'}
libopus-wasm@0.0.1:
resolution: {integrity: sha512-w//dxORfJfLl4quaA5nEA0xZlbbtS/Gba+NrZyCDPmAQh2THX4KI6aI600f+jpxf6v3p9ApGCx0/DCCe4Xftug==}
libopus-wasm@0.1.0:
resolution: {integrity: sha512-/aurGcAVgy0GcBEUzFaX9pm9qv7zYcy8W5hBXFiK+cyqOXAX4lOS6rlFogkY9CcSIajhjnuXyixsbmziSHCDMQ==}
engines: {node: '>=20'}
libsignal@6.0.0:
@@ -11294,7 +11294,7 @@ snapshots:
kysely@0.29.2: {}
libopus-wasm@0.0.1: {}
libopus-wasm@0.1.0: {}
libsignal@6.0.0:
dependencies:
+1 -1
View File
@@ -16,7 +16,7 @@ minimumReleaseAgeExclude:
- "basic-ftp"
- "baileys@7.0.0-rc13"
- "hono"
- "libopus-wasm@0.0.1"
- "libopus-wasm@0.1.0"
- "libsignal@6.0.0"
- "openclaw"
- "protobufjs"