Skip to content

feat(discord): add history_full_thread option to bypass self-message partition - #51414

Open
taras-polishchuk wants to merge 2 commits into
NousResearch:mainfrom
taras-polishchuk:feat/discord-full-thread-history
Open

feat(discord): add history_full_thread option to bypass self-message partition#51414
taras-polishchuk wants to merge 2 commits into
NousResearch:mainfrom
taras-polishchuk:feat/discord-full-thread-history

Conversation

@taras-polishchuk

Copy link
Copy Markdown

Summary

Adds an opt-in history_full_thread config / DISCORD_HISTORY_FULL_THREAD env var that disables the self-message partition-stop in _fetch_channel_context. When enabled, the bot walks the entire thread up to history_backfill_limit instead of stopping at its most recent reply, so the agent sees the full thread context on each trigger.

Default is unchanged (false) — preserves existing behaviour, prompt-cache layout, and token economics for all current users.

Motivation

Long-running investigation threads where the bot has replied multiple times. Today, after the bot's most recent reply, only the messages since that reply are surfaced to the agent on each trigger — earlier exchanges in the same thread are invisible, even though they're in the same Discord thread.

Use cases:

  • Resuming a multi-day thread without losing context the user previously shared
  • Threads where multiple humans + bot are collaborating and the user wants the bot to see the whole discussion
  • Operator debugging — quickly inspect the full state of a busy thread

Trade-off

Messages already in the session transcript are duplicated in the prompt. Token cost scales linearly with the active window. history_backfill_limit (default 50) bounds the worst case — set to 200 in the operator's deployment.

Configuration

discord:
  history_backfill_limit: 200
  history_full_thread: true   # NEW — opt-in

Or via env: DISCORD_HISTORY_FULL_THREAD=1

Tests

  • 4 new cases in tests/gateway/test_discord_free_response.py:
    • test_fetch_channel_context_full_thread_walks_past_self_messages — happy path, full thread traversed in chronological order
    • test_fetch_channel_context_full_thread_default_is_partition_moderegression guard: default must remain partition mode (no silent-on-by-default change)
    • test_fetch_channel_context_full_thread_env_var_overrides_default — runtime toggle via env without config edit
    • test_fetch_channel_context_full_thread_respects_limithistory_backfill_limit cap honoured in the new mode
  • Existing 8 fetch-context tests still pass (no regression in partition mode)
  • 94/94 Discord adapter tests pass overall

Risk

Low. Feature is opt-in, defaults preserved, existing prompt-cache layout unchanged for the 99% of users who don't flip the flag. The new code path is one boolean short-circuit on an existing break-statement.

Diff: +44 -3 lines in adapter.py, +147 -1 lines in tests.

@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jun 23, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Add history_full_thread option to bypass self-message partition.

Looks Good

  • Clean feature addition with clear documentation
  • Config-driven (extra config or env var)
  • Properly handles the trade-off (more context vs token cost)
  • Default preserves existing behavior (False)
  • Well-structured with clear precedence rules

Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused Discord context option. The underlying limitation still exists on current main (plugins/platforms/discord/adapter.py:5133-5138), but two changes are needed before this implements the requested behavior.

Problems

  • The full-thread branch only bypasses the local break (adapter.py:4431 on this PR), while the existing hot-path cache still passes after=_last_self_message_id into channel.history() (adapter.py:4350-4354, 4410). Since normal sends populate that cache (adapter.py:2092 on current main), older messages are excluded before the loop can see them.
  • The PR advertises discord.history_full_thread, but changes neither the YAML bridge nor docs. The existing bridge covers only history_backfill and history_backfill_limit (adapter.py:8313-8317 on current main), so the documented YAML key will not activate this code path.

Suggested changes

  • Disable the after cache boundary in full-thread mode and add a cached-path regression test.
  • Add the config default, YAML bridge, and Discord documentation; keep configuration user-facing through config.yaml.

Automated hermes-sweeper review.

# Skip this stop when full_thread mode is enabled — the user
# wants the entire thread context, even messages preceding
# prior bot replies.
if not full_thread and msg.author == self._client.user:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

full_thread still uses the earlier _last_self_message_id cache as channel.history(after=...) (lines 4350-4354 and 4410). On the usual hot path, messages before the last bot response are therefore excluded before this condition runs. Disable that cache boundary in full-thread mode and add a regression test that seeds _last_self_message_id.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid catch — thanks. The local break-skip on its own was a no-op on the hot path: by the time control reached the loop, channel.history() had already been narrowed by after=self._last_self_message_id[...] (adapter.py:4350-4354 and 4410), so the messages I wanted to walk past were filtered out before the partition check ever ran. Cold start works as advertised, hot path didn't.

Fixed in this push:

  • Moved the bypass to the cache-construction site: in _fetch_channel_context, the _after_obj assignment is now wrapped in if not full_thread: so the cache is only consulted in partition mode. Same shape as the existing if not full_thread and msg.author == self._client.user: break guard, just applied at the right layer.
  • Added test_fetch_channel_context_full_thread_ignores_last_self_cache in tests/gateway/test_discord_free_response.py — seeds _last_self_message_id["888"] = "150" and asserts channel.history() is called with after=None while history_full_thread: True. Without the fix the recorded after is a discord.Object(id=150).
  • Added a counterpart test_fetch_channel_context_partition_mode_still_uses_cache so a future refactor that disables the cache globally (in either direction) gets caught — partition mode keeps the optimisation intact.

Env var fallback (DISCORD_HISTORY_FULL_THREAD) is kept as the ops-escape hatch, with explicit env-var precedence over YAML in the bridge — same convention as DISCORD_HISTORY_BACKFILL and friends.

Ready for re-review on this thread.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-ping after rebase: this concern is now resolved at the new location of the cache-construction guard. See commit 477e3f01a6 (rebased as feat(discord): add history_full_thread option) which still wraps the _after_obj assignment in if not full_thread:. The regression test test_fetch_channel_context_full_thread_ignores_last_self_cache (still passing on the rebased branch) seeds _last_self_message_id and asserts the cache is bypassed in full-thread mode. Re-requesting your re-review on the rebased head 723212debd.

in the session transcript are duplicated in the prompt. Token cost
scales linearly with the active window.
"""
configured = self.config.extra.get("history_full_thread")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR's documented discord.history_full_thread key is not seeded into PlatformConfig.extra: this PR does not update _apply_yaml_config, DEFAULT_CONFIG, or the Discord docs. Wire the YAML key through the existing config bridge; behavioral settings must be user-facing in config.yaml rather than only in a new environment variable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right — the docstring on _discord_history_full_thread advertised config.extra["history_full_thread"] but the YAML→env bridge in _apply_yaml_config only knew about history_backfill and history_backfill_limit. Setting the documented YAML key was a dead letter.

Fixed in this push:

  • Wired discord.history_full_thread through _apply_yaml_config (adapter.py, alongside the existing history_backfill_limit block) — same not os.getenv(...) precedence guard as the neighbouring keys, so env var still wins over YAML when set.
  • Added the default to hermes_cli/config.py DEFAULT_CONFIG["discord"] ("history_full_thread": False) so hermes config show / hermes setup reflect it.
  • Added regression coverage in tests/gateway/test_config.py:
    • test_bridges_discord_history_full_thread_from_config_yaml — happy path.
    • test_history_full_thread_yaml_bridge_respects_existing_env_var — env-var precedence contract.
    • test_history_full_thread_yaml_bridge_accepts_false_value — explicit false propagates instead of being silently dropped.
  • Documented at website/docs/user-guide/messaging/discord.md (and the zh-Hans translation): new #### discord.history_full_thread section with the type, default, YAML example, trade-offs (token cost + cache bypass), and the env-var override. Quick-block at the top of the file's discord: snippet also lists the key.
  • cli-config.yaml.example got the matching commented line in the platform block.

The PR's headline behaviour is unchanged — history_full_thread defaults to False, partition mode stays as-is, env var remains a fallback — but the YAML key is now actually load-bearing.

Re-review on both threads welcome.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-ping after rebase: this concern is now resolved — the YAML wiring the previous version added is preserved through the rebase. history_full_thread is still seeded into DEFAULT_CONFIG in hermes_cli/config.py, threaded through _apply_yaml_config in plugins/platforms/discord/adapter.py, and documented in both website/docs/user-guide/messaging/discord.md and the ZH translation. Re-requesting your re-review on head 723212debd.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 15, 2026
…partition

Adds an opt-in `history_full_thread` config / DISCORD_HISTORY_FULL_THREAD
env var that disables the self-message partition-stop in
`_fetch_channel_context`. When enabled, the bot walks the entire thread
up to `history_backfill_limit` instead of stopping at its most recent
reply, so the agent sees the full thread context on each trigger.

Default is unchanged (`false`) — preserves existing behaviour and
prompt-cache layout for all current users.

Use case: long-running investigation threads where the bot has already
replied multiple times. Without this flag, the user only sees messages
since the most recent bot reply; with it, the user sees the full thread
including earlier exchanges.

Trade-off: messages already in the session transcript are duplicated in
the prompt. Token cost scales linearly with the active window. The
`history_backfill_limit` cap (default 50, raised to 200 in this PR's
deployment) bounds the worst case.

Tests: 4 new cases in test_discord_free_response.py covering the new
mode, the default-still-partitioned regression, env-var override, and
limit enforcement. 94/94 Discord adapter tests pass.
…ast-self cache

Resolves review feedback on NousResearch#51414 from teknium1.

Two changes, both required before the feature implements its advertised
behaviour:

1. _last_self_message_id cache bypass on hot path.

   The previous patch only skipped the in-loop partition break, so on the
   usual send path channel.history() had already been narrowed by
   after=self._last_self_message_id[...] before the loop ran.  The full
   thread was silently filtered before the partition check could apply.

   Move the guard to the cache-construction site: when history_full_thread
   is on, the _after_obj stays None and channel.history() gets the cold-
   start scan.  Partition mode is unchanged.

   test_fetch_channel_context_full_thread_ignores_last_self_cache seeds
   _last_self_message_id with a real value and asserts channel.history()
   is called with after=None while history_full_thread is on.  A counter-
   test guards the partition path so a future refactor can't silently
   disable the cache globally.

2. YAML -> env bridge for discord.history_full_thread.

   The PR's docstring advertised config.extra['history_full_thread'] but
   _apply_yaml_config only knew history_backfill and history_backfill_limit.
   Users who set the documented YAML key got a dead letter.  Wire the
   key through with the same not os.getenv(...) precedence guard used by
   the neighbouring keys; env var remains a fallback for ops toggles.

   Defaults to False in hermes_cli/config.py DEFAULT_CONFIG so
   hermes config show reflects the new key.  Regression coverage in
   tests/gateway/test_config.py covers the bridge, env-var precedence,
   and explicit false propagation.

   website/docs/user-guide/messaging/discord.md (and the zh-Hans
   translation) document the new key, the trade-offs (token cost,
   cache bypass), and the env-var escape hatch.  cli-config.yaml.example
   lists the commented key in the platform block.

Behaviour preserved: history_full_thread still defaults to False; partition
mode is untouched.  All 121 tests in test_discord_free_response.py and
test_config.py pass.
@taras-polishchuk
taras-polishchuk force-pushed the feat/discord-full-thread-history branch from 03af2fe to 723212d Compare July 28, 2026 20:29
@taras-polishchuk

Copy link
Copy Markdown
Author

Rebase complete — ready for re-review

Rebased onto current main (c8cdeb4 — 6,249 commits since original branch point).

Conflict resolution (3 files)

main added a discord.missed_message_backfill config/docs block between the lines the PR touches. Resolved by keeping both blocks — neither was a real conflict, just an adjacent insertion.

  • hermes_cli/config.py — both missed_message_backfill (main) and history_full_thread (PR) blocks kept.
  • website/docs/user-guide/messaging/discord.md — both docs sections kept.
  • …/i18n/zh-Hans/…/discord.md — both docs sections kept.

plugins/platforms/discord/adapter.py (the actually-changed file) auto-merged cleanly — the structural drift in main did not collide on the PR lines.

Identity fix

The original feat(discord) commit on this branch was authored as Hermes Agent (Taras) <hermes@local> — a bypass pattern from a previous session. Rebuilt that one commit's author with git commit --amend --reset-author --no-edit so the tree is unchanged but author/committer now match the rest of the PR.

723212debd  Taras Polishchuk <poli.taras.shchuk@gmail.com>  fix(discord): wire history_full_thread through YAML bridge + bypass last-self cache
477e3f01a6  Taras Polishchuk <poli.taras.shchuk@gmail.com>  feat(discord): add history_full_thread option to bypass self-message partition

Both SHAs are new (re-written commits); the previous 03af2fed… and 197872dd… are preserved in backup refs of the fork only.

Tests on rebased branch

65/65 in tests/gateway/test_discord_free_response.py
154/154 in tests/gateway/test_config.py
219/219 combined

Including the 6 PR-introduced tests:
- test_fetch_channel_context_full_thread_walks_past_self_messages
- test_fetch_channel_context_full_thread_default_is_partition_mode
- test_fetch_channel_context_full_thread_env_var_overrides_default
- test_fetch_channel_context_full_thread_respects_limit
- test_fetch_channel_context_full_thread_ignores_last_self_cache
- test_fetch_channel_context_partition_mode_still_uses_cache

mergeable=MERGEABLE. tonydwb's earlier APPROVED was on the pre-rebase commits — requesting re-review below. Also asking teknium1 to re-confirm given his earlier comments and the simplify-to-resolve here.

— Taras

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 P3 Low — cosmetic, nice to have platform/discord Discord bot adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants