Skip to content

feat(discord): channel history backfill — default on, broadened scope#25984

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-3587b63a
May 14, 2026
Merged

feat(discord): channel history backfill — default on, broadened scope#25984
teknium1 merged 2 commits into
mainfrom
hermes/hermes-3587b63a

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Salvages and extends @simpolism's #25463 (closes #13054). Cherry-picks the original feature commit with authorship preserved, then flips the default to on and removes the shared-session gate so backfill fires anywhere require_mention creates a transcript gap.

Summary

Discord history backfill is now on by default and fires for both shared and per-user sessions. In all cases the bot prepends a [Recent channel messages] block to the trigger so the agent sees what it missed.

Changes on top of #25463

  • hermes_cli/config.py: discord.history_backfill default → true
  • gateway/platforms/discord.py: drop the _is_shared gate; keep the DM skip and _needed_mention gate; env var default → true
  • cli-config.yaml.example + website/docs/user-guide/messaging/discord.md: update defaults and prose; add the DISCORD_HISTORY_BACKFILL / _LIMIT env-var rows that the original PR description mentioned but didn't include
  • Tests: flip test_discord_per_user_channel_does_not_backfilltest_discord_per_user_channel_backfills_too (new behavior), add test_discord_dm_does_not_backfill (DM-skip is invariant), give FakeThread a no-op history() so existing thread tests don't hit a fake discord.Forbidden now that backfill fires on threads

Behavior by surface

Surface Behavior
Server channel + require_mention: true Backfill scans channel since bot's last response
Thread Backfill scans thread only (Discord API limits thread.history() to that thread)
DM Skipped — every DM triggers the bot, no gap
Free-response channel / bot's auto-thread Skipped — no mention gating, no gap

Per-user sessions (the default) benefit too: each user's session is still missing other participants' context and their own pre-mention messages — backfill fills both.

Validation

Targeted tests:

tests/gateway/test_discord_free_response.py
tests/gateway/test_config.py
tests/gateway/test_session.py
→ 160/160 pass

Broader sanity:

tests/gateway/ -k discord
→ 400/400 pass

Closes #13054
Supersedes #25463 (#13063, #13120 referenced as prior art)
Co-authored-by: @simpolism

simpolism and others added 2 commits May 14, 2026 15:43
Adds optional channel-context backfill for Discord shared-channel sessions
so the agent can see recent messages it missed between its own turns
(typically when require_mention=true filters out most traffic).

Previously the agent only saw the @mention message that triggered it, which
led to disorienting replies in active multi-user channels where the
conversation context was invisible. With backfill enabled, a configurable
number of recent messages are fetched per-turn and prepended to the trigger
message as a context block, kept separate from sender-prefix logic so
attribution remains clean.

This re-opens the work from #13063 (approved by @OutThisLife on 2026-04-20,
closed when I closed the branch to address the simpolism:main head-branch
issue plus an ordering bug I caught later in live use). Filing against the
freshly-rewritten problem statement in #13054 so the design is grounded in
the failure mode rather than the implementation shape.

The implementation follows the **push-mode last-self-anchored** design from
the two options laid out in #13054. See the issue for the trade-off
discussion vs pull-mode (#13120 was an earlier closed PR using that shape).
Treating this as a reference implementation — happy to rewrite as
last-trigger anchoring or as a hybrid with #13120 if maintainers prefer.

Changes:

- gateway/platforms/discord.py:
  - new `_discord_history_backfill()` / `_discord_history_backfill_limit()`
    helpers (config.extra > env > default), mirroring the existing
    `_discord_require_mention()` shape
  - new `_fetch_channel_context()` that scans `channel.history()` backwards
    from the trigger to the bot's last message (or limit), formats as
    `[Recent channel messages] / [name] msg / ...`, respects DISCORD_ALLOW_BOTS,
    skips system messages
  - per-channel `_last_self_message_id` cache to narrow the fetch window
    on hot paths (avoids full history scan when the bot has spoken recently)
  - **IMPORTANT**: passes `oldest_first=False` explicitly to `channel.history()`.
    discord.py 2.x silently flips the default to True when `after=` is supplied,
    which would select the EARLIEST N messages after our last response instead
    of the LATEST N before the trigger. In high-traffic windows this would
    return stale tool traces and drop the actual final answer the user is
    asking about. See regression test below. Caught in live use during a
    Codex tool-trace burst on May 13 2026.
- gateway/config.py: discord_history_backfill + discord_history_backfill_limit
  settings + yaml→env bridge
- gateway/platforms/base.py: channel_context field on MessageEvent
- gateway/run.py: prepend channel_context after sender-prefix so the
  [sender name] tag applies to the trigger message alone, not to the backfill
- hermes_cli/config.py: defaults for new discord.history_backfill and
  discord.history_backfill_limit keys
- cli-config.yaml.example: documented defaults
- tests/gateway/test_discord_free_response.py: 7 new tests covering
  cold-start backfill, self-message stop boundary, other-bot filtering,
  cache hot-path narrowing, stale-cache fallback, shared-channel +
  per-user backfill paths, and the ordering regression test
  (`test_fetch_channel_context_cache_uses_latest_window_when_after_set`)
- tests/gateway/test_config.py: yaml→env bridge tests
- tests/gateway/test_session.py: prefix-order edge cases
- website/docs/user-guide/messaging/discord.md: env vars + config keys +
  usage docs

Tested on Ubuntu 24.04 — empirically validated in my own multi-bot Discord
research server for the past three weeks.

Fixes #13054
Supersedes #13063 (closed)
Follow-up to snav's PR #25463 contribution: flip default to on, broaden
scope so backfill fires whenever require_mention gates the bot (not just
shared-session channels).

Why:
- The mention-gate creates a session-transcript gap regardless of whether
  the channel is shared or per-user. In per-user sessions, Alice's session
  is still missing other participants' messages and her own pre-mention
  messages — backfill fills both gaps.
- Threads naturally scope to thread-only history because discord.py's
  channel.history() on a thread returns only that thread's messages.
- DMs still skip — every DM triggers the bot, so the session transcript
  is already complete.

Changes:
- hermes_cli/config.py: discord.history_backfill default → true
- gateway/platforms/discord.py: drop the _is_shared gate, keep _is_dm
  skip and _needed_mention gate; env var DISCORD_HISTORY_BACKFILL
  default → 'true'
- cli-config.yaml.example + website docs: update defaults and prose;
  add the DISCORD_HISTORY_BACKFILL / _LIMIT env var rows that were
  documented in the PR description but missing from the env-var table
- tests/gateway/test_discord_free_response.py:
  - flip test_discord_per_user_channel_does_not_backfill →
    test_discord_per_user_channel_backfills_too (new behavior)
  - add test_discord_dm_does_not_backfill (DM skip is invariant)
  - give FakeThread a no-op history() so existing thread tests don't hit
    a fake discord.Forbidden when backfill now fires on threads too

Tests: 160/160 in target files; 400/400 across all tests/gateway/ -k discord.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-3587b63a vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8374 on HEAD, 8367 on base (🆕 +7)

🆕 New issues (2):

Rule Count
unresolved-attribute 2
First entries
gateway/platforms/discord.py:3730: [unresolved-attribute] unresolved-attribute: Attribute `Forbidden` is not defined on `None` in union `Unknown | None`
gateway/platforms/discord.py:3677: [unresolved-attribute] unresolved-attribute: Attribute `Object` is not defined on `None` in union `Unknown | None`

✅ Fixed issues: none

Unchanged: 4406 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@teknium1 teknium1 merged commit 4abfb6b into main May 14, 2026
14 of 17 checks passed
@teknium1 teknium1 deleted the hermes/hermes-3587b63a branch May 14, 2026 22:50
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists platform/discord Discord bot adapter comp/gateway Gateway runner, session dispatch, delivery labels May 14, 2026
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 P2 Medium — degraded but workaround exists platform/discord Discord bot adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: send Discord history when require_mentions: true

3 participants