feat(discord): default history backfill on, expand to per-user + threads

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.
This commit is contained in:
teknium1 2026-05-14 15:49:01 -07:00 committed by Teknium
parent e84fe483bc
commit 4abfb6bc24
5 changed files with 80 additions and 20 deletions

View file

@ -87,6 +87,12 @@ class FakeThread:
self.guild = getattr(parent, "guild", None) or SimpleNamespace(name=guild_name)
self.topic = None
def history(self, *, limit, before, after=None, oldest_first=None):
async def _iter():
return
yield
return _iter()
@pytest.fixture
def adapter(monkeypatch):
@ -820,7 +826,9 @@ async def test_discord_shared_channel_backfill_prepends_context(adapter, monkeyp
@pytest.mark.asyncio
async def test_discord_per_user_channel_does_not_backfill(adapter, monkeypatch):
async def test_discord_per_user_channel_backfills_too(adapter, monkeypatch):
"""Per-user sessions also benefit from backfill: Alice's session is missing
other-channel-participants' context and her own pre-mention messages."""
monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true")
monkeypatch.delenv("DISCORD_FREE_RESPONSE_CHANNELS", raising=False)
monkeypatch.setenv("DISCORD_AUTO_THREAD", "false")
@ -837,9 +845,42 @@ async def test_discord_per_user_channel_does_not_backfill(adapter, monkeypatch):
await adapter._handle_message(message)
adapter._fetch_channel_context.assert_not_awaited()
adapter._fetch_channel_context.assert_awaited_once()
event = adapter.handle_message.await_args.args[0]
assert event.text == "hello with mention"
assert event.channel_context is None
assert event.channel_context == "[Recent channel messages]\n[Alice] context"
@pytest.mark.asyncio
async def test_discord_dm_does_not_backfill(adapter, monkeypatch):
"""DMs skip backfill — every DM triggers the bot, so there's no mention gap."""
monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true")
adapter.config.extra["history_backfill"] = True
adapter._fetch_channel_context = AsyncMock(return_value="[Recent channel messages]\n[Alice] context")
bot_user = adapter._client.user
dm_channel = SimpleNamespace(
id=999,
name=None,
guild=None,
topic=None,
)
# Make isinstance(channel, discord.DMChannel) return True
monkeypatch.setattr(
discord_platform.discord, "DMChannel", type(dm_channel), raising=False,
)
message = make_message(
channel=dm_channel,
content="hello in DM",
mentions=[],
)
await adapter._handle_message(message)
adapter._fetch_channel_context.assert_not_awaited()
if adapter.handle_message.await_args is not None:
event = adapter.handle_message.await_args.args[0]
assert event.channel_context is None