Skip to content

fix(slack): stop duplicating one authored message on entities, unfurls and the Block Kit dump - #25

Merged
nikitaBarkov merged 2 commits into
mainfrom
nikita.barkov/update_slack_dup
Aug 10, 2026
Merged

fix(slack): stop duplicating one authored message on entities, unfurls and the Block Kit dump#25
nikitaBarkov merged 2 commits into
mainfrom
nikita.barkov/update_slack_dup

Conversation

@nikitaBarkov

Copy link
Copy Markdown

What does this PR do?

Closes two more ways one authored Slack message reaches the agent twice, on top of the formatting normalization from #22.

Slack delivers the same message in both event.text (flat) and event.blocks (structured). The adapter renders the blocks so quotes, lists and forwards are not lost, and treats that rendering as additional content whenever it does not compare equal to the flat text. #22 taught that comparison about inline code, styles, links, fenced code and bot mentions. Two gaps remained, plus one duplication that does not go through the comparison at all.

1. HTML entities. Slack escapes &, < and > in the flat text only and leaves the block payload raw, so the two copies of the same message never compared equal once a URL carried query parameters. A thread "Copy link" always carries ?thread_ts=…&cid=…, which is why pasting a thread link duplicated the message every time while a plain permalink was fine. Verified on main by calling the helpers directly: the thread-link case returned the whole message as "additional text"; escaping both sides returned "".

2. Permalink unfurls in hydration. is_msg_unfurl attachments carry the linked message's own body. _handle_slack_message() already skipped them, _extract_text_from_slack_attachments() did not — so thread and parent hydration appended a second copy of a message the agent was already reading. One code path ignored what the other appended.

3. The Block Kit payload dump. _serialize_slack_blocks_for_agent() inlines a redacted JSON view of the blocks so the agent can inspect structure it cannot otherwise read. NousResearch#11426 introduced it "for non-rich_text messages", but the guard bailed out only when every block was rich_text — so one bot section next to the user's own text dumped that text too. rich_text is the authored message, already rendered into the message text, and the scalar allowlist drops url by design: the repeat arrives as the same sentence with every link deleted and a double space where the link used to be. That is the exact shape reported in a live workspace.

Related Issue

No fork issue. Continuation of #22 (formatting normalization). Upstream counterpart: NousResearch#80240 — the same three fixes are squashed into that PR on top of the #22 commit. Prior upstream work: NousResearch#66204 and NousResearch#70191 (link representation), NousResearch#11426 (introduced the dump narrowed here).

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

Two commits, each self-contained and green on its own.

fix(slack): stop duplicating text on escaped entities and unfurls

  • plugins/platforms/slack/adapter.py: new _unescape_slack_entities(); _normalize_slack_text_for_dedupe() unescapes before canonicalizing links, so both sides of the comparison see the same angle brackets and ampersands.
  • plugins/platforms/slack/adapter.py: _extract_text_from_slack_attachments() skips is_msg_unfurl, matching the live inbound path. Regular attachments are untouched — alert bots put their entire content there.
  • plugins/platforms/slack/adapter.py: the URLs: list in _render_message_text() compares against the unescaped message text. Without this a raw block URL was not found in the escaped text and was re-listed as a third copy; this one only surfaced under test.

fix(slack): keep the authored message out of the Block Kit payload dump

Teststests/gateway/test_slack.py, new TestSlackAuthoredTextDeduplication (12 tests):

  • parametrized entity equivalence: thread permalink with ?thread_ts=…&cid=…, AT&amp;T, &lt;div&gt;, labelled link;
  • both merge sites end to end: _handle_slack_message() (live inbound) and _render_message_text() (hydration);
  • is_msg_unfurl skipped during hydration;
  • Block Kit dump: absent for a plain authored message, present but without the authored text when a bot section is mixed in;
  • negative cases that must keep working: a genuine rich_text_quote is still appended, a regular alert attachment is still surfaced, and section + actions bot blocks are still described.

How to Test

  1. Paste a thread permalink taken via Copy link (the URL carries ?thread_ts=…&cid=…) into a bot thread. Previously the message arrived twice — once with the link, once without; now it arrives once.
  2. Send a message mixing a link with a genuine quote — the quote is still delivered.
  3. Post a Block Kit message from a bot (section + actions) — the agent still sees the block payload.
  4. scripts/run_tests.sh tests/gateway/test_slack.py210 passed (207 on the first commit alone).
  5. scripts/run_tests.sh tests/gateway/4919 passed. The 6 failures are pre-existing and environmental (missing optional XML dep for wecom, Linux-only abstract sockets on macOS) and reproduce on main without this branch.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate — nothing open upstream touches these paths
  • My PR contains only changes related to this fix (two files)
  • I've run all affected tests and they pass
  • I've added regression tests for the bug and its edge cases — reverting the adapter diff fails all 12
  • I've verified the behavior in a live Slack workspace

Documentation & Housekeeping

  • Documentation updates — N/A, no user-facing behavior or configuration changed
  • cli-config.yaml.example — N/A, no config changes
  • CONTRIBUTING.md / AGENTS.md — N/A, no architecture or workflow changes
  • Cross-platform impact considered — pure Python Slack adapter behavior
  • Tool descriptions/schemas — N/A, no tool changes

Notes

Out of scope on purpose: repeated delivery of the same message as a second event. Slack re-sends a message as message_changed once it attaches a link preview, and _processed_message_ts[ts] is written only at the very end of _handle_slack_message(), after 17 awaits — so a slow first pass can let the unfurl copy through the guard. That is a different bug class, it is not reproduced from logs yet, and a naive fix would break the "an @mention added by edit still wakes the bot once" behavior. It needs its own change, and it collides with open upstream PR NousResearch#73450.

Two more ways one authored Slack message reached the agent twice, on top
of the formatting normalization from #22.

Slack escapes `&`, `<` and `>` in the flat `event.text` but leaves the
`blocks` payload raw, so the two representations of the same message never
compared equal once a URL carried query parameters. A thread "Copy link"
always carries `?thread_ts=...&cid=...`, so linking a thread duplicated
the message every time. The dedupe path now unescapes before canonicalizing
links, and the `URLs:` list in hydration compares against the unescaped
text — otherwise a raw block URL was not found in the escaped message and
was re-listed as if new.

Permalink unfurls (`is_msg_unfurl`) carry the linked message's own body.
`_handle_slack_message()` already skipped them; `_extract_text_from_slack_attachments()`
did not, so thread and parent hydration appended a second copy of a message
the agent was already reading. Regular attachments are untouched — alert
bots put their entire content there.
`_serialize_slack_blocks_for_agent()` inlines a redacted JSON view of the
message's blocks so the agent can inspect structure it cannot otherwise
read. NousResearch#11426 introduced it "for non-`rich_text` messages", but the guard
bailed out only when *every* block was `rich_text`, so a single bot
`section` alongside the user's own text dumped that text as well.

`rich_text` is the authored message, already rendered into the message text
by `_extract_text_from_slack_blocks()`. Repeating it is duplication on its
own, and because the scalar allowlist drops `url` by design, the repeat
arrives as the same sentence with every link deleted and a double space
where the link used to be. Only non-`rich_text` blocks are serialized now;
`section`, `actions` and accessory blocks are still described in full.
@nikitaBarkov
nikitaBarkov merged commit 483321d into main Aug 10, 2026
40 of 43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant