Conversation
…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 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 |
felix-windsor
left a comment
There was a problem hiding this comment.
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.
|
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
Suggested changes
Automated hermes-sweeper review. |
|
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. |
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 toMAX_MESSAGE_LENGTH - 3chars plus"..."and returnedSendResult(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
Changes Made
gateway/platforms/discord.py—DiscordAdapter.edit_messageis now overflow-aware, with a new helper_edit_overflow_split:MAX_MESSAGE_LENGTH, route through_edit_overflow_splitinstead of issuing a doomed edit.error code: 50035 / Must be 2000 or fewer in lengthmid-edit (formatter inflation, server-side rule changes), the same split path runs on the failure side rather than treating overflow as a hard failure._edit_overflow_splitedits the original message with chunk 1 and sends each remaining chunk as a newchannel.sendthreaded as a reply to the previous chunk so Discord groups the reply visually.message_id(so subsequent streaming edits target the most recent chunk) and the existingSendResult.continuation_message_idstuple lists every continuation in send order._last_self_message_idcache — 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_*.pysuite (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 formessage_id-points-at-last-visible,_last_self_message_idcache update, single-chunk defensive call.How to Test
Check out the branch and set up the venv:
Run the new regression suite:
Expected: 12 passed.
Run the broader Discord suites my fix touches to confirm no cross-file regression:
Expected: 113 passed.
(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
fix(discord),test(discord))scripts/run_tests.sh tests/gateway/test_discord_edit_message_overflow.pyand all tests passDocumentation & Housekeeping
docs/, docstrings) — the newedit_messageand_edit_overflow_splitdocstrings document the contract; the production-code comment cites the Telegram fix commit (bf1f40996) and the issue number for future readers.cli-config.yaml.exampleif I added/changed config keys — N/A.CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A.Screenshots / Logs
Note on pre-existing failures
tests/gateway/test_discord_document_handling.pyhas 12 failing tests on the currentmainbranch. They are unrelated to this PR (verified viagit 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-1619pre-fix) — found the silent"..."truncation.GatewayStreamConsumer(gateway/stream_consumer.py) — confirmed it usesedit_messagefor 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_splithelper andcontinuation_message_idstuple onSendResult.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.