Skip to content

fix(discord): limit implicit reply fanout#100784

Merged
steipete merged 6 commits into
mainfrom
codex/fix-discord-reply-fanout
Jul 6, 2026
Merged

fix(discord): limit implicit reply fanout#100784
steipete merged 6 commits into
mainfrom
codex/fix-discord-reply-fanout

Conversation

@steipete

@steipete steipete commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Closes #99068. Supersedes #99150.

Discord replyToMode: "first" and "batched" could attach the same native message_reference to every physical chunk of one logical reply. Long text, media captions, video caption/file pairs, voice payload fallbacks, and component downgrades could therefore notify the referenced user multiple times.

Why This Change Was Made

The fix carries one source-aware replyToFirstChunkOnly decision from the Discord outbound policy boundary into every Discord-owned physical send path. Implicit single-use replies attach the reference once; all, direct sends without a single-use mode, and explicit reply tags remain reusable.

Maintainer review added the missing split-video matrix, aligned omitted replyToIdSource with the existing createReplyToFanout contract, and removed unused voice plumbing. It also replaced the 14-20 argument text/media helpers with named options, carried the actual per-message reply target into delivery progress, and made mixed adapter receipt metadata authoritative during final core aggregation. This maintainer-owned replacement is necessary because GitHub's verified fork-edit mutation for the old contributor branch exceeded the 45 MB request limit after rebasing; the original contributor remains co-author and is credited here.

User Impact

One logical implicit Discord reply now produces one native reply notification even when Discord requires several physical messages. Explicit replies and replyToMode: "all" continue to attach native reply references to every intended message.

Evidence

  • Blacksmith Testbox-through-Crabbox tbx_01kwv79p9t30gce0ekr460d14h: 164 focused assertions across Discord outbound adapter, physical send, components, shared message planning, and Plugin SDK reply policy; a later exact patch run passed another 100 focused Discord/reply-policy assertions including upload fallback and per-message receipt coverage.
  • Same Testbox: extension production/test typechecks, extension lint, architecture guards, database-first guard, media helper guard, and runtime cycle check passed on the reviewed patch.
  • Worktree fallback after the replacement Testbox was reclaimed: src/channels/message/receipt.test.ts, 5/5 assertions, covering preservation of mixed nested reply metadata through final route aggregation.
  • AutoReview caught and drove fixes for positional callback shifts, omitted reply-source compatibility, and route-level receipt reapplication; final whole-branch result clean at 0.84 confidence.
  • Original PR live Discord HTTP-boundary proof showed implicit first-mode request references [source, null, null] and all-mode references [source, source, source] through real Discord sends.
  • git diff --check and targeted oxfmt passed.

Known proof boundary: the real Discord send was performed on the contributor implementation before the bounded maintainer follow-up fixes; exact HTTP-boundary, receipt, type, and lint checks cover the final branch, with the native hosted gate required before merge.

@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord size: L maintainer Maintainer-authored PR labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 970adb5856

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +206 to +209
replyToFirstChunkOnly: shouldReplyToFirstDiscordChunkOnly({
replyToIdSource,
replyToMode,
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Omit the flag when it is not enabled

When replyToMode is absent or the reply is not single-use, this expression still forwards replyToFirstChunkOnly: false to the injected Discord send dependency. Existing ordinary-send adapter proofs assert the exact option bag for non-threaded/non-first-mode sends (extensions/discord/src/channel.message-adapter.test.ts:94, :118, :196), so those paths now fail solely because an unrelated optional flag became observable; only include this property when it is true.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs changes before merge. Reviewed July 6, 2026, 6:04 AM ET / 10:04 UTC.

Summary
The PR carries a Discord first-chunk reply policy through outbound send paths and receipt aggregation so implicit first/batched replies attach a native Discord reference only to the first physical chunk.

PR surface: Source +105, Tests +185. Total +290 across 12 files.

Reproducibility: yes. by source inspection: current main passes the same replyTo into every Discord text chunk, and the linked issue includes live request-body evidence from v2026.6.11. I did not run a live Discord repro during this read-only review.

Review metrics: 1 noteworthy metric.

  • Discord send option surface: 1 added optional flag (replyToFirstChunkOnly). The new flag is the merge-sensitive contract point because false versus omitted is observable to injected send dependencies and exact adapter tests.

Stored data model
Persistent data-model change detected: vector/embedding metadata: src/channels/message/receipt.test.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #99068
Summary: This PR is the current maintainer-owned candidate fix for the canonical Discord implicit reply fanout issue; older/open contributor PRs target the same symptom with narrower or superseded branches.

Members:

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

Merge readiness
Overall: 🦐 gold shrimp
Proof: 🐚 platinum hermit
Patch quality: 🦐 gold shrimp
Result: needs maintainer review before merge.

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

Rank-up moves:

  • Omit replyToFirstChunkOnly unless it is true in all Discord outbound option bags.
  • Rerun the focused Discord adapter/send/receipt tests and refresh the failing exact-head checks.

Risk before merge

  • [P1] Merging before fixing the option construction makes ordinary Discord sends expose a new optional flag with value false, breaking existing exact option-bag proofs and injected send dependencies that treat omitted options as the compatibility contract.
  • [P1] The patch intentionally changes native Discord reply targeting and receipt metadata, so exact-head Discord send, adapter, and receipt proof should stay green before merge.

Maintainer options:

  1. Fix the optional flag before merge (recommended)
    Update all Discord outbound option bags so replyToFirstChunkOnly is present only when true, preserving the new fanout behavior without changing ordinary sends.
  2. Accept the observable false flag
    Maintainers could intentionally broaden the injected Discord send option contract, but that would require updating existing exact option-bag proofs and accepting the compatibility churn.
Copy recommended automerge instruction
@clawsweeper automerge

Special instructions:
Update the Discord outbound adapter option construction so `replyToFirstChunkOnly` is included only when true across text and media paths, keep the first-chunk behavior tests, and rerun focused Discord adapter/send/receipt tests plus the affected type/lint checks.

Next step before merge

  • [P2] Queue a narrow PR-branch repair because the remaining blocker is mechanical option construction, not a product decision.

Security
Cleared: The reviewed diff changes Discord message-routing code and tests without adding dependencies, workflow execution, secret handling, or new external code paths.

Review findings

  • [P2] Only include the first-chunk flag when enabled — extensions/discord/src/outbound-adapter.ts:206-209
Review details

Best possible solution:

Keep this as the canonical Discord fix, omit replyToFirstChunkOnly unless it is enabled, then land after focused Discord adapter/send/receipt tests and exact-head CI pass.

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

Yes by source inspection: current main passes the same replyTo into every Discord text chunk, and the linked issue includes live request-body evidence from v2026.6.11. I did not run a live Discord repro during this read-only review.

Is this the best way to solve the issue?

No, not yet. The owner-boundary fix shape is right, but the PR must omit the new optional flag when it is not enabled so ordinary send contracts and tests remain stable.

Full review comments:

  • [P2] Only include the first-chunk flag when enabled — extensions/discord/src/outbound-adapter.ts:206-209
    This forwards replyToFirstChunkOnly: false whenever replyToMode is absent or not single-use. Existing channel message adapter proofs assert exact option bags for ordinary text/media/threaded sends, so those paths now see an unrelated optional flag and fail; build the option bag so this property is present only when it is true across the text and media send calls.
    Confidence: 0.94

Overall correctness: patch is incorrect
Overall confidence: 0.88

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add P2: This is a normal-priority Discord delivery bug fix with limited blast radius and one focused merge blocker.
  • add merge-risk: 🚨 message-delivery: The PR changes which physical Discord messages receive native reply references and receipt reply metadata.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes live Discord HTTP-boundary output proving the central fanout behavior plus final-branch focused checks; it does not remove the separate code-review blocker.
  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🐚 platinum hermit and patch quality is 🦐 gold shrimp.
  • add status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (live_output): The PR body includes live Discord HTTP-boundary output proving the central fanout behavior plus final-branch focused checks; it does not remove the separate code-review blocker.

Label justifications:

  • P2: This is a normal-priority Discord delivery bug fix with limited blast radius and one focused merge blocker.
  • merge-risk: 🚨 message-delivery: The PR changes which physical Discord messages receive native reply references and receipt reply metadata.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🐚 platinum hermit and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (live_output): The PR body includes live Discord HTTP-boundary output proving the central fanout behavior plus final-branch focused checks; it does not remove the separate code-review blocker.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes live Discord HTTP-boundary output proving the central fanout behavior plus final-branch focused checks; it does not remove the separate code-review blocker.
Evidence reviewed

PR surface:

Source +105, Tests +185. Total +290 across 12 files.

View PR surface stats
Area Files Added Removed Net
Source 8 241 136 +105
Tests 4 196 11 +185
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 12 437 147 +290

Acceptance criteria:

  • [P1] pnpm test extensions/discord/src/channel.message-adapter.test.ts extensions/discord/src/outbound-adapter.test.ts extensions/discord/src/send.sends-basic-channel-messages.test.ts src/channels/message/receipt.test.ts --reporter=verbose.
  • [P1] pnpm check:changed -- extensions/discord/src/outbound-adapter.ts extensions/discord/src/channel.message-adapter.test.ts extensions/discord/src/outbound-adapter.test.ts extensions/discord/src/send.sends-basic-channel-messages.test.ts src/channels/message/receipt.ts src/channels/message/receipt.test.ts.

What I checked:

  • Current main fanout source: Current main builds every Discord text chunk with the closed-over replyTo, so each lower-level chunk becomes a native message_reference when replyTo is set. (extensions/discord/src/send.shared.ts:349, 3221d43d891c)
  • PR shared-send fix: The PR head computes a per-chunk reply target and suppresses replyTo after the first chunk when replyToFirstChunkOnly is enabled. (extensions/discord/src/send.shared.ts:359, 550af765af6e)
  • Blocking option-shape regression: The PR still forwards replyToFirstChunkOnly: false on ordinary sends, while existing adapter capability proofs assert exact option bags without that optional flag. (extensions/discord/src/outbound-adapter.ts:206, 550af765af6e)
  • Related issue and proof context: The linked issue reports OpenClaw v2026.6.11 sending repeated Discord message_reference values across chunks; the PR body includes live Discord HTTP-boundary proof for the source implementation and final-branch focused checks.
  • Feature history: GitHub commit history for the Discord send and channel receipt paths shows recent relevant work by steipete, zhangguiping-xydt, lin-hongkuan, and joshavant.

Likely related people:

  • steipete: Recent GitHub path history shows multiple Discord send/refactor commits in the affected send paths, and this PR is a maintainer-owned replacement for the related contributor branch. (role: recent area contributor; confidence: high; commits: 41810a462e3c, 827b0de0ce74, f0adbd48e8bc; files: extensions/discord/src/send.shared.ts, extensions/discord/src/send.outbound.ts, extensions/discord/src/outbound-adapter.ts)
  • zhangguiping-xydt: Recent path history includes outbound recovery and partial delivery evidence work that touched the same Discord send/receipt surfaces. (role: recent adjacent contributor; confidence: medium; commits: 741d9524bec2; files: extensions/discord/src/send.shared.ts, extensions/discord/src/send.outbound.ts, extensions/discord/src/outbound-adapter.ts)
  • lin-hongkuan: Recent Discord oversized-upload work is directly adjacent to this PR's upload fallback and media-caption paths. (role: recent area contributor; confidence: medium; commits: 9607c3a2bfa9; files: extensions/discord/src/send.shared.ts)
  • joshavant: GitHub path history shows prior Discord send-target fixes in the affected send.outbound path. (role: adjacent owner; confidence: medium; commits: c5b987274ae9; files: extensions/discord/src/send.outbound.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: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels Jul 6, 2026
@steipete

steipete commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Land-ready maintainer pass complete at 01e97e0d489a079bed0646fe0a9ce6aefa792f5d.

What changed:

  • replaced parallel reply id / first-chunk booleans with one closed { messageId, scope } model;
  • carried the scope through text, media, component, voice, action, retry, and fallback paths;
  • made delivery receipts record the concrete native reply per physical message, including voice sends;
  • updated exact adapter contract tests and removed contradictory option states.

Proof:

  • Testbox tbx_01kww4efb5z80kyshf6f97ckwf: 269 focused assertions passed;
  • Testbox tbx_01kww5351q0ywaeef2s0rewc26: final 106 focused assertions passed (Actions);
  • Testbox tbx_01kww560k9xg26sxmchmf4gndt: extension test typecheck passed (Actions);
  • Testbox tbx_01kww4r24jmqmcjep5tjfjw83c: extension production/test typechecks, full extension lint, database/media/sidecar/import-cycle guards passed (Actions);
  • fresh autoreview: no accepted/actionable findings;
  • live Discord API proof from this exact branch: implicit first emitted references [true, false], reusable all emitted [true, true], and serialized receipt parts matched both sends. All proof messages were deleted.

Per the maintainer request for this batch, I did not block landing on a new exact-head hosted CI completion; the focused, broad Testbox, type/lint, autoreview, and real Discord proofs above cover the changed surface.

@steipete
steipete merged commit bfb89d3 into main Jul 6, 2026
95 checks passed
@steipete
steipete deleted the codex/fix-discord-reply-fanout branch July 6, 2026 16:55
@steipete

steipete commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Merged via squash.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 01e97e0d48

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +205 to +206
const messageReference = params.opts.reply
? { message_id: params.opts.reply.messageId, fail_if_not_exists: false }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep SDK replyTo component edits working

When a caller uses the deprecated public openclaw/plugin-sdk/discord facade to edit a component message with replyTo, the facade type still exposes replyTo and forwards the options unchanged (src/plugin-sdk/discord.ts:63), but this path now only looks at opts.reply. Those calls silently stop adding message_reference and stop recording reply metadata; either translate the facade's replyTo to a DiscordReplyReference or keep accepting the legacy field here.

Useful? React with 👍 / 👎.

Comment on lines 65 to 69
fallbackChannelId: string;
kind: MessageReceiptPartKind;
threadId?: string | number;
replyToId?: string;
reply?: DiscordReplyReference;
}): DiscordSendResult {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve webhook reply receipts

After this parameter rename, the webhook send path was not migrated: sendWebhookMessageDiscord still calls createDiscordSendResult with the legacy replyToId field (extensions/discord/src/send.webhook.ts:151). In bound-thread webhook replies the Discord request still includes message_reference, but the returned receipt now drops replyToId, so durable metadata no longer matches the native reply that was sent.

Useful? React with 👍 / 👎.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 7, 2026
* fix(discord): limit implicit reply fanout

Co-authored-by: qingminlong <[email protected]>

* fix(discord): preserve oversized fallback fanout

* refactor(discord): make reply metadata per-message

* fix(discord): keep reply receipts serializable

* docs(changelog): defer Discord entry to release

* refactor(discord): model native reply fanout

---------

Co-authored-by: qingminlong <[email protected]>
giodl73-repo pushed a commit to giodl73-repo/openclaw that referenced this pull request Jul 8, 2026
* fix(discord): limit implicit reply fanout

Co-authored-by: qingminlong <[email protected]>

* fix(discord): preserve oversized fallback fanout

* refactor(discord): make reply metadata per-message

* fix(discord): keep reply receipts serializable

* docs(changelog): defer Discord entry to release

* refactor(discord): model native reply fanout

---------

Co-authored-by: qingminlong <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord maintainer Maintainer-authored PR merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: L status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Discord: replyToMode "first" — chunked reply sends the native reply reference on every physical chunk (reply-notification spam)

1 participant