Skip to content

fix(discord): keep audio voice replies threaded#95978

Merged
vincentkoc merged 2 commits into
openclaw:mainfrom
nxmxbbd:fix/discord-audio-replyto-fanout
Jun 25, 2026
Merged

fix(discord): keep audio voice replies threaded#95978
vincentkoc merged 2 commits into
openclaw:mainfrom
nxmxbbd:fix/discord-audio-replyto-fanout

Conversation

@nxmxbbd

@nxmxbbd nxmxbbd commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes #95977.

Fixes an issue where Discord audioAsVoice payloads with text or additional media would only thread the first internal voice send when replyToMode was single-use. The follow-up text and media sends belong to the same logical voice reply, but they resolved replyTo after the implicit reply target had already been consumed.

Why This Change Was Made

The Discord audioAsVoice branch now captures the resolved reply target once before sending the voice payload, then reuses that captured value for the voice send, the text follow-up, and additional media sends. The normal non-audioAsVoice helper paths still resolve reply targets per send, so existing single-use behavior for ordinary multi-send payloads is unchanged.

User Impact

Discord voice replies with accompanying text or extra media stay threaded to the source message instead of leaving follow-up sends unthreaded. Regular Discord payloads still only use an implicit single-use reply target on the first delivered message.

Evidence

  • RED before source change: after updating the audioAsVoice regression expectation, the current source failed because the text follow-up received replyTo: undefined instead of "reply-1".
  • Live Discord sandbox proof after fix: nonce oc-audio-replyto-2026-06-23T03-28-44-084Z-1dde4ee6 sent a real seed message, then discordOutbound.sendPayload delivered real voice, text, and image follow-up messages. Independent Discord API readback confirmed all three payload messages referenced the seed; a redacted local artifact contains no token, channel id, or raw message ids.
  • Focused regressions: node scripts/run-vitest.mjs extensions/discord/src/outbound-adapter.test.ts -t audioAsVoice
  • Full touched test file: node scripts/run-vitest.mjs extensions/discord/src/outbound-adapter.test.ts
  • Lint: node scripts/run-oxlint.mjs extensions/discord/src/outbound-payload.ts extensions/discord/src/outbound-adapter.test.ts
  • Format: node_modules/.bin/oxfmt --check --threads=1 extensions/discord/src/outbound-payload.ts extensions/discord/src/outbound-adapter.test.ts
  • Types: node scripts/run-tsgo.mjs -p extensions/discord/tsconfig.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/discord-extension.tsbuildinfo
  • Diff hygiene: git diff --check

Real behavior proof

  • Behavior or issue addressed:
    Discord audioAsVoice payload sends use multiple internal Discord send calls for one logical response. With an implicit single-use reply target, resolving replyTo separately at each internal send caused the text follow-up and extra media send to lose the reply target after the voice send consumed it.

  • Real environment tested:
    Local OpenClaw checkout using the Discord outbound adapter test harness, the repository's Vitest wrapper, and a live sandbox Discord text channel. Unit tests inspect the production outbound option objects directly; the live run sends through discordOutbound.sendPayload and confirms Discord API message references after delivery.

  • Exact steps or command run after this patch:

node scripts/run-vitest.mjs extensions/discord/src/outbound-adapter.test.ts -t audioAsVoice
node scripts/run-vitest.mjs extensions/discord/src/outbound-adapter.test.ts
node scripts/run-oxlint.mjs extensions/discord/src/outbound-payload.ts extensions/discord/src/outbound-adapter.test.ts
node_modules/.bin/oxfmt --check --threads=1 extensions/discord/src/outbound-payload.ts extensions/discord/src/outbound-adapter.test.ts
node scripts/run-tsgo.mjs -p extensions/discord/tsconfig.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/discord-extension.tsbuildinfo
git diff --check
node_modules/.bin/tsx /tmp/openclaw-discord-audio-replyto-live-proof.mts
  • Evidence after fix:
Focused audioAsVoice shard: 4 passed, 32 skipped.
Full touched test file: 36 passed (36).

Found 0 warnings and 0 errors.
All matched files use the correct format.
Discord package typecheck passed.
git diff --check passed.

Live Discord sandbox nonce: oc-audio-replyto-2026-06-23T03-28-44-084Z-1dde4ee6
Send order: voice -> text -> media.
All three real payload messages referenced the seed message in Discord API readback.
Voice message: 1 attachment and Discord voice-message flag set.
Text follow-up: content included the nonce.
Image follow-up: 1 attachment.
  • Observed result after fix:
    The audioAsVoice regressions now assert the voice send, text follow-up, and extra media send all carry replyTo: "reply-1" for both single-use modes (first and batched). The live Discord sandbox run confirmed the same shape after real delivery: voice, text, and image follow-up messages all referenced the seed message. The same file still covers ordinary non-audioAsVoice single-use behavior with ["reply-1", undefined] for chunked approval payload fallbacks.

  • What was not tested:
    No non-sandbox Discord channel or production user workflow was used. The live proof was limited to a sandbox Discord text channel plus local automated API readback.

AI-assisted; contributor manually reviewed and is responsible for the change.

@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord size: XS labels Jun 23, 2026
@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 25, 2026, 4:32 AM ET / 08:32 UTC.

Summary
The PR captures the Discord audioAsVoice reply target once and reuses it for the voice, text, and extra media sends, with regression coverage for single-use reply modes.

PR surface: Source +4, Tests +25. Total +29 across 2 files.

Reproducibility: yes. source-level: current main calls the stateful single-use reply supplier separately for each audioAsVoice internal send, so later sends can receive undefined. I did not run tests because this is a read-only review.

Review metrics: none identified.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #95977
Summary: This PR is the candidate fix for the linked Discord audioAsVoice reply-threading bug report.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Next step before merge

  • [P2] No repair lane is needed because the PR already contains the narrow implementation, regression coverage, and real behavior proof; maintainers should review and land or decline it normally.

Security
Cleared: The diff only changes Discord plugin reply-option reuse and focused tests; it does not touch dependencies, workflows, secrets, install scripts, package metadata, or other supply-chain surfaces.

Review details

Best possible solution:

Land this narrow Discord plugin fix after normal maintainer merge checks, preserving the existing single-use fanout contract for ordinary multi-send payloads.

Do we have a high-confidence way to reproduce the issue?

Yes, source-level: current main calls the stateful single-use reply supplier separately for each audioAsVoice internal send, so later sends can receive undefined. I did not run tests because this is a read-only review.

Is this the best way to solve the issue?

Yes. Capturing the resolved reply target only inside the audioAsVoice logical multipart branch is narrower than changing shared reply fanout, helper semantics, or ordinary text/media payload delivery.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against c030b305a444.

Label changes

Label justifications:

  • P2: The PR fixes a concrete Discord message-threading bug with limited channel-specific blast radius.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes after-fix live Discord sandbox API readback showing voice, text, and image follow-up messages all referenced the seed message, plus focused validation output.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix live Discord sandbox API readback showing voice, text, and image follow-up messages all referenced the seed message, plus focused validation output.
Evidence reviewed

PR surface:

Source +4, Tests +25. Total +29 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 9 5 +4
Tests 1 27 2 +25
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 36 7 +29

What I checked:

Likely related people:

  • steipete: GitHub path history shows repeated work on Discord outbound reply threading and shared reply fanout policy that define the affected behavior. (role: feature-history owner; confidence: high; commits: 5e640b93dae6, 7ef4ecf499ce, 1bdf5307d99d; files: extensions/discord/src/outbound-payload.ts, src/infra/outbound/reply-policy.ts, src/plugin-sdk/reply-payload.ts)
  • vincentkoc: Recent history shows a Discord outbound payload-options refactor on the central touched file, and local blame for the current shallow snapshot points to an SDK baseline refresh by this author. (role: recent area contributor; confidence: medium; commits: 94df665cdcc9, 9514faca2721; files: extensions/discord/src/outbound-payload.ts, extensions/discord/src/outbound-send-context.ts, src/infra/outbound/reply-policy.ts)
  • joshavant: Recent history shows adjacent maintenance on the Discord voice send path used by the audioAsVoice branch. (role: adjacent owner; confidence: medium; commits: c5b987274ae9; files: extensions/discord/src/send.voice.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels Jun 23, 2026
@vincentkoc
vincentkoc force-pushed the fix/discord-audio-replyto-fanout branch from dc87507 to 8a78fa6 Compare June 25, 2026 08:25
@vincentkoc
vincentkoc merged commit 99f56cd into openclaw:main Jun 25, 2026
89 checks passed
@vincentkoc

Copy link
Copy Markdown
Member

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 26, 2026
* fix(discord): keep audio voice replies threaded

* chore: retrigger CI after base recovery
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
* fix(discord): keep audio voice replies threaded

* chore: retrigger CI after base recovery
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Discord audioAsVoice follow-up sends lose reply threading

2 participants