You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Remaining webhook-side Discord features from chat@4.32–4.41:
Slash commands can be answered ephemerally through an interaction_flags callback, and the flag sticks to every follow-up.
Select-menu interactions report the selected value.
Operators can make the bot respond to every message in allowlisted channels (respond_to_channel_ids).
@everyone/@here count as mentions only when opted in.
The adapter can rename thread channels.
Upstream changes
022a5027 feat(discord): add ephemeral slash command responses (#514), chat@4.32.0. Adds config interactionFlags(context), where context is {channelId, command, interaction, text, user}. The result goes on the deferred type-5 response as data.flags and is stored as initialResponseFlags on the slash context. The only flag is DiscordInteractionResponseFlag.Ephemeral = 64.
5341f909 feat(discord): ignore @everyone/@here pings unless respondToGlobalMentions is set (#701), chat@4.34.0. respondToGlobalMentions (default false) makes data.mention_everyone === true count as a mention.
6c2a3918 feat(discord): rename thread channels (#693), chat@4.34.0. setThreadTitle(threadId, title) does nothing when there is no thread segment; otherwise it PATCHes /channels/{thread}{name} on the parent-validated thread.
26c05225 feat(discord): add channel response allowlist (#715), chat@4.35.0. respondToChannelIds / DISCORD_RESPOND_TO_CHANNEL_IDS (comma list): non-bot messages whose parent channel is listed count as mentions, so a thread is created for top-level ones.
src/chat_sdk/adapters/discord/adapter.py:297-303 (handle_webhook) returns {"type": DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE} with no data.flags. The forwarded-gateway path (_handle_forwarded_interaction, :625-633, and _defer_gateway_interaction, :649-668) posts {"type": response_type} only. grep -rn 'interaction_flags\|initial_response_flags' src/chat_sdk/adapters/discord finds nothing.
types.py:78-83DiscordSlashCommandContext has no flags field. post_message (adapter.py:923-938) sends the slash follow-up without flags.
adapter.py:399-403 (_handle_component_interaction) sets value=decoded.value if decoded.value is not None else decoded.action_id and never reads data["values"].
adapter.py:704-711 (_handle_forwarded_message): is_mentioned is the forwarder's data.get("is_mention", False) OR a user ping OR a role ping. mention_everyone is not read. grep -rn 'respond_to_channel_ids\|RESPOND_TO\|respond_to_global' src/chat_sdk/adapters/discord finds nothing.
types.py:20-47DiscordAdapterConfig has no interaction_flags, respond_to_channel_ids or respond_to_global_mentions.
Scope
Add a DiscordInteractionFlagsContext dataclass (channel_id, command, interaction, text, user) and DiscordInteractionResponseFlag.EPHEMERAL = 64 to types.py.
Add these fields to DiscordAdapterConfig:
interaction_flags: Callable[[DiscordInteractionFlagsContext], int | None] | None
respond_to_channel_ids: list[str] | None
respond_to_global_mentions: bool | None
Add initial_response_flags: int | None = None to DiscordSlashCommandContext.
Split _handle_application_command_interaction in two:
_build_application_command_context(interaction) -> DiscordInteractionFlagsContext | None, which parses channel_id, command and text once.
A handler that takes (context, initial_response_flags, options).
HTTP path: compute the flags and return {"type": 5, "data": {"flags": f}} when f is not None. Forwarded path: pass the flags to _defer_gateway_interaction.
post_message slash follow-up: when initial_response_flags is not None, send flags = initial | payload.get("flags", 0) on the @original edit and later follow-ups.
Component actions: value = values[0] when data.get("values") is a non-empty list, else decoded.value if it is not None, else decoded.action_id.
_handle_forwarded_message: compute is_mentioned as user ping OR role ping OR (respond_to_global_mentions and data.get("mention_everyone") is True) OR (the author is not a bot and parent_channel_id in respond_to_channel_ids). Stop reading the forwarder's data["is_mention"] (decision below).
Resolve respond_to_channel_ids as config.respond_to_channel_ids when it is not None, else DISCORD_RESPOND_TO_CHANNEL_IDS split on , and stripped (upstream keeps empty entries; dropping them is harmless), else []. Do not copy the or in the existing mention_role_ids parse (adapter.py:137-139): an explicit [] must win over the env var (upstream ??). respond_to_global_mentions likewise uses is not None, default False.
interactionFlags may return undefined. Test with is not None: a return of 0 is a real flag value and must still be sent.
Bitfields: initial | (flags if flags is not None else 0).
An empty-string select value is legal. Pick values[0] whenever the list is non-empty, even if values[0] == "".
mention_everyone is True: strict identity, matching === true.
The allowlist checks the parent channel id, so messages inside threads of an allowlisted channel qualify. A top-level message in an allowlisted channel creates a thread, as a mention does today (adapter.py:714).
The callback is synchronous (as upstream). Python-only hardening: if it raises, log and fall back to no flags rather than failing the ACK (upstream lets it throw); record this in docs/UPSTREAM_SYNC.md.
Design decision: upstream no longer honours a forwarder-supplied is_mention field. Recommended default: drop it (parity, and it closes an @everyone leak for forwarders that computed it with discord.js mentions.has). Record it in docs/UPSTREAM_SYNC.md and the CHANGELOG as a behaviour change for custom forwarders.
Tests
packages/adapter-discord/src/index.test.ts is not fidelity-mapped (see #78). Port into tests/test_discord_adapter.py / tests/test_discord_extended.py:
"sets initial deferred slash command interaction flags from config"
"sets gateway deferred slash command interaction flags from config". Upstream drives discord.js deferReply; adapt the assertion to the Python forwarded GATEWAY_INTERACTION_CREATE path, where _defer_gateway_interaction must carry data.flags.
"uses selected values from select interactions"
"should resolve respondToChannelIds from DISCORD_RESPOND_TO_CHANNEL_IDS env var"
"keeps allowlisted forwarded messages in their Discord thread"
describe("respondToGlobalMentions handling"):
"ignores @everyone in forwarded gateway messages by default"
"treats @everyone in forwarded gateway messages as a mention when respondToGlobalMentions is true"
"still detects direct user mentions when @everyone is ignored"
N/A (drive the discord.js listener): "creates a thread for messages in an allowlisted channel", "reuses the existing thread for messages in a thread of an allowlisted channel", "does not treat a parentless thread message as allowlisted", "ignores @everyone in legacy gateway mode by default", "treats @everyone in legacy gateway mode as a mention when respondToGlobalMentions is true". Carry their assertions to the forwarded path only where not covered above.
Python-specific: a raising flags callback still ACKs; an empty-string select value is kept; an explicit respond_to_channel_ids=[] beats the env var. AsyncMock for _discord_fetch.
Acceptance criteria
With interaction_flags returning 64, the deferred response and every follow-up carry the ephemeral flag.
@everyone does not trigger mention handlers unless opted in, and allowlisted channels trigger them without a mention.
The full validation command from CLAUDE.md passes.
docs/UPSTREAM_SYNC.md records the Components V2 deferral, the forwarder is_mention decision and the flags-callback error handling.
A CHANGELOG entry under "Unreleased (4.41 wave)" calls out the new options and that forwarder-supplied is_mention is ignored.
Summary
Remaining webhook-side Discord features from chat@4.32–4.41:
interaction_flagscallback, and the flag sticks to every follow-up.respond_to_channel_ids).@everyone/@herecount as mentions only when opted in.Upstream changes
022a5027feat(discord): add ephemeral slash command responses (#514), chat@4.32.0. Adds configinteractionFlags(context), wherecontextis{channelId, command, interaction, text, user}. The result goes on the deferred type-5 response asdata.flagsand is stored asinitialResponseFlagson the slash context. The only flag isDiscordInteractionResponseFlag.Ephemeral = 64.5341f909feat(discord): ignore @everyone/@here pings unless respondToGlobalMentions is set (#701), chat@4.34.0.respondToGlobalMentions(defaultfalse) makesdata.mention_everyone === truecount as a mention.6c2a3918feat(discord): rename thread channels (#693), chat@4.34.0.setThreadTitle(threadId, title)does nothing when there is no thread segment; otherwise it PATCHes/channels/{thread}{name}on the parent-validated thread.0fdb9029feat(discord): Components support (#678), chat@4.34.0. For a component action the value isdata.values[0] ?? decoded.value ?? actionId. The Components V2 card renderer (contentFormat) is deferred, see [4.41/DEF1] Deferred upstream 4.32–4.41 features (demand-gated backlog) #189.26c05225feat(discord): add channel response allowlist (#715), chat@4.35.0.respondToChannelIds/DISCORD_RESPOND_TO_CHANNEL_IDS(comma list): non-bot messages whose parent channel is listed count as mentions, so a thread is created for top-level ones.b7c9316bfix(chat): tighten conversation boundaries (#875), chat@4.40.0. This is the follow-up part:initialResponseFlagsis OR'd into every slash response (flags: initial | (payload.flags ?? 0)), so follow-ups stay ephemeral. The parent-channel part is in [4.41/D1] Discord correctness & security: thread-parent validation, starter-message routing, mentions/URLs, forwarded snapshots, downloads #229.61b98fcachore(discord): update discord-api-types to 0.38 and dedupe types (#927), chat@4.41.0. Relevant part: stops reading the forwarder-suppliedis_mentionfield (see Porting notes); the rest is in [4.41/D1] Discord correctness & security: thread-parent validation, starter-message routing, mentions/URLs, forwarded snapshots, downloads #229 or N/A (Discord adapter: port native Gateway WebSocket support (HTTP-only today) #57).Current Python behavior
src/chat_sdk/adapters/discord/adapter.py:297-303(handle_webhook) returns{"type": DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE}with nodata.flags. The forwarded-gateway path (_handle_forwarded_interaction,:625-633, and_defer_gateway_interaction,:649-668) posts{"type": response_type}only.grep -rn 'interaction_flags\|initial_response_flags' src/chat_sdk/adapters/discordfinds nothing.types.py:78-83DiscordSlashCommandContexthas no flags field.post_message(adapter.py:923-938) sends the slash follow-up without flags.adapter.py:399-403(_handle_component_interaction) setsvalue=decoded.value if decoded.value is not None else decoded.action_idand never readsdata["values"].adapter.py:704-711(_handle_forwarded_message):is_mentionedis the forwarder'sdata.get("is_mention", False)OR a user ping OR a role ping.mention_everyoneis not read.grep -rn 'respond_to_channel_ids\|RESPOND_TO\|respond_to_global' src/chat_sdk/adapters/discordfinds nothing.grep -rn 'set_thread_title' src/chat_sdkfinds nothing.types.py:20-47DiscordAdapterConfighas nointeraction_flags,respond_to_channel_idsorrespond_to_global_mentions.Scope
DiscordInteractionFlagsContextdataclass (channel_id,command,interaction,text,user) andDiscordInteractionResponseFlag.EPHEMERAL = 64totypes.py.DiscordAdapterConfig:interaction_flags: Callable[[DiscordInteractionFlagsContext], int | None] | Nonerespond_to_channel_ids: list[str] | Nonerespond_to_global_mentions: bool | Noneinitial_response_flags: int | None = NonetoDiscordSlashCommandContext._handle_application_command_interactionin two:_build_application_command_context(interaction) -> DiscordInteractionFlagsContext | None, which parseschannel_id,commandandtextonce.(context, initial_response_flags, options).{"type": 5, "data": {"flags": f}}whenf is not None. Forwarded path: pass the flags to_defer_gateway_interaction.post_messageslash follow-up: wheninitial_response_flags is not None, sendflags = initial | payload.get("flags", 0)on the@originaledit and later follow-ups.value = values[0]whendata.get("values")is a non-empty list, elsedecoded.valueif itis not None, elsedecoded.action_id._handle_forwarded_message: computeis_mentionedas user ping OR role ping OR (respond_to_global_mentions and data.get("mention_everyone") is True) OR (the author is not a bot andparent_channel_id in respond_to_channel_ids). Stop reading the forwarder'sdata["is_mention"](decision below).async set_thread_title(thread_id, title): return if there is no thread; otherwise call_resolve_thread_channel_id(from [4.41/D1] Discord correctness & security: thread-parent validation, starter-message routing, mentions/URLs, forwarded snapshots, downloads #229) and thenPATCH /channels/{id}{"name": title}.respond_to_channel_idsasconfig.respond_to_channel_idswhen itis not None, elseDISCORD_RESPOND_TO_CHANNEL_IDSsplit on,and stripped (upstream keeps empty entries; dropping them is harmless), else[]. Do not copy theorin the existingmention_role_idsparse (adapter.py:137-139): an explicit[]must win over the env var (upstream??).respond_to_global_mentionslikewise usesis not None, defaultFalse.Out of scope
content_format), the 40-component and 4000-character validation, and V2 select rendering: [4.41/DEF1] Deferred upstream 4.32–4.41 features (demand-gated backlog) #189. Record them as non-parity.is_mentionreturningTrue|None: [4.41/C2a] Core mentions & message model: tri-state is_mention, mention regex, Author.email/is_system, Message.reply_to #192.Porting notes
interactionFlagsmay returnundefined. Test withis not None: a return of0is a real flag value and must still be sent.initial | (flags if flags is not None else 0).values[0]whenever the list is non-empty, even ifvalues[0] == "".mention_everyone is True: strict identity, matching=== true.adapter.py:714).docs/UPSTREAM_SYNC.md.is_mentionfield. Recommended default: drop it (parity, and it closes an@everyoneleak for forwarders that computed it with discord.jsmentions.has). Record it indocs/UPSTREAM_SYNC.mdand the CHANGELOG as a behaviour change for custom forwarders.Tests
packages/adapter-discord/src/index.test.tsis not fidelity-mapped (see #78). Port intotests/test_discord_adapter.py/tests/test_discord_extended.py:deferReply; adapt the assertion to the Python forwardedGATEWAY_INTERACTION_CREATEpath, where_defer_gateway_interactionmust carrydata.flags.describe("respondToGlobalMentions handling"):describe("setThreadTitle"): "renames Discord thread channels"N/A (drive the discord.js listener): "creates a thread for messages in an allowlisted channel", "reuses the existing thread for messages in a thread of an allowlisted channel", "does not treat a parentless thread message as allowlisted", "ignores @everyone in legacy gateway mode by default", "treats @everyone in legacy gateway mode as a mention when respondToGlobalMentions is true". Carry their assertions to the forwarded path only where not covered above.
Python-specific: a raising flags callback still ACKs; an empty-string select value is kept; an explicit
respond_to_channel_ids=[]beats the env var.AsyncMockfor_discord_fetch.Acceptance criteria
interaction_flagsreturning64, the deferred response and every follow-up carry the ephemeral flag.@everyonedoes not trigger mention handlers unless opted in, and allowlisted channels trigger them without a mention.docs/UPSTREAM_SYNC.mdrecords the Components V2 deferral, the forwarderis_mentiondecision and the flags-callback error handling.is_mentionis ignored.Dependencies
Blocked by #229.
Metadata
sync/4.41-d2Part of #184.