Skip to content

fix(slack): guard rich_text builders against empty content rejected as invalid_blocks - #59621

Closed
kamonspecial wants to merge 2 commits into
NousResearch:mainfrom
kamonspecial:fix/slack-rich-text-empty-content-guards
Closed

fix(slack): guard rich_text builders against empty content rejected as invalid_blocks#59621
kamonspecial wants to merge 2 commits into
NousResearch:mainfrom
kamonspecial:fix/slack-rich-text-empty-content-guards

Conversation

@kamonspecial

Copy link
Copy Markdown
Contributor

What does this PR do?

The Block Kit renderer can emit rich_text shapes that Slack rejects with
invalid_blocks, which makes the whole payload fail — the message loses its
rich rendering entirely. Slack's rules:

  • a rich_text_section / rich_text_preformatted / rich_text_quote must not
    have an empty elements list, nor a text element of zero length
    ("missing element" / "must be more than 0 characters"), and
  • a header must not carry an empty plain_text.

The renderer produces exactly those shapes for common inputs:

input pre-fix output
markdown table with a blank cell, or a ragged (short) row padded with "" cell section with a 0-char text element
empty fenced code block (typical around empty tool output) rich_text_preformatted with a 0-char text element
blank quote line (> ) empty rich_text_quote elements
empty list item (- ) section with a 0-char text element
emphasis-only header (# ***, # ) header with empty plain_text

Any single one of these poisons the entire chat.postMessage payload. All of
them were hit in production (live invalid_blocks rejections; the empty table
cell being the most frequent — ragged rows are everywhere in LLM output).

Fix: route every rich_text builder's child elements through a shared
_nonempty_elements guard — drop zero-length text elements, substitute a
single space when nothing remains (renders as blank, stays schema-valid) — and
skip headers that reduce to empty after markdown-marker stripping.

Relationship to #56618 (harden rich table block fallback): complementary,
no overlapping hunks. #56618 fixes column_settings and adds a retry-without-
blocks safety net, i.e. it makes Block Kit rejections recoverable; this PR
makes the common empty-content cases render correctly in the first place
without it, any table containing a single empty cell silently loses native
rendering on every send even with the retry net in place. This PR deliberately
does not touch _table_block/column_settings or the adapter send path so
both PRs apply cleanly in either order.

Follows #58691 (config bridge that makes the rich_blocks opt-in reachable) —
these rendering failures surface as soon as the flag is actually enabled.

Related Issue

The empty-content rejection has no dedicated issue (root cause and repro are
above). The neighbouring column_settings failure is tracked in #56615 and
fixed by #56618.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • plugins/platforms/slack/block_kit.py — new _nonempty_elements helper;
    applied in _rich_text_cell, _preformatted_block, _quote_block,
    _list_block; _header_block returns None for empty headers and the
    render_blocks caller skips it.
  • tests/gateway/test_slack_block_kit.py — new TestEmptyContentGuards:
    ragged/blank table cells, empty code fence + blank quote + empty list item,
    multi-line quote keeps its "\n" separators (guard must not strip them),
    emphasis-only header dropped, and a well-formed-content control.
  • scripts/release.py — AUTHOR_MAP entry for the commit email (also added in
    fix(slack): bridge rich_blocks opt-in from a top-level slack: block to extra #58691; whichever lands second rebases the one line away).

How to Test

  1. pytest tests/gateway/test_slack_block_kit.py tests/gateway/test_slack_block_kit_adapter.py -q
  2. The new tests fail on the pre-fix renderer (0-char text elements / empty
    header reach the payload) and pass with the guard.
  3. Live-verified against a running gateway with rich_blocks enabled: a table
    containing blank and ragged cells now posts as a native table block; the
    same message previously failed with
    invalid_blocks: must be more than 0 characters [json-pointer:/blocks/N/rows/…].

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(slack): …)
  • I searched for existing PRs — fix(slack): harden rich table block fallback #56618 is adjacent but non-overlapping (see above)
  • My PR contains only changes related to this fix
  • I've run the affected suites — test_slack_block_kit.py + test_slack_block_kit_adapter.py: all pass
  • I've added tests for my changes
  • I've tested on my platform: macOS (Darwin 25.4.0)

Documentation & Housekeeping

  • Docstrings updated (_nonempty_elements, _rich_text_cell, _header_block)
  • cli-config.yaml.example — N/A (no config change)
  • CONTRIBUTING.md / AGENTS.md — N/A
  • Cross-platform impact — N/A (pure dict/string handling)
  • Tool descriptions/schemas — N/A

@alt-glitch alt-glitch added type/bug Something isn't working comp/plugins Plugin system and bundled plugins platform/slack Slack app adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P3 Low — cosmetic, nice to have labels Jul 6, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #56618 / #57128 (both fix null column_settings) and #56615 (issue). This is a complementary, non-overlapping fix in the same block_kit.py renderer — it targets empty-content rich_text shapes (0-char text elements / empty plain_text) that Slack rejects with invalid_blocks, a distinct root cause from the null-column_settings cluster. Not a duplicate.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused guard. The reported premise is still present on current main: _inline_elements() emits a zero-length fallback text element for empty input at plugins/platforms/slack/block_kit.py:141, and current table/list builders pass that output directly into rich-text sections at plugins/platforms/slack/block_kit.py:210 and plugins/platforms/slack/block_kit.py:259. Empty preformatted content and stripped headers are likewise emitted directly at plugins/platforms/slack/block_kit.py:168 and plugins/platforms/slack/block_kit.py:154.

The proposed guard covers every direct _inline_elements() rich-text builder call found in the current renderer, plus the independent preformatted and header paths. git diff --quiet 7426c09b..HEAD -- plugins/platforms/slack/block_kit.py tests/gateway/test_slack_block_kit.py confirms main has not independently changed these files since the PR base. GitHub currently reports a merge conflict, but the renderer and its test file have not moved.

Automated hermes-sweeper review.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 15, 2026
…s invalid_blocks

Slack rejects a rich_text_section / rich_text_preformatted / rich_text_quote
whose elements list is empty or contains a zero-length text element, and a
header whose plain_text is empty. The Block Kit renderer emits exactly those
shapes for common inputs: a markdown table with a blank cell or a ragged
(short) row, an empty fenced code block, a blank quote line, an empty list
item, and an emphasis-only header ("# ***"). Any one of them poisons the
whole payload — chat.postMessage fails with invalid_blocks ("missing element"
/ "must be more than 0 characters") and the message loses its rich rendering
entirely.

Route every rich_text builder's child elements through _nonempty_elements
(drop zero-length text elements; substitute a single space when nothing
remains) and skip headers that reduce to empty after markdown-marker
stripping. Observed in production via live chat.postMessage rejections;
regression tests cover each case plus a well-formed-content control.

Complementary to NousResearch#56618 (column_settings hardening + no-blocks retry): that
change makes Block Kit rejections recoverable, this one makes the common
empty-content cases render correctly in the first place. No overlapping hunks.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #69317 — your commit was cherry-picked onto current main with your authorship preserved in git history: your empty rich_text guards were cherry-picked (the AUTHOR_MAP edit was swapped for a contributors/emails/ mapping — that dict is frozen).

Thanks for the contribution!

@teknium1 teknium1 closed this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/slack Slack app 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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants