Conversation
The missed-message backfill admitted recovered messages with
claim=False, so a successfully backfilled message never entered the
dedup cache. Discord replays missed events on a successful resume —
and on_ready, which starts the backfill, fires after resumes too — so
a live replay of the same message raced or followed the REST-scan
dispatch and was admitted again: two agent runs, two replies for one
user message. The adapter's own discard() calls on the failure paths
('Release a claimed message ID') were no-ops, proving the claim was
intended. Admit with claim=True; whichever path (live or backfill)
claims first now wins and the other drops the duplicate.
|
Thanks for the focused regression fix. The premise is verified on current main: recovered dispatch calls The proposed Automated hermes-sweeper review. |
What does this PR do?
Fixes duplicate processing of Discord messages after a reconnect: the missed-message backfill never claims the dedup ID, so a message it recovers can be dispatched a second time by the live gateway.
Two Discord behaviors make this fire in practice: the gateway replays missed events on a successful resume, and
on_ready— which kicks off the missed-message backfill — fires after resumes too. The backfill scans channel history over REST and dispatches what it finds; the replayed live event for the same message races or follows it.The backfill's dispatch (
_dispatch_recovered_message) admitted messages withclaim=False, so:contains()check and the admission check only ever looked — the recovered message never entered the dedup cache;claim=Trueadmission) and dispatched again — two agent runs, two replies for one user message;discard()calls ("Release a claimed message ID") were no-ops, proving the claim was always intended.The fix admits recovered messages with
claim=True. Whichever path — live ingress or backfill — claims the ID first wins; the other drops the duplicate. The existingdiscard()calls on the cancel/error paths now correctly release the claim so a later retry can proceed.Related Issue
No GitHub issue — discovered via code review and reproduced live (see below). Happy to file one first if preferred.
Type of Change
Changes Made
plugins/platforms/discord/adapter.py:_dispatch_recovered_messageadmits withclaim=True(with a comment explaining why); the scan-loop comment updated to describe the actual claim ownership (cheap pre-filter at scan time, atomic claim at dispatch).tests/gateway/test_discord_missed_message_backfill.py: three regression tests — a backfilled message enters the dedup cache and a subsequent live admission of the same message is dropped; a second backfill dispatch of the same message is a no-op; a dispatch that raises releases the claim (via the pre-existingdiscard()error path) so a retry dispatches and stays claimed.Behavior notes:
_discord_message_admissionclaims before applying bot/self filters, so a backfill message rejected on policy grounds now consumes the ID. Both ingress paths use the identical admission function, so there is no divergence between live and backfill policy.MessageDeduplicatordefault): a live replay arriving more than 5 minutes after a backfill dispatch would be admitted. Resume replays arrive within seconds of reconnect, so this boundary doesn't affect the reported symptom.How to Test
Reproduction (against pre-fix code, using the existing test fixtures):
Focused validation completed:
bash scripts/run_tests.sh tests/gateway/test_discord_missed_message_backfill.py— 18/18 pass.mainmakes exactly the 3 new tests fail (15 pass); restoring it returns to 18/18.test_discord_double_dispatch.py test_discord_connect.py test_discord_send.py test_discord_free_response.py test_message_deduplicator.py test_duplicate_reply_suppression.py test_dedupe_user_turns.py+ the backfill file — 79/79 pass. (Note: the existingtest_discord_double_dispatch.pycovers the thread-starter pre-seed path, a different double-dispatch vector.)uvx --from ruff==0.15.10 ruff check plugins/platforms/discord/adapter.py tests/gateway/test_discord_missed_message_backfill.py— clean.git diff --check— clean._handle_messageinvoked exactly once across all three; a dispatch raising mid-loop releases the claim via the pre-existingdiscard()error path and a retry then dispatches and stays claimed (driven through the real_run_missed_message_backfillloop).The full repo-wide suite was not run for this change; the Discord/dedup suites above cover the changed path, and GitHub CI owns full-suite validation.
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — full suite not run locally (see How to Test; all Discord/dedup suites pass)Documentation & Housekeeping
docs/, docstrings) — or N/A (inline comments updated to match the new claim ownership)cli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/ALogs
Sabotage verification output: