Skip to content

Long clarify choices stay readable on Slack, Discord, Google Chat and the relay (#78115, salvage #77547 #93154) - #111093

Open
teknium1 wants to merge 1 commit into
mainfrom
buzz-fix/clarify-choices-readable
Open

teknium1 wants to merge 1 commit into
mainfrom
buzz-fix/clarify-choices-readable

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Multiple-choice clarify prompts on Slack, Discord, Google Chat and the relay now show every option in full, so a long option is readable and answerable instead of a clipped button label (#78115).

Changes

  • gateway/platforms/helpers.py::numbered_clarify_choices / display_width: one shared rule. When any option is wider than the platform's label budget (CJK counts double), return the numbered full-text list; otherwise None and the adapter keeps its current per-choice labels.
  • Slack: numbered list appended to the question section; buttons labelled 1..N. Values (clarify_id|idx) and action ids unchanged, so _handle_clarify_action and the double-click guard are untouched.
  • Relay: numbered list in the prompt text, positional labels; ids stay c0..cN/other; the pending-prompt registry still maps back to the choice text.
  • Discord: labels already truncated at a word boundary; the numbered full text now also goes into the embed's Choices field (mirrored content unchanged).
  • Google Chat: numbered list in the text widget, positional button text; the choice parameter still carries the full string.
  • Telegram and WhatsApp Cloud already rendered this way and are the model for the rest.

Validation

  • Before: send_clarify(["Build a REST API with auth, rate limits & <pagination>", "MVP only"]) on Slack → button label Build a REST API with auth, rate limits & <pag… (clipped by the client under the 75-char API cap), body ❓ Scope? only.
  • After: body ❓ Scope?\n\n1. Build a REST API with auth, rate limits &amp; &lt;pagination&gt;\n2. MVP only, buttons 1, 2, ✏️ Other….
  • Two invariant tests (Slack incl. a 25-codepoint/50-column CJK case, relay), both red on origin/main and green here; tests/gateway/test_{slack,discord,google_chat}*.py + tests/gateway/relay 889 tests green.
  • Short options are byte-identical to before (existing button-label assertions still pass).

Root cause: four adapters independently put the option text on the button and trusted the API length cap, but every client clips by rendered width with no tooltip.

Supersedes #77547 (@hrsong99, Slack-only, added per-option section blocks) and #93154 (@tw0316, relay-only); both diagnosed the same class and are credited here. Surfaced again today on X by the original reporter.

Infographic

Clarify choices stay readable

…nal (#78115)

Slack, Discord, Google Chat and the relay all put the option text on the
button label. Every client clips labels well below the API cap (Slack ~48
columns, narrower on mobile, no tooltip), so a 50-character option from a
grilling/wayfinder skill was unreadable and the prompt unanswerable.

When any option is wider than the label budget, the numbered full text goes
in the message body and the buttons are labelled 1..N (Telegram and WhatsApp
Cloud already worked this way). A press still resolves to the canonical
choice text via the clarify entry; positional values/ids are unchanged.
CJK wide characters count as two columns.

One shared helper (gateway.platforms.helpers.numbered_clarify_choices) so
the four adapters cannot drift again.
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 32d76fb — fix(gateway): long clarify choices render in full, buttons g

⚠️ Warnings

OSV vulnerability scan · View job

76 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 6m10s vs 5m47s (+6.6%). 8 job(s) slower, 5 faster, 1 unchanged.

  • OS-specific tests / Windows-only tests: +27.0s
  • Check contributors / check-attribution: +23.0s
  • Python tests / Run tests: +16.0s
  • Python lints / Windows footguns (blocking): +13.0s
  • OS-specific tests / macOS-only tests: -12.0s

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins platform/slack Slack app adapter platform/discord Discord bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 14, 2026
@kvnloo

kvnloo commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Exact-head pick-one on #111093 head 32d76fb.

Competing open PRs for long clarify-option readability across messaging surfaces:

KEEP #111093: multi-platform + relay consolidation that already names the single-path leaves.

CHECK #77547 / #93154: close as salvaged/superseded by #111093.

Abort if any listed head drifts.

@kvnloo

kvnloo commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Exact-head pick-one on #111093 head 32d76fb.

Competing open PRs for long clarify-option readability across messaging surfaces:

KEEP #111093: multi-platform + relay consolidation that already names the single-path leaves.

CHECK #77547 / #93154: close as salvaged/superseded by #111093.

Abort if any listed head drifts.

@kvnloo kvnloo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shared trigger is the right call. One width rule in numbered_clarify_choices (gateway/platforms/helpers.py) replaces four adapters guessing label budgets on their own, and counting CJK double matches how the clients actually clip. But the trigger is only half the budget. The other half is where the full text lands. Discord's send_clarify caps the numbered list at 1000 characters before appending the hint, and Discord rejects any embed field value over 1024. With enough long options the field reaches about 1060 characters and the API refuses the whole message. In user terms: you ask Hermes a question with a dozen long options on Discord and the question never arrives at all. No clipped label, no numbered list, just a failed send while the agent waits for an answer the user never saw.

Verdict: comment

  1. Discord "Choices" embed field can exceed the 1024-character field-value cap. send_clarify in plugins/platforms/discord/adapter.py builds the value as the numbered list sliced to 1000 plus the 58-character hint, reaching about 1060 characters. The embed description goes through _embed_body trimming but field values get no length check anywhere in _send_prompt, so Discord returns 400 and the clarify prompt never sends. Reachable: choices are capped at 24, and roughly ten 90-character options already push the list past 961. Suggest capping the slice at 1024 minus the hint length instead of a flat 1000, or trimming the combined string.

  2. (non-blocking) The PR body says WhatsApp Cloud already renders this way. It does not. send_clarify in plugins/platforms/whatsapp/adapter.py sends a native poll, not a numbered list. Telegram (plugins/platforms/telegram/adapter.py) is the accurate model. Docs-only correction.

  3. (non-blocking) The Discord numbered list gets no markdown escaping. Slack passes escape=_esc into numbered_clarify_choices, but the Discord call passes none, so a choice containing * or _ renders with unintended formatting in the embed field while its button label shows it literally. Cosmetic.

  4. (non-blocking) No tests for the Discord or Google Chat numbered paths. The new invariant tests cover Slack (including the 25-codepoint CJK case) and the relay; the Discord embed truncation and the Google Chat positional-button switch are untested. A Discord test with a dozen long choices would have caught finding 1.

  5. (nit, non-blocking) display_width in gateway/platforms/helpers.py counts combining marks (for example the variation selector in "✏️") as one column each, slightly inflating emoji-heavy labels. Conservative direction, triggers the numbered list a hair early. No user-visible harm.

Verified against base: short-option paths are byte-identical to before on all four adapters (Slack's escaping and the 75-char label path unchanged when the helper returns None; relay text and options unchanged; Google Chat body and buttons unchanged since a None result implies the old 80-char cap never fired; Discord hint unchanged). Values, action ids, and the c0..cN/other ids are untouched, so _handle_clarify_action, the double-click guard, and the pending-prompt registry behave exactly as before.

@guvengursel

Copy link
Copy Markdown

Thanks for this — filed #113555 earlier today and the triager pointed me here. The
numbered_clarify_choices() shape (full list in the body, positional button labels,
value contract untouched) is exactly the smaller-change direction I'd want for this.

One edge case worth a look, in the Slack send_clarify hunk: the numbered path
activates precisely when choices are long, and the section body is still hard-truncated
at the 3000-char budget (body[:budget] + "...") after the list is appended. With four
long options the cut can land mid-list, leaving positional buttons ("3", "4", …) that
point at options no longer visible in the body — positional labels without their list
are unanswerable, which is the same failure class this PR fixes. Consistent degradation
would close it: when numbered is active and over budget, drop trailing options from
the buttons AND the list together (they shrink in lockstep), instead of mid-body "…".

Separately, happy to share data from a variant we've been running in production since
Sep 4 on our own gateway: same numbered list in the section body, but the buttons are
replaced by ONE static_select (labels repeated verbatim, same clarify_id|<idx|other>
values, handler falls back to selected_option.value). Twelve days of daily use,
including mobile, with no readability complaints — select rows render at full width, so
labels never clip anywhere, and the body budget stops being critical since the picker
always shows the full text. It does add a second read path in the handler plus a
registered action id, so I understand if it's more change than you want here; treating
this PR as the body-list carrier and the picker as a possible follow-up would also sit
fine with me. Either way I can send the regression tests (long-choice section-cap
round-trip, unauthorized-pick coverage) as a follow-up commit if useful.

@whyyagswhy

Copy link
Copy Markdown
Contributor

Independent verification on the PR head (32d76fb): the two touched test files pass locally, 18/18 (canonical runner). Diff check and Windows-footgun scan clean.

Traced all four adapters: Slack, Google Chat, and the relay switch to positional labels only when the helper returns text, and button values, callback ids, and option ids are unchanged, so a press still resolves to the canonical choice. Discord buttons were already positional (N. choice within the 80-char cap); the PR adds the full list to the embed body there. Helper edge cases hold: empty choices return None via all([]), non-string choices go through str, escape is per-adapter.

No findings.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P2 Medium — degraded but workaround exists platform/discord Discord bot adapter platform/slack Slack app adapter 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.

5 participants