fix(discord): use libopus structured decode errors

This commit is contained in:
Peter Steinberger
2026-06-01 23:43:08 +01:00
parent e8120a72e1
commit 2d17cb295d
6 changed files with 37 additions and 17 deletions

View File

@@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@discordjs/voice": "0.19.2", "@discordjs/voice": "0.19.2",
"discord-api-types": "0.38.48", "discord-api-types": "0.38.48",
"libopus-wasm": "0.1.0", "libopus-wasm": "0.2.0",
"typebox": "1.1.39", "typebox": "1.1.39",
"undici": "8.3.0", "undici": "8.3.0",
"ws": "8.21.0" "ws": "8.21.0"
@@ -352,9 +352,9 @@
] ]
}, },
"node_modules/libopus-wasm": { "node_modules/libopus-wasm": {
"version": "0.1.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/libopus-wasm/-/libopus-wasm-0.1.0.tgz", "resolved": "https://registry.npmjs.org/libopus-wasm/-/libopus-wasm-0.2.0.tgz",
"integrity": "sha512-/aurGcAVgy0GcBEUzFaX9pm9qv7zYcy8W5hBXFiK+cyqOXAX4lOS6rlFogkY9CcSIajhjnuXyixsbmziSHCDMQ==", "integrity": "sha512-x/2Gu1/C6L3IICY09zyfp984AWiOYjn53u4WfdY3yh+3KTzMN8Xkm77q3lenWMVIk5SnSzjGEkQT+VQMFHLBHQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=20" "node": ">=20"

View File

@@ -10,7 +10,7 @@
"dependencies": { "dependencies": {
"@discordjs/voice": "0.19.2", "@discordjs/voice": "0.19.2",
"discord-api-types": "0.38.48", "discord-api-types": "0.38.48",
"libopus-wasm": "0.1.0", "libopus-wasm": "0.2.0",
"typebox": "1.1.39", "typebox": "1.1.39",
"undici": "8.3.0", "undici": "8.3.0",
"ws": "8.21.0" "ws": "8.21.0"

View File

@@ -1,4 +1,4 @@
import { OpusError } from "libopus-wasm"; import { OpusError, OpusErrorCode } from "libopus-wasm";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { import {
analyzeVoiceReceiveError, analyzeVoiceReceiveError,
@@ -33,7 +33,11 @@ describe("voice receive recovery", () => {
}); });
it("treats corrupt Opus packets as non-recoverable decode noise", () => { it("treats corrupt Opus packets as non-recoverable decode noise", () => {
expect(analyzeVoiceReceiveError(new OpusError(-4, "not inspected", "decode"))).toEqual({ expect(
analyzeVoiceReceiveError(
new OpusError(OpusErrorCode.InvalidPacket, "not inspected", "decode"),
),
).toEqual({
message: "not inspected", message: "not inspected",
isAbortLike: false, isAbortLike: false,
isDecodeCorruption: true, isDecodeCorruption: true,
@@ -42,6 +46,23 @@ describe("voice receive recovery", () => {
}); });
}); });
it("treats structurally equivalent Opus errors as decode corruption", () => {
const analysis = analyzeVoiceReceiveError({
name: "OpusError",
message: "libopus decode failed (-4): corrupted stream",
code: OpusErrorCode.InvalidPacket,
codeName: "InvalidPacket",
operation: "decode",
});
expect(analysis).toMatchObject({
isAbortLike: false,
isDecodeCorruption: true,
shouldAttemptPassthrough: false,
countsAsDecryptFailure: false,
});
});
it("does not classify corrupt Opus packet text without the Opus error contract", () => { it("does not classify corrupt Opus packet text without the Opus error contract", () => {
expect( expect(
analyzeVoiceReceiveError(new Error("libopus decode failed (-4): corrupted stream")), analyzeVoiceReceiveError(new Error("libopus decode failed (-4): corrupted stream")),

View File

@@ -1,4 +1,4 @@
import { OpusError } from "libopus-wasm"; import { OpusErrorCode, isOpusError } from "libopus-wasm";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime"; import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
const DECRYPT_FAILURE_WINDOW_MS = 30_000; const DECRYPT_FAILURE_WINDOW_MS = 30_000;
@@ -6,7 +6,6 @@ const DECRYPT_FAILURE_RECONNECT_THRESHOLD = 3;
const DECRYPT_FAILURE_MARKER = "DecryptionFailed("; const DECRYPT_FAILURE_MARKER = "DecryptionFailed(";
const DAVE_PASSTHROUGH_DISABLED_MARKER = "UnencryptedWhenPassthroughDisabled"; const DAVE_PASSTHROUGH_DISABLED_MARKER = "UnencryptedWhenPassthroughDisabled";
const WASM_MEMORY_ACCESS_MARKER = "memory access out of bounds"; const WASM_MEMORY_ACCESS_MARKER = "memory access out of bounds";
const OPUS_INVALID_PACKET_CODE = -4;
export const DAVE_RECEIVE_PASSTHROUGH_INITIAL_EXPIRY_SECONDS = 30; export const DAVE_RECEIVE_PASSTHROUGH_INITIAL_EXPIRY_SECONDS = 30;
export const DAVE_RECEIVE_PASSTHROUGH_REARM_EXPIRY_SECONDS = 15; export const DAVE_RECEIVE_PASSTHROUGH_REARM_EXPIRY_SECONDS = 15;
@@ -85,8 +84,8 @@ function isAbortLikeReceiveError(err: unknown): boolean {
function isOpusDecodeInvalidPacketError(err: unknown): boolean { function isOpusDecodeInvalidPacketError(err: unknown): boolean {
return ( return (
err instanceof OpusError && isOpusError(err) &&
err.code === OPUS_INVALID_PACKET_CODE && err.code === OpusErrorCode.InvalidPacket &&
(err.operation === "decode" || err.operation === "decodeFloat") (err.operation === "decode" || err.operation === "decodeFloat")
); );
} }

10
pnpm-lock.yaml generated
View File

@@ -678,8 +678,8 @@ importers:
specifier: 0.38.48 specifier: 0.38.48
version: 0.38.48 version: 0.38.48
libopus-wasm: libopus-wasm:
specifier: 0.1.0 specifier: 0.2.0
version: 0.1.0 version: 0.2.0
typebox: typebox:
specifier: 1.1.39 specifier: 1.1.39
version: 1.1.39 version: 1.1.39
@@ -5541,8 +5541,8 @@ packages:
resolution: {integrity: sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==} resolution: {integrity: sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==}
engines: {node: '>=22.0.0'} engines: {node: '>=22.0.0'}
libopus-wasm@0.1.0: libopus-wasm@0.2.0:
resolution: {integrity: sha512-/aurGcAVgy0GcBEUzFaX9pm9qv7zYcy8W5hBXFiK+cyqOXAX4lOS6rlFogkY9CcSIajhjnuXyixsbmziSHCDMQ==} resolution: {integrity: sha512-x/2Gu1/C6L3IICY09zyfp984AWiOYjn53u4WfdY3yh+3KTzMN8Xkm77q3lenWMVIk5SnSzjGEkQT+VQMFHLBHQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
libsignal@6.0.0: libsignal@6.0.0:
@@ -11150,7 +11150,7 @@ snapshots:
kysely@0.29.2: {} kysely@0.29.2: {}
libopus-wasm@0.1.0: {} libopus-wasm@0.2.0: {}
libsignal@6.0.0: libsignal@6.0.0:
dependencies: dependencies:

View File

@@ -17,7 +17,7 @@ minimumReleaseAgeExclude:
- "basic-ftp" - "basic-ftp"
- "baileys@7.0.0-rc13" - "baileys@7.0.0-rc13"
- "hono" - "hono"
- "libopus-wasm@0.1.0" - "libopus-wasm@0.2.0"
- "libsignal@6.0.0" - "libsignal@6.0.0"
- "openclaw" - "openclaw"
- "protobufjs" - "protobufjs"