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).