Skip to content

fix(discord): split-and-deliver oversized edits instead of silent truncation (#27881) - #27961

Closed
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/27881-discord-edit-message-overflow
Closed

xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/27881-discord-edit-message-overflow

Conversation

@xxxigm

@xxxigm xxxigm commented May 18, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #27881 — Discord Gateway: Premature conversation turn termination during autonomous workflows (P1).

The reported symptom ("agent terminates mid-task, requires re-prompting") was actually a silent-truncation bug in DiscordAdapter.edit_message: when streaming (or tool-progress) edits grew past Discord's hard 2000-character cap, the adapter clipped the payload to MAX_MESSAGE_LENGTH - 3 chars plus "..." and returned SendResult(success=True). The gateway's stream consumer believed the full reply had been delivered, but everything past the truncation boundary was silently discarded. The user perceived the agent as stopping mid-task and had to re-prompt.

Telegram already gained the equivalent split-and-deliver fix in commit bf1f40996; Discord didn't. This PR ports that pattern.

Related Issue

Fixes #27881.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Security fix
  • Documentation update
  • Tests (adding or improving test coverage)
  • Refactor (no behavior change)
  • New skill (bundled or hub)

Changes Made

gateway/platforms/discord.py — DiscordAdapter.edit_message is now overflow-aware, with a new helper _edit_overflow_split:

  • Pre-flight check — when the formatted payload exceeds MAX_MESSAGE_LENGTH, route through _edit_overflow_split instead of issuing a doomed edit.
  • Reactive fallback — if Discord returns the documented error code: 50035 / Must be 2000 or fewer in length mid-edit (formatter inflation, server-side rule changes), the same split path runs on the failure side rather than treating overflow as a hard failure.
  • Split-and-deliver — _edit_overflow_split edits the original message with chunk 1 and sends each remaining chunk as a new channel.send threaded as a reply to the previous chunk so Discord groups the reply visually.
  • Return contract — success now reports the LAST visible message id in message_id (so subsequent streaming edits target the most recent chunk) and the existing SendResult.continuation_message_ids tuple lists every continuation in send order.
  • Partial delivery — if a mid-stream continuation send fails the helper still reports success with however many continuations landed. The stream consumer's next tick retries the tail; dropping chunks the user already saw would be the worse outcome.
  • _last_self_message_id cache — updated to the final visible chunk after a split so the history-backfill fast path stays consistent.

Backward compatibility: payloads ≤ 2000 chars take the original single-edit path unchanged. The existing tests/gateway/test_discord_*.py suite (113 tests across send, reply-mode, reactions, imports, system messages, free response) is green with this change.

tests/gateway/test_discord_edit_message_overflow.py (new file, 12 regression tests):

  • TestEditMessageHappyPath (2) — short content edits in place, no-client returns failure.
  • TestEditMessageOverflowIssue27881 (6) — direct repro: 6000-char payload splits, byte coverage preserved, final marker survives end-to-end, continuations are threaded as replies, no "..." truncation marker leaks into delivered chunks, first-chunk-edit failure propagates, mid-stream continuation failure reports partial success.
  • TestReactiveOverflowDetection (1) — Discord 50035 mid-edit triggers the split path.
  • TestEditOverflowSplitHelper (3) — direct helper tests for message_id-points-at-last-visible, _last_self_message_id cache update, single-chunk defensive call.

How to Test

  1. Check out the branch and set up the venv:

    python3 -m venv .venv && source .venv/bin/activate && pip install -e ".[all,dev]"
    
  2. Run the new regression suite:

    scripts/run_tests.sh tests/gateway/test_discord_edit_message_overflow.py -v
    

    Expected: 12 passed.

  3. Run the broader Discord suites my fix touches to confirm no cross-file regression:

    scripts/run_tests.sh tests/gateway/test_discord_send.py \
                         tests/gateway/test_discord_reply_mode.py \
                         tests/gateway/test_discord_reactions.py \
                         tests/gateway/test_discord_imports.py \
                         tests/gateway/test_discord_edit_message_overflow.py \
                         tests/gateway/test_discord_system_messages.py \
                         tests/gateway/test_discord_free_response.py
    

    Expected: 113 passed.

  4. (Optional) end-to-end against a real Discord bot: send a prompt that triggers a long streamed reply (e.g. "explain X in detail with a code example"). Pre-fix the bot would deliver one truncated message ending in "..."; post-fix you see the original message edited with chunk 1 (no "...") plus N continuation messages threaded as replies under it carrying the rest of the reply.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (2 commits: fix(discord), test(discord))
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run scripts/run_tests.sh tests/gateway/test_discord_edit_message_overflow.py and all tests pass
  • I've added tests for my changes (12 new regression tests)
  • I've tested on my platform: macOS 15.2 (Darwin 24.6.0), Python 3.12

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — the new edit_message and _edit_overflow_split docstrings document the contract; the production-code comment cites the Telegram fix commit (bf1f40996) and the issue number for future readers.
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A.
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A.
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — no platform-specific code paths.
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A.

Screenshots / Logs

$ scripts/run_tests.sh tests/gateway/test_discord_edit_message_overflow.py -v
4 workers [12 items]
............                                                             [100%]
============================== 12 passed in 1.25s ==============================

$ scripts/run_tests.sh tests/gateway/test_discord_send.py \
                       tests/gateway/test_discord_reply_mode.py \
                       tests/gateway/test_discord_reactions.py \
                       tests/gateway/test_discord_imports.py \
                       tests/gateway/test_discord_edit_message_overflow.py \
                       tests/gateway/test_discord_system_messages.py \
                       tests/gateway/test_discord_free_response.py
4 workers [113 items]
........................................................................ [ 63%]
.........................................                                [100%]
============================= 113 passed in 1.46s ==============================

Note on pre-existing failures

tests/gateway/test_discord_document_handling.py has 12 failing tests on the current main branch. They are unrelated to this PR (verified via git stash — same 12 failures with this branch's changes stashed away). Tracking that should be a separate issue if not already filed.

Root cause analysis summary

The bug report described a vague symptom ("turn terminates prematurely during autonomous workflows") with no specific reproduction. I traced through:

  • DiscordAdapter.edit_message (gateway/platforms/discord.py:1596-1619 pre-fix) — found the silent "..." truncation.
  • GatewayStreamConsumer (gateway/stream_consumer.py) — confirmed it uses edit_message for token-by-token streaming and tool-progress edits.
  • TelegramAdapter.edit_message (gateway/platforms/telegram.py:1700-1946) — confirmed Telegram already has the split-and-deliver fix for the same class of bug, including the _edit_overflow_split helper and continuation_message_ids tuple on SendResult.

The Discord-specific path was an obvious orphan: every other contributor to "turn ends mid-task" (heartbeat, websocket reconnect, processing-complete callback, reactions, timeouts) was either platform-agnostic or already correct. The silent-truncation path is the one place where Discord deviated from Telegram and would manifest exactly as described.

xxxigm added 2 commits May 18, 2026 18:46
…ncation

When ``DiscordAdapter.edit_message`` received content longer than the
2000-char Discord cap it clipped the payload to
``MAX_MESSAGE_LENGTH - 3`` chars plus ``"..."`` and returned
``SendResult(success=True)``.  The gateway stream consumer believed
the full reply had been delivered, but the tail was silently
discarded.  During autonomous multi-step Discord workflows the user
perceived the agent as terminating mid-task and had to re-prompt to
continue -- the exact P1 symptom in NousResearch#27881.

Telegram already had the equivalent fix (commit ``bf1f40996``); this
change ports the pattern to Discord:

  * Pre-flight check: when the formatted payload exceeds
    ``MAX_MESSAGE_LENGTH``, route through a new
    ``_edit_overflow_split`` helper instead of issuing a doomed
    edit.  The helper edits the original message with chunk 1 and
    sends each remaining chunk as a new ``channel.send`` threaded as
    a reply to the previous chunk so Discord groups them visually.
  * Reactive fallback: if Discord returns the documented 50035
    "Must be 2000 or fewer in length" error mid-edit (formatter
    inflation, server-side rule change), the same split path is
    taken on the failure side.
  * Return contract: success now reports the LAST visible message
    id in ``message_id`` (so subsequent streaming edits target the
    most recent chunk) and the new
    ``continuation_message_ids`` tuple lists every continuation in
    send order.  ``SendResult.continuation_message_ids`` already
    exists on the dataclass for the matching Telegram contract.
  * Partial delivery: if a mid-stream continuation send fails the
    helper still reports success with however many continuations
    landed -- the stream consumer's next tick can retry the tail.
    Dropping chunks the user already saw would be the worse
    outcome.
  * ``_last_self_message_id`` cache is updated to the final visible
    chunk so the history-backfill fast path stays consistent after
    a split.

Backward-compat: payloads <= 2000 chars take the original single-edit
path unchanged; the rendered TOML, return shape, and side-effects
match the pre-fix behaviour for this case (verified by the existing
``tests/gateway/test_discord_*.py`` suite).

Fixes NousResearch#27881.
…27881

Add ``tests/gateway/test_discord_edit_message_overflow.py`` -- 12
regression tests across four classes pinning the split-and-deliver
contract introduced by the production fix.

TestEditMessageHappyPath (2):
  * Content under MAX_MESSAGE_LENGTH edits in place untouched and
    returns no continuations.
  * No connected client -> graceful failure (no crash).

TestEditMessageOverflowIssue27881 (6) -- the direct NousResearch#27881
regression tests:
  * 6000-char payload splits into the original message + N
    continuations, success=True, error=None.
  * No tail loss: total delivered byte coverage >= input length, and
    the final marker survives end-to-end (the user-facing symptom in
    the bug report).
  * Every continuation is sent with a non-None ``reference`` so
    Discord renders the reply as a contiguous thread.
  * No silent ``"..."`` truncation marker appears in any delivered
    chunk (matches input that contains no ``"..."``).
  * First-chunk-edit failure for a non-overflow reason propagates as
    SendResult(success=False) -- the stream consumer needs to know.
  * Mid-stream continuation send failure returns success with the
    chunks that landed and a continuation count strictly less than
    the full split would have needed; the stream consumer's next
    tick retries the tail.

TestReactiveOverflowDetection (1):
  * Discord 50035 "Must be 2000 or fewer in length" error returned
    mid-edit triggers the split path instead of being treated as a
    hard failure (formatter inflation / future server-side rule
    change safety net).

TestEditOverflowSplitHelper (3) -- direct unit tests for the helper
without going through the full edit_message wrapper:
  * Single-chunk input (defensive call) still delivers.
  * Returned message_id always points at the LAST visible message
    (final continuation) so subsequent streaming edits target the
    most recent visible chunk.
  * The ``_last_self_message_id`` cache is updated to the final
    visible chunk so the history-backfill fast path stays
    consistent after a split.

All 12 new tests pass; the broader Discord suite (113 tests across
test_discord_send, test_discord_reply_mode, test_discord_reactions,
test_discord_imports, test_discord_system_messages,
test_discord_free_response, test_discord_edit_message_overflow) is
green.

Note: ``tests/gateway/test_discord_document_handling.py`` has 12
pre-existing failures on main that are unrelated to this PR (verified
via ``git stash``).
@BoardJames-Bot

Copy link
Copy Markdown

BoardJames triage: this looks shared/systemic rather than branch-local. The PR-specific checks (lint/nix/e2e/builds/attribution/history) are green where completed; the remaining blocker is the main Tests / test job, which is currently failing/timing out across unrelated PRs and on main itself (latest main run hit the same aux/session_search + kanban dashboard + compression/Anthropic test drift). I pushed the missing aux/session_search default fix onto the existing systemic fix PR #27931 and validated the affected files locally (125 passed). Next action is maintainer review/workflow approval/merge of #27931, then rerun this PR's Tests / test; no branch-local author action is indicated from the logs I can see.

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter duplicate This issue or pull request already exists P1 High — major feature broken, no workaround labels May 18, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

Duplicate of #23703 — same fix (split-and-deliver for Discord oversized edits), same approach. #23703 was triaged 2026-05-11 and is still open. This PR references #27881 (the P1 issue) but the implementation is identical to the earlier PR.

@felix-windsor felix-windsor 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.

Ran: ./scripts/run_tests.sh tests/gateway/test_discord_edit_message_overflow.py (12 passed).\n\nThe split-and-deliver approach matches Telegram's prior fix and seems like a plausible root cause for the 'stops mid-task' symptom in #27881 (silent truncation on >2000-char edits).\n\nNo further changes suggested from this quick pass.

@teknium1

teknium1 commented Jun 13, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks for the detailed fix and regression coverage. I verified the underlying bug still exists on current main, but this needs a few changes before it can be salvaged cleanly.

Problems

  • The production change targets gateway/platforms/discord.py, but Discord was migrated to the bundled plugin in cc8e5ec2a; current main’s adapter is plugins/platforms/discord/adapter.py, where the silent truncation still exists at plugins/platforms/discord/adapter.py:1796-1798.
  • The new test imports gateway.platforms.discord at PR head tests/gateway/test_discord_edit_message_overflow.py:81; current main’s Discord tests import plugins.platforms.discord.adapter (for example tests/gateway/test_discord_send.py:45).
  • Partial continuation failure currently returns success in the PR (gateway/platforms/discord.py:1778-1805 in the PR head). Current main’s stream consumer has a partial-overflow fallback contract (gateway/stream_consumer.py:1314-1341) used by Telegram, whose helper returns success=False plus raw_response["partial_overflow"] on continuation failure (gateway/platforms/telegram.py:2751-2780).

Suggested changes

  • Port the fix/tests to plugins/platforms/discord/adapter.py and plugin imports.
  • Mirror Telegram’s partial-overflow failure contract for continuation-send failures so the missing tail is delivered by fallback.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jun 29, 2026
@teknium1

Copy link
Copy Markdown
Collaborator

Fixed on main via #55592 (commit af5cea0). Your finding was correct — edit_message silently truncated oversized edits and returned success. The merged implementation gates the split on finalize=True (mid-stream it truncates a preview in place) to avoid the #48648 mid-stream re-split loop, which the original split-on-every-overflow approach predated. You're co-authored on the commit. Thanks for catching this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery duplicate This issue or pull request already exists P1 High — major feature broken, no workaround platform/discord Discord bot adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discord Gateway: Premature conversation turn termination during autonomous workflows

5 participants