Files
hermes-agent/plugins
teknium1 8c6881231a fix(discord): backfill stops re-dispatching a message once its turn delivered, and bounds the rest
Three defects let the missed-message backfill re-run the same inbound message on
every reconnect (24 turns in 12 h for one message on the reporter's install):

- Completion was only ever recorded by the send-path ledger writer, keyed on the
  Discord reply anchor. With reply_to_mode "off", a streamed final delivered via
  edit_message(finalize=True), a fresh-final send or a media-only reply there is no
  anchor, so the row stayed processed/replied=0 and _should_backfill_discord_message
  re-admitted it forever. The adapter already learns the outcome per inbound id in
  _record_discord_processing_complete: on SUCCESS (base confirmed delivery, or the
  stream already delivered) it now writes status='responded', replied=1.
- The scan wrote status='discovered' over every candidate at the top of each pass,
  erasing the queued/processing claim so the 10-minute active-claim guard could never
  fire; two back-to-back scans dispatched the same message twice. The 'discovered'
  write no longer overwrites an existing row's status or updated_at.
- A stored per-channel cursor REPLACED the window floor (12-hour-old messages stayed
  candidates under window_seconds: 3600) and the parent's cursor object was passed
  down to child threads. after = max(cursor, now - window) and threads only see the
  window floor plus their own cursor.
- max_dispatches caps one scan, not one row. New missed_message_backfill.max_attempts
  (default 3; env DISCORD_MISSED_MESSAGE_BACKFILL_MAX_ATTEMPTS) is a lifetime ceiling
  on re-dispatch of a single message; `attempts` now counts dispatches only.

Live repro (real DiscordAdapter, temp HERMES_HOME ledger, fake client yielding the
same message per scan, fresh adapter per scan = reconnect): streamed final with
reply_to_mode=off 3 scans -> before 3 dispatches, after 1; turn that never completes
3 scans -> 3/1; turn that always fails 6 scans -> 6/3; 12h-old cursor with a 1h window
-> before 2 out-of-window dispatches (parent + thread), after 0; control: an
unanswered message still dispatches exactly once, and a cursor newer than the window
still narrows the scan.

Part of #113631 (with the salvaged anchor fallback from #113633 by @KoNit-K).
2026-09-18 09:44:59 -07:00
..