feat(discord): outbound @Name → <@id> via config.yaml (re-scope of #69206) + forum path - #75633
feat(discord): outbound @Name → <@id> via config.yaml (re-scope of #69206) + forum path#75633mehmetkr-31 wants to merge 3 commits into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for carrying forward the prior runtime work and for covering the forum early-return path.
Problems
- The resolver iterates
guild.membersatplugins/platforms/discord/adapter.py:5156, but startup requests the privileged member intent only for named allowlist entries or role authorization (adapter.py:1185-1193). Turning on the new setting alone does not ensure the member cache can resolve the target name. - Resolution is added to
send()(adapter.py:2943) but not toedit_message(), which edits raw formatted content atadapter.py:3207-3241; overflow continuations also send raw chunks atadapter.py:3317-3347. Streaming/edit delivery remains outside the feature. - The re-scope says the env bridge is private, but the added default-config comment advertises
DISCORD_RESOLVE_MENTIONSas an env override (hermes_cli/config_defaults.py:1884-1885) and the new test preserves that public override (tests/gateway/test_discord_outbound_mentions.py:121-128).
Suggested changes
- Gate member-intent setup on this opt-in and document/test the portal prerequisite.
- Cover
edit_message()and overflow continuations. - Keep the compatibility bridge private or obtain maintainer direction for exposing the new env override.
Automated hermes-sweeper review.
| return content | ||
| pairs = [] | ||
| seen = set() | ||
| for member in (getattr(guild, "members", None) or []): |
There was a problem hiding this comment.
guild.members is not reliably populated unless the privileged members intent is requested. The startup condition currently enables that intent only for named allowlist entries or role authorization (adapter.py:1185-1193); include this opt-in in that condition and document/test the Discord Portal requirement.
| # a user or another bot by name; a bare "@Name" from an LLM is otherwise | ||
| # inert text. Done before the forum branch so a forum thread's starter | ||
| # post gets the same treatment as an ordinary channel message. | ||
| content = await self._resolve_outbound_mentions(content, channel) |
There was a problem hiding this comment.
Please cover the corresponding outbound edit path as well: edit_message() formats and calls msg.edit() without resolving names (adapter.py:3207-3241), and its overflow helper sends continuation chunks directly. Streaming responses can otherwise still deliver inert @Name text.
| # the guild's own members, so the person or bot is actually pinged. A | ||
| # model reliably writes "@Name" rather than the raw <@id> Discord needs, | ||
| # which otherwise renders as inert plain text. @everyone and roles stay | ||
| # governed by the existing allowed_mentions safe defaults. Env override: |
There was a problem hiding this comment.
The predecessor review requested a config.yaml setting with any compatibility bridge kept private. Advertising DISCORD_RESOLVE_MENTIONS here, together with the new explicit-env-wins test, makes this a new user-facing non-secret env override rather than a private implementation bridge.
Related: #69206 is the closed predecessor. This re-scope keeps the resolver opt-in through |
5a80aa6 to
1c7c1ce
Compare
|
All three are fair. Fixed in 1.
|
| reverted | result |
|---|---|
edit_message() resolution |
2 failed — final streamed edit keeps the raw @Name |
| resolution moved to every edit | 1 failed — a partial name was resolved mid-stream |
| the members-intent clause | 1 failed |
| nothing | 22 passed |
tests/gateway/: 16 failures on this branch, 16 on a pristine origin/main worktree at the same base — identical set. @tieubao's two commits are still carried unsquashed.
20a22f1 to
6b4650f
Compare
SummaryOne PR addresses #69203. #75633 adds opt-in outbound Discord @Name-to-<@id> resolution across ordinary sends, forum starter posts, finalized streaming edits, and overflow continuations, while wiring the required member intent and documenting its Discord Portal prerequisite. Related pull requests
Suggested consolidationKeep #75633 open with a salvage path: preserve its opt-in resolver, standard/forum/final-edit/overflow call-site coverage, member-intent gating, documentation, and regression tests, then obtain contributor re-review confirming that the concerns in the existing keep_open review are resolved. There are no competing PRs to close as duplicates. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I69203(["issue #69203 (open)"])
P75633["PR #75633 (open)"]
P75633 -->|best fix| I69203
class I69203 open
class P75633 open
class P75633 best
class P75633 target
click I69203 "https://github.com/NousResearch/hermes-agent/issues/69203"
click P75633 "https://github.com/NousResearch/hermes-agent/pull/75633"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 1 pull request and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 27 kB of PR diffs, 5 kB of issue/PR text, 5 kB of discussion (6 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
An LLM writes @name rather than the raw <@id> Discord needs, so an agent's attempt to tag a user or hand off to another bot is inert plain text. The Feishu adapter already resolves mentions on outbound; this adds the same to Discord, reusing the guild-member name->id matching the adapter already has (_resolve_allowed_usernames). Opt-in via DISCORD_RESOLVE_MENTIONS (unset/false = byte-identical behavior). @everyone/roles stay governed by the existing allowed_mentions safe defaults. Fixes NousResearch#69203 Signed-off-by: Han Ngo <nntruonghan@gmail.com>
6b4650f to
f6502e6
Compare
… edit + forum paths Re-scope of NousResearch#69206 (closed under the env-var-for-config policy) to the shape the sweeper asked for: the opt-in is the documented ``discord.resolve_outbound_mentions`` setting in config.yaml, bridged to the internal ``DISCORD_RESOLVE_MENTIONS`` var by ``_apply_yaml_config``, exactly as the neighbouring ``discord.approval_mentions`` works. The bridge is kept private -- the default-config comment documents only the config key, and no test pins the env override. Three delivery paths, not one. The original resolved mentions in send() only: * the forum branch returned before the resolver ran, so a forum thread's starter post kept the inert @name; * edit_message() -- which is how every streamed reply is delivered -- never resolved at all, and neither did its _edit_overflow_split() continuation chunks, which re-format the same content. The resolver now runs above the forum branch in send(), and once in edit_message() on the FINAL edit. Mid-stream is deliberately skipped: the text is still partial there, so a member named "Al" matches while "@alice" is only half-written, and an edit can deliver that ping. Resolving once at finalize means the message the user keeps is the correct one, and it also covers the overflow-split path. guild.members is empty unless the privileged Server Members intent is requested, which startup asked for only for named allowlist entries or role authorization -- so turning the setting on resolved nothing and gave no clue why. The condition moves to a module-level _needs_members_intent() helper that includes this opt-in and is testable without a live client. The "*" open-mode wildcard still does not pull the intent in, so the migrate-from-OpenClaw path is unchanged. The Developer Portal prerequisite is documented where someone setting this up actually reads: the intent table and its explanation in the Discord setup guide, the discord: config block next to the other settings, and voice-mode.md, whose "only needed if your allowlist uses usernames" claim was now incomplete. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f6502e6 to
91e9bd4
Compare
|
Closing this to clear stale work — but flagging what goes with it, because it is not only mine. This PR carries two commits by @tieubao / Han Ngo ( So the work is finished and matches the stated ask; it has simply been open three weeks, and measured against this repo's merge behaviour (157 external merges sampled across four weeks, none slower than 24h, median ~20 minutes) it is not waiting on a queue. Nothing is lost: the branch and all three commits are intact, authorship included. If the re-scope is still wanted, say so and I will rebase it onto current main and reopen — @tieubao's commits stay unsquashed either way. |
Closes #69203. This is the re-scope #69206 was closed asking for:
@tieubao's commits are carried unchanged and unsquashed, so the runtime work stays theirs — the triage review already called it "clean and does what the issue asks" and "a solid parity fix." Only the gating and one missed call path are mine.
What changed from #69206
1. The knob is config.yaml, not a new env var. The opt-in is now
discord.resolve_outbound_mentions, documented inhermes_cli/config_defaults.pynext toapproval_mentions, and bridged to the internalDISCORD_RESOLVE_MENTIONSvar by_apply_yaml_config. That is the same arrangementdiscord.approval_mentionsalready uses (adapter.py:6782reads_env_bool("DISCORD_APPROVAL_MENTIONS"), bridged atadapter.py:9731-9736), so this adds no new user-facing environment variable — it plugs into the bridge the adapter already has.2. Forum posts were being skipped. In #69206 the resolver ran after the forum-parent branch had already returned, so
_send_to_forum()passed the raw content straight intocreate_thread(content=...)and a forum starter post kept the inert@Name. The call now sits above that branch, so an ordinary channel message and a forum post get identical treatment. Same bug class, second call path.Verification — each break point verified to fail
discord.resolve_outbound_mentions: true did not reach the adaptersend()call sitesend() delivered the raw @Name — the resolver is not on the send paththe forum thread starter kept the raw @NameThe
TestSendCallSiteclass exists specifically because the unit tests call_resolve_outbound_mentionsdirectly and would all still pass with thesend()call site deleted — a resolver nothing calls is dead code, and I wanted a test that says so.No test reads adapter source;
@tieubaohad already replaced the earlierast.parse/execversion with one that importsDiscordAdapterand rides the real import path. I additionally moved the flag handling ontomonkeypatchso it can't leak into unrelated tests.tests/gateway/+tests/hermes_cli/test_config*.py: 16 failures on this branch, 16 on a pristineorigin/mainworktree at the same base — identical set. Nothing introduced, nothing masked.contributors/emails/nntruonghan@gmail.com→tieubaoadded percontributors/README.md, since this PR carries their commits.