Skip to content

fix(gateway/slack): ephemeral slash-command ack, private notice delivery, format_message fixes#18198

Merged
kshitijk4poor merged 4 commits into
mainfrom
fix/slack-ephemeral-slash-ack
May 1, 2026
Merged

fix(gateway/slack): ephemeral slash-command ack, private notice delivery, format_message fixes#18198
kshitijk4poor merged 4 commits into
mainfrom
fix/slack-ephemeral-slash-ack

Conversation

@kshitijk4poor
Copy link
Copy Markdown
Collaborator

@kshitijk4poor kshitijk4poor commented May 1, 2026

Summary

Fixes #18182 and salvages #9340 — comprehensive Slack ephemeral messaging improvements:

  1. Slash commands now show ephemeral acknowledgements (/q, /btw, /stop, /model, etc.) and route command replies ephemerally, matching Discord's behavior.
  2. Operational notices (e.g. sethome prompt) can now be delivered privately via chat_postEphemeral when slack.notice_delivery: private is configured.
  3. format_message bug fixes — markdown images no longer produce broken Slack links, and literal asterisks with spaces (a * b * c) are no longer mistakenly italicized.

Changes

Commit 1: fix(gateway/slack): ephemeral ack and routing for slash commands

Fixes #18182 — Two gaps combined to produce the bug:

Gap 1 fix — Immediate ephemeral ack:
handle_hermes_command now passes response_type="ephemeral" and "Running /cmd…" text to ack(). Previously the bare await ack() sent an empty 200 OK, which Slack silently swallowed.

Gap 2 fix — Ephemeral reply routing via response_url:

  • _handle_slash_command stashes the Slack response_url from the command payload in _slash_command_contexts (keyed by (channel_id, user_id)) before dispatching.
  • send() checks for a pending slash context. When found, POSTs to response_url with replace_original: true to swap the ack with the real reply, keeping it ephemeral.
  • Stale contexts garbage-collected on lookup (120s TTL). Non-fatal fallback if POST fails.

Commit 2: feat(gateway): private notice delivery and Slack format_message fixes

Salvaged from PR #9340 by @probepark. Cherry-picked onto current main with original authorship preserved.

File What
gateway/config.py _normalize_notice_delivery() + GatewayConfig.get_notice_delivery() with per-platform config bridging
gateway/platforms/base.py send_private_notice() default implementation (falls through to send())
gateway/platforms/slack.py send_private_notice() via chat_postEphemeral
gateway/run.py _deliver_platform_notice() helper replaces direct adapter.send() for the sethome notice, with private→public fallback
gateway/platforms/slack.py app_mention handler now forwards to _handle_slack_message (safe due to ts-based dedup) instead of no-op
gateway/platforms/slack.py format_message: negative lookbehind prevents markdown images from becoming broken Slack links; italic regex requires non-whitespace boundaries

Commit 3: chore: add probepark to AUTHOR_MAP

Commit 4: fix(gateway/slack): review fixes — scope ephemeral to commands, user isolation

Self-review caught and fixed:

  1. Critical — Free-form /hermes <question> routed agent reply ephemeral. The context was stashed unconditionally, so /hermes what's the weather would make the full agent response invisible to the channel. Fix: Only stash when text.startswith("/").

  2. Critical — Concurrent users on same channel could steal each other's ephemeral context. _pop_slash_context scanned by channel_id only, so User B's response could consume User A's response_url. Fix: Added a ContextVar (_slash_user_id) that threads the invoking user's ID from _handle_slash_command through to send(). _pop_slash_context now matches the exact (channel_id, user_id) key. ContextVars propagate to child asyncio.Tasks, so the value survives through handle_message_process_message_background_send_with_retrysend().

  3. Medium — _send_slash_ephemeral skipped truncate_message(). Long responses could silently fail. Fixed.

  4. Warning — Bare except Exception: pass in _deliver_platform_notice. Fixed: Logs at debug level.

  5. Docs — app_mention dedup dependency on shared event ts. Added comment.

Files changed

File Lines What
gateway/platforms/slack.py +197/-7 Ephemeral ack, response_url routing, ContextVar isolation, send_private_notice, app_mention fix, format_message fixes
gateway/config.py +25 Notice delivery config normalization and bridging
gateway/platforms/base.py +20 send_private_notice() abstract with fallback
gateway/run.py +65/-25 _deliver_platform_notice() helper with debug logging
tests/gateway/test_slack.py +316 14 ephemeral ack tests (incl. concurrent-user isolation + free-form question regression) + 3 notice/format tests
tests/gateway/test_config.py +36 Notice delivery config bridging tests
tests/gateway/test_notice_delivery.py +67 Private notice delivery E2E tests (new file)
scripts/release.py +2 AUTHOR_MAP entry for probepark

Test plan

  • 14 new slash ephemeral tests (concurrent-user isolation, free-form question regression, stash/pop/TTL, response_url routing, fallback)
  • 3 notice delivery tests (private, fallback-to-public, default public)
  • 3 format_message tests (markdown image, literal asterisks, send_private_notice)
  • Full gateway suite: 210 passed (targeted), 4316 passed (full)
  • py_compile verified on all modified files

Slack slash commands (/q, /btw, /stop, /model, etc.) previously showed
no user-visible acknowledgement and posted command replies as public
channel messages.  This diverged from Discord, which uses ephemeral
deferred responses for slash commands.

Changes:
- handle_hermes_command now passes response_type='ephemeral' and a
  'Running /cmd…' text to ack(), giving the user immediate 'Only visible
  to you' feedback when they invoke any native slash command.
- _handle_slash_command stashes the Slack response_url from the command
  payload in a per-channel context dict before dispatching to
  handle_message.
- send() checks for a pending slash context and, when found, POSTs to
  the response_url with replace_original=true to swap the initial ack
  with the real command reply (e.g. 'Queued for the next turn.'),
  keeping it ephemeral.
- Stale slash contexts are garbage-collected on lookup (120s TTL).
- The response_url POST is non-fatal: if it fails, the user already saw
  the initial ack, and send() returns success=True.

Fixes #18182
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/slack Slack app adapter labels May 1, 2026
probepark and others added 2 commits May 1, 2026 09:07
Adds platform-level private notice delivery abstraction so operational
messages (e.g. sethome prompt) can be sent ephemerally on Slack when
configured with `slack.notice_delivery: private`.

Changes:
- gateway/config.py: _normalize_notice_delivery() + GatewayConfig.get_notice_delivery()
  with per-platform config bridging
- gateway/platforms/base.py: send_private_notice() default implementation
  (falls through to send())
- gateway/platforms/slack.py: send_private_notice() via chat_postEphemeral
- gateway/run.py: _deliver_platform_notice() helper replaces direct
  adapter.send() for the sethome notice, with private→public fallback
- gateway/platforms/slack.py: app_mention handler now forwards to
  _handle_slack_message (safe due to ts-based dedup) instead of no-op pass,
  fixing edge-case Slack configs where mentions arrive only as app_mention
- gateway/platforms/slack.py format_message: negative lookbehind prevents
  markdown images (![]()) from becoming broken Slack links; italic regex
  now requires non-whitespace boundaries so 'a * b * c' stays literal

Based on PR #9340 by @probepark.
Required for contributor_audit.py strict mode on the salvaged
PR #9340 commit.
@kshitijk4poor kshitijk4poor changed the title fix(gateway/slack): ephemeral ack and routing for slash commands fix(gateway/slack): ephemeral slash-command ack, private notice delivery, format_message fixes May 1, 2026
…isolation

Self-review fixes for the slash ephemeral ack:

- Only stash response_url when text starts with '/' (gateway command).
  Free-form questions via '/hermes <question>' must produce public agent
  replies visible to the whole channel, not ephemeral.
- Use a ContextVar (_slash_user_id) to thread the invoking user's ID
  from _handle_slash_command through to send().  _pop_slash_context now
  matches the exact (channel_id, user_id) key when the ContextVar is
  set, preventing concurrent users on the same channel from stealing
  each other's ephemeral context.  ContextVars propagate to child
  asyncio.Tasks, so the value survives through handle_message →
  _process_message_background → _send_with_retry → send().
- Add truncate_message() in _send_slash_ephemeral to prevent silent
  failures on long responses (response_url has the same ~40k limit).
- Log send_private_notice failures at debug level instead of bare
  except/pass — aids diagnostics without spamming.
- Document app_mention dedup dependency on shared event ts.
- Add tests: free-form question must NOT stash context, concurrent
  users on the same channel get isolated contexts, non-slash send()
  path fallback behavior.
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 P2 Medium — degraded but workaround exists platform/slack Slack app adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Slack slash commands (/q, /btw, etc.) produce no "Only visible to you" acknowledgement

3 participants