@openclaw/discord 2026.9.5 → 2026.9.6
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.
- package/dist/.setup/{activities-api-DBwRt5ES.mjs → activities-api-COU4-0nR.mjs} +4 -4
- package/dist/.setup/{approval-handler.runtime-Du3n5ypX.mjs → approval-handler.runtime-CbBktBb4.mjs} +5 -5
- package/dist/.setup/{approval-message-updates-BN7JtScm.mjs → approval-message-updates-jf9zMaIC.mjs} +1 -1
- package/dist/.setup/{audit-C8uaKoa9.mjs → audit-vKbh-LFR.mjs} +3 -3
- package/dist/.setup/{channel-ChKdlBgH.mjs → channel-Dv4e3Jpc.mjs} +19 -16
- package/dist/.setup/{channel-actions.runtime-CJmiFfps.mjs → channel-actions.runtime-leZuw01r.mjs} +13 -7
- package/dist/.setup/{channel.setup-BlC3-Kzz.mjs → channel.setup-0NX6FFwi.mjs} +1 -1
- package/dist/.setup/{components-MqMWUWnE.mjs → components-yBEb75bB.mjs} +1 -2
- package/dist/.setup/{conversation-binding-route-_rz33QHO.mjs → conversation-binding-route-B7qImK_b.mjs} +6 -3
- package/dist/.setup/{doctor-DxcdkXzz.mjs → doctor-D1R-uFIu.mjs} +2 -2
- package/dist/.setup/{handle-action.guild-admin-DAFyh_yV.mjs → handle-action.guild-admin-DcFa8NwA.mjs} +3 -3
- package/dist/.setup/{message-handler-DnUrDpaU.mjs → message-handler-X6ejmbtY.mjs} +6 -5
- package/dist/.setup/{message-handler.preflight-CtqaF_fb.mjs → message-handler.preflight-DE_POxFc.mjs} +14 -117
- package/dist/.setup/message-handler.preflight-pluralkit-8Xji34AF.mjs +143 -0
- package/dist/.setup/{message-handler.process-Dz1xvuQU.mjs → message-handler.process-CCmIVnxy.mjs} +387 -317
- package/dist/.setup/{outbound-adapter-C8v79z9Z.mjs → outbound-adapter-ClIgmu4F.mjs} +5 -5
- package/dist/.setup/{outbound-session-route-CqWbuMOm.mjs → outbound-session-route-VulUE0yV.mjs} +7 -6
- package/dist/.setup/{preflight-audio-DrnqFA8U.mjs → preflight-audio-DHGIHEpv.mjs} +2 -1
- package/dist/.setup/{provider-72zIvN-i.mjs → provider-iOf73HW-.mjs} +425 -286
- package/dist/.setup/{provider-session.runtime-BW7QrF5C.mjs → provider-session.runtime-DrGAYnZv.mjs} +3 -3
- package/dist/.setup/provider.runtime-BmjW_pIw.mjs +2 -0
- package/dist/.setup/{realtime-session.runtime-DTccTgTM.mjs → realtime-session.runtime-Dz8uVutq.mjs} +338 -297
- package/dist/.setup/receive-recovery-DHst8WRx.mjs +517 -0
- package/dist/.setup/{retry-DBfcEgmO.mjs → retry-BEYkDy0P.mjs} +78 -13
- package/dist/.setup/{runtime-SchNfUsQ.mjs → runtime-B8lJfyrA.mjs} +30 -11
- package/dist/.setup/{runtime-BVoa7hhx.mjs → runtime-DisDbixd.mjs} +1 -1
- package/dist/.setup/{send-CKDczUNL.mjs → send-CIBzvXjS.mjs} +23 -9
- package/dist/.setup/{send.components-DWQjXhPV.mjs → send.components-ChY21qr-.mjs} +4 -4
- package/dist/.setup/{send.outbound-D0hQ7s50.mjs → send.outbound-QTyuFupn.mjs} +15 -16
- package/dist/.setup/{send.shared-Zwx6BxQ5.mjs → send.shared-VNvWfX2T.mjs} +2 -2
- package/dist/.setup/setup-plugin-api-Dvr41dyx.mjs +3 -0
- package/dist/.setup/{shared-CaA3Vv4n.mjs → shared-DHPDaMSx.mjs} +1 -1
- package/dist/.setup/{subagent-hooks-Bired9q6.mjs → subagent-hooks-CjW7r8Pe.mjs} +1 -1
- package/dist/.setup/{target-resolver-B6MrxbYv.mjs → target-resolver-BXpy6VT8.mjs} +1 -1
- package/dist/.setup/targets-CvemhN3i.mjs +3 -0
- package/dist/.setup/{thread-bindings-CK5272V3.mjs → thread-bindings-CEh2dhqP.mjs} +3 -3
- package/dist/.setup/{thread-bindings.manager-eexkKyrG.mjs → thread-bindings.manager-ve-i6oqt.mjs} +94 -45
- package/dist/.setup/{transcripts-source-CuWYgayu.mjs → transcripts-source-DVegW0WI.mjs} +61 -7
- package/dist/.setup/{voice-runtime-DHVQ2mFz.mjs → voice-runtime-elTtjEIG.mjs} +553 -901
- package/dist/action-runtime-api.js +1 -1
- package/dist/activities-api.js +1 -1
- package/dist/api.js +10 -10
- package/dist/channel-plugin-api.js +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime-api.js +12 -12
- package/dist/setup-entry.js +1 -1
- package/dist/setup-plugin-api.js +1 -1
- package/dist/src/voice/audio-worker.runtime.js +1065 -0
- package/dist/subagent-hooks-api.js +1 -1
- package/dist/transcripts-source-api.js +1 -1
- package/node_modules/@napi-rs/wasm-runtime/dist/emnapi-plugins.cjs +8 -4
- package/node_modules/@napi-rs/wasm-runtime/dist/emnapi-plugins.js +8 -4
- package/node_modules/@napi-rs/wasm-runtime/dist/fs-proxy.cjs +1 -1
- package/node_modules/@napi-rs/wasm-runtime/dist/fs.js +770 -695
- package/node_modules/@napi-rs/wasm-runtime/fs-proxy.js +3 -2
- package/node_modules/@napi-rs/wasm-runtime/package.json +1 -1
- package/node_modules/@tybys/wasm-util/LICENSE +21 -0
- package/node_modules/@tybys/wasm-util/package.json +1 -1
- package/node_modules/@types/node/README.md +1 -1
- package/node_modules/@types/node/package.json +2 -2
- package/node_modules/@types/node/web-globals/streams.d.ts +4 -0
- package/node_modules/zod/README.md +2 -2
- package/node_modules/zod/compile.cjs +19 -0
- package/node_modules/zod/index.cjs +19 -0
- package/node_modules/zod/locales/index.cjs +19 -0
- package/node_modules/zod/mini/index.cjs +19 -0
- package/node_modules/zod/package.json +8 -7
- package/node_modules/zod/src/v3/helpers/errorUtil.ts +1 -1
- package/node_modules/zod/src/v4/classic/external.ts +2 -0
- package/node_modules/zod/src/v4/classic/from-json-schema.ts +248 -25
- package/node_modules/zod/src/v4/classic/schemas.ts +243 -168
- package/node_modules/zod/src/v4/classic/tests/assignability.test.ts +60 -9
- package/node_modules/zod/src/v4/classic/tests/bigint.test.ts +6 -0
- package/node_modules/zod/src/v4/classic/tests/continuability.test.ts +2 -2
- package/node_modules/zod/src/v4/classic/tests/cyclic-data.test.ts +265 -0
- package/node_modules/zod/src/v4/classic/tests/discriminated-unions.test.ts +112 -6
- package/node_modules/zod/src/v4/classic/tests/enum.test.ts +9 -0
- package/node_modules/zod/src/v4/classic/tests/error.test.ts +130 -1
- package/node_modules/zod/src/v4/classic/tests/from-json-schema.test.ts +157 -0
- package/node_modules/zod/src/v4/classic/tests/iban.test.ts +81 -0
- package/node_modules/zod/src/v4/classic/tests/instance-footprint.test.ts +40 -2
- package/node_modules/zod/src/v4/classic/tests/instanceof.test.ts +27 -7
- package/node_modules/zod/src/v4/classic/tests/issue-schema.test.ts +88 -56
- package/node_modules/zod/src/v4/classic/tests/number.test.ts +39 -0
- package/node_modules/zod/src/v4/classic/tests/object.test.ts +56 -0
- package/node_modules/zod/src/v4/classic/tests/optin-ladder.test.ts +3 -2
- package/node_modules/zod/src/v4/classic/tests/prefault.test.ts +70 -0
- package/node_modules/zod/src/v4/classic/tests/properties.test.ts +141 -0
- package/node_modules/zod/src/v4/classic/tests/recursive-types.test.ts +46 -1
- package/node_modules/zod/src/v4/classic/tests/standard-schema.test.ts +19 -0
- package/node_modules/zod/src/v4/classic/tests/string.test.ts +95 -1
- package/node_modules/zod/src/v4/classic/tests/template-literal.test.ts +33 -1
- package/node_modules/zod/src/v4/classic/tests/to-json-schema.test.ts +234 -38
- package/node_modules/zod/src/v4/classic/tests/validate.test.ts +569 -0
- package/node_modules/zod/src/v4/core/api.ts +44 -28
- package/node_modules/zod/src/v4/core/checks.ts +62 -102
- package/node_modules/zod/src/v4/core/compile.ts +218 -93
- package/node_modules/zod/src/v4/core/core.ts +1 -3
- package/node_modules/zod/src/v4/core/doc.ts +6 -2
- package/node_modules/zod/src/v4/core/errors.ts +3 -6
- package/node_modules/zod/src/v4/core/json-schema-generator.ts +2 -2
- package/node_modules/zod/src/v4/core/json-schema-processors.ts +185 -81
- package/node_modules/zod/src/v4/core/memoizer.ts +75 -28
- package/node_modules/zod/src/v4/core/parse.ts +37 -16
- package/node_modules/zod/src/v4/core/regexes.ts +14 -4
- package/node_modules/zod/src/v4/core/schemas.ts +296 -132
- package/node_modules/zod/src/v4/core/tests/cached.test.ts +54 -0
- package/node_modules/zod/src/v4/core/tests/compile-differential.test.ts +35 -0
- package/node_modules/zod/src/v4/core/tests/compile-generative.test.ts +445 -0
- package/node_modules/zod/src/v4/core/tests/compile.test.ts +399 -2
- package/node_modules/zod/src/v4/core/tests/email-regex.test.ts +140 -0
- package/node_modules/zod/src/v4/core/tests/locales/tg.test.ts +85 -0
- package/node_modules/zod/src/v4/core/tests/polyfill-collision.test.ts +46 -0
- package/node_modules/zod/src/v4/core/tests/url-no-canparse.test.ts +118 -16
- package/node_modules/zod/src/v4/core/to-json-schema.ts +9 -7
- package/node_modules/zod/src/v4/core/util.ts +218 -147
- package/node_modules/zod/src/v4/core/versions.ts +2 -2
- package/node_modules/zod/src/v4/locales/ar.ts +2 -0
- package/node_modules/zod/src/v4/locales/az.ts +2 -0
- package/node_modules/zod/src/v4/locales/be.ts +2 -0
- package/node_modules/zod/src/v4/locales/bg.ts +2 -0
- package/node_modules/zod/src/v4/locales/bn.ts +2 -0
- package/node_modules/zod/src/v4/locales/ca.ts +2 -0
- package/node_modules/zod/src/v4/locales/ckb.ts +2 -0
- package/node_modules/zod/src/v4/locales/cs.ts +2 -0
- package/node_modules/zod/src/v4/locales/da.ts +2 -0
- package/node_modules/zod/src/v4/locales/de.ts +2 -0
- package/node_modules/zod/src/v4/locales/el.ts +2 -0
- package/node_modules/zod/src/v4/locales/en.ts +2 -0
- package/node_modules/zod/src/v4/locales/eo.ts +2 -0
- package/node_modules/zod/src/v4/locales/es.ts +2 -0
- package/node_modules/zod/src/v4/locales/fa.ts +2 -0
- package/node_modules/zod/src/v4/locales/fi.ts +2 -0
- package/node_modules/zod/src/v4/locales/fr-CA.ts +2 -0
- package/node_modules/zod/src/v4/locales/fr.ts +2 -0
- package/node_modules/zod/src/v4/locales/gu.ts +2 -0
- package/node_modules/zod/src/v4/locales/he.ts +2 -0
- package/node_modules/zod/src/v4/locales/hi.ts +2 -0
- package/node_modules/zod/src/v4/locales/hr.ts +2 -0
- package/node_modules/zod/src/v4/locales/hu.ts +2 -0
- package/node_modules/zod/src/v4/locales/hy.ts +2 -0
- package/node_modules/zod/src/v4/locales/id.ts +2 -0
- package/node_modules/zod/src/v4/locales/index.ts +1 -0
- package/node_modules/zod/src/v4/locales/is.ts +2 -0
- package/node_modules/zod/src/v4/locales/it.ts +2 -0
- package/node_modules/zod/src/v4/locales/ja.ts +2 -0
- package/node_modules/zod/src/v4/locales/ka.ts +2 -0
- package/node_modules/zod/src/v4/locales/km.ts +2 -0
- package/node_modules/zod/src/v4/locales/kn.ts +2 -0
- package/node_modules/zod/src/v4/locales/ko.ts +2 -0
- package/node_modules/zod/src/v4/locales/lt.ts +2 -0
- package/node_modules/zod/src/v4/locales/mk.ts +2 -0
- package/node_modules/zod/src/v4/locales/ms.ts +2 -0
- package/node_modules/zod/src/v4/locales/ne.ts +2 -0
- package/node_modules/zod/src/v4/locales/nl.ts +2 -0
- package/node_modules/zod/src/v4/locales/nn.ts +2 -0
- package/node_modules/zod/src/v4/locales/no.ts +2 -0
- package/node_modules/zod/src/v4/locales/ota.ts +2 -0
- package/node_modules/zod/src/v4/locales/pl.ts +2 -0
- package/node_modules/zod/src/v4/locales/ps.ts +2 -0
- package/node_modules/zod/src/v4/locales/pt-BR.ts +2 -0
- package/node_modules/zod/src/v4/locales/pt.ts +2 -0
- package/node_modules/zod/src/v4/locales/ro.ts +2 -0
- package/node_modules/zod/src/v4/locales/ru.ts +2 -0
- package/node_modules/zod/src/v4/locales/sk.ts +2 -0
- package/node_modules/zod/src/v4/locales/sl.ts +2 -0
- package/node_modules/zod/src/v4/locales/sv.ts +2 -0
- package/node_modules/zod/src/v4/locales/ta.ts +2 -0
- package/node_modules/zod/src/v4/locales/tg.ts +135 -0
- package/node_modules/zod/src/v4/locales/th.ts +2 -0
- package/node_modules/zod/src/v4/locales/tk.ts +2 -0
- package/node_modules/zod/src/v4/locales/tr.ts +2 -0
- package/node_modules/zod/src/v4/locales/uk.ts +2 -0
- package/node_modules/zod/src/v4/locales/ur.ts +2 -0
- package/node_modules/zod/src/v4/locales/uz.ts +2 -0
- package/node_modules/zod/src/v4/locales/vi.ts +2 -0
- package/node_modules/zod/src/v4/locales/yo.ts +2 -0
- package/node_modules/zod/src/v4/locales/zh-CN.ts +2 -0
- package/node_modules/zod/src/v4/locales/zh-TW.ts +2 -0
- package/node_modules/zod/src/v4/mini/external.ts +2 -0
- package/node_modules/zod/src/v4/mini/schemas.ts +32 -7
- package/node_modules/zod/src/v4/mini/tests/checks.test.ts +1 -1
- package/node_modules/zod/src/v4/mini/tests/computed.test.ts +11 -17
- package/node_modules/zod/src/v4/mini/tests/index.test.ts +86 -0
- package/node_modules/zod/src/v4/mini/tests/recursive-types.test.ts +16 -0
- package/node_modules/zod/src/v4/mini/tests/string.test.ts +23 -0
- package/node_modules/zod/v3/ZodError.cjs +19 -0
- package/node_modules/zod/v3/errors.cjs +19 -0
- package/node_modules/zod/v3/external.cjs +19 -0
- package/node_modules/zod/v3/helpers/enumUtil.cjs +19 -0
- package/node_modules/zod/v3/helpers/errorUtil.cjs +20 -1
- package/node_modules/zod/v3/helpers/errorUtil.js +1 -1
- package/node_modules/zod/v3/helpers/parseUtil.cjs +19 -0
- package/node_modules/zod/v3/helpers/partialUtil.cjs +19 -0
- package/node_modules/zod/v3/helpers/typeAliases.cjs +19 -0
- package/node_modules/zod/v3/helpers/util.cjs +19 -0
- package/node_modules/zod/v3/index.cjs +19 -0
- package/node_modules/zod/v3/standard-schema.cjs +19 -0
- package/node_modules/zod/v3/types.cjs +19 -0
- package/node_modules/zod/v4/classic/checks.cjs +19 -0
- package/node_modules/zod/v4/classic/coerce.cjs +19 -0
- package/node_modules/zod/v4/classic/compat.cjs +19 -0
- package/node_modules/zod/v4/classic/deep-partial.cjs +19 -0
- package/node_modules/zod/v4/classic/errors.cjs +19 -0
- package/node_modules/zod/v4/classic/external.cjs +22 -1
- package/node_modules/zod/v4/classic/external.d.cts +1 -1
- package/node_modules/zod/v4/classic/external.d.ts +1 -1
- package/node_modules/zod/v4/classic/external.js +1 -1
- package/node_modules/zod/v4/classic/from-json-schema.cjs +264 -24
- package/node_modules/zod/v4/classic/from-json-schema.js +245 -24
- package/node_modules/zod/v4/classic/in-out.cjs +19 -0
- package/node_modules/zod/v4/classic/index.cjs +19 -0
- package/node_modules/zod/v4/classic/iso.cjs +19 -0
- package/node_modules/zod/v4/classic/parse.cjs +19 -0
- package/node_modules/zod/v4/classic/schemas.cjs +100 -35
- package/node_modules/zod/v4/classic/schemas.d.cts +15 -1
- package/node_modules/zod/v4/classic/schemas.d.ts +15 -1
- package/node_modules/zod/v4/classic/schemas.js +77 -33
- package/node_modules/zod/v4/core/api.cjs +45 -22
- package/node_modules/zod/v4/core/api.d.cts +12 -6
- package/node_modules/zod/v4/core/api.d.ts +12 -6
- package/node_modules/zod/v4/core/api.js +25 -22
- package/node_modules/zod/v4/core/checks.cjs +51 -97
- package/node_modules/zod/v4/core/checks.d.cts +17 -1
- package/node_modules/zod/v4/core/checks.d.ts +17 -1
- package/node_modules/zod/v4/core/checks.js +31 -96
- package/node_modules/zod/v4/core/compile.cjs +173 -78
- package/node_modules/zod/v4/core/compile.d.cts +18 -4
- package/node_modules/zod/v4/core/compile.d.ts +18 -4
- package/node_modules/zod/v4/core/compile.js +154 -79
- package/node_modules/zod/v4/core/core.cjs +20 -1
- package/node_modules/zod/v4/core/core.js +1 -1
- package/node_modules/zod/v4/core/doc.cjs +26 -2
- package/node_modules/zod/v4/core/doc.js +7 -2
- package/node_modules/zod/v4/core/errors.cjs +21 -5
- package/node_modules/zod/v4/core/errors.js +2 -5
- package/node_modules/zod/v4/core/index.cjs +19 -0
- package/node_modules/zod/v4/core/json-schema-generator.cjs +20 -1
- package/node_modules/zod/v4/core/json-schema-generator.js +2 -2
- package/node_modules/zod/v4/core/json-schema-processors.cjs +188 -72
- package/node_modules/zod/v4/core/json-schema-processors.d.cts +16 -0
- package/node_modules/zod/v4/core/json-schema-processors.d.ts +16 -0
- package/node_modules/zod/v4/core/json-schema-processors.js +170 -74
- package/node_modules/zod/v4/core/json-schema.cjs +19 -0
- package/node_modules/zod/v4/core/memoizer.cjs +111 -23
- package/node_modules/zod/v4/core/memoizer.js +69 -23
- package/node_modules/zod/v4/core/parse.cjs +56 -16
- package/node_modules/zod/v4/core/parse.js +37 -16
- package/node_modules/zod/v4/core/regexes.cjs +31 -6
- package/node_modules/zod/v4/core/regexes.d.cts +3 -0
- package/node_modules/zod/v4/core/regexes.d.ts +3 -0
- package/node_modules/zod/v4/core/regexes.js +10 -4
- package/node_modules/zod/v4/core/registries.cjs +19 -0
- package/node_modules/zod/v4/core/schemas.cjs +302 -114
- package/node_modules/zod/v4/core/schemas.d.cts +28 -8
- package/node_modules/zod/v4/core/schemas.d.ts +28 -8
- package/node_modules/zod/v4/core/schemas.js +279 -113
- package/node_modules/zod/v4/core/standard-schema.cjs +19 -0
- package/node_modules/zod/v4/core/to-json-schema.cjs +28 -7
- package/node_modules/zod/v4/core/to-json-schema.d.cts +4 -3
- package/node_modules/zod/v4/core/to-json-schema.d.ts +4 -3
- package/node_modules/zod/v4/core/to-json-schema.js +7 -6
- package/node_modules/zod/v4/core/util.cjs +177 -143
- package/node_modules/zod/v4/core/util.d.cts +25 -4
- package/node_modules/zod/v4/core/util.d.ts +25 -4
- package/node_modules/zod/v4/core/util.js +156 -143
- package/node_modules/zod/v4/core/versions.cjs +21 -2
- package/node_modules/zod/v4/core/versions.d.cts +1 -1
- package/node_modules/zod/v4/core/versions.d.ts +1 -1
- package/node_modules/zod/v4/core/versions.js +2 -2
- package/node_modules/zod/v4/core/visit.cjs +19 -0
- package/node_modules/zod/v4/index.cjs +19 -0
- package/node_modules/zod/v4/locales/ar.cjs +2 -0
- package/node_modules/zod/v4/locales/ar.js +2 -0
- package/node_modules/zod/v4/locales/az.cjs +2 -0
- package/node_modules/zod/v4/locales/az.js +2 -0
- package/node_modules/zod/v4/locales/be.cjs +2 -0
- package/node_modules/zod/v4/locales/be.js +2 -0
- package/node_modules/zod/v4/locales/bg.cjs +2 -0
- package/node_modules/zod/v4/locales/bg.js +2 -0
- package/node_modules/zod/v4/locales/bn.cjs +2 -0
- package/node_modules/zod/v4/locales/bn.js +2 -0
- package/node_modules/zod/v4/locales/ca.cjs +2 -0
- package/node_modules/zod/v4/locales/ca.js +2 -0
- package/node_modules/zod/v4/locales/ckb.cjs +2 -0
- package/node_modules/zod/v4/locales/ckb.js +2 -0
- package/node_modules/zod/v4/locales/cs.cjs +2 -0
- package/node_modules/zod/v4/locales/cs.js +2 -0
- package/node_modules/zod/v4/locales/da.cjs +2 -0
- package/node_modules/zod/v4/locales/da.js +2 -0
- package/node_modules/zod/v4/locales/de.cjs +2 -0
- package/node_modules/zod/v4/locales/de.js +2 -0
- package/node_modules/zod/v4/locales/el.cjs +2 -0
- package/node_modules/zod/v4/locales/el.js +2 -0
- package/node_modules/zod/v4/locales/en.cjs +2 -0
- package/node_modules/zod/v4/locales/en.js +2 -0
- package/node_modules/zod/v4/locales/eo.cjs +2 -0
- package/node_modules/zod/v4/locales/eo.js +2 -0
- package/node_modules/zod/v4/locales/es.cjs +2 -0
- package/node_modules/zod/v4/locales/es.js +2 -0
- package/node_modules/zod/v4/locales/fa.cjs +2 -0
- package/node_modules/zod/v4/locales/fa.js +2 -0
- package/node_modules/zod/v4/locales/fi.cjs +2 -0
- package/node_modules/zod/v4/locales/fi.js +2 -0
- package/node_modules/zod/v4/locales/fr-CA.cjs +2 -0
- package/node_modules/zod/v4/locales/fr-CA.js +2 -0
- package/node_modules/zod/v4/locales/fr.cjs +2 -0
- package/node_modules/zod/v4/locales/fr.js +2 -0
- package/node_modules/zod/v4/locales/gu.cjs +2 -0
- package/node_modules/zod/v4/locales/gu.js +2 -0
- package/node_modules/zod/v4/locales/he.cjs +2 -0
- package/node_modules/zod/v4/locales/he.js +2 -0
- package/node_modules/zod/v4/locales/hi.cjs +2 -0
- package/node_modules/zod/v4/locales/hi.js +2 -0
- package/node_modules/zod/v4/locales/hr.cjs +2 -0
- package/node_modules/zod/v4/locales/hr.js +2 -0
- package/node_modules/zod/v4/locales/hu.cjs +2 -0
- package/node_modules/zod/v4/locales/hu.js +2 -0
- package/node_modules/zod/v4/locales/hy.cjs +2 -0
- package/node_modules/zod/v4/locales/hy.js +2 -0
- package/node_modules/zod/v4/locales/id.cjs +2 -0
- package/node_modules/zod/v4/locales/id.js +2 -0
- package/node_modules/zod/v4/locales/index.cjs +22 -1
- package/node_modules/zod/v4/locales/index.d.cts +1 -0
- package/node_modules/zod/v4/locales/index.d.ts +1 -0
- package/node_modules/zod/v4/locales/index.js +1 -0
- package/node_modules/zod/v4/locales/is.cjs +2 -0
- package/node_modules/zod/v4/locales/is.js +2 -0
- package/node_modules/zod/v4/locales/it.cjs +2 -0
- package/node_modules/zod/v4/locales/it.js +2 -0
- package/node_modules/zod/v4/locales/ja.cjs +2 -0
- package/node_modules/zod/v4/locales/ja.js +2 -0
- package/node_modules/zod/v4/locales/ka.cjs +2 -0
- package/node_modules/zod/v4/locales/ka.js +2 -0
- package/node_modules/zod/v4/locales/km.cjs +2 -0
- package/node_modules/zod/v4/locales/km.js +2 -0
- package/node_modules/zod/v4/locales/kn.cjs +2 -0
- package/node_modules/zod/v4/locales/kn.js +2 -0
- package/node_modules/zod/v4/locales/ko.cjs +2 -0
- package/node_modules/zod/v4/locales/ko.js +2 -0
- package/node_modules/zod/v4/locales/lt.cjs +2 -0
- package/node_modules/zod/v4/locales/lt.js +2 -0
- package/node_modules/zod/v4/locales/mk.cjs +2 -0
- package/node_modules/zod/v4/locales/mk.js +2 -0
- package/node_modules/zod/v4/locales/ms.cjs +2 -0
- package/node_modules/zod/v4/locales/ms.js +2 -0
- package/node_modules/zod/v4/locales/ne.cjs +2 -0
- package/node_modules/zod/v4/locales/ne.js +2 -0
- package/node_modules/zod/v4/locales/nl.cjs +2 -0
- package/node_modules/zod/v4/locales/nl.js +2 -0
- package/node_modules/zod/v4/locales/nn.cjs +2 -0
- package/node_modules/zod/v4/locales/nn.js +2 -0
- package/node_modules/zod/v4/locales/no.cjs +2 -0
- package/node_modules/zod/v4/locales/no.js +2 -0
- package/node_modules/zod/v4/locales/ota.cjs +2 -0
- package/node_modules/zod/v4/locales/ota.js +2 -0
- package/node_modules/zod/v4/locales/pl.cjs +2 -0
- package/node_modules/zod/v4/locales/pl.js +2 -0
- package/node_modules/zod/v4/locales/ps.cjs +2 -0
- package/node_modules/zod/v4/locales/ps.js +2 -0
- package/node_modules/zod/v4/locales/pt-BR.cjs +2 -0
- package/node_modules/zod/v4/locales/pt-BR.js +2 -0
- package/node_modules/zod/v4/locales/pt.cjs +2 -0
- package/node_modules/zod/v4/locales/pt.js +2 -0
- package/node_modules/zod/v4/locales/ro.cjs +2 -0
- package/node_modules/zod/v4/locales/ro.js +2 -0
- package/node_modules/zod/v4/locales/ru.cjs +2 -0
- package/node_modules/zod/v4/locales/ru.js +2 -0
- package/node_modules/zod/v4/locales/sk.cjs +2 -0
- package/node_modules/zod/v4/locales/sk.js +2 -0
- package/node_modules/zod/v4/locales/sl.cjs +2 -0
- package/node_modules/zod/v4/locales/sl.js +2 -0
- package/node_modules/zod/v4/locales/sv.cjs +2 -0
- package/node_modules/zod/v4/locales/sv.js +2 -0
- package/node_modules/zod/v4/locales/ta.cjs +2 -0
- package/node_modules/zod/v4/locales/ta.js +2 -0
- package/node_modules/zod/v4/locales/tg.cjs +144 -0
- package/node_modules/zod/v4/locales/tg.d.cts +5 -0
- package/node_modules/zod/v4/locales/tg.d.ts +4 -0
- package/node_modules/zod/v4/locales/tg.js +117 -0
- package/node_modules/zod/v4/locales/th.cjs +2 -0
- package/node_modules/zod/v4/locales/th.js +2 -0
- package/node_modules/zod/v4/locales/tk.cjs +2 -0
- package/node_modules/zod/v4/locales/tk.js +2 -0
- package/node_modules/zod/v4/locales/tr.cjs +2 -0
- package/node_modules/zod/v4/locales/tr.js +2 -0
- package/node_modules/zod/v4/locales/uk.cjs +2 -0
- package/node_modules/zod/v4/locales/uk.js +2 -0
- package/node_modules/zod/v4/locales/ur.cjs +2 -0
- package/node_modules/zod/v4/locales/ur.js +2 -0
- package/node_modules/zod/v4/locales/uz.cjs +2 -0
- package/node_modules/zod/v4/locales/uz.js +2 -0
- package/node_modules/zod/v4/locales/vi.cjs +2 -0
- package/node_modules/zod/v4/locales/vi.js +2 -0
- package/node_modules/zod/v4/locales/yo.cjs +2 -0
- package/node_modules/zod/v4/locales/yo.js +2 -0
- package/node_modules/zod/v4/locales/zh-CN.cjs +2 -0
- package/node_modules/zod/v4/locales/zh-CN.js +2 -0
- package/node_modules/zod/v4/locales/zh-TW.cjs +2 -0
- package/node_modules/zod/v4/locales/zh-TW.js +2 -0
- package/node_modules/zod/v4/mini/checks.cjs +19 -0
- package/node_modules/zod/v4/mini/coerce.cjs +19 -0
- package/node_modules/zod/v4/mini/deep-partial.cjs +19 -0
- package/node_modules/zod/v4/mini/external.cjs +22 -1
- package/node_modules/zod/v4/mini/external.d.cts +1 -1
- package/node_modules/zod/v4/mini/external.d.ts +1 -1
- package/node_modules/zod/v4/mini/external.js +1 -1
- package/node_modules/zod/v4/mini/in-out.cjs +19 -0
- package/node_modules/zod/v4/mini/index.cjs +19 -0
- package/node_modules/zod/v4/mini/iso.cjs +19 -0
- package/node_modules/zod/v4/mini/parse.cjs +19 -0
- package/node_modules/zod/v4/mini/schemas.cjs +44 -8
- package/node_modules/zod/v4/mini/schemas.d.cts +6 -1
- package/node_modules/zod/v4/mini/schemas.d.ts +6 -1
- package/node_modules/zod/v4/mini/schemas.js +21 -6
- package/node_modules/zod/v4-mini/index.cjs +19 -0
- package/package.json +8 -5
- package/dist/.setup/message-handler.history-CJOsN9w5.mjs +0 -28
- package/dist/.setup/provider.runtime-DuhmKdDU.mjs +0 -2
- package/dist/.setup/setup-plugin-api-CINIEBLa.mjs +0 -3
- package/dist/.setup/targets-D1mAB5yO.mjs +0 -3
|
@@ -1,135 +1,31 @@
|
|
|
1
1
|
import { Mt as getGuildVoiceState, Ut as __exportAll, l as ReadyListener, p as VoiceStateUpdateListener, t as discord_exports, u as ResumedListener, y as isUnknownDiscordVoiceStateError } from "./discord-BXpHW-cu.mjs";
|
|
2
2
|
import { h as resolveDiscordOwnerAccess, o as normalizeDiscordSlug, x as formatDiscordUserTag } from "./channel-type-DKnjV1XW.mjs";
|
|
3
3
|
import { t as getDiscordRuntime } from "./runtime-DgnVQ7zW.mjs";
|
|
4
|
-
import { y as parseDiscordTarget } from "./retry-
|
|
5
|
-
import { o as formatMention } from "./send.outbound-
|
|
6
|
-
import { c as
|
|
7
|
-
import { m as
|
|
8
|
-
import {
|
|
4
|
+
import { y as parseDiscordTarget } from "./retry-BEYkDy0P.mjs";
|
|
5
|
+
import { o as formatMention } from "./send.outbound-QTyuFupn.mjs";
|
|
6
|
+
import { c as authorizeDiscordVoiceIngress, l as resolveDiscordVoiceAccess, r as resolveDiscordTranscriptsCapture, s as resolveDiscordVoiceEnabled, t as bindDiscordCaptureReceipts, u as resolveDiscordVoiceAccessTarget } from "./transcripts-source-DVegW0WI.mjs";
|
|
7
|
+
import { a as finishVoiceDecryptRecovery, c as resetVoiceReceiveRecoveryState, m as writeVoiceWavFile, n as analyzeVoiceReceiveError, o as noteVoiceDecryptFailure, r as createVoiceReceiveRecoveryState, t as DECRYPT_FAILURE_WINDOW_MS, y as restoreDiscordAudioError } from "./receive-recovery-DHst8WRx.mjs";
|
|
8
|
+
import { m as buildDiscordGroupSystemPrompt } from "./provider-iOf73HW-.mjs";
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
11
11
|
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
12
12
|
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
13
|
-
import { formatErrorMessage as formatErrorMessage$1, readErrorName } from "openclaw/plugin-sdk/error-runtime";
|
|
13
|
+
import { formatErrorMessage as formatErrorMessage$1, readErrorName, toErrorObject } from "openclaw/plugin-sdk/error-runtime";
|
|
14
14
|
import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
|
|
15
15
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
16
|
-
import { createSubsystemLogger, logVerbose
|
|
16
|
+
import { createSubsystemLogger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
17
17
|
import { escapeRegExp, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
18
18
|
import { stripInlineDirectiveTagsForDisplay } from "openclaw/plugin-sdk/text-chunking";
|
|
19
|
-
import {
|
|
19
|
+
import { unlinkIfExists } from "openclaw/plugin-sdk/media-runtime";
|
|
20
|
+
import { Worker } from "node:worker_threads";
|
|
21
|
+
import { PassThrough, Readable } from "node:stream";
|
|
20
22
|
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
21
|
-
import { resolvePreferredOpenClawTmpDir, tempWorkspace } from "openclaw/plugin-sdk/temp-path";
|
|
22
|
-
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
|
|
23
23
|
import { enqueueRoutedSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { StringDecoder } from "node:string_decoder";
|
|
28
|
-
import { Application, OpusError, createDecoder, createEncoder } from "libopus-wasm";
|
|
24
|
+
import { resolveAgentDir } from "openclaw/plugin-sdk/agent-runtime";
|
|
25
|
+
import { EventEmitter } from "node:events";
|
|
26
|
+
import { controlRealtimeVoiceAgentRun, parseRealtimeVoiceAgentControlToolArgs, shouldAutoControlRealtimeVoiceAgentText } from "openclaw/plugin-sdk/realtime-voice";
|
|
29
27
|
import { resolveRealtimeBootstrapContextInstructions } from "openclaw/plugin-sdk/realtime-bootstrap-context";
|
|
30
|
-
|
|
31
|
-
let cachedDiscordVoiceSdk = null;
|
|
32
|
-
function loadDiscordVoiceSdk() {
|
|
33
|
-
if (cachedDiscordVoiceSdk) return cachedDiscordVoiceSdk;
|
|
34
|
-
cachedDiscordVoiceSdk = createRequire(import.meta.url)("@discordjs/voice");
|
|
35
|
-
return cachedDiscordVoiceSdk;
|
|
36
|
-
}
|
|
37
|
-
//#endregion
|
|
38
|
-
//#region extensions/discord/src/voice/realtime-player.ts
|
|
39
|
-
const DISCORD_REALTIME_PLAYBACK_IDLE_MS = 2e3;
|
|
40
|
-
/** One physical player serves every speaker lane in the room. */
|
|
41
|
-
var DiscordRealtimePlayer = class {
|
|
42
|
-
constructor(player) {
|
|
43
|
-
this.player = player;
|
|
44
|
-
this.queue = [];
|
|
45
|
-
this.lanes = /* @__PURE__ */ new Set();
|
|
46
|
-
this.changing = false;
|
|
47
|
-
this.closed = false;
|
|
48
|
-
this.onIdle = () => {
|
|
49
|
-
const request = this.current;
|
|
50
|
-
this.current = void 0;
|
|
51
|
-
if (request) this.transition(() => request.onIdle());
|
|
52
|
-
};
|
|
53
|
-
player.on(loadDiscordVoiceSdk().AudioPlayerStatus.Idle, this.onIdle);
|
|
54
|
-
}
|
|
55
|
-
registerLane(lane) {
|
|
56
|
-
this.lanes.add(lane);
|
|
57
|
-
return () => this.lanes.delete(lane);
|
|
58
|
-
}
|
|
59
|
-
enqueue(request) {
|
|
60
|
-
if (this.closed || this.current === request) return;
|
|
61
|
-
if (!this.queue.includes(request)) this.queue.push(request);
|
|
62
|
-
this.drain();
|
|
63
|
-
}
|
|
64
|
-
isRetiring(request) {
|
|
65
|
-
if (this.current !== request) return false;
|
|
66
|
-
const state = this.player.state;
|
|
67
|
-
return state.status !== loadDiscordVoiceSdk().AudioPlayerStatus.Idle && state.resource.silenceRemaining >= 0;
|
|
68
|
-
}
|
|
69
|
-
cancel(request) {
|
|
70
|
-
this.queue = this.queue.filter((queued) => queued !== request);
|
|
71
|
-
if (this.current !== request) {
|
|
72
|
-
this.drain();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
this.current = void 0;
|
|
76
|
-
this.transition(() => this.player.stop(true));
|
|
77
|
-
}
|
|
78
|
-
handleBargeIn(reason = "barge-in") {
|
|
79
|
-
const current = this.current;
|
|
80
|
-
if (current) return current.onBargeIn(reason);
|
|
81
|
-
let interrupted = false;
|
|
82
|
-
const activeLanes = Array.from(this.lanes).filter((lane) => lane.hasOutput());
|
|
83
|
-
for (const lane of activeLanes) interrupted = lane.onBargeIn(reason) || interrupted;
|
|
84
|
-
return interrupted;
|
|
85
|
-
}
|
|
86
|
-
isActive() {
|
|
87
|
-
return this.current !== void 0 || Array.from(this.lanes).some((lane) => lane.hasOutput());
|
|
88
|
-
}
|
|
89
|
-
cancelForControl() {
|
|
90
|
-
this.transition(() => {
|
|
91
|
-
this.queue = [];
|
|
92
|
-
for (const lane of this.lanes) lane.cancelForControl();
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
close() {
|
|
96
|
-
this.closed = true;
|
|
97
|
-
this.queue = [];
|
|
98
|
-
this.lanes.clear();
|
|
99
|
-
this.current = void 0;
|
|
100
|
-
this.player.off(loadDiscordVoiceSdk().AudioPlayerStatus.Idle, this.onIdle);
|
|
101
|
-
this.player.stop(true);
|
|
102
|
-
}
|
|
103
|
-
/** Prevent retiring one lane from granting playback to a sibling that is also retiring. */
|
|
104
|
-
transition(action) {
|
|
105
|
-
const wasChanging = this.changing;
|
|
106
|
-
this.changing = true;
|
|
107
|
-
try {
|
|
108
|
-
action();
|
|
109
|
-
} finally {
|
|
110
|
-
this.changing = wasChanging;
|
|
111
|
-
this.drain();
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
drain() {
|
|
115
|
-
if (this.closed || this.changing || this.current) return;
|
|
116
|
-
const next = this.queue[0];
|
|
117
|
-
if (!next?.isReady()) return;
|
|
118
|
-
this.queue.shift();
|
|
119
|
-
this.current = next;
|
|
120
|
-
this.transition(() => {
|
|
121
|
-
try {
|
|
122
|
-
this.player.play(next.createResource());
|
|
123
|
-
next.onStart();
|
|
124
|
-
} catch (error) {
|
|
125
|
-
this.current = void 0;
|
|
126
|
-
this.player.stop(true);
|
|
127
|
-
next.onError(error);
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
//#endregion
|
|
28
|
+
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "openclaw/plugin-sdk/process-runtime";
|
|
133
29
|
//#region extensions/discord/src/voice/log-preview.ts
|
|
134
30
|
const DISCORD_VOICE_LOG_PREVIEW_CHARS = 500;
|
|
135
31
|
function formatVoiceLogPreview(text) {
|
|
@@ -156,7 +52,7 @@ function formatRealtimeInterruptionLog(event) {
|
|
|
156
52
|
}
|
|
157
53
|
}
|
|
158
54
|
function formatRealtimeLifecycleLog(event) {
|
|
159
|
-
if (!event.type.startsWith("session.")) return;
|
|
55
|
+
if (!event.type.startsWith("session.") || event.type.startsWith("session.output_audio") || event.type.endsWith(".delta") || event.type.endsWith(".append")) return;
|
|
160
56
|
const detail = event.detail ? ` ${event.detail}` : "";
|
|
161
57
|
return `discord voice: realtime lifecycle ${event.direction}:${event.type}${detail}`;
|
|
162
58
|
}
|
|
@@ -165,7 +61,7 @@ function shouldLogRealtimeVerboseEvent(event) {
|
|
|
165
61
|
}
|
|
166
62
|
//#endregion
|
|
167
63
|
//#region extensions/discord/src/voice/agent-control.ts
|
|
168
|
-
const logger$
|
|
64
|
+
const logger$10 = createSubsystemLogger("discord/voice");
|
|
169
65
|
async function controlDiscordVoiceAgentRun(params) {
|
|
170
66
|
const context = params.resolveContext ? await params.resolveContext() : params.context;
|
|
171
67
|
const assertCurrent = () => {
|
|
@@ -214,7 +110,7 @@ async function maybeControlDiscordVoiceAgentRun(params) {
|
|
|
214
110
|
};
|
|
215
111
|
}
|
|
216
112
|
function logDiscordVoiceAgentControlResult(entry, result) {
|
|
217
|
-
logger$
|
|
113
|
+
logger$10.info(`discord voice: realtime active-run control handled mode=${result.mode} ok=${result.ok} active=${result.active} reason=${result.reason ?? "none"} voiceSession=${entry.voiceSessionKey} supervisorSession=${entry.route.sessionKey} agent=${entry.route.agentId}`);
|
|
218
114
|
}
|
|
219
115
|
async function handleDiscordVoiceAgentControlToolCall(params) {
|
|
220
116
|
let result;
|
|
@@ -255,368 +151,11 @@ function formatVoiceIngressPrompt(transcript, speakerLabel) {
|
|
|
255
151
|
return [DISCORD_VOICE_SPOKEN_OUTPUT_CONTRACT, voiceInput].join("\n\n");
|
|
256
152
|
}
|
|
257
153
|
//#endregion
|
|
258
|
-
//#region extensions/discord/src/voice/audio.ts
|
|
259
|
-
const SAMPLE_RATE = 48e3;
|
|
260
|
-
const CHANNELS = 2;
|
|
261
|
-
const BIT_DEPTH = 16;
|
|
262
|
-
const FFMPEG_ERROR_OUTPUT_BYTES = 8192;
|
|
263
|
-
const DISCORD_OPUS_FRAME_SIZE = 960;
|
|
264
|
-
const DISCORD_OPUS_MAX_DECODE_FRAME_SIZE = SAMPLE_RATE * 120 / 1e3;
|
|
265
|
-
const DISCORD_OPUS_ENCODE_BATCH_FRAMES = 8;
|
|
266
|
-
const DISCORD_OPUS_FRAME_BYTES = DISCORD_OPUS_FRAME_SIZE * CHANNELS * (BIT_DEPTH / 8);
|
|
267
|
-
const FFMPEG_PCM_ARGUMENTS = [
|
|
268
|
-
"-analyzeduration",
|
|
269
|
-
"0",
|
|
270
|
-
"-loglevel",
|
|
271
|
-
"error",
|
|
272
|
-
"-vn",
|
|
273
|
-
"-sn",
|
|
274
|
-
"-dn",
|
|
275
|
-
"-f",
|
|
276
|
-
"s16le",
|
|
277
|
-
"-ar",
|
|
278
|
-
String(SAMPLE_RATE),
|
|
279
|
-
"-ac",
|
|
280
|
-
String(CHANNELS)
|
|
281
|
-
];
|
|
282
|
-
let warnedOpusMissing = false;
|
|
283
|
-
function buildWavBuffer(chunks) {
|
|
284
|
-
const pcmBytes = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
285
|
-
const blockAlign = 4;
|
|
286
|
-
const byteRate = SAMPLE_RATE * blockAlign;
|
|
287
|
-
const wav = Buffer.allocUnsafe(44 + pcmBytes);
|
|
288
|
-
const header = wav.subarray(0, 44);
|
|
289
|
-
header.write("RIFF", 0);
|
|
290
|
-
header.writeUInt32LE(36 + pcmBytes, 4);
|
|
291
|
-
header.write("WAVE", 8);
|
|
292
|
-
header.write("fmt ", 12);
|
|
293
|
-
header.writeUInt32LE(16, 16);
|
|
294
|
-
header.writeUInt16LE(1, 20);
|
|
295
|
-
header.writeUInt16LE(CHANNELS, 22);
|
|
296
|
-
header.writeUInt32LE(SAMPLE_RATE, 24);
|
|
297
|
-
header.writeUInt32LE(byteRate, 28);
|
|
298
|
-
header.writeUInt16LE(blockAlign, 32);
|
|
299
|
-
header.writeUInt16LE(BIT_DEPTH, 34);
|
|
300
|
-
header.write("data", 36);
|
|
301
|
-
header.writeUInt32LE(pcmBytes, 40);
|
|
302
|
-
let offset = 44;
|
|
303
|
-
for (const chunk of chunks) offset += chunk.copy(wav, offset);
|
|
304
|
-
return wav;
|
|
305
|
-
}
|
|
306
|
-
function createDiscordOpusEncodeStream() {
|
|
307
|
-
return new DiscordOpusEncodeStream();
|
|
308
|
-
}
|
|
309
|
-
function createDiscordOpusPlaybackStream(input) {
|
|
310
|
-
const inputSource = typeof input === "string" ? input : "pipe:0";
|
|
311
|
-
const ffmpeg = spawn(resolveFfmpegBin(), [
|
|
312
|
-
"-i",
|
|
313
|
-
inputSource,
|
|
314
|
-
...FFMPEG_PCM_ARGUMENTS,
|
|
315
|
-
"pipe:1"
|
|
316
|
-
], {
|
|
317
|
-
stdio: [
|
|
318
|
-
"pipe",
|
|
319
|
-
"pipe",
|
|
320
|
-
"pipe"
|
|
321
|
-
],
|
|
322
|
-
windowsHide: true
|
|
323
|
-
});
|
|
324
|
-
const opusStream = createDiscordOpusEncodeStream();
|
|
325
|
-
const stderr = Buffer.alloc(FFMPEG_ERROR_OUTPUT_BYTES);
|
|
326
|
-
let stderrBytes = 0;
|
|
327
|
-
let ffmpegClosed = false;
|
|
328
|
-
const killFfmpeg = (signal = "SIGTERM") => {
|
|
329
|
-
if (!ffmpegClosed && !ffmpeg.killed) ffmpeg.kill(signal);
|
|
330
|
-
};
|
|
331
|
-
ffmpeg.stderr.on("data", (chunk) => {
|
|
332
|
-
if (stderrBytes < FFMPEG_ERROR_OUTPUT_BYTES) stderrBytes += chunk.copy(stderr, stderrBytes, 0, FFMPEG_ERROR_OUTPUT_BYTES - stderrBytes);
|
|
333
|
-
});
|
|
334
|
-
ffmpeg.once("error", (err) => {
|
|
335
|
-
opusStream.destroy(err);
|
|
336
|
-
});
|
|
337
|
-
ffmpeg.once("close", (code, signal) => {
|
|
338
|
-
ffmpegClosed = true;
|
|
339
|
-
if (code && code !== 0) {
|
|
340
|
-
const stderrText = new StringDecoder("utf8").write(stderr.subarray(0, stderrBytes)).trim();
|
|
341
|
-
const suffix = stderrText ? `: ${stderrText}` : "";
|
|
342
|
-
opusStream.destroy(/* @__PURE__ */ new Error(`ffmpeg exited with code ${code}${suffix}`));
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (signal) opusStream.destroy(/* @__PURE__ */ new Error(`ffmpeg exited with signal ${signal}`));
|
|
346
|
-
});
|
|
347
|
-
for (const readable of [ffmpeg.stdout, ffmpeg.stderr]) readable.on("error", (err) => {
|
|
348
|
-
killFfmpeg("SIGKILL");
|
|
349
|
-
opusStream.destroy(err);
|
|
350
|
-
});
|
|
351
|
-
ffmpeg.stdin.on("error", (err) => {
|
|
352
|
-
if (err.code !== "EPIPE") opusStream.destroy(err);
|
|
353
|
-
});
|
|
354
|
-
ffmpeg.stdout.pipe(opusStream);
|
|
355
|
-
opusStream.once("close", () => {
|
|
356
|
-
if (!opusStream.readableEnded) killFfmpeg();
|
|
357
|
-
});
|
|
358
|
-
if (typeof input !== "string") {
|
|
359
|
-
input.on("error", (err) => {
|
|
360
|
-
ffmpeg.stdin.destroy(err);
|
|
361
|
-
opusStream.destroy(err);
|
|
362
|
-
});
|
|
363
|
-
input.pipe(ffmpeg.stdin);
|
|
364
|
-
} else ffmpeg.stdin.end();
|
|
365
|
-
return opusStream;
|
|
366
|
-
}
|
|
367
|
-
var DiscordOpusEncodeStream = class extends Duplex {
|
|
368
|
-
#partialFrame = Buffer.alloc(DISCORD_OPUS_FRAME_BYTES);
|
|
369
|
-
#partialBytes = 0;
|
|
370
|
-
#pending;
|
|
371
|
-
#scheduled;
|
|
372
|
-
#readBlocked = false;
|
|
373
|
-
#partialFlushRequested = false;
|
|
374
|
-
#encoder;
|
|
375
|
-
#packetPcmBytes = /* @__PURE__ */ new WeakMap();
|
|
376
|
-
constructor() {
|
|
377
|
-
super({ readableObjectMode: true });
|
|
378
|
-
}
|
|
379
|
-
_construct(done) {
|
|
380
|
-
createEncoder({
|
|
381
|
-
application: Application.Audio,
|
|
382
|
-
channels: CHANNELS,
|
|
383
|
-
sampleRate: SAMPLE_RATE
|
|
384
|
-
}).then((encoder) => {
|
|
385
|
-
this.#encoder = encoder;
|
|
386
|
-
done();
|
|
387
|
-
}, (err) => done(err instanceof Error ? err : new Error(formatErrorMessage(err))));
|
|
388
|
-
}
|
|
389
|
-
_write(chunk, _encoding, done) {
|
|
390
|
-
this.#pending = {
|
|
391
|
-
chunk,
|
|
392
|
-
offset: 0,
|
|
393
|
-
done
|
|
394
|
-
};
|
|
395
|
-
this.#schedule();
|
|
396
|
-
}
|
|
397
|
-
_read() {
|
|
398
|
-
this.#readBlocked = false;
|
|
399
|
-
this.#schedule();
|
|
400
|
-
}
|
|
401
|
-
#schedule() {
|
|
402
|
-
if (this.destroyed || this.#scheduled || this.#readBlocked || !this.#pending) return;
|
|
403
|
-
this.#scheduled = setImmediate(() => {
|
|
404
|
-
this.#scheduled = void 0;
|
|
405
|
-
this.#encodeBatch();
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
#encodeBatch() {
|
|
409
|
-
const pending = this.#pending;
|
|
410
|
-
if (!pending || this.destroyed) return;
|
|
411
|
-
try {
|
|
412
|
-
for (let count = 0; count < DISCORD_OPUS_ENCODE_BATCH_FRAMES; count += 1) {
|
|
413
|
-
const remainingBytes = pending.chunk.length - pending.offset;
|
|
414
|
-
if (this.#partialBytes > 0 || remainingBytes < DISCORD_OPUS_FRAME_BYTES) {
|
|
415
|
-
const copied = pending.chunk.copy(this.#partialFrame, this.#partialBytes, pending.offset, pending.offset + DISCORD_OPUS_FRAME_BYTES - this.#partialBytes);
|
|
416
|
-
pending.offset += copied;
|
|
417
|
-
this.#partialBytes += copied;
|
|
418
|
-
if (this.#partialBytes < DISCORD_OPUS_FRAME_BYTES) {
|
|
419
|
-
this.#pending = void 0;
|
|
420
|
-
pending.done();
|
|
421
|
-
this.#flushRequestedPartialFrame();
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
this.#partialBytes = 0;
|
|
425
|
-
this.#readBlocked = !this.#encodeFrame(this.#partialFrame);
|
|
426
|
-
} else {
|
|
427
|
-
const frame = pending.chunk.subarray(pending.offset, pending.offset + DISCORD_OPUS_FRAME_BYTES);
|
|
428
|
-
pending.offset += DISCORD_OPUS_FRAME_BYTES;
|
|
429
|
-
this.#readBlocked = !this.#encodeFrame(frame);
|
|
430
|
-
}
|
|
431
|
-
if (this.destroyed || this.#readBlocked) return;
|
|
432
|
-
}
|
|
433
|
-
this.#schedule();
|
|
434
|
-
} catch (err) {
|
|
435
|
-
this.#pending = void 0;
|
|
436
|
-
pending.done(err instanceof Error ? err : new Error(formatErrorMessage(err)));
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
_final(done) {
|
|
440
|
-
try {
|
|
441
|
-
this.flushPartialFrame();
|
|
442
|
-
this.push(null);
|
|
443
|
-
done();
|
|
444
|
-
} catch (err) {
|
|
445
|
-
done(err instanceof Error ? err : new Error(formatErrorMessage(err)));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
flushPartialFrameWhenReady() {
|
|
449
|
-
this.#partialFlushRequested = true;
|
|
450
|
-
if (this.#partialBytes > 0) this.#flushRequestedPartialFrame();
|
|
451
|
-
}
|
|
452
|
-
#flushRequestedPartialFrame() {
|
|
453
|
-
if (!this.#partialFlushRequested || this.#pending || this.writableLength > 0) return;
|
|
454
|
-
this.#partialFlushRequested = false;
|
|
455
|
-
try {
|
|
456
|
-
this.flushPartialFrame();
|
|
457
|
-
} catch (error) {
|
|
458
|
-
this.destroy(error instanceof Error ? error : new Error(formatErrorMessage(error)));
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
flushPartialFrame() {
|
|
462
|
-
if (this.destroyed || this.#pending || this.#partialBytes === 0) return false;
|
|
463
|
-
const pcmBytes = this.#partialBytes;
|
|
464
|
-
this.#partialFrame.fill(0, pcmBytes);
|
|
465
|
-
this.#partialBytes = 0;
|
|
466
|
-
this.#readBlocked = !this.#encodeFrame(this.#partialFrame, pcmBytes);
|
|
467
|
-
return true;
|
|
468
|
-
}
|
|
469
|
-
takePcmBytes(packet) {
|
|
470
|
-
const bytes = this.#packetPcmBytes.get(packet) ?? 0;
|
|
471
|
-
this.#packetPcmBytes.delete(packet);
|
|
472
|
-
return bytes;
|
|
473
|
-
}
|
|
474
|
-
_destroy(err, done) {
|
|
475
|
-
this.#encoder?.free();
|
|
476
|
-
clearImmediate(this.#scheduled);
|
|
477
|
-
this.#scheduled = void 0;
|
|
478
|
-
this.#partialFlushRequested = false;
|
|
479
|
-
const pending = this.#pending;
|
|
480
|
-
this.#pending = void 0;
|
|
481
|
-
pending?.done(err ?? /* @__PURE__ */ new Error("Discord Opus encoder was destroyed"));
|
|
482
|
-
this.#partialBytes = 0;
|
|
483
|
-
this.#partialFrame = Buffer.alloc(0);
|
|
484
|
-
done(err);
|
|
485
|
-
}
|
|
486
|
-
#encodeFrame(frame, pcmBytes = frame.length) {
|
|
487
|
-
const packet = Buffer.from(this.#encoder.encode(frame, { frameSize: DISCORD_OPUS_FRAME_SIZE }));
|
|
488
|
-
this.#packetPcmBytes.set(packet, pcmBytes);
|
|
489
|
-
return this.push(packet);
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
function pcmInt16ToBuffer(pcm) {
|
|
493
|
-
return Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
494
|
-
}
|
|
495
|
-
async function decodeOpusStreamChunks(stream, params) {
|
|
496
|
-
try {
|
|
497
|
-
for await (const { pcm, packet } of decodeOpusFrames(stream, params)) await params.onChunk(pcm, packet);
|
|
498
|
-
} catch (err) {
|
|
499
|
-
params.onError?.(err);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
async function* decodeOpusFrames(stream, params) {
|
|
503
|
-
let decoder;
|
|
504
|
-
try {
|
|
505
|
-
decoder = await createDecoder({
|
|
506
|
-
channels: CHANNELS,
|
|
507
|
-
sampleRate: SAMPLE_RATE
|
|
508
|
-
});
|
|
509
|
-
} catch (err) {
|
|
510
|
-
params.onError?.(err);
|
|
511
|
-
if (!warnedOpusMissing) {
|
|
512
|
-
warnedOpusMissing = true;
|
|
513
|
-
params.onWarn(`discord voice: no usable opus decoder available (libopus-wasm: ${formatErrorMessage(err)}); cannot decode voice audio`);
|
|
514
|
-
}
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
params.onVerbose("opus decoder: libopus-wasm");
|
|
518
|
-
try {
|
|
519
|
-
for await (const chunk of stream) {
|
|
520
|
-
if (!chunk || !(chunk instanceof Buffer) || chunk.length === 0) continue;
|
|
521
|
-
const decoded = decoder.decode(chunk, { maxFrameSize: DISCORD_OPUS_MAX_DECODE_FRAME_SIZE });
|
|
522
|
-
if (decoded.length > 0) yield {
|
|
523
|
-
pcm: pcmInt16ToBuffer(decoded),
|
|
524
|
-
packet: chunk
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
} catch (err) {
|
|
528
|
-
params.onError?.(err);
|
|
529
|
-
if (shouldLogVerbose()) logVerbose(`discord voice: opus decode failed: ${formatErrorMessage(err)}`);
|
|
530
|
-
} finally {
|
|
531
|
-
decoder.free();
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
function createDiscordPcmToRealtimeConverter() {
|
|
535
|
-
const resampler = createStreamingPcmResampler(SAMPLE_RATE, 24e3);
|
|
536
|
-
let trailingFrame = Buffer.alloc(0);
|
|
537
|
-
return {
|
|
538
|
-
process(pcm) {
|
|
539
|
-
const input = trailingFrame.length > 0 ? Buffer.concat([trailingFrame, pcm]) : pcm;
|
|
540
|
-
const completeBytes = input.length - input.length % 4;
|
|
541
|
-
trailingFrame = Buffer.from(input.subarray(completeBytes));
|
|
542
|
-
const mono = Buffer.alloc(completeBytes / 2);
|
|
543
|
-
for (let offset = 0; offset < completeBytes; offset += 4) mono.writeInt16LE(Math.round((input.readInt16LE(offset) + input.readInt16LE(offset + 2)) / 2), offset / 2);
|
|
544
|
-
return resampler.process(mono);
|
|
545
|
-
},
|
|
546
|
-
flush() {
|
|
547
|
-
trailingFrame = Buffer.alloc(0);
|
|
548
|
-
return resampler.flush();
|
|
549
|
-
}
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
function duplicateMonoChannels(mono) {
|
|
553
|
-
const stereo = Buffer.alloc(mono.length * 2);
|
|
554
|
-
for (let offset = 0; offset < mono.length; offset += 2) {
|
|
555
|
-
const sample = mono.readInt16LE(offset);
|
|
556
|
-
stereo.writeInt16LE(sample, offset * 2);
|
|
557
|
-
stereo.writeInt16LE(sample, offset * 2 + 2);
|
|
558
|
-
}
|
|
559
|
-
return stereo;
|
|
560
|
-
}
|
|
561
|
-
function createRealtimePcmToDiscordConverter() {
|
|
562
|
-
let resampler = createStreamingPcmResampler(24e3, SAMPLE_RATE);
|
|
563
|
-
let history = Buffer.alloc(0);
|
|
564
|
-
let trailingByte = Buffer.alloc(0);
|
|
565
|
-
let replayBytes = 0;
|
|
566
|
-
let flushed = false;
|
|
567
|
-
const takeOutput = (pcm) => {
|
|
568
|
-
const skippedBytes = Math.min(replayBytes, pcm.length);
|
|
569
|
-
replayBytes -= skippedBytes;
|
|
570
|
-
return duplicateMonoChannels(pcm.subarray(skippedBytes));
|
|
571
|
-
};
|
|
572
|
-
return {
|
|
573
|
-
process(pcm) {
|
|
574
|
-
const input = trailingByte.length > 0 ? Buffer.concat([trailingByte, pcm]) : pcm;
|
|
575
|
-
const completeBytes = input.length - input.length % 2;
|
|
576
|
-
const completePcm = input.subarray(0, completeBytes);
|
|
577
|
-
trailingByte = Buffer.from(input.subarray(completeBytes));
|
|
578
|
-
history = Buffer.concat([history, completePcm.subarray(-64)]).subarray(-64);
|
|
579
|
-
return takeOutput(resampler.process(completePcm));
|
|
580
|
-
},
|
|
581
|
-
drain() {
|
|
582
|
-
if (flushed) return Buffer.alloc(0);
|
|
583
|
-
const output = takeOutput(resampler.flush());
|
|
584
|
-
resampler = createStreamingPcmResampler(24e3, SAMPLE_RATE);
|
|
585
|
-
replayBytes = history.length * 2 - resampler.process(history).length;
|
|
586
|
-
return output;
|
|
587
|
-
},
|
|
588
|
-
flush() {
|
|
589
|
-
flushed = true;
|
|
590
|
-
trailingByte = Buffer.alloc(0);
|
|
591
|
-
history = Buffer.alloc(0);
|
|
592
|
-
return takeOutput(resampler.flush());
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
async function writeVoiceWavFile(chunks) {
|
|
597
|
-
const wav = buildWavBuffer(chunks);
|
|
598
|
-
const workspace = await tempWorkspace({
|
|
599
|
-
rootDir: resolvePreferredOpenClawTmpDir(),
|
|
600
|
-
prefix: "discord-voice-"
|
|
601
|
-
});
|
|
602
|
-
try {
|
|
603
|
-
return {
|
|
604
|
-
path: await workspace.write("segment.wav", wav),
|
|
605
|
-
durationSeconds: (wav.length - 44) / (BIT_DEPTH / 8 * CHANNELS * SAMPLE_RATE),
|
|
606
|
-
cleanup: () => workspace[Symbol.asyncDispose]()
|
|
607
|
-
};
|
|
608
|
-
} catch (error) {
|
|
609
|
-
await workspace.cleanup();
|
|
610
|
-
throw error;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
//#endregion
|
|
614
154
|
//#region extensions/discord/src/voice/session.ts
|
|
615
155
|
const MIN_SEGMENT_SECONDS = .35;
|
|
616
156
|
const CAPTURE_FINALIZE_GRACE_MS = 2e3;
|
|
617
157
|
const VOICE_CONNECT_READY_TIMEOUT_MS = 3e4;
|
|
618
158
|
const VOICE_RECONNECT_GRACE_MS = 15e3;
|
|
619
|
-
const PLAYBACK_READY_TIMEOUT_MS = 6e4;
|
|
620
159
|
function resolveVoiceTimeoutMs(value, fallbackMs) {
|
|
621
160
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return fallbackMs;
|
|
622
161
|
return Math.floor(value);
|
|
@@ -823,7 +362,7 @@ var DiscordRealtimeRecording = class {
|
|
|
823
362
|
//#endregion
|
|
824
363
|
//#region extensions/discord/src/voice/ingress.ts
|
|
825
364
|
const DISCORD_VOICE_MESSAGE_PROVIDER = "discord-voice";
|
|
826
|
-
const logger$
|
|
365
|
+
const logger$9 = createSubsystemLogger("discord/voice");
|
|
827
366
|
function summarizeAgentTurnPayloads(payloads) {
|
|
828
367
|
let textPayloads = 0;
|
|
829
368
|
let nonEmptyTextPayloads = 0;
|
|
@@ -889,12 +428,12 @@ async function runDiscordVoiceAgentTurn(params) {
|
|
|
889
428
|
if (!context || params.entry.captureOnly || params.entry.sessionLifecycle.status !== "active" || context.isCurrent?.() === false) return null;
|
|
890
429
|
params.signal?.throwIfAborted();
|
|
891
430
|
const voiceModel = normalizeOptionalString(params.discordConfig.voice?.model);
|
|
892
|
-
const runId = params.voiceSelection
|
|
431
|
+
const runId = params.voiceSelection ? randomUUID() : void 0;
|
|
893
432
|
const unbind = runId ? params.voiceSelection?.bindRun({
|
|
894
433
|
runId,
|
|
895
434
|
assertCurrent: () => {
|
|
896
435
|
params.signal?.throwIfAborted();
|
|
897
|
-
if (params.entry.sessionLifecycle.status !== "active" || context.isCurrent?.() === false
|
|
436
|
+
if (params.entry.sessionLifecycle.status !== "active" || context.isCurrent?.() === false) throw new Error("Discord voice access is no longer valid for this call");
|
|
898
437
|
}
|
|
899
438
|
}) : void 0;
|
|
900
439
|
let result;
|
|
@@ -920,7 +459,7 @@ async function runDiscordVoiceAgentTurn(params) {
|
|
|
920
459
|
}
|
|
921
460
|
const payloads = result.payloads ?? [];
|
|
922
461
|
const text = payloads.map((payload) => payload.text).filter((entry) => typeof entry === "string" && entry.trim()).join("\n").trim();
|
|
923
|
-
if (!text) logger$
|
|
462
|
+
if (!text) logger$9.info(`discord voice: agent turn produced no speakable payloads guild=${params.entry.guildId} channel=${params.entry.channelId} voiceSession=${params.entry.voiceSessionKey} supervisorSession=${params.entry.route.sessionKey} agent=${params.entry.route.agentId} user=${params.userId} ${summarizeAgentTurnPayloads(payloads)}`);
|
|
924
463
|
return {
|
|
925
464
|
context,
|
|
926
465
|
text
|
|
@@ -935,10 +474,10 @@ async function resolveDiscordVoiceRealtimeBootstrapContext(params) {
|
|
|
935
474
|
agentId: params.entry.route.agentId,
|
|
936
475
|
sessionKey: params.entry.route.sessionKey,
|
|
937
476
|
files,
|
|
938
|
-
warn: (message) => logger$
|
|
477
|
+
warn: (message) => logger$9.warn(`discord voice: realtime bootstrap context: ${message}`)
|
|
939
478
|
});
|
|
940
479
|
} catch (error) {
|
|
941
|
-
logger$
|
|
480
|
+
logger$9.warn(`discord voice: realtime bootstrap context unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
942
481
|
return;
|
|
943
482
|
}
|
|
944
483
|
}
|
|
@@ -1125,7 +664,7 @@ async function resolveDiscordVoiceIngressContextWithParticipants(params) {
|
|
|
1125
664
|
}
|
|
1126
665
|
//#endregion
|
|
1127
666
|
//#region extensions/discord/src/voice/membership.ts
|
|
1128
|
-
const logger$
|
|
667
|
+
const logger$8 = createSubsystemLogger("discord/voice");
|
|
1129
668
|
const MAX_INFERRED_PARTICIPANTS = 256;
|
|
1130
669
|
var DiscordVoiceMembershipTracker = class {
|
|
1131
670
|
constructor(client, speakerContext, accountId) {
|
|
@@ -1158,7 +697,7 @@ var DiscordVoiceMembershipTracker = class {
|
|
|
1158
697
|
};
|
|
1159
698
|
this.states.set(entry, state);
|
|
1160
699
|
const initialLines = formatDiscordVoiceParticipantStateLines(roster);
|
|
1161
|
-
if (this.publish(entry, this.initialRosterEvent(entry, initialLines))) logger$
|
|
700
|
+
if (this.publish(entry, this.initialRosterEvent(entry, initialLines))) logger$8.info(`discord voice: participant roster event queued guild=${entry.guildId} channel=${entry.channelId} participants=${roster.totalCount} supervisorSession=${entry.route.sessionKey}`);
|
|
1162
701
|
const activationRevision = state.revision;
|
|
1163
702
|
(async () => {
|
|
1164
703
|
const lines = await resolveDiscordVoiceParticipantLines({
|
|
@@ -1169,7 +708,7 @@ var DiscordVoiceMembershipTracker = class {
|
|
|
1169
708
|
if (lines.join("\n") === initialLines.join("\n")) return;
|
|
1170
709
|
if (!state.active || state.revision !== activationRevision || entry.sessionLifecycle.status === "stopped") return;
|
|
1171
710
|
if (!this.publish(entry, this.initialRosterEvent(entry, lines))) return;
|
|
1172
|
-
logger$
|
|
711
|
+
logger$8.info(`discord voice: enriched participant roster event queued guild=${entry.guildId} channel=${entry.channelId} participants=${roster.totalCount} supervisorSession=${entry.route.sessionKey}`);
|
|
1173
712
|
})().catch((err) => {
|
|
1174
713
|
this.logFailure(entry, err);
|
|
1175
714
|
});
|
|
@@ -1185,7 +724,7 @@ var DiscordVoiceMembershipTracker = class {
|
|
|
1185
724
|
`The agent left guild_id=${JSON.stringify(entry.guildId)} channel_id=${JSON.stringify(entry.channelId)}.`,
|
|
1186
725
|
"Any prior roster or membership updates for this voice session are no longer live. Do not respond to this event on its own."
|
|
1187
726
|
].join("\n"))) return;
|
|
1188
|
-
logger$
|
|
727
|
+
logger$8.info(`discord voice: participant session-ended event queued guild=${entry.guildId} channel=${entry.channelId} supervisorSession=${entry.route.sessionKey}`);
|
|
1189
728
|
}
|
|
1190
729
|
countHumanParticipants(entry, botUserId) {
|
|
1191
730
|
const state = this.states.get(entry);
|
|
@@ -1221,7 +760,7 @@ var DiscordVoiceMembershipTracker = class {
|
|
|
1221
760
|
...rosterLines.length > 0 ? rosterLines : ["- none"],
|
|
1222
761
|
"This roster snapshot supersedes prior voice membership context. Do not respond to this event on its own."
|
|
1223
762
|
].join("\n"))) return;
|
|
1224
|
-
logger$
|
|
763
|
+
logger$8.info(`discord voice: inferred participant-present event queued guild=${entry.guildId} channel=${entry.channelId} user=${normalizedUserId} supervisorSession=${entry.route.sessionKey}`);
|
|
1225
764
|
}
|
|
1226
765
|
track(entry, data, previousVoiceState) {
|
|
1227
766
|
if (!entry) return;
|
|
@@ -1252,7 +791,7 @@ var DiscordVoiceMembershipTracker = class {
|
|
|
1252
791
|
...rosterLines.length > 0 ? rosterLines : ["- none"],
|
|
1253
792
|
"This roster snapshot supersedes prior voice membership context. Do not respond to this event on its own."
|
|
1254
793
|
].join("\n"))) return;
|
|
1255
|
-
logger$
|
|
794
|
+
logger$8.info(`discord voice: participant ${isPresent ? "joined" : "left"} event queued guild=${entry.guildId} channel=${entry.channelId} user=${userId} supervisorSession=${entry.route.sessionKey}`);
|
|
1256
795
|
}
|
|
1257
796
|
publish(entry, text) {
|
|
1258
797
|
try {
|
|
@@ -1263,7 +802,7 @@ var DiscordVoiceMembershipTracker = class {
|
|
|
1263
802
|
}
|
|
1264
803
|
}
|
|
1265
804
|
logFailure(entry, err) {
|
|
1266
|
-
logger$
|
|
805
|
+
logger$8.warn(`discord voice: participant notification failed guild=${entry.guildId} channel=${entry.channelId}: ${formatErrorMessage(err)}`);
|
|
1267
806
|
}
|
|
1268
807
|
roster(entry, botUserId, additionalUserIds) {
|
|
1269
808
|
return collectDiscordVoiceParticipants({
|
|
@@ -1380,103 +919,8 @@ var DiscordVoiceSpeakerContextResolver = class {
|
|
|
1380
919
|
}
|
|
1381
920
|
};
|
|
1382
921
|
//#endregion
|
|
1383
|
-
//#region extensions/discord/src/voice/receive-recovery.ts
|
|
1384
|
-
const DECRYPT_FAILURE_WINDOW_MS = 3e4;
|
|
1385
|
-
const DECRYPT_FAILURE_RECONNECT_THRESHOLD = 3;
|
|
1386
|
-
const DECRYPT_FAILURE_MARKER = "DecryptionFailed(";
|
|
1387
|
-
const DAVE_PASSTHROUGH_DISABLED_MARKER = "UnencryptedWhenPassthroughDisabled";
|
|
1388
|
-
const WASM_MEMORY_ACCESS_MARKER = "memory access out of bounds";
|
|
1389
|
-
const OPUS_INVALID_PACKET_CODE = -4;
|
|
1390
|
-
function createVoiceReceiveRecoveryState() {
|
|
1391
|
-
return {
|
|
1392
|
-
decryptFailureCount: 0,
|
|
1393
|
-
lastDecryptFailureAt: 0,
|
|
1394
|
-
decryptRecoveryInFlight: false
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
function isAbortLikeReceiveError(err) {
|
|
1398
|
-
if (!err || typeof err !== "object") return false;
|
|
1399
|
-
const name = "name" in err && typeof err.name === "string" ? err.name : "";
|
|
1400
|
-
const message = "message" in err && typeof err.message === "string" ? err.message : "";
|
|
1401
|
-
return name === "AbortError" || message === "Premature close" || message.includes("The operation was aborted") || message.includes("aborted");
|
|
1402
|
-
}
|
|
1403
|
-
function isOpusDecodeInvalidPacketError(err) {
|
|
1404
|
-
if (!err || typeof err !== "object") return false;
|
|
1405
|
-
const maybeOpusError = err;
|
|
1406
|
-
const isDecodeOperation = maybeOpusError.operation === "decode" || maybeOpusError.operation === "decodeFloat";
|
|
1407
|
-
const isInvalidPacket = maybeOpusError.code === OPUS_INVALID_PACKET_CODE || maybeOpusError.codeName === "InvalidPacket";
|
|
1408
|
-
return isDecodeOperation && isInvalidPacket && (err instanceof OpusError || maybeOpusError.name === "OpusError");
|
|
1409
|
-
}
|
|
1410
|
-
function analyzeVoiceReceiveError(err) {
|
|
1411
|
-
const message = formatErrorMessage(err);
|
|
1412
|
-
const normalizedMessage = message.toLowerCase();
|
|
1413
|
-
const shouldAttemptPassthrough = message.includes(DAVE_PASSTHROUGH_DISABLED_MARKER);
|
|
1414
|
-
const isWasmMemoryAccessFailure = normalizedMessage.includes(WASM_MEMORY_ACCESS_MARKER);
|
|
1415
|
-
return {
|
|
1416
|
-
message,
|
|
1417
|
-
isAbortLike: isAbortLikeReceiveError(err),
|
|
1418
|
-
isDecodeCorruption: isOpusDecodeInvalidPacketError(err),
|
|
1419
|
-
shouldAttemptPassthrough,
|
|
1420
|
-
countsAsDecryptFailure: message.includes(DECRYPT_FAILURE_MARKER) || shouldAttemptPassthrough || isWasmMemoryAccessFailure
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
function noteVoiceDecryptFailure(state, now = Date.now()) {
|
|
1424
|
-
if (now - state.lastDecryptFailureAt > 3e4) state.decryptFailureCount = 0;
|
|
1425
|
-
state.lastDecryptFailureAt = now;
|
|
1426
|
-
state.decryptFailureCount += 1;
|
|
1427
|
-
const firstFailure = state.decryptFailureCount === 1;
|
|
1428
|
-
if (state.decryptFailureCount < DECRYPT_FAILURE_RECONNECT_THRESHOLD || state.decryptRecoveryInFlight) return {
|
|
1429
|
-
firstFailure,
|
|
1430
|
-
shouldRecover: false
|
|
1431
|
-
};
|
|
1432
|
-
state.decryptRecoveryInFlight = true;
|
|
1433
|
-
resetVoiceReceiveRecoveryState(state);
|
|
1434
|
-
return {
|
|
1435
|
-
firstFailure,
|
|
1436
|
-
shouldRecover: true
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
function resetVoiceReceiveRecoveryState(state) {
|
|
1440
|
-
state.decryptFailureCount = 0;
|
|
1441
|
-
state.lastDecryptFailureAt = 0;
|
|
1442
|
-
}
|
|
1443
|
-
function finishVoiceDecryptRecovery(state) {
|
|
1444
|
-
state.decryptRecoveryInFlight = false;
|
|
1445
|
-
}
|
|
1446
|
-
function isDaveReinitializing(session) {
|
|
1447
|
-
return session.reinitializing === true;
|
|
1448
|
-
}
|
|
1449
|
-
function recoverDaveZeroTransition(params) {
|
|
1450
|
-
const { target, sdk, onWarn } = params;
|
|
1451
|
-
const networkingState = target.connection.state.networking?.state;
|
|
1452
|
-
const daveSession = networkingState?.dave;
|
|
1453
|
-
if (target.connection.state.status !== sdk.VoiceConnectionStatus.Ready || networkingState?.code !== sdk.NetworkingStatusCode.Ready || daveSession?.lastTransitionId !== 0 || daveSession.reinitializing !== false || typeof daveSession.recoverFromInvalidTransition !== "function") return "not-attempted";
|
|
1454
|
-
try {
|
|
1455
|
-
daveSession.recoverFromInvalidTransition(0);
|
|
1456
|
-
return "recovered";
|
|
1457
|
-
} catch (err) {
|
|
1458
|
-
onWarn(`discord voice: failed to recover DAVE transition 0 guild=${target.guildId} channel=${target.channelId}: ${formatErrorMessage(err)}`);
|
|
1459
|
-
return isDaveReinitializing(daveSession) ? "failed" : "not-attempted";
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
function enableDaveReceivePassthrough(params) {
|
|
1463
|
-
const { target, sdk, reason, expirySeconds, onVerbose, onWarn } = params;
|
|
1464
|
-
const networkingState = target.connection.state.networking?.state;
|
|
1465
|
-
if (target.connection.state.status !== sdk.VoiceConnectionStatus.Ready || !networkingState || networkingState.code !== sdk.NetworkingStatusCode.Ready && networkingState.code !== sdk.NetworkingStatusCode.Resuming) return false;
|
|
1466
|
-
const daveSession = networkingState.dave?.session;
|
|
1467
|
-
if (!daveSession) return false;
|
|
1468
|
-
try {
|
|
1469
|
-
daveSession.setPassthroughMode(true, expirySeconds);
|
|
1470
|
-
onVerbose(`enabled DAVE receive passthrough: guild ${target.guildId} channel ${target.channelId} expiry=${expirySeconds}s reason=${reason}`);
|
|
1471
|
-
return true;
|
|
1472
|
-
} catch (err) {
|
|
1473
|
-
onWarn(`discord voice: failed to enable DAVE passthrough guild=${target.guildId} channel=${target.channelId} reason=${reason}: ${formatErrorMessage(err)}`);
|
|
1474
|
-
return false;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
//#endregion
|
|
1478
922
|
//#region extensions/discord/src/voice/voice-following.ts
|
|
1479
|
-
const logger$
|
|
923
|
+
const logger$7 = createSubsystemLogger("discord/voice");
|
|
1480
924
|
const FOLLOW_USERS_RECONCILE_INTERVAL_MS = 1e4;
|
|
1481
925
|
const FOLLOW_USERS_RECONCILE_MAX_GUILDS_PER_RUN = 4;
|
|
1482
926
|
const FOLLOW_USERS_RECONCILE_MAX_REST_LOOKUPS_PER_RUN = 32;
|
|
@@ -1511,14 +955,11 @@ function resolveFollowUsersEnabled(voiceConfig) {
|
|
|
1511
955
|
}
|
|
1512
956
|
function logFollowUserReconcileVerbose(reason, message) {
|
|
1513
957
|
if (reason === "interval") {
|
|
1514
|
-
logger$
|
|
958
|
+
logger$7.trace(`discord voice: ${message}`);
|
|
1515
959
|
return;
|
|
1516
960
|
}
|
|
1517
961
|
logVoiceVerbose(message);
|
|
1518
962
|
}
|
|
1519
|
-
function resolveVoiceConnectionGroup$1(accountId) {
|
|
1520
|
-
return `openclaw:${accountId}`;
|
|
1521
|
-
}
|
|
1522
963
|
var DiscordVoiceFollowing = class {
|
|
1523
964
|
constructor(params) {
|
|
1524
965
|
this.params = params;
|
|
@@ -1548,7 +989,7 @@ var DiscordVoiceFollowing = class {
|
|
|
1548
989
|
channelId
|
|
1549
990
|
})) {
|
|
1550
991
|
if (existing && existing.channelId !== channelId) {
|
|
1551
|
-
logger$
|
|
992
|
+
logger$7.warn(`discord voice: bot moved to allowed channel guild=${guildId} from=${existing.channelId} to=${channelId}; rebuilding voice session`);
|
|
1552
993
|
await this.params.join({
|
|
1553
994
|
guildId,
|
|
1554
995
|
channelId
|
|
@@ -1556,20 +997,12 @@ var DiscordVoiceFollowing = class {
|
|
|
1556
997
|
}
|
|
1557
998
|
return;
|
|
1558
999
|
}
|
|
1559
|
-
logger$
|
|
1000
|
+
logger$7.warn(`discord voice: bot moved to non-allowed channel guild=${guildId} channel=${channelId}; leaving`);
|
|
1560
1001
|
if (existing) await this.params.leave({ guildId });
|
|
1561
|
-
else
|
|
1562
|
-
const voiceSdk = loadDiscordVoiceSdk();
|
|
1563
|
-
const connection = voiceSdk.getVoiceConnection(guildId, resolveVoiceConnectionGroup$1(this.params.accountId));
|
|
1564
|
-
if (connection) this.params.destroyVoiceConnection({
|
|
1565
|
-
connection,
|
|
1566
|
-
voiceSdk,
|
|
1567
|
-
reason: `non-allowed voice state guild ${guildId} channel ${channelId}`
|
|
1568
|
-
});
|
|
1569
|
-
}
|
|
1002
|
+
else await this.params.stopTransport(guildId);
|
|
1570
1003
|
const target = this.resolveVoiceResidencyTarget(guildId);
|
|
1571
1004
|
if (target) {
|
|
1572
|
-
logger$
|
|
1005
|
+
logger$7.warn(`discord voice: rejoining allowed voice channel guild=${guildId} channel=${target.channelId}`);
|
|
1573
1006
|
await this.params.join(target);
|
|
1574
1007
|
}
|
|
1575
1008
|
}
|
|
@@ -1602,7 +1035,7 @@ var DiscordVoiceFollowing = class {
|
|
|
1602
1035
|
channelId
|
|
1603
1036
|
})) {
|
|
1604
1037
|
this.followedUserChannels.delete(followKey);
|
|
1605
|
-
logger$
|
|
1038
|
+
logger$7.warn(`discord voice: followed user joined non-allowed channel guild=${guildId} user=${userId} channel=${channelId}; ignoring`);
|
|
1606
1039
|
if (existing && wasFollowedVoiceSession && !this.hasFollowedUserInChannel(existing)) await this.handoffToAnotherFollowedUserOrLeave({
|
|
1607
1040
|
guildId,
|
|
1608
1041
|
userId,
|
|
@@ -1622,12 +1055,12 @@ var DiscordVoiceFollowing = class {
|
|
|
1622
1055
|
const recoveryAttemptAt = this.params.getRecoveryAttempt(guildId);
|
|
1623
1056
|
if (!existing && previousFollowedChannelId === channelId && recoveryAttemptAt !== void 0) {
|
|
1624
1057
|
if (Date.now() - recoveryAttemptAt < 3e4) {
|
|
1625
|
-
logger$
|
|
1058
|
+
logger$7.warn(`discord voice: automatic follow suppressed during DAVE recovery cooldown guild=${guildId} channel=${channelId}; retry /vc join after the voice gateway recovers`);
|
|
1626
1059
|
return;
|
|
1627
1060
|
}
|
|
1628
1061
|
this.params.deleteRecoveryAttempt(guildId);
|
|
1629
1062
|
}
|
|
1630
|
-
logger$
|
|
1063
|
+
logger$7.info(`discord voice: following user guild=${guildId} user=${userId} channel=${channelId}`);
|
|
1631
1064
|
const result = await this.params.join({
|
|
1632
1065
|
guildId,
|
|
1633
1066
|
channelId
|
|
@@ -1636,7 +1069,7 @@ var DiscordVoiceFollowing = class {
|
|
|
1636
1069
|
if (!result.ok) {
|
|
1637
1070
|
if (this.params.getSession(guildId)?.channelId === channelId) this.followedVoiceGuilds.add(guildId);
|
|
1638
1071
|
else this.followedUserChannels.delete(followKey);
|
|
1639
|
-
logger$
|
|
1072
|
+
logger$7.warn(`discord voice: failed to follow user guild=${guildId} user=${userId} channel=${channelId}: ${result.message}`);
|
|
1640
1073
|
return;
|
|
1641
1074
|
}
|
|
1642
1075
|
this.followedVoiceGuilds.add(guildId);
|
|
@@ -1672,7 +1105,7 @@ var DiscordVoiceFollowing = class {
|
|
|
1672
1105
|
if (this.followUsersReconcileTimer) return;
|
|
1673
1106
|
this.followUsersReconcileTimer = setInterval(() => {
|
|
1674
1107
|
this.reconcileFollowedUsers("interval").catch((err) => {
|
|
1675
|
-
logger$
|
|
1108
|
+
logger$7.warn(`discord voice: follow user reconciliation failed: ${formatErrorMessage(err)}`);
|
|
1676
1109
|
});
|
|
1677
1110
|
}, FOLLOW_USERS_RECONCILE_INTERVAL_MS);
|
|
1678
1111
|
this.followUsersReconcileTimer.unref?.();
|
|
@@ -1698,7 +1131,7 @@ var DiscordVoiceFollowing = class {
|
|
|
1698
1131
|
for (const userId of plan.userIds) {
|
|
1699
1132
|
const voiceState = await getGuildVoiceState(this.params.client.rest, plan.guildId, userId).catch((err) => {
|
|
1700
1133
|
if (!isUnknownDiscordVoiceStateError(err)) {
|
|
1701
|
-
logger$
|
|
1134
|
+
logger$7.warn(`follow-user reconcile skipped (transient voice-state error) guild=${plan.guildId} user=${userId} trigger=${reason}: ${formatErrorMessage(err)}`);
|
|
1702
1135
|
return "transient-error";
|
|
1703
1136
|
}
|
|
1704
1137
|
logFollowUserReconcileVerbose(reason, `follow user reconcile reason=${reason}: no voice state guild ${plan.guildId} user ${userId}: ${formatErrorMessage(err)}`);
|
|
@@ -1798,18 +1231,18 @@ var DiscordVoiceFollowing = class {
|
|
|
1798
1231
|
async handoffToAnotherFollowedUserOrLeave(params) {
|
|
1799
1232
|
const target = this.resolveFollowedUserHandoffTarget(params.guildId, params.existing.channelId);
|
|
1800
1233
|
if (target) {
|
|
1801
|
-
logger$
|
|
1234
|
+
logger$7.info(`discord voice: followed user ${params.reason} guild=${params.guildId} user=${params.userId}; moving to remaining followed user channel=${target.channelId}`);
|
|
1802
1235
|
const result = await this.params.join(target, { preserveFollowState: true });
|
|
1803
1236
|
if (result.ok) this.followedVoiceGuilds.add(params.guildId);
|
|
1804
1237
|
else {
|
|
1805
|
-
logger$
|
|
1238
|
+
logger$7.warn(`discord voice: failed to hand off followed user session guild=${params.guildId} channel=${target.channelId}: ${result.message}`);
|
|
1806
1239
|
this.followedVoiceGuilds.delete(params.guildId);
|
|
1807
1240
|
this.deleteFollowedUserChannelsForGuild(params.guildId);
|
|
1808
1241
|
await this.params.leave({ guildId: params.guildId });
|
|
1809
1242
|
}
|
|
1810
1243
|
return;
|
|
1811
1244
|
}
|
|
1812
|
-
logger$
|
|
1245
|
+
logger$7.info(`discord voice: followed user ${params.reason} guild=${params.guildId} user=${params.userId}; leaving channel=${params.existing.channelId}`);
|
|
1813
1246
|
await this.params.leave({ guildId: params.guildId });
|
|
1814
1247
|
}
|
|
1815
1248
|
async disconnectStaleFollowedBotVoiceState(params) {
|
|
@@ -1819,7 +1252,7 @@ var DiscordVoiceFollowing = class {
|
|
|
1819
1252
|
const existing = this.params.getSession(guildId);
|
|
1820
1253
|
if (existing) {
|
|
1821
1254
|
if (this.followedVoiceGuilds.has(guildId)) {
|
|
1822
|
-
logger$
|
|
1255
|
+
logger$7.info(`discord voice: follow reconcile leaving local session guild=${guildId} channel=${existing.channelId} reason=${reason}`);
|
|
1823
1256
|
await this.params.leave({ guildId });
|
|
1824
1257
|
}
|
|
1825
1258
|
return;
|
|
@@ -1828,7 +1261,7 @@ var DiscordVoiceFollowing = class {
|
|
|
1828
1261
|
if (!botUserId) return;
|
|
1829
1262
|
const botVoiceState = await getGuildVoiceState(this.params.client.rest, guildId, botUserId).catch((err) => {
|
|
1830
1263
|
if (!isUnknownDiscordVoiceStateError(err)) {
|
|
1831
|
-
logger$
|
|
1264
|
+
logger$7.warn(`discord voice: follow reconcile skipped transient bot voice state error guild=${guildId} reason=${reason}: ${formatErrorMessage(err)}`);
|
|
1832
1265
|
return "transient-error";
|
|
1833
1266
|
}
|
|
1834
1267
|
logFollowUserReconcileVerbose(reason, `follow user reconcile reason=${reason}: no bot voice state guild ${guildId}: ${formatErrorMessage(err)}`);
|
|
@@ -1838,10 +1271,10 @@ var DiscordVoiceFollowing = class {
|
|
|
1838
1271
|
if (!botChannelId) return;
|
|
1839
1272
|
const gateway = this.params.client.getPlugin("voice")?.getGateway(guildId);
|
|
1840
1273
|
if (!gateway) {
|
|
1841
|
-
logger$
|
|
1274
|
+
logger$7.warn(`discord voice: follow reconcile cannot disconnect stale bot voice state guild=${guildId} channel=${botChannelId}; gateway unavailable`);
|
|
1842
1275
|
return;
|
|
1843
1276
|
}
|
|
1844
|
-
logger$
|
|
1277
|
+
logger$7.info(`discord voice: follow reconcile disconnecting stale bot voice state guild=${guildId} channel=${botChannelId} reason=${reason}`);
|
|
1845
1278
|
gateway.updateVoiceState({
|
|
1846
1279
|
guild_id: guildId,
|
|
1847
1280
|
channel_id: null,
|
|
@@ -1868,7 +1301,8 @@ function stopVoiceCaptureState(state) {
|
|
|
1868
1301
|
state.clear();
|
|
1869
1302
|
for (const capture of captures) {
|
|
1870
1303
|
clearVoiceCaptureFinalizeTimer(capture);
|
|
1871
|
-
capture.
|
|
1304
|
+
if (capture.stopInput) capture.stopInput();
|
|
1305
|
+
else capture.stream?.destroy();
|
|
1872
1306
|
}
|
|
1873
1307
|
}
|
|
1874
1308
|
function clearVoiceCaptureFinalizeTimer(capture) {
|
|
@@ -1992,13 +1426,13 @@ async function synthesizeVoiceReplyAudio(params) {
|
|
|
1992
1426
|
}
|
|
1993
1427
|
//#endregion
|
|
1994
1428
|
//#region extensions/discord/src/voice/segment.ts
|
|
1995
|
-
const logger$
|
|
1429
|
+
const logger$6 = createSubsystemLogger("discord/voice");
|
|
1996
1430
|
async function processDiscordVoiceSegment(params) {
|
|
1997
1431
|
const { entry, wavPath, userId, durationSeconds } = params;
|
|
1998
1432
|
const conversationCurrent = () => !entry.captureOnly && entry.sessionLifecycle.status === "active" && params.isConversationCurrent();
|
|
1999
1433
|
logVoiceVerbose(`segment processing (${durationSeconds.toFixed(2)}s): guild ${entry.guildId} channel ${entry.channelId}`);
|
|
2000
1434
|
const ingress = params.resolveIngressContext().catch((error) => {
|
|
2001
|
-
logger$
|
|
1435
|
+
logger$6.warn(`discord voice: conversation authorization failed: ${formatErrorMessage$1(error)}`);
|
|
2002
1436
|
return null;
|
|
2003
1437
|
});
|
|
2004
1438
|
const conversationAuthorized = ingress.then((context) => Boolean(context) && conversationCurrent());
|
|
@@ -2072,14 +1506,14 @@ async function respondToDiscordVoiceTranscript(params) {
|
|
|
2072
1506
|
text: transcript
|
|
2073
1507
|
}).catch((error) => {
|
|
2074
1508
|
if (readErrorName(error) === "AbortError") {
|
|
2075
|
-
logger$
|
|
1509
|
+
logger$6.warn(`discord voice: active-run control cancelled: ${formatErrorMessage$1(error)}`);
|
|
2076
1510
|
return null;
|
|
2077
1511
|
}
|
|
2078
|
-
logger$
|
|
1512
|
+
logger$6.warn(`discord voice: active-run control failed; falling back to normal segment handling: ${formatErrorMessage$1(error)}`);
|
|
2079
1513
|
});
|
|
2080
1514
|
if (control === null || !conversationCurrent()) return;
|
|
2081
1515
|
if (control?.handled) {
|
|
2082
|
-
logger$
|
|
1516
|
+
logger$6.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}`);
|
|
2083
1517
|
replyText = control.speakText ?? "";
|
|
2084
1518
|
} else {
|
|
2085
1519
|
const prompt = formatVoiceIngressPrompt(transcript, ingress.speakerLabel);
|
|
@@ -2119,13 +1553,13 @@ async function respondToDiscordVoiceTranscript(params) {
|
|
|
2119
1553
|
return;
|
|
2120
1554
|
}
|
|
2121
1555
|
if (voiceReplyAudio.status === "failed") {
|
|
2122
|
-
logger$
|
|
1556
|
+
logger$6.warn(`discord voice: TTS failed: ${voiceReplyAudio.error ?? "unknown error"}`);
|
|
2123
1557
|
return;
|
|
2124
1558
|
}
|
|
2125
1559
|
const streamFailure = voiceReplyAudio.mode === "file" ? voiceReplyAudio.streamFailure : void 0;
|
|
2126
1560
|
if (streamFailure && !entry.ttsStreamFallbackWarned) {
|
|
2127
1561
|
entry.ttsStreamFallbackWarned = true;
|
|
2128
|
-
logger$
|
|
1562
|
+
logger$6.warn(`discord voice: streaming TTS failed provider=${streamFailure.provider} reasonCode=${streamFailure.reasonCode}; using file fallback`);
|
|
2129
1563
|
}
|
|
2130
1564
|
logVoiceVerbose(`tts ok (${voiceReplyAudio.speakText.length} chars): guild ${entry.guildId} channel ${entry.channelId}`);
|
|
2131
1565
|
const releaseAudio = voiceReplyAudio.mode === "stream" ? voiceReplyAudio.release : () => unlinkIfExists(voiceReplyAudio.audioPath);
|
|
@@ -2134,32 +1568,22 @@ async function respondToDiscordVoiceTranscript(params) {
|
|
|
2134
1568
|
return;
|
|
2135
1569
|
}
|
|
2136
1570
|
params.enqueuePlayback(entry, async () => {
|
|
2137
|
-
const voiceSdk = loadDiscordVoiceSdk();
|
|
2138
|
-
const playbackLifecycle = new AbortController();
|
|
2139
|
-
let playbackStarted = false;
|
|
2140
|
-
const cancelStoppedPlayback = () => (!playbackStarted || entry.sessionLifecycle.status === "stopped") && playbackLifecycle.abort();
|
|
2141
1571
|
try {
|
|
2142
1572
|
if (entry.sessionLifecycle.status === "stopped") return;
|
|
2143
|
-
entry.player.on(voiceSdk.AudioPlayerStatus.Idle, cancelStoppedPlayback);
|
|
2144
1573
|
const input = voiceReplyAudio.mode === "stream" ? Readable.fromWeb(voiceReplyAudio.audioStream) : voiceReplyAudio.audioPath;
|
|
2145
1574
|
logVoiceVerbose(`playback start: guild ${entry.guildId} channel ${entry.channelId} ${voiceReplyAudio.mode}`);
|
|
2146
|
-
|
|
2147
|
-
entry.
|
|
2148
|
-
await voiceSdk.entersState(entry.player, voiceSdk.AudioPlayerStatus.Playing, AbortSignal.any([AbortSignal.timeout(PLAYBACK_READY_TIMEOUT_MS), playbackLifecycle.signal]));
|
|
2149
|
-
playbackStarted = true;
|
|
2150
|
-
await voiceSdk.entersState(entry.player, voiceSdk.AudioPlayerStatus.Idle, playbackLifecycle.signal);
|
|
2151
|
-
logVoiceVerbose(`playback done: guild ${entry.guildId} channel ${entry.channelId}`);
|
|
1575
|
+
await entry.audio.play(input);
|
|
1576
|
+
logVoiceVerbose("playback done: guild " + entry.guildId + " channel " + entry.channelId);
|
|
2152
1577
|
} catch (error) {
|
|
2153
1578
|
if (entry.sessionLifecycle.status !== "stopped") throw error;
|
|
2154
1579
|
} finally {
|
|
2155
|
-
entry.player.off(voiceSdk.AudioPlayerStatus.Idle, cancelStoppedPlayback);
|
|
2156
1580
|
await releaseAudio?.();
|
|
2157
1581
|
}
|
|
2158
1582
|
});
|
|
2159
1583
|
}
|
|
2160
1584
|
//#endregion
|
|
2161
1585
|
//#region extensions/discord/src/voice/voice-recording.ts
|
|
2162
|
-
const logger$
|
|
1586
|
+
const logger$5 = createSubsystemLogger("discord/voice");
|
|
2163
1587
|
const PCM_BYTES_PER_MILLISECOND = 192;
|
|
2164
1588
|
const MAX_PENDING_RECORDING_JOBS = 128;
|
|
2165
1589
|
const MAX_PENDING_RECORDING_BYTES = 67108864;
|
|
@@ -2180,6 +1604,7 @@ var DiscordVoiceRecording = class {
|
|
|
2180
1604
|
this.chunks = [];
|
|
2181
1605
|
this.bytes = 0;
|
|
2182
1606
|
this.chunked = false;
|
|
1607
|
+
this.recordingEpoch = 0n;
|
|
2183
1608
|
this.startedAt = 0;
|
|
2184
1609
|
this.completion = Promise.resolve();
|
|
2185
1610
|
const budget = params.entry.audioInputBudget;
|
|
@@ -2190,9 +1615,10 @@ var DiscordVoiceRecording = class {
|
|
|
2190
1615
|
this.params.onExcluded();
|
|
2191
1616
|
return;
|
|
2192
1617
|
}
|
|
2193
|
-
this.chunked ||= receipt.
|
|
2194
|
-
if (receipt.capture !== this.capture) await this.flush();
|
|
1618
|
+
this.chunked ||= receipt.recordingEpoch !== 0n;
|
|
1619
|
+
if (receipt.capture !== this.capture || receipt.recordingEpoch !== this.recordingEpoch) await this.flush();
|
|
2195
1620
|
this.capture = receipt.capture;
|
|
1621
|
+
this.recordingEpoch = receipt.recordingEpoch;
|
|
2196
1622
|
if (!this.capture?.isCurrent() && !this.params.canConverse()) {
|
|
2197
1623
|
this.params.onExcluded();
|
|
2198
1624
|
return;
|
|
@@ -2201,7 +1627,7 @@ var DiscordVoiceRecording = class {
|
|
|
2201
1627
|
this.chunks = [];
|
|
2202
1628
|
this.bytes = 0;
|
|
2203
1629
|
this.overflow = /* @__PURE__ */ new Error("Discord voice audio exceeds the transcription limit; speak a shorter segment.");
|
|
2204
|
-
logger$
|
|
1630
|
+
logger$5.warn(`discord voice: ${this.overflow.message}`);
|
|
2205
1631
|
throw this.overflow;
|
|
2206
1632
|
}
|
|
2207
1633
|
for (let offset = 0; offset < pcm.length;) {
|
|
@@ -2247,7 +1673,7 @@ var DiscordVoiceRecording = class {
|
|
|
2247
1673
|
try {
|
|
2248
1674
|
await wav.cleanup();
|
|
2249
1675
|
} catch (error) {
|
|
2250
|
-
logger$
|
|
1676
|
+
logger$5.warn(`discord voice: recording cleanup failed: ${formatErrorMessage(error)}`);
|
|
2251
1677
|
} finally {
|
|
2252
1678
|
releaseBudget();
|
|
2253
1679
|
}
|
|
@@ -2276,7 +1702,7 @@ var DiscordVoiceRecording = class {
|
|
|
2276
1702
|
});
|
|
2277
1703
|
} catch (error) {
|
|
2278
1704
|
this.params.onExcluded();
|
|
2279
|
-
logger$
|
|
1705
|
+
logger$5.warn(`discord voice: recording failed: ${formatErrorMessage(error)}`);
|
|
2280
1706
|
} finally {
|
|
2281
1707
|
await cleanup();
|
|
2282
1708
|
}
|
|
@@ -2288,7 +1714,7 @@ var DiscordVoiceRecording = class {
|
|
|
2288
1714
|
};
|
|
2289
1715
|
//#endregion
|
|
2290
1716
|
//#region extensions/discord/src/voice/voice-receive.ts
|
|
2291
|
-
const logger$
|
|
1717
|
+
const logger$4 = createSubsystemLogger("discord/voice");
|
|
2292
1718
|
const MAX_PENDING_OPUS_PACKETS = 1e3;
|
|
2293
1719
|
const MAX_PENDING_OPUS_BYTES = 1048576;
|
|
2294
1720
|
var DiscordVoiceReceive = class {
|
|
@@ -2296,15 +1722,12 @@ var DiscordVoiceReceive = class {
|
|
|
2296
1722
|
this.params = params;
|
|
2297
1723
|
this.daveRecoveryAttempts = /* @__PURE__ */ new Map();
|
|
2298
1724
|
}
|
|
2299
|
-
scheduleCaptureFinalize(entry, userId,
|
|
2300
|
-
|
|
1725
|
+
scheduleCaptureFinalize(entry, userId, _reason) {
|
|
1726
|
+
if (entry.capture.get(userId)?.stream) return;
|
|
2301
1727
|
scheduleVoiceCaptureFinalize({
|
|
2302
1728
|
state: entry.capture,
|
|
2303
1729
|
userId,
|
|
2304
|
-
delayMs:
|
|
2305
|
-
onFinalize: () => {
|
|
2306
|
-
logVoiceVerbose(`capture finalize: guild ${entry.guildId} channel ${entry.channelId} user ${userId} reason=${reason} grace=${graceMs}ms`);
|
|
2307
|
-
}
|
|
1730
|
+
delayMs: resolveVoiceTimeoutMs(this.params.discordConfig.voice?.captureSilenceGraceMs, CAPTURE_FINALIZE_GRACE_MS)
|
|
2308
1731
|
});
|
|
2309
1732
|
}
|
|
2310
1733
|
async handleSpeakingStart(entry, userId, origin = "native") {
|
|
@@ -2320,7 +1743,7 @@ var DiscordVoiceReceive = class {
|
|
|
2320
1743
|
}
|
|
2321
1744
|
const capture = entry.transcripts;
|
|
2322
1745
|
const realtime = entry.realtimeLifecycle.status === "active" ? entry.realtimeLifecycle.instance : void 0;
|
|
2323
|
-
const playing = entry.
|
|
1746
|
+
const playing = entry.audio.playerStatus === "playing";
|
|
2324
1747
|
const conversationAllowed = origin === "native" && !entry.captureOnly && !(playing && !realtime?.canReceiveDuringPlayback());
|
|
2325
1748
|
if (!capture && !conversationAllowed) {
|
|
2326
1749
|
logVoiceVerbose(`capture ignored: guild ${entry.guildId} channel ${entry.channelId} user ${userId} reason=${playing ? "protected playback" : "inactive capture"}`);
|
|
@@ -2344,11 +1767,12 @@ var DiscordVoiceReceive = class {
|
|
|
2344
1767
|
await this.receiveSpeaker(entry, userId, reservation, conversationAllowed, realtimeIngress);
|
|
2345
1768
|
} finally {
|
|
2346
1769
|
const stream = reservation.stream;
|
|
2347
|
-
if (finishVoiceCapture(entry.capture, userId, reservation)
|
|
1770
|
+
if (!stream) finishVoiceCapture(entry.capture, userId, reservation);
|
|
1771
|
+
else if (!stream.destroyed) stream.destroy();
|
|
2348
1772
|
}
|
|
2349
1773
|
}
|
|
2350
1774
|
captureCurrentSpeakers(entry) {
|
|
2351
|
-
for (const userId of entry.
|
|
1775
|
+
for (const userId of entry.audio.speakingUsers) this.handleSpeakingStart(entry, userId, "scan").catch((error) => logger$4.warn(`discord voice: capture failed: ${formatErrorMessage(error)}`));
|
|
2352
1776
|
}
|
|
2353
1777
|
responseContext(entry, userId) {
|
|
2354
1778
|
return {
|
|
@@ -2366,48 +1790,75 @@ var DiscordVoiceReceive = class {
|
|
|
2366
1790
|
return guild && typeof guild.name === "string" && guild.name.trim() ? guild.name : void 0;
|
|
2367
1791
|
},
|
|
2368
1792
|
enqueuePlayback: (playbackEntry, task) => {
|
|
2369
|
-
playbackEntry.playbackQueue = playbackEntry.playbackQueue.then(task).catch((err) => logger$
|
|
1793
|
+
playbackEntry.playbackQueue = playbackEntry.playbackQueue.then(task).catch((err) => logger$4.warn(`discord voice: playback failed: ${formatErrorMessage(err)}`));
|
|
2370
1794
|
}
|
|
2371
1795
|
};
|
|
2372
1796
|
}
|
|
2373
1797
|
async receiveSpeaker(entry, userId, reservation, conversationAllowed, admittedIngress) {
|
|
2374
|
-
const voiceSdk = loadDiscordVoiceSdk();
|
|
2375
1798
|
const realtime = entry.realtimeLifecycle.status === "active" ? entry.realtimeLifecycle.instance : void 0;
|
|
2376
|
-
const protectedPlayback = () => entry.
|
|
1799
|
+
const protectedPlayback = () => entry.audio.playerStatus === "playing" && !realtime?.canReceiveDuringPlayback();
|
|
2377
1800
|
this.enableDaveReceivePassthrough(entry, `speaker ${userId} start`, 15);
|
|
2378
|
-
const stream = reservation.stream = entry.connection.receiver.subscribe(userId, { end: { behavior: voiceSdk.EndBehaviorType.Manual } });
|
|
2379
1801
|
if (!entry.audioInputBudget.enabled && !realtime) {
|
|
2380
|
-
logger$
|
|
1802
|
+
logger$4.warn("discord voice: capture skipped: audio understanding is disabled; enable tools.media.audio.enabled to transcribe voice.");
|
|
2381
1803
|
return;
|
|
2382
1804
|
}
|
|
1805
|
+
const captureReceipts = this.params.bindCaptureReceipts(entry);
|
|
1806
|
+
let stream;
|
|
1807
|
+
try {
|
|
1808
|
+
stream = entry.audio.subscribe(userId, captureReceipts.state);
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
captureReceipts.close();
|
|
1811
|
+
throw error;
|
|
1812
|
+
}
|
|
1813
|
+
reservation.stream = stream;
|
|
1814
|
+
reservation.stopInput = () => stream.stopInput();
|
|
1815
|
+
clearVoiceCaptureFinalizeTimer(reservation);
|
|
1816
|
+
const finalizeReservation = () => finishVoiceCapture(entry.capture, userId, reservation);
|
|
1817
|
+
if (stream.physicalFinalized) finalizeReservation();
|
|
1818
|
+
else stream.once("finalized", finalizeReservation);
|
|
2383
1819
|
const input = new PassThrough({ objectMode: true });
|
|
2384
1820
|
const receipts = /* @__PURE__ */ new WeakMap();
|
|
2385
1821
|
let failed = false;
|
|
2386
1822
|
let pendingPackets = 0;
|
|
2387
1823
|
let pendingBytes = 0;
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
const
|
|
2391
|
-
if (!
|
|
1824
|
+
let resetReceiveRecovery = false;
|
|
1825
|
+
const acceptPacket = (frame) => {
|
|
1826
|
+
const packet = Buffer.from(frame.packet);
|
|
1827
|
+
if (failed || !packet.length || stream.destroyed) {
|
|
1828
|
+
stream.acknowledge(frame);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
const capture = captureReceipts.resolve(frame.recordingEpoch);
|
|
1832
|
+
if (!capture && (!conversationAllowed || !this.params.isEntryCurrent(entry))) {
|
|
1833
|
+
stream.acknowledge(frame);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
2392
1836
|
if (pendingPackets >= MAX_PENDING_OPUS_PACKETS || packet.length > MAX_PENDING_OPUS_BYTES - pendingBytes) {
|
|
2393
1837
|
onError(/* @__PURE__ */ new Error("Discord voice receive backlog exceeded; try speaking again."));
|
|
2394
|
-
finishVoiceCapture(entry.capture, userId, reservation);
|
|
2395
1838
|
input.destroy();
|
|
2396
1839
|
stream.destroy();
|
|
2397
1840
|
return;
|
|
2398
1841
|
}
|
|
1842
|
+
if (!resetReceiveRecovery && frame.pcm.byteLength > 0 && this.params.isEntryCurrent(entry)) {
|
|
1843
|
+
resetReceiveRecovery = true;
|
|
1844
|
+
this.resetDecryptFailureState(entry);
|
|
1845
|
+
}
|
|
2399
1846
|
pendingPackets += 1;
|
|
2400
1847
|
pendingBytes += packet.length;
|
|
2401
1848
|
const receivedPacket = Buffer.from(packet);
|
|
2402
1849
|
receipts.set(receivedPacket, {
|
|
2403
1850
|
capture,
|
|
2404
|
-
startedAt:
|
|
1851
|
+
startedAt: frame.receivedAt,
|
|
1852
|
+
recordingEpoch: frame.recordingEpoch
|
|
1853
|
+
});
|
|
1854
|
+
input.write({
|
|
1855
|
+
pcm: Buffer.from(frame.pcm),
|
|
1856
|
+
packet: receivedPacket,
|
|
1857
|
+
frame
|
|
2405
1858
|
});
|
|
2406
|
-
input.write(receivedPacket);
|
|
2407
1859
|
};
|
|
2408
1860
|
const endInput = () => input.end();
|
|
2409
1861
|
let aborted = false;
|
|
2410
|
-
let resetReceiveRecovery = false;
|
|
2411
1862
|
const onError = (error) => {
|
|
2412
1863
|
const analysis = analyzeVoiceReceiveError(error);
|
|
2413
1864
|
if (analysis.isAbortLike && !analysis.countsAsDecryptFailure) {
|
|
@@ -2432,7 +1883,7 @@ var DiscordVoiceReceive = class {
|
|
|
2432
1883
|
isCurrent: () => this.params.isEntryCurrent(entry),
|
|
2433
1884
|
canAdmit: () => !protectedPlayback(),
|
|
2434
1885
|
createTurn: realtime ? (context) => realtime.beginSpeakerTurn(context, userId, realtimeRecording) : void 0,
|
|
2435
|
-
warn: (message) => logger$
|
|
1886
|
+
warn: (message) => logger$4.warn(message)
|
|
2436
1887
|
}) : void 0;
|
|
2437
1888
|
const recording = new DiscordVoiceRecording({
|
|
2438
1889
|
entry,
|
|
@@ -2458,39 +1909,40 @@ var DiscordVoiceReceive = class {
|
|
|
2458
1909
|
let conversationCompletion;
|
|
2459
1910
|
try {
|
|
2460
1911
|
if (!conversation && !entry.transcripts?.isCurrent()) return;
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
if (!
|
|
2469
|
-
|
|
2470
|
-
|
|
1912
|
+
const processFrame = async (pcm, packet) => {
|
|
1913
|
+
const receipt = receipts.get(packet);
|
|
1914
|
+
if (!receipt || failed) return;
|
|
1915
|
+
pendingPackets -= 1;
|
|
1916
|
+
pendingBytes -= packet.length;
|
|
1917
|
+
receipts.delete(packet);
|
|
1918
|
+
if (!receipt.capture && conversation && !conversation.ingress) {
|
|
1919
|
+
if (!await waitForVoiceCaptureAdmission({
|
|
1920
|
+
capture: reservation,
|
|
1921
|
+
conversationAuthorized: conversation.ready.then(() => conversation.ingress !== null),
|
|
1922
|
+
isRecordingCurrent: () => entry.transcripts?.isCurrent() === true
|
|
1923
|
+
})) {
|
|
1924
|
+
stream.destroy();
|
|
1925
|
+
return;
|
|
2471
1926
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
1927
|
+
}
|
|
1928
|
+
if (failed) return;
|
|
1929
|
+
realtimeRecording?.noteReceipt(receipt);
|
|
1930
|
+
conversation?.sendAudio(pcm, receipt);
|
|
1931
|
+
await recording.append(pcm, receipt);
|
|
1932
|
+
};
|
|
1933
|
+
try {
|
|
1934
|
+
for await (const decoded of input) {
|
|
1935
|
+
const frame = decoded;
|
|
1936
|
+
try {
|
|
1937
|
+
await processFrame(frame.pcm, frame.packet);
|
|
1938
|
+
} finally {
|
|
1939
|
+
stream.acknowledge(frame.frame);
|
|
2482
1940
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
},
|
|
2488
|
-
onError,
|
|
2489
|
-
onVerbose: logVoiceVerbose,
|
|
2490
|
-
onWarn: (message) => logger$3.warn(message)
|
|
2491
|
-
});
|
|
1941
|
+
}
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
onError(error);
|
|
1944
|
+
}
|
|
2492
1945
|
await recording.finish();
|
|
2493
|
-
finishVoiceCapture(entry.capture, userId, reservation);
|
|
2494
1946
|
stream.destroy();
|
|
2495
1947
|
if (conversation) {
|
|
2496
1948
|
if (realtime) conversationCompletion = entry.conversations.finishAudio(conversation);
|
|
@@ -2507,13 +1959,14 @@ var DiscordVoiceReceive = class {
|
|
|
2507
1959
|
ingress: currentIngress,
|
|
2508
1960
|
transcript
|
|
2509
1961
|
});
|
|
2510
|
-
}).catch((error) => logger$
|
|
1962
|
+
}).catch((error) => logger$4.warn(`discord voice: processing failed: ${formatErrorMessage(error)}`));
|
|
2511
1963
|
}
|
|
2512
1964
|
}
|
|
2513
1965
|
} finally {
|
|
2514
1966
|
realtimeRecording?.sealBatch();
|
|
2515
|
-
if (conversationCompletion) conversationCompletion.catch((error) => logger$
|
|
1967
|
+
if (conversationCompletion) conversationCompletion.catch((error) => logger$4.warn(`discord voice: conversation failed: ${formatErrorMessage(error)}`));
|
|
2516
1968
|
else if (conversation) entry.conversations.release(conversation);
|
|
1969
|
+
captureReceipts.close();
|
|
2517
1970
|
stream.off("data", acceptPacket);
|
|
2518
1971
|
stream.off("end", endInput);
|
|
2519
1972
|
stream.off("close", endInput);
|
|
@@ -2531,46 +1984,19 @@ var DiscordVoiceReceive = class {
|
|
|
2531
1984
|
logVoiceVerbose(`receive decode skipped: ${analysis.message}`);
|
|
2532
1985
|
return;
|
|
2533
1986
|
}
|
|
2534
|
-
logger$
|
|
2535
|
-
if (
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
target: entry,
|
|
2539
|
-
sdk: loadDiscordVoiceSdk(),
|
|
2540
|
-
onWarn: (message) => logger$3.warn(message)
|
|
2541
|
-
}) === "failed") {
|
|
2542
|
-
this.startDecryptRecovery(entry, true);
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
this.enableDaveReceivePassthrough(entry, "receive decrypt error", 15);
|
|
1987
|
+
logger$4.warn(`discord voice: receive error: ${analysis.message}`);
|
|
1988
|
+
if (err instanceof Error && "daveRecoveryFailed" in err && err.daveRecoveryFailed === true) {
|
|
1989
|
+
this.startDecryptRecovery(entry, true);
|
|
1990
|
+
return;
|
|
2547
1991
|
}
|
|
2548
1992
|
if (!analysis.countsAsDecryptFailure) return;
|
|
2549
1993
|
const decryptFailure = noteVoiceDecryptFailure(entry.receiveRecovery);
|
|
2550
|
-
if (decryptFailure.firstFailure) logger$
|
|
1994
|
+
if (decryptFailure.firstFailure) logger$4.warn("discord voice: DAVE decrypt failures detected; voice receive may be unstable (upstream: discordjs/discord.js#11419)");
|
|
2551
1995
|
if (!decryptFailure.shouldRecover) return;
|
|
2552
1996
|
this.startDecryptRecovery(entry);
|
|
2553
1997
|
}
|
|
2554
1998
|
enableDaveReceivePassthrough(entry, reason, expirySeconds) {
|
|
2555
|
-
|
|
2556
|
-
return enableDaveReceivePassthrough({
|
|
2557
|
-
target: {
|
|
2558
|
-
guildId: entry.guildId,
|
|
2559
|
-
channelId: entry.channelId,
|
|
2560
|
-
connection: entry.connection
|
|
2561
|
-
},
|
|
2562
|
-
sdk: {
|
|
2563
|
-
VoiceConnectionStatus: { Ready: voiceSdk.VoiceConnectionStatus.Ready },
|
|
2564
|
-
NetworkingStatusCode: {
|
|
2565
|
-
Ready: voiceSdk.NetworkingStatusCode.Ready,
|
|
2566
|
-
Resuming: voiceSdk.NetworkingStatusCode.Resuming
|
|
2567
|
-
}
|
|
2568
|
-
},
|
|
2569
|
-
reason,
|
|
2570
|
-
expirySeconds,
|
|
2571
|
-
onVerbose: logVoiceVerbose,
|
|
2572
|
-
onWarn: (message) => logger$3.warn(message)
|
|
2573
|
-
});
|
|
1999
|
+
entry.audio.enablePassthrough(reason, expirySeconds);
|
|
2574
2000
|
}
|
|
2575
2001
|
async resolveDiscordVoiceIngressContext(entry, userId) {
|
|
2576
2002
|
return await resolveDiscordVoiceIngressContextWithParticipants({
|
|
@@ -2591,7 +2017,7 @@ var DiscordVoiceReceive = class {
|
|
|
2591
2017
|
const currentContext = await this.resolveDiscordVoiceIngressContext(entry, userId);
|
|
2592
2018
|
params.signal?.throwIfAborted();
|
|
2593
2019
|
if (!this.params.isEntryCurrent(entry) || !params.isCurrent() || !currentContext || currentContext.isCurrent?.() === false || currentContext.senderIsOwner !== context.senderIsOwner) throw new DOMException("Discord voice speaker authorization changed before delegation", "AbortError");
|
|
2594
|
-
logger$
|
|
2020
|
+
logger$4.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)}`);
|
|
2595
2021
|
const turn = await runDiscordVoiceAgentTurn({
|
|
2596
2022
|
entry,
|
|
2597
2023
|
accountId: this.params.accountId,
|
|
@@ -2615,7 +2041,7 @@ var DiscordVoiceReceive = class {
|
|
|
2615
2041
|
logVoiceVerbose(`realtime agent unauthorized: guild ${entry.guildId} channel ${entry.channelId} user ${userId}`);
|
|
2616
2042
|
return "";
|
|
2617
2043
|
}
|
|
2618
|
-
logger$
|
|
2044
|
+
logger$4.info(`discord voice: agent turn answer (${turn.text.length} chars) guild=${entry.guildId} channel=${entry.channelId} voiceSession=${entry.voiceSessionKey} supervisorSession=${entry.route.sessionKey} agent=${entry.route.agentId}: ${formatVoiceLogPreview(turn.text)}`);
|
|
2619
2045
|
return turn.text;
|
|
2620
2046
|
}
|
|
2621
2047
|
startDecryptRecovery(entry, force = false) {
|
|
@@ -2628,14 +2054,14 @@ var DiscordVoiceReceive = class {
|
|
|
2628
2054
|
entry.receiveRecovery.decryptRecoveryInFlight = true;
|
|
2629
2055
|
if (this.daveRecoveryAttempts.has(entry.guildId)) {
|
|
2630
2056
|
const windowSeconds = DECRYPT_FAILURE_WINDOW_MS / 1e3;
|
|
2631
|
-
logger$
|
|
2057
|
+
logger$4.warn(`discord voice: DAVE recovery failed again within ${windowSeconds} seconds; disconnecting guild=${entry.guildId} channel=${entry.channelId} to avoid a reconnect loop; retry /vc join after the voice gateway recovers`);
|
|
2632
2058
|
recovery = this.params.leave({ guildId: entry.guildId }, { preserveFollowState: this.params.isFollowOwnedGuild(entry.guildId) });
|
|
2633
2059
|
} else {
|
|
2634
2060
|
this.daveRecoveryAttempts.set(entry.guildId, now);
|
|
2635
2061
|
recovery = this.recoverFromDecryptFailures(entry);
|
|
2636
2062
|
}
|
|
2637
2063
|
} else recovery = this.recoverFromDecryptFailures(entry);
|
|
2638
|
-
recovery.catch((recoverErr) => logger$
|
|
2064
|
+
recovery.catch((recoverErr) => logger$4.warn(`discord voice: decrypt recovery failed: ${formatErrorMessage(recoverErr)}`)).finally(() => {
|
|
2639
2065
|
finishVoiceDecryptRecovery(entry.receiveRecovery);
|
|
2640
2066
|
});
|
|
2641
2067
|
}
|
|
@@ -2645,12 +2071,12 @@ var DiscordVoiceReceive = class {
|
|
|
2645
2071
|
}
|
|
2646
2072
|
async recoverFromDecryptFailures(entry) {
|
|
2647
2073
|
const active = this.params.getSession(entry.guildId);
|
|
2648
|
-
if (!active || active.
|
|
2074
|
+
if (!active || active.audio !== entry.audio) return;
|
|
2649
2075
|
const preserveFollowState = this.params.isFollowOwnedGuild(entry.guildId);
|
|
2650
|
-
logger$
|
|
2076
|
+
logger$4.warn(`discord voice: repeated decrypt failures; attempting rejoin for guild ${entry.guildId} channel ${entry.channelId}`);
|
|
2651
2077
|
const leaveResult = await this.params.leave({ guildId: entry.guildId }, { preserveFollowState });
|
|
2652
2078
|
if (!leaveResult.ok) {
|
|
2653
|
-
logger$
|
|
2079
|
+
logger$4.warn(`discord voice: decrypt recovery leave failed: ${leaveResult.message}`);
|
|
2654
2080
|
return;
|
|
2655
2081
|
}
|
|
2656
2082
|
const result = await this.params.join({
|
|
@@ -2661,10 +2087,334 @@ var DiscordVoiceReceive = class {
|
|
|
2661
2087
|
autoJoinWhenOccupied: entry.autoJoinWhenOccupied,
|
|
2662
2088
|
captureOnly: entry.captureOnly
|
|
2663
2089
|
});
|
|
2664
|
-
if (!result.ok) logger$
|
|
2090
|
+
if (!result.ok) logger$4.warn(`discord voice: rejoin after decrypt failures failed: ${result.message}`);
|
|
2665
2091
|
}
|
|
2666
2092
|
};
|
|
2667
2093
|
//#endregion
|
|
2094
|
+
//#region extensions/discord/src/voice/audio-worker-thread.ts
|
|
2095
|
+
/** Source, core-bundled and standalone plugin workers use the same launch owner. */
|
|
2096
|
+
function createDiscordAudioWorkerThread(options) {
|
|
2097
|
+
const url = resolveRuntimeWorkerUrl({
|
|
2098
|
+
currentModuleUrl: import.meta.url,
|
|
2099
|
+
sourceWorkerName: "audio-worker.runtime",
|
|
2100
|
+
distWorkerPath: "extensions/discord/src/voice/audio-worker.runtime.js",
|
|
2101
|
+
package: {
|
|
2102
|
+
name: "@openclaw/discord",
|
|
2103
|
+
distWorkerPath: "src/voice/audio-worker.runtime.js"
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
return new Worker(url, {
|
|
2107
|
+
workerData: options,
|
|
2108
|
+
execArgv: resolveRuntimeWorkerArgv(url).slice(0, -1)
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
//#endregion
|
|
2112
|
+
//#region extensions/discord/src/voice/audio-transport.ts
|
|
2113
|
+
const logger$3 = createSubsystemLogger("discord/voice");
|
|
2114
|
+
const STOP_GRACE_MS = 2e3;
|
|
2115
|
+
/** A received frame owns cloned PCM; its credit remains held until the consumer settles. */
|
|
2116
|
+
var DiscordAudioCapture = class extends Readable {
|
|
2117
|
+
finalizePhysicalSubscription() {
|
|
2118
|
+
if (this.physicalFinalized) return;
|
|
2119
|
+
this.physicalFinalized = true;
|
|
2120
|
+
this.emit("finalized");
|
|
2121
|
+
}
|
|
2122
|
+
constructor(id, send) {
|
|
2123
|
+
super({ objectMode: true });
|
|
2124
|
+
this.id = id;
|
|
2125
|
+
this.send = send;
|
|
2126
|
+
this.physicalFinalized = false;
|
|
2127
|
+
}
|
|
2128
|
+
_read() {}
|
|
2129
|
+
stopInput() {
|
|
2130
|
+
this.send({
|
|
2131
|
+
type: "capture-stop",
|
|
2132
|
+
id: this.id
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
acknowledge(frame) {
|
|
2136
|
+
this.send({
|
|
2137
|
+
type: "capture-ack",
|
|
2138
|
+
id: this.id,
|
|
2139
|
+
bytes: frame.packet.byteLength
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
_destroy(error, done) {
|
|
2143
|
+
this.send({
|
|
2144
|
+
type: "capture-stop",
|
|
2145
|
+
id: this.id
|
|
2146
|
+
});
|
|
2147
|
+
done(error);
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
/** Control-plane handle, never a substitute SDK connection or audio player. */
|
|
2151
|
+
var DiscordAudioTransport = class extends EventEmitter {
|
|
2152
|
+
constructor(options, adapterCreator) {
|
|
2153
|
+
super();
|
|
2154
|
+
this.speakingUsers = /* @__PURE__ */ new Set();
|
|
2155
|
+
this.playerStatus = "idle";
|
|
2156
|
+
this.connectionStatus = "signalling";
|
|
2157
|
+
this.captures = /* @__PURE__ */ new Map();
|
|
2158
|
+
this.files = /* @__PURE__ */ new Map();
|
|
2159
|
+
this.nextId = 0;
|
|
2160
|
+
this.stopping = false;
|
|
2161
|
+
this.terminal = false;
|
|
2162
|
+
this.adapterDestroyed = false;
|
|
2163
|
+
this.physicalStopped = new Promise((resolve) => {
|
|
2164
|
+
this.physicalStopResolve = resolve;
|
|
2165
|
+
});
|
|
2166
|
+
this.ready = new Promise((resolve, reject) => {
|
|
2167
|
+
this.readyResolve = resolve;
|
|
2168
|
+
this.readyReject = reject;
|
|
2169
|
+
});
|
|
2170
|
+
this.ready.catch(() => {});
|
|
2171
|
+
this.worker = createDiscordAudioWorkerThread(options);
|
|
2172
|
+
try {
|
|
2173
|
+
this.adapter = adapterCreator({
|
|
2174
|
+
onVoiceServerUpdate: (data) => this.send({
|
|
2175
|
+
type: "gateway-server",
|
|
2176
|
+
data
|
|
2177
|
+
}),
|
|
2178
|
+
onVoiceStateUpdate: (data) => this.send({
|
|
2179
|
+
type: "gateway-state",
|
|
2180
|
+
data
|
|
2181
|
+
}),
|
|
2182
|
+
destroy: () => {
|
|
2183
|
+
this.stop();
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
this.worker.terminate();
|
|
2188
|
+
throw error;
|
|
2189
|
+
}
|
|
2190
|
+
this.worker.on("message", (event) => this.receive(event));
|
|
2191
|
+
this.worker.on("error", (error) => this.finish(toErrorObject(error, "Discord audio worker failed")));
|
|
2192
|
+
this.exited = new Promise((resolve) => {
|
|
2193
|
+
this.worker.once("exit", (code) => {
|
|
2194
|
+
this.finish(/* @__PURE__ */ new Error("Discord audio worker exited (" + code + ")."));
|
|
2195
|
+
resolve();
|
|
2196
|
+
});
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
allocateId() {
|
|
2200
|
+
return ++this.nextId;
|
|
2201
|
+
}
|
|
2202
|
+
send(command, transferList) {
|
|
2203
|
+
if (this.terminal || this.stopping && command.type !== "stop") return;
|
|
2204
|
+
try {
|
|
2205
|
+
this.worker.postMessage(command, transferList);
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
this.finish(error instanceof Error ? error : new Error(String(error)));
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
subscribe(userId, recordingEpoch) {
|
|
2211
|
+
if (this.stopping || this.terminal) throw new Error("Discord voice session stopped before capture.");
|
|
2212
|
+
const id = this.allocateId();
|
|
2213
|
+
const capture = new DiscordAudioCapture(id, (command) => this.send(command));
|
|
2214
|
+
this.captures.set(id, capture);
|
|
2215
|
+
capture.once("close", () => {
|
|
2216
|
+
if (capture.physicalFinalized) this.captures.delete(id);
|
|
2217
|
+
});
|
|
2218
|
+
this.send({
|
|
2219
|
+
type: "capture",
|
|
2220
|
+
id,
|
|
2221
|
+
userId,
|
|
2222
|
+
recordingEpoch
|
|
2223
|
+
});
|
|
2224
|
+
return capture;
|
|
2225
|
+
}
|
|
2226
|
+
enablePassthrough(reason, expirySeconds) {
|
|
2227
|
+
this.send({
|
|
2228
|
+
type: "passthrough",
|
|
2229
|
+
reason,
|
|
2230
|
+
expirySeconds
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
stopPlayback() {
|
|
2234
|
+
this.send({ type: "player-stop" });
|
|
2235
|
+
}
|
|
2236
|
+
async play(input) {
|
|
2237
|
+
if (this.stopping || this.terminal) throw new Error("Discord voice session stopped before playback.");
|
|
2238
|
+
const id = this.allocateId();
|
|
2239
|
+
const abort = new AbortController();
|
|
2240
|
+
const complete = new Promise((resolve, reject) => {
|
|
2241
|
+
this.files.set(id, {
|
|
2242
|
+
resolve,
|
|
2243
|
+
reject,
|
|
2244
|
+
abort
|
|
2245
|
+
});
|
|
2246
|
+
});
|
|
2247
|
+
complete.catch(() => {});
|
|
2248
|
+
this.send(typeof input === "string" ? {
|
|
2249
|
+
type: "file-play",
|
|
2250
|
+
id,
|
|
2251
|
+
path: input
|
|
2252
|
+
} : {
|
|
2253
|
+
type: "stream-play",
|
|
2254
|
+
id
|
|
2255
|
+
});
|
|
2256
|
+
const pump = typeof input === "string" ? Promise.resolve() : (async () => {
|
|
2257
|
+
const destroy = () => input.destroy();
|
|
2258
|
+
abort.signal.addEventListener("abort", destroy, { once: true });
|
|
2259
|
+
try {
|
|
2260
|
+
for await (const chunk of input) {
|
|
2261
|
+
if (abort.signal.aborted) break;
|
|
2262
|
+
const frame = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2263
|
+
for (let offset = 0; offset < frame.length && !abort.signal.aborted; offset += 65536) {
|
|
2264
|
+
const playback = this.files.get(id);
|
|
2265
|
+
if (!playback) return;
|
|
2266
|
+
const drained = new Promise((resolve) => {
|
|
2267
|
+
playback.drain = resolve;
|
|
2268
|
+
});
|
|
2269
|
+
this.send({
|
|
2270
|
+
type: "stream-chunk",
|
|
2271
|
+
id,
|
|
2272
|
+
audio: frame.subarray(offset, offset + 65536)
|
|
2273
|
+
});
|
|
2274
|
+
await drained;
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
this.send({
|
|
2278
|
+
type: "stream-end",
|
|
2279
|
+
id
|
|
2280
|
+
});
|
|
2281
|
+
} finally {
|
|
2282
|
+
abort.signal.removeEventListener("abort", destroy);
|
|
2283
|
+
}
|
|
2284
|
+
})();
|
|
2285
|
+
try {
|
|
2286
|
+
await Promise.all([pump, complete]);
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
this.stopPlayback();
|
|
2289
|
+
throw error;
|
|
2290
|
+
} finally {
|
|
2291
|
+
abort.abort();
|
|
2292
|
+
this.files.get(id)?.drain?.();
|
|
2293
|
+
this.files.delete(id);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
stop() {
|
|
2297
|
+
if (this.stopTask) return this.stopTask;
|
|
2298
|
+
this.stopping = true;
|
|
2299
|
+
this.send({ type: "stop" });
|
|
2300
|
+
const drained = (async () => {
|
|
2301
|
+
let timer;
|
|
2302
|
+
try {
|
|
2303
|
+
await Promise.race([this.exited, new Promise((resolve) => {
|
|
2304
|
+
timer = setTimeout(() => {
|
|
2305
|
+
this.worker.terminate().then(() => resolve());
|
|
2306
|
+
}, STOP_GRACE_MS);
|
|
2307
|
+
})]);
|
|
2308
|
+
} finally {
|
|
2309
|
+
clearTimeout(timer);
|
|
2310
|
+
this.finish(/* @__PURE__ */ new Error("Discord voice session stopped."));
|
|
2311
|
+
}
|
|
2312
|
+
})();
|
|
2313
|
+
this.stopTask = Promise.race([this.physicalStopped, drained]);
|
|
2314
|
+
return this.stopTask;
|
|
2315
|
+
}
|
|
2316
|
+
receive(event) {
|
|
2317
|
+
if (this.terminal) return;
|
|
2318
|
+
switch (event.type) {
|
|
2319
|
+
case "gateway-send":
|
|
2320
|
+
if (!this.adapter.sendPayload(event.payload)) this.send({ type: "gateway-failed" });
|
|
2321
|
+
break;
|
|
2322
|
+
case "gateway-destroy":
|
|
2323
|
+
this.destroyAdapter();
|
|
2324
|
+
this.physicalStopResolve();
|
|
2325
|
+
break;
|
|
2326
|
+
case "ready":
|
|
2327
|
+
this.readyResolve();
|
|
2328
|
+
break;
|
|
2329
|
+
case "stopped":
|
|
2330
|
+
this.finish(/* @__PURE__ */ new Error("Discord voice session stopped."), true);
|
|
2331
|
+
break;
|
|
2332
|
+
case "connection":
|
|
2333
|
+
this.connectionStatus = event.status;
|
|
2334
|
+
if (event.status === "destroyed") this.emit("stopped");
|
|
2335
|
+
break;
|
|
2336
|
+
case "player":
|
|
2337
|
+
this.playerStatus = event.status;
|
|
2338
|
+
break;
|
|
2339
|
+
case "speaking":
|
|
2340
|
+
if (event.speaking) this.speakingUsers.add(event.userId);
|
|
2341
|
+
else this.speakingUsers.delete(event.userId);
|
|
2342
|
+
if (!this.stopping) this.emit("speaking", event.userId, event.speaking);
|
|
2343
|
+
break;
|
|
2344
|
+
case "capture-frame": {
|
|
2345
|
+
const capture = this.captures.get(event.id);
|
|
2346
|
+
if (capture && !capture.destroyed) capture.push({
|
|
2347
|
+
...event.frame,
|
|
2348
|
+
pcm: Buffer.from(event.frame.pcm),
|
|
2349
|
+
packet: Buffer.from(event.frame.packet)
|
|
2350
|
+
});
|
|
2351
|
+
break;
|
|
2352
|
+
}
|
|
2353
|
+
case "capture-error":
|
|
2354
|
+
this.captures.get(event.id)?.emit("error", restoreDiscordAudioError(event.error));
|
|
2355
|
+
break;
|
|
2356
|
+
case "capture-finalized": {
|
|
2357
|
+
const capture = this.captures.get(event.id);
|
|
2358
|
+
capture?.finalizePhysicalSubscription();
|
|
2359
|
+
if (capture?.destroyed) this.captures.delete(event.id);
|
|
2360
|
+
break;
|
|
2361
|
+
}
|
|
2362
|
+
case "capture-end":
|
|
2363
|
+
this.captures.get(event.id)?.push(null);
|
|
2364
|
+
break;
|
|
2365
|
+
case "file-end": {
|
|
2366
|
+
const playback = this.files.get(event.id);
|
|
2367
|
+
this.files.delete(event.id);
|
|
2368
|
+
playback?.abort.abort();
|
|
2369
|
+
playback?.drain?.();
|
|
2370
|
+
if (event.error) playback?.reject(restoreDiscordAudioError(event.error));
|
|
2371
|
+
else playback?.resolve();
|
|
2372
|
+
break;
|
|
2373
|
+
}
|
|
2374
|
+
case "stream-drain": {
|
|
2375
|
+
const playback = this.files.get(event.id);
|
|
2376
|
+
playback?.drain?.();
|
|
2377
|
+
if (playback) playback.drain = void 0;
|
|
2378
|
+
break;
|
|
2379
|
+
}
|
|
2380
|
+
case "error":
|
|
2381
|
+
this.readyReject(restoreDiscordAudioError(event.error));
|
|
2382
|
+
logger$3.warn("discord voice: " + event.error.message);
|
|
2383
|
+
break;
|
|
2384
|
+
case "log": if (event.level === "warn") logger$3.warn(event.message);
|
|
2385
|
+
else logVerbose(event.message);
|
|
2386
|
+
}
|
|
2387
|
+
if (!this.stopping) this.emit("event", event);
|
|
2388
|
+
}
|
|
2389
|
+
destroyAdapter() {
|
|
2390
|
+
if (this.adapterDestroyed) return;
|
|
2391
|
+
this.adapterDestroyed = true;
|
|
2392
|
+
this.adapter.destroy();
|
|
2393
|
+
}
|
|
2394
|
+
finish(error, drained = false) {
|
|
2395
|
+
if (this.terminal) return;
|
|
2396
|
+
this.terminal = true;
|
|
2397
|
+
this.connectionStatus = "destroyed";
|
|
2398
|
+
this.playerStatus = "idle";
|
|
2399
|
+
this.readyReject(error);
|
|
2400
|
+
this.destroyAdapter();
|
|
2401
|
+
this.speakingUsers.clear();
|
|
2402
|
+
for (const capture of this.captures.values()) if (drained) capture.push(null);
|
|
2403
|
+
else capture.destroy(error);
|
|
2404
|
+
this.captures.clear();
|
|
2405
|
+
for (const playback of this.files.values()) {
|
|
2406
|
+
playback.abort.abort();
|
|
2407
|
+
playback.drain?.();
|
|
2408
|
+
playback.reject(error);
|
|
2409
|
+
}
|
|
2410
|
+
this.files.clear();
|
|
2411
|
+
this.emit("stopped");
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
function createDiscordAudioTransport(options, adapterCreator) {
|
|
2415
|
+
return new DiscordAudioTransport(options, adapterCreator);
|
|
2416
|
+
}
|
|
2417
|
+
//#endregion
|
|
2668
2418
|
//#region extensions/discord/src/voice/voice-conversation-input.ts
|
|
2669
2419
|
const MAX_PENDING_CONVERSATIONS = 8;
|
|
2670
2420
|
const MAX_PENDING_CONVERSATION_AUDIO_BYTES = 1048576;
|
|
@@ -2889,37 +2639,19 @@ const logger$2 = createSubsystemLogger("discord/voice");
|
|
|
2889
2639
|
function isVoiceSessionStopped(entry) {
|
|
2890
2640
|
return entry.sessionLifecycle.status === "stopped";
|
|
2891
2641
|
}
|
|
2892
|
-
function isVoiceConnectionDestroyed(connection, voiceSdk) {
|
|
2893
|
-
return connection.state.status === voiceSdk.VoiceConnectionStatus.Destroyed;
|
|
2894
|
-
}
|
|
2895
|
-
function destroyVoiceConnectionSafely(params) {
|
|
2896
|
-
if (isVoiceConnectionDestroyed(params.connection, params.voiceSdk)) {
|
|
2897
|
-
logVoiceVerbose(`destroy skipped: ${params.reason}; connection already destroyed`);
|
|
2898
|
-
return;
|
|
2899
|
-
}
|
|
2900
|
-
try {
|
|
2901
|
-
params.connection.destroy();
|
|
2902
|
-
} catch (err) {
|
|
2903
|
-
const message = formatErrorMessage(err);
|
|
2904
|
-
if (message.includes("already been destroyed")) {
|
|
2905
|
-
logVoiceVerbose(`destroy skipped: ${params.reason}; ${message}`);
|
|
2906
|
-
return;
|
|
2907
|
-
}
|
|
2908
|
-
logger$2.warn(`discord voice: destroy failed: ${params.reason}: ${message}`);
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
function isRetryableVoiceJoinReadyError(error) {
|
|
2912
|
-
return formatErrorMessage(error).toLowerCase().includes("operation was aborted");
|
|
2913
|
-
}
|
|
2914
|
-
function resolveVoiceConnectionGroup(accountId) {
|
|
2915
|
-
return `openclaw:${accountId}`;
|
|
2916
|
-
}
|
|
2917
2642
|
var DiscordVoiceSessions = class {
|
|
2643
|
+
async stopTransport(guildId, audio = this.transports.get(guildId)) {
|
|
2644
|
+
if (!audio) return;
|
|
2645
|
+
await audio.stop();
|
|
2646
|
+
if (this.transports.get(guildId) === audio) this.transports.delete(guildId);
|
|
2647
|
+
}
|
|
2918
2648
|
constructor(params) {
|
|
2919
2649
|
this.params = params;
|
|
2920
2650
|
this.pendingStops = /* @__PURE__ */ new Set();
|
|
2651
|
+
this.transports = /* @__PURE__ */ new Map();
|
|
2921
2652
|
}
|
|
2922
2653
|
async waitForStops() {
|
|
2654
|
+
await Promise.allSettled([...this.transports.keys()].map((guildId) => this.stopTransport(guildId)));
|
|
2923
2655
|
await Promise.allSettled(this.pendingStops);
|
|
2924
2656
|
}
|
|
2925
2657
|
refreshGuildRoster(guildId) {
|
|
@@ -3016,77 +2748,37 @@ var DiscordVoiceSessions = class {
|
|
|
3016
2748
|
const connectReadyTimeoutMs = resolveVoiceTimeoutMs(voiceConfig?.connectTimeoutMs, VOICE_CONNECT_READY_TIMEOUT_MS);
|
|
3017
2749
|
const reconnectGraceMs = resolveVoiceTimeoutMs(voiceConfig?.reconnectGraceMs, VOICE_RECONNECT_GRACE_MS);
|
|
3018
2750
|
logVoiceVerbose(`join: DAVE settings encryption=${daveEncryption === false ? "off" : "on"} tolerance=${decryptionFailureTolerance ?? "default"} connectTimeout=${connectReadyTimeoutMs}ms reconnectGrace=${reconnectGraceMs}ms`);
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
daveEncryption,
|
|
3040
|
-
decryptionFailureTolerance
|
|
3041
|
-
});
|
|
3042
|
-
const remainingConnectReadyTimeoutMs = Math.max(1, connectReadyDeadlineMs - Date.now());
|
|
3043
|
-
try {
|
|
3044
|
-
await voiceSdk.entersState(joinedConnection, voiceSdk.VoiceConnectionStatus.Ready, remainingConnectReadyTimeoutMs);
|
|
3045
|
-
connection = joinedConnection;
|
|
3046
|
-
logVoiceVerbose(`join: connected to guild ${guildId} channel ${channelId}`);
|
|
3047
|
-
break;
|
|
3048
|
-
} catch (err) {
|
|
3049
|
-
destroyVoiceConnectionSafely({
|
|
3050
|
-
connection: joinedConnection,
|
|
3051
|
-
voiceSdk,
|
|
3052
|
-
reason: `failed join cleanup guild ${guildId} channel ${channelId}`
|
|
3053
|
-
});
|
|
3054
|
-
if (attempt === 1 && isRetryableVoiceJoinReadyError(err) && !this.params.destroyed() && connectReadyDeadlineMs > Date.now()) {
|
|
3055
|
-
logVoiceVerbose(`join: retrying aborted ready wait guild ${guildId} channel ${channelId}`);
|
|
3056
|
-
continue;
|
|
3057
|
-
}
|
|
3058
|
-
logger$2.warn(`discord voice: join failed before ready: guild ${guildId} channel ${channelId} timeout=${connectReadyTimeoutMs}ms error=${formatErrorMessage(err)}`);
|
|
3059
|
-
return {
|
|
3060
|
-
ok: false,
|
|
3061
|
-
message: `Failed to join voice channel: ${formatErrorMessage(err)}`
|
|
3062
|
-
};
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
if (!connection) return {
|
|
3066
|
-
ok: false,
|
|
3067
|
-
message: "Failed to join voice channel."
|
|
3068
|
-
};
|
|
3069
|
-
if (authority && !authority.isCurrent()) {
|
|
3070
|
-
destroyVoiceConnectionSafely({
|
|
3071
|
-
connection,
|
|
3072
|
-
voiceSdk,
|
|
3073
|
-
reason: `cancelled join guild ${guildId} channel ${channelId}`
|
|
3074
|
-
});
|
|
3075
|
-
return cancelledJoinResult();
|
|
3076
|
-
}
|
|
3077
|
-
if (this.params.destroyed()) {
|
|
3078
|
-
destroyVoiceConnectionSafely({
|
|
3079
|
-
connection,
|
|
3080
|
-
voiceSdk,
|
|
3081
|
-
reason: `manager stopped during join guild ${guildId} channel ${channelId}`
|
|
3082
|
-
});
|
|
2751
|
+
await this.stopTransport(guildId);
|
|
2752
|
+
if (this.params.destroyed() || authority?.isCurrent() === false) return cancelledJoinResult();
|
|
2753
|
+
const audio = createDiscordAudioTransport({
|
|
2754
|
+
guildId,
|
|
2755
|
+
channelId,
|
|
2756
|
+
group: "openclaw:" + this.params.accountId,
|
|
2757
|
+
selfDeaf: false,
|
|
2758
|
+
selfMute: false,
|
|
2759
|
+
daveEncryption,
|
|
2760
|
+
decryptionFailureTolerance,
|
|
2761
|
+
connectTimeoutMs: connectReadyTimeoutMs,
|
|
2762
|
+
reconnectGraceMs,
|
|
2763
|
+
captureSilenceGraceMs: resolveVoiceTimeoutMs(voiceConfig?.captureSilenceGraceMs, CAPTURE_FINALIZE_GRACE_MS),
|
|
2764
|
+
realtime: isDiscordRealtimeVoiceMode(voiceMode)
|
|
2765
|
+
}, adapterCreator);
|
|
2766
|
+
this.transports.set(guildId, audio);
|
|
2767
|
+
try {
|
|
2768
|
+
await audio.ready;
|
|
2769
|
+
} catch (error) {
|
|
2770
|
+
await this.stopTransport(guildId);
|
|
3083
2771
|
return {
|
|
3084
2772
|
ok: false,
|
|
3085
|
-
message: "
|
|
2773
|
+
message: "Failed to join voice channel: " + formatErrorMessage(error),
|
|
3086
2774
|
guildId,
|
|
3087
2775
|
channelId
|
|
3088
2776
|
};
|
|
3089
2777
|
}
|
|
2778
|
+
if (this.params.destroyed() || authority?.isCurrent() === false) {
|
|
2779
|
+
await this.stopTransport(guildId);
|
|
2780
|
+
return cancelledJoinResult();
|
|
2781
|
+
}
|
|
3090
2782
|
const sessionChannelId = channelInfo?.id ?? channelId;
|
|
3091
2783
|
if (sessionChannelId !== channelId) logVoiceVerbose(`join: using session channel ${sessionChannelId} for voice channel ${channelId}`);
|
|
3092
2784
|
let routeInfo;
|
|
@@ -3099,11 +2791,7 @@ var DiscordVoiceSessions = class {
|
|
|
3099
2791
|
voiceConfig
|
|
3100
2792
|
});
|
|
3101
2793
|
} catch (err) {
|
|
3102
|
-
|
|
3103
|
-
connection,
|
|
3104
|
-
voiceSdk,
|
|
3105
|
-
reason: `voice agent session route failed guild ${guildId} channel ${channelId}`
|
|
3106
|
-
});
|
|
2794
|
+
await this.stopTransport(guildId);
|
|
3107
2795
|
return {
|
|
3108
2796
|
ok: false,
|
|
3109
2797
|
message: `Failed to resolve Discord voice agent session: ${formatErrorMessage(err)}`,
|
|
@@ -3113,8 +2801,6 @@ var DiscordVoiceSessions = class {
|
|
|
3113
2801
|
}
|
|
3114
2802
|
const { route, voiceRoute, agentSessionMode, agentSessionTarget } = routeInfo;
|
|
3115
2803
|
logger$2.info(`discord voice: joining guild=${guildId} channel=${channelId} mode=${voiceMode} agent=${route.agentId} voiceSession=${voiceRoute.sessionKey} supervisorSession=${route.sessionKey} agentSessionMode=${agentSessionMode}${agentSessionTarget ? ` agentSessionTarget=${agentSessionTarget}` : ""} voiceModel=${voiceConfig?.model ?? "route-default"} realtimeProvider=${voiceConfig?.realtime?.provider ?? "auto"} realtimeModel=${voiceConfig?.realtime?.model ?? "provider-default"} realtimeVoice=${voiceConfig?.realtime?.speakerVoice ?? voiceConfig?.realtime?.speakerVoiceId ?? "provider-default"}`);
|
|
3116
|
-
const player = isDiscordRealtimeVoiceMode(voiceMode) ? voiceSdk.createAudioPlayer({ behaviors: { maxMissedFrames: DISCORD_REALTIME_PLAYBACK_IDLE_MS / 20 } }) : voiceSdk.createAudioPlayer();
|
|
3117
|
-
connection.subscribe(player);
|
|
3118
2804
|
let stopCompletion;
|
|
3119
2805
|
const stopEntry = (optionsLocal) => {
|
|
3120
2806
|
if (entry.sessionLifecycle.status === "stopped") return stopCompletion;
|
|
@@ -3124,12 +2810,9 @@ var DiscordVoiceSessions = class {
|
|
|
3124
2810
|
};
|
|
3125
2811
|
if (this.params.sessions.get(guildId) === entry) this.params.sessions.delete(guildId);
|
|
3126
2812
|
this.params.membership.deactivate(entry);
|
|
3127
|
-
|
|
3128
|
-
connection.receiver.speaking.off("end", speakingEndHandler);
|
|
2813
|
+
audio.off("speaking", speakingHandler);
|
|
3129
2814
|
stopVoiceCaptureState(entry.capture);
|
|
3130
|
-
|
|
3131
|
-
connection.off(voiceSdk.VoiceConnectionStatus.Destroyed, destroyedHandler);
|
|
3132
|
-
player.off("error", playerErrorHandler);
|
|
2815
|
+
audio.off("stopped", destroyedHandler);
|
|
3133
2816
|
const realtimeLifecycle = entry.realtimeLifecycle;
|
|
3134
2817
|
entry.realtimeLifecycle = {
|
|
3135
2818
|
status: "stopped",
|
|
@@ -3142,28 +2825,22 @@ var DiscordVoiceSessions = class {
|
|
|
3142
2825
|
} catch (error) {
|
|
3143
2826
|
logger$2.warn(`discord voice: realtime close failed: ${formatErrorMessage(error)}`);
|
|
3144
2827
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
connection,
|
|
3148
|
-
voiceSdk,
|
|
3149
|
-
reason: optionsLocal.reason
|
|
3150
|
-
});
|
|
2828
|
+
const audioCompletion = this.stopTransport(guildId, audio);
|
|
2829
|
+
realtimeCompletion = Promise.allSettled([realtimeCompletion, audioCompletion]).then(() => void 0);
|
|
3151
2830
|
const finish = () => {
|
|
3152
2831
|
entry.conversations.close();
|
|
3153
2832
|
this.params.onSessionStopped(entry, optionsLocal.reason);
|
|
3154
2833
|
};
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
});
|
|
3166
|
-
} else finish();
|
|
2834
|
+
stopCompletion = realtimeCompletion.catch((error) => logger$2.warn(`discord voice: realtime close failed: ${formatErrorMessage(error)}`)).then(finish);
|
|
2835
|
+
const completion = stopCompletion;
|
|
2836
|
+
this.pendingStops.add(completion);
|
|
2837
|
+
const forget = () => {
|
|
2838
|
+
this.pendingStops.delete(completion);
|
|
2839
|
+
};
|
|
2840
|
+
completion.then(forget, (error) => {
|
|
2841
|
+
forget();
|
|
2842
|
+
logger$2.warn(`discord voice: session stop failed: ${formatErrorMessage(error)}`);
|
|
2843
|
+
});
|
|
3167
2844
|
return stopCompletion;
|
|
3168
2845
|
};
|
|
3169
2846
|
const getTranscripts = this.params.getTranscripts;
|
|
@@ -3179,8 +2856,7 @@ var DiscordVoiceSessions = class {
|
|
|
3179
2856
|
sessionChannelId,
|
|
3180
2857
|
voiceSessionKey: voiceRoute.sessionKey,
|
|
3181
2858
|
route,
|
|
3182
|
-
|
|
3183
|
-
player,
|
|
2859
|
+
audio,
|
|
3184
2860
|
playbackQueue: Promise.resolve(),
|
|
3185
2861
|
processingQueue: Promise.resolve(),
|
|
3186
2862
|
conversations: new DiscordVoiceConversationQueue(),
|
|
@@ -3196,47 +2872,19 @@ var DiscordVoiceSessions = class {
|
|
|
3196
2872
|
generation: 0
|
|
3197
2873
|
},
|
|
3198
2874
|
stop(reason) {
|
|
3199
|
-
return stopEntry({
|
|
3200
|
-
destroyConnection: true,
|
|
3201
|
-
reason: reason ?? `stop guild ${guildId} channel ${channelId}`
|
|
3202
|
-
});
|
|
2875
|
+
return stopEntry({ reason: reason ?? `stop guild ${guildId} channel ${channelId}` });
|
|
3203
2876
|
}
|
|
3204
2877
|
};
|
|
3205
|
-
const speakingHandler = (userId) => {
|
|
3206
|
-
this.params.receive.handleSpeakingStart(entry, userId).catch((
|
|
3207
|
-
logger$2.warn(
|
|
2878
|
+
const speakingHandler = (userId, speaking) => {
|
|
2879
|
+
if (speaking) this.params.receive.handleSpeakingStart(entry, userId).catch((error) => {
|
|
2880
|
+
logger$2.warn("discord voice: capture failed: " + formatErrorMessage(error));
|
|
3208
2881
|
});
|
|
3209
|
-
|
|
3210
|
-
const speakingEndHandler = (userId) => {
|
|
3211
|
-
this.params.receive.scheduleCaptureFinalize(entry, userId, "speaker end");
|
|
3212
|
-
};
|
|
3213
|
-
const disconnectedHandler = () => {
|
|
3214
|
-
(async () => {
|
|
3215
|
-
try {
|
|
3216
|
-
logVoiceVerbose(`disconnected: attempting recovery guild ${guildId} channel ${channelId} grace=${reconnectGraceMs}ms`);
|
|
3217
|
-
await Promise.race([voiceSdk.entersState(connection, voiceSdk.VoiceConnectionStatus.Signalling, reconnectGraceMs), voiceSdk.entersState(connection, voiceSdk.VoiceConnectionStatus.Connecting, reconnectGraceMs)]);
|
|
3218
|
-
logVoiceVerbose(`disconnected: recovery started guild ${guildId} channel ${channelId}`);
|
|
3219
|
-
} catch (err) {
|
|
3220
|
-
logger$2.warn(`discord voice: disconnect recovery failed: guild ${guildId} channel ${channelId} timeout=${reconnectGraceMs}ms error=${formatErrorMessage(err)}; destroying connection`);
|
|
3221
|
-
stopEntry({
|
|
3222
|
-
destroyConnection: true,
|
|
3223
|
-
reason: `disconnect recovery failed guild ${guildId} channel ${channelId}`
|
|
3224
|
-
});
|
|
3225
|
-
}
|
|
3226
|
-
})();
|
|
2882
|
+
else this.params.receive.scheduleCaptureFinalize(entry, userId, "speaker end");
|
|
3227
2883
|
};
|
|
3228
2884
|
const destroyedHandler = () => {
|
|
3229
|
-
stopEntry({
|
|
3230
|
-
destroyConnection: false,
|
|
3231
|
-
reason: `destroyed guild ${guildId} channel ${channelId}`
|
|
3232
|
-
});
|
|
3233
|
-
};
|
|
3234
|
-
const playerErrorHandler = (err) => {
|
|
3235
|
-
logger$2.warn(`discord voice: playback error: ${formatErrorMessage(err)}`);
|
|
2885
|
+
stopEntry({ reason: "audio worker stopped" });
|
|
3236
2886
|
};
|
|
3237
|
-
|
|
3238
|
-
connection.on(voiceSdk.VoiceConnectionStatus.Destroyed, destroyedHandler);
|
|
3239
|
-
player.on("error", playerErrorHandler);
|
|
2887
|
+
audio.on("stopped", destroyedHandler);
|
|
3240
2888
|
if (!entry.captureOnly && isDiscordRealtimeVoiceMode(voiceMode)) {
|
|
3241
2889
|
const realtimeResult = await this.attachRealtimeSession(entry, voiceMode, { isCurrent: authority?.isCurrent });
|
|
3242
2890
|
if (!realtimeResult.ok) {
|
|
@@ -3259,8 +2907,7 @@ var DiscordVoiceSessions = class {
|
|
|
3259
2907
|
};
|
|
3260
2908
|
}
|
|
3261
2909
|
this.params.receive.enableDaveReceivePassthrough(entry, "post-join warmup", 30);
|
|
3262
|
-
|
|
3263
|
-
connection.receiver.speaking.on("end", speakingEndHandler);
|
|
2910
|
+
audio.on("speaking", speakingHandler);
|
|
3264
2911
|
this.params.sessions.set(guildId, entry);
|
|
3265
2912
|
this.params.membership.activate(entry, this.params.botUserId());
|
|
3266
2913
|
logger$2.info(`discord voice: joined guild=${guildId} channel=${channelId} mode=${voiceMode} agent=${route.agentId} voiceSession=${voiceRoute.sessionKey} supervisorSession=${route.sessionKey} voiceModel=${voiceConfig?.model ?? "route-default"}`);
|
|
@@ -3305,7 +2952,7 @@ var DiscordVoiceSessions = class {
|
|
|
3305
2952
|
ok: false,
|
|
3306
2953
|
message: "Discord realtime voice session stopped before startup completed."
|
|
3307
2954
|
};
|
|
3308
|
-
const { DiscordRealtimeVoiceSession } = await import("./realtime-session.runtime-
|
|
2955
|
+
const { DiscordRealtimeVoiceSession } = await import("./realtime-session.runtime-Dz8uVutq.mjs");
|
|
3309
2956
|
const realtime = new DiscordRealtimeVoiceSession({
|
|
3310
2957
|
accountId: this.params.accountId,
|
|
3311
2958
|
bootstrapContextInstructions,
|
|
@@ -3473,6 +3120,11 @@ var DiscordVoiceManager = class {
|
|
|
3473
3120
|
});
|
|
3474
3121
|
this.membership = new DiscordVoiceMembershipTracker(params.client, speakerContext, params.accountId);
|
|
3475
3122
|
this.receive = new DiscordVoiceReceive({
|
|
3123
|
+
bindCaptureReceipts: ({ guildId, channelId }) => bindDiscordCaptureReceipts({
|
|
3124
|
+
accountId: params.accountId,
|
|
3125
|
+
guildId,
|
|
3126
|
+
channelId
|
|
3127
|
+
}, this),
|
|
3476
3128
|
readPolicy: this.readPolicy,
|
|
3477
3129
|
accountId: params.accountId,
|
|
3478
3130
|
admissionAllowFrom,
|
|
@@ -3497,7 +3149,7 @@ var DiscordVoiceManager = class {
|
|
|
3497
3149
|
client: params.client,
|
|
3498
3150
|
deleteRecoveryAttempt: (guildId) => this.receive.daveRecoveryAttempts.delete(guildId),
|
|
3499
3151
|
destroyed: () => this.destroyed,
|
|
3500
|
-
|
|
3152
|
+
stopTransport: (guildId) => this.voiceSessions.stopTransport(guildId),
|
|
3501
3153
|
discordConfig: params.discordConfig,
|
|
3502
3154
|
getRecoveryAttempt: (guildId) => this.receive.daveRecoveryAttempts.get(guildId),
|
|
3503
3155
|
getSession: (guildId) => this.sessions.get(guildId),
|
|
@@ -3952,4 +3604,4 @@ var DiscordVoiceManager = class {
|
|
|
3952
3604
|
}
|
|
3953
3605
|
};
|
|
3954
3606
|
//#endregion
|
|
3955
|
-
export { DiscordVoiceGuildCreateListener, DiscordVoiceManager, DiscordVoiceReadyListener, DiscordVoiceResumedListener, DiscordVoiceStateUpdateListener,
|
|
3607
|
+
export { DiscordVoiceGuildCreateListener, DiscordVoiceManager, DiscordVoiceReadyListener, DiscordVoiceResumedListener, DiscordVoiceStateUpdateListener, handleDiscordVoiceAgentControlToolCall as a, formatRealtimeInterruptionLog as c, shouldLogRealtimeVerboseEvent as d, formatVoiceIngressPrompt as i, formatRealtimeLifecycleLog as l, DiscordRealtimeRecording as n, logDiscordVoiceAgentControlResult as o, logVoiceVerbose as r, maybeControlDiscordVoiceAgentRun as s, voice_runtime_exports as t, formatVoiceLogPreview as u };
|