Skip to content

fix(discord): anchor handoff threads in parent channel - #63459

Open
SEStarkman wants to merge 3 commits into
NousResearch:mainfrom
SEStarkman:fix/discord-handoff-thread-anchor
Open

fix(discord): anchor handoff threads in parent channel#63459
SEStarkman wants to merge 3 commits into
NousResearch:mainfrom
SEStarkman:fix/discord-handoff-thread-anchor

Conversation

@SEStarkman

@SEStarkman SEStarkman commented Jul 12, 2026

Copy link
Copy Markdown

Summary

  • create Discord handoff threads from a visible parent-channel seed message
  • preserve direct channel-level thread creation as the fallback
  • persist successful handoff thread IDs in the participation tracker so mentionless replies continue when thread_require_mention=false
  • preserve mention gating when thread_require_mention=true, plus DM and failed-create behavior

Root causes

  1. create_handoff_thread() preferred parent.create_thread(), producing an anchorless public thread that Discord mobile did not expose reliably in the parent timeline.
  2. Both successful handoff creation paths returned without calling self._threads.mark(thread_id), so continuable cron threads were absent from discord_threads.json and mentionless replies were dropped before session routing.

Verification

  • TDD regression reproduced: 3 expected failures before the tracker fix (seed persistence, fallback persistence, mentionless continuation)
  • scripts/run_tests.sh tests/gateway/test_discord_send.py tests/e2e/test_discord_adapter.py -q (34 passed)
  • scripts/run_tests.sh tests/cron/test_scheduler.py tests/gateway/test_discord_*.py tests/e2e/test_discord_adapter.py -q (765 passed)
  • .venv/bin/ruff check plugins/platforms/discord/adapter.py tests/gateway/test_discord_send.py tests/e2e/test_discord_adapter.py
  • .venv/bin/python -m py_compile plugins/platforms/discord/adapter.py tests/gateway/test_discord_send.py tests/e2e/test_discord_adapter.py cron/scheduler.py
  • git diff --check
  • independent Codex review: no blockers

Deployment

No migration or config change. Restart the Hermes gateway after installing/checking out the merged version; this PR does not restart, update, merge, or deploy anything.

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 12, 2026

@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 addressing both the discoverability and continuation-state gaps. The current-main premise is verified: plugins/platforms/discord/adapter.py:5498-5507 creates an unanchored thread and :6212-6220 requires the id to be in _threads before permitting mentionless replies.

Problems

  • plugins/platforms/discord/adapter.py:5519 sends the visible seed before seed_msg.create_thread(). If that call fails, the direct fallback at :5540 succeeds but leaves the seed in the parent timeline even though it does not anchor the returned thread. The PR's own fallback test at tests/gateway/test_discord_send.py:545-572 exercises this exact sequence without checking cleanup.

Suggested changes

  • Keep the seed message and, after a successful direct fallback, best-effort delete it or revise it to make the unanchored fallback explicit. Add an assertion to the fallback test.

Automated hermes-sweeper review.

return None
seed_msg = await send(f"\U0001f9f5 Hermes handoff: **{thread_name}**")
thread = await seed_msg.create_thread(
thread = await create(

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.

If seed_msg.create_thread() failed after the send at line 5519, this direct fallback returns an unanchored thread while leaving a visible “Hermes handoff” seed that does not open it. Please retain the seed and best-effort delete it (or edit it to accurately describe the fallback) after direct creation succeeds; extend the fallback test to cover that outcome.

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.

Confirmed and fixed in 789076d25. The adapter now retains the seed, marks the successfully created direct-fallback thread, then best-effort deletes the stale seed. Discord cleanup refusal is debug-logged and cannot turn a successful fallback into failure.

Regression coverage now asserts both successful deletion and cleanup-failure/non-fatal behavior; the anchored path and participation tracking remain covered.

Verification:

  • scripts/run_tests.sh tests/gateway/test_discord_send.py tests/e2e/test_discord_adapter.py -q — 35 passed
  • scripts/run_tests.sh tests/cron/test_scheduler.py tests/gateway/test_discord_*.py tests/e2e/test_discord_adapter.py -q — 747 passed
  • .venv/bin/ruff check plugins/platforms/discord/adapter.py tests/gateway/test_discord_send.py tests/e2e/test_discord_adapter.py — passed
  • .venv/bin/python -m py_compile plugins/platforms/discord/adapter.py tests/gateway/test_discord_send.py tests/e2e/test_discord_adapter.py cron/scheduler.py — passed
  • git diff --check — passed

@Majber

Majber commented Aug 22, 2026

Copy link
Copy Markdown

The seed-message-first path is a good improvement, but the direct fallback still has a correctness gap.

discord.TextChannel.create_thread(..., message=None, type=None) defaults to ChannelType.private_thread in discord.py. The fallback currently calls:

thread = await create(
    name=thread_name,
    auto_archive_duration=1440,
    reason=reason,
)

Therefore, when the visible seed path fails but direct creation succeeds, Hermes can create a private thread instead of the intended public continuable thread. We reproduced this against the installed discord.py signature/source; type is None is normalized to ChannelType.private_thread.

Please make the direct fallback explicit:

thread = await create(
    name=thread_name,
    type=discord.ChannelType.public_thread,
    auto_archive_duration=1440,
    reason=reason,
)

Suggested regression assertion:

assert create_thread.await_args.kwargs["type"] is discord.ChannelType.public_thread

We currently carry this one-line fix locally; it applies cleanly to current origin/main and is covered by the focused Discord regression suite.

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-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants