fix(slack): render pasted message permalinks instead of duplicating the message - #26
Merged
nikitaBarkov merged 1 commit intoAug 11, 2026
Conversation
nikitaBarkov
force-pushed
the
nikita.barkov/slack-message-mention-duplication
branch
9 times, most recently
from
August 11, 2026 08:36
07ee0e1 to
43161d5
Compare
Slack sends an authored message twice: flat in `event.text` and structurally in `event.blocks`. The blocks are rendered so quoted and forwarded content is not lost, and whatever the render carries beyond the flat text is appended to the message. A pasted message permalink arrives as a `message_mention` element, which the renderer did not know: the link vanished from the render, the two sides stopped comparing equal, and the author's own sentence was appended a second time with a blank where the link had been. Confirmed on a live gateway log, and fixed by the same run. The renderer now reads `message_mention` like `link`, and any unknown inline type by its `url`/`text`/`fallback`, so a type Slack adds later still renders instead of vanishing. `team`, `color` and a `date` without a `fallback` render into the flat form Slack sends. The comparison also reads down the optional label Slack may attach to a mention (`<@U…|name>`, `<#C…|general>`, `<!subteam^S…|@marketing>`, `<!here|@here>`), strips the bot's own mention after that label so every form of it is dropped, and accepts any autolink scheme rather than `https`/`mailto` only. Every field is read as a string or not at all. Block Kit carries text as an object in many places, and a non-string field reaches the renderer's `str.join` and raises there, which costs the whole message: four such payloads raise before this change and none after it. `url` is optional on `message_mention` while `channel_id` and `message_ts` are not, and those two are the permalink's own components, so an element without a url renders the permalink's tail. Neither the workspace host nor the thread query can be rebuilt from the element, so the comparison reduces a permalink on either side to that same tail. An element carrying neither a url nor a label still renders as nothing, and a message containing one is still appended twice. Suppressing such a render is worse: an app message whose body lives only in the blocks disappears, and a forwarded quote is dropped. An unrendered element is the lesser cost. Tests cover the pasted permalink through both merge sites, the same element without a url against both flat forms of the link, unknown types with a url and with a label, all four labelled mention forms, the bot's own mention in its three flat forms, every `date` form, a non-http scheme, and the negative cases: quotes, alert attachments and app message bodies must still be delivered. Co-authored-by: Junie <junie@jetbrains.com>
nikitaBarkov
force-pushed
the
nikita.barkov/slack-message-mention-duplication
branch
from
August 11, 2026 09:01
43161d5 to
5ab27d9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes a pasted Slack message permalink being delivered to the agent twice — once with the link, once as the same sentence with a blank where the link had been.
Slack sends an authored message twice: flat in
event.textand structurally inevent.blocks. The blocks are rendered so quoted and forwarded content is not lost, and whatever the render carries beyond the flat text is appended to the message. A pasted permalink arrives as amessage_mentionelement, which_render_inline_elements()did not know — its list of eight inline types had noelsebranch, so the element was dropped silently. The link vanished from the render, the two sides stopped comparing equal, and the sentence was appended a second time.Confirmed on a live gateway log: one
inbound messageline per message (so this is a rendering bug, not a repeated delivery), the transcript carrying the sentence twice with a double space where the URL had been, and the same messages arriving once after the fix.Related Issue
None — reported directly.
Type of Change
Changes Made
plugins/platforms/slack/adapter.py: inline-element rendering moved into_render_slack_inline_element().message_mentionis read likelink(a url plus an optional label), and any unknown type by itsurl/text/fallback, so a type Slack adds later still renders instead of vanishing.plugins/platforms/slack/adapter.py:team,colorand adatewithout afallbackrender into the flat form Slack sends rather than into nothing.plugins/platforms/slack/adapter.py:urlis optional onmessage_mentionwhilechannel_idandmessage_tsare not, and those two are the permalink's own components, so an element without a url renders the permalink's tail. Neither the workspace host nor the thread query can be rebuilt from the element, so_normalize_slack_text_for_dedupe()reduces a permalink on either side to that same tail. That reduction stops at the query rather than running to the next space: a labelled link is canonicalized tolabel (url), and swallowing the closing parenthesis left the two sides unequal again whenever only one of them carried?thread_ts=….plugins/platforms/slack/adapter.py:_normalize_slack_text_for_dedupe()reads down the optional label Slack may attach to a mention (<@U…|name>,<#C…|general>,<!subteam^S…|@marketing>,<!here|@here>), strips the bot's own mention after that label so every form of it is dropped, and matches any autolink scheme instead ofhttps/mailtoonly.plugins/platforms/slack/adapter.py: every field is read as a string or not at all, through one helper. Block Kit carries text as an object in many places, so an element -- an unknown one above all -- may hold one where a string belongs, and it would raise in_render_inline_elements()'sstr.joinand cost the whole message. Four such payloads raise onmainand none do here.tests/gateway/test_slack.py: 35 tests added toTestSlackAuthoredTextDeduplication.Known gap, deliberately left open: an element carrying neither a url nor a label still renders as nothing, and a message containing one is still appended twice. Suppressing such a render is worse — an app message whose body lives only in the blocks disappears, and a forwarded quote is dropped. An unrendered element is the lesser cost, and a duplicate is what
mainalready does.How to Test
.../archives/C…/p…) and send it to the bot.?thread_ts=…&cid=…), the same one given a label of your own, a link carrying a label, a channel / user group /@heremention, and atel:link — each arrives once.scripts/run_tests.sh tests/gateway/test_slack.py— 245 passed.scripts/run_tests.sh tests/gateway/— 4957 passed (6 pre-existing environment failures: an optional XML dependency for wecom, Linux-only abstract sockets on macOS).Checklist
Code
mainfails 28 of the 47 tests inTestSlackAuthoredTextDeduplicationtextfield and the block render:_normalize_slack_text_for_dedupe()is called only from that comparison, never from the text the agent receives, so a canonicalization mistake can cost an unrendered element, never an altered or missing messageDocumentation & Housekeeping
cli-config.yaml.example— N/A, no config changesCONTRIBUTING.md/AGENTS.md— N/A, no architecture or workflow changesNotes
Out of scope: Slack also re-delivers a message as
message_changedwhen it attaches an unfurl preview, and_processed_message_ts[ts]is written only at the end of_handle_slack_message, after every longawait. That is a separate class — repeated delivery rather than a duplicated render — it did not occur in the logs for this report, and a naive fix breaks "an @mention added by an edit still wakes the bot". Upstream PR NousResearch#73450 already rewrites that branch.