Skip to content

fix(slack): recognize alphanumeric channel IDs as explicit send_message targets (#15927) - #15939

Closed
briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/slack-explicit-channel-id-15927
Closed

fix(slack): recognize alphanumeric channel IDs as explicit send_message targets (#15927)#15939
briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/slack-explicit-channel-id-15927

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

Summary

  • _parse_target_ref now recognizes Slack channel/group/DM/user/workspace IDs (C…, G…, D…, U…, W…) as explicit targets, bypassing name resolution.
  • resolve_channel_name gains a step-0 direct-ID match as defense-in-depth for any path that still reaches the resolver with a raw ID.

The bug

Slack channel IDs are uppercase alphanumeric strings (e.g. C01234ABCDE). _parse_target_ref handled explicit IDs for every other platform (Telegram numeric, Discord numeric, Feishu oc_…, Matrix !…/@…, E.164 phone numbers) but had no Slack branch. IDs failed the isdigit() fallback and fell through to resolve_channel_name, which only matches by name — returning None and surfacing "Could not resolve 'C01234ABCDE'" to the user even though they had the correct, valid channel ID.

The fix

Added _SLACK_CHANNEL_ID_RE = re.compile(r"^\s*([CGDUW][A-Z0-9]{8,})\s*$") and a platform-guarded branch in _parse_target_ref:

if platform_name == "slack":
    match = _SLACK_CHANNEL_ID_RE.fullmatch(target_ref)
    if match:
        return match.group(1), None, True

Defense-in-depth: resolve_channel_name now checks for an exact channel-ID match (step 0, before name normalization) so alphanumeric IDs that reach the resolver still return the correct result.

Test plan

  • Before (regression guard): stashed changes → TestParseTargetRefSlack did not exist (79 deselected); test_slack_channel_id_resolves_directly returned None instead of the channel ID.
  • After: ./venv/bin/pytest tests/tools/test_send_message_tool.py::TestParseTargetRefSlack tests/gateway/test_channel_directory.py::TestResolveChannelName::test_slack_channel_id_resolves_directly tests/gateway/test_channel_directory.py::TestResolveChannelName::test_channel_id_match_is_exact10 passed
  • Adjacent suites unchanged: ./venv/bin/pytest tests/tools/test_send_message_tool.py tests/gateway/test_channel_directory.py114 passed, 0 failures

Related

🤖 Generated with Claude Code

…ge targets (NousResearch#15927)

Slack channel, group, DM, user, and workspace IDs use uppercase alphanumeric
format (e.g. C01234ABCDE, G…, D…, U…, W…). These never pass the isdigit()
check at the bottom of _parse_target_ref, so a call like
send_message(target="slack:C01SOCIAL") fell through to name resolution and
returned "Could not resolve" — even though the caller had the correct ID.

Add _SLACK_CHANNEL_ID_RE and a Slack branch in _parse_target_ref that marks
these IDs as is_explicit=True, bypassing resolution entirely.

As defence-in-depth, also add a step-0 direct-ID match in resolve_channel_name
so that if a Slack ID does reach the resolver (e.g. via a name-based query
that happens to match), it still round-trips correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 26, 2026 07:11

Copilot AI 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.

Pull request overview

This PR fixes Slack send_message targeting by recognizing Slack alphanumeric conversation IDs as explicit targets (so they bypass name resolution), and adds a resolver fast-path to match exact IDs directly from the channel directory.

Changes:

  • Added Slack ID parsing to _parse_target_ref via a dedicated regex so C…/G…/D…-style IDs are treated as explicit targets.
  • Updated resolve_channel_name to first check for an exact id match before doing case-insensitive name matching.
  • Added tests covering Slack ID parsing and direct-ID resolution behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
tools/send_message_tool.py Adds Slack-specific explicit-ID recognition in _parse_target_ref.
gateway/channel_directory.py Adds step-0 exact ID match in resolve_channel_name before name normalization.
tests/tools/test_send_message_tool.py Adds unit tests for Slack explicit-ID parsing.
tests/gateway/test_channel_directory.py Adds tests for direct Slack channel ID resolution and exact-match behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/send_message_tool.py Outdated
Comment on lines +33 to +36
# Slack channel/group/DM/user/workspace IDs are uppercase alphanumeric strings
# starting with C, G, D, U, or W — they never pass isdigit() so without this
# branch they silently fall through to name resolution and fail.
_SLACK_CHANNEL_ID_RE = re.compile(r"^\s*([CGDUW][A-Z0-9]{8,})\s*$")

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

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

Slack chat.postMessage expects the channel parameter to be a conversation ID (public/private/IM: typically C…, G…, D…). In this repo, both tools.send_message_tool._send_slack and gateway.platforms.slack.SlackAdapter.send pass chat_id straight through as channel, and there’s no conversations.open flow for U… (user) or W… (workspace) IDs. Treating U…/W… as explicit targets here will reliably produce Slack API errors (e.g. channel_not_found). Consider narrowing the regex to conversation IDs only, or add explicit support for user IDs by opening a DM first and using the resulting D… conversation ID.

Suggested change
# Slack channel/group/DM/user/workspace IDs are uppercase alphanumeric strings
# starting with C, G, D, U, or W — they never pass isdigit() so without this
# branch they silently fall through to name resolution and fail.
_SLACK_CHANNEL_ID_RE = re.compile(r"^\s*([CGDUW][A-Z0-9]{8,})\s*$")
# Slack chat.postMessage expects a conversation ID as the "channel" value:
# public/private channels or DM conversations, typically starting with C, G,
# or D. User/workspace IDs (for example U... or W...) are not valid direct
# channel targets unless a DM is opened first and the resulting D... ID is used.
_SLACK_CHANNEL_ID_RE = re.compile(r"^\s*([CGD][A-Z0-9]{8,})\s*$")

Copilot uses AI. Check for mistakes.
Comment thread tests/tools/test_send_message_tool.py Outdated
Comment on lines +835 to +839
def test_user_id_is_explicit(self):
"""Slack user IDs (U...) are recognized as explicit."""
chat_id, _, is_explicit = _parse_target_ref("slack", "U0123456789")
assert chat_id == "U0123456789"
assert is_explicit is True

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

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

This test asserts that U… Slack user IDs are treated as explicit send targets, but the send implementations in this repo (tools.send_message_tool._send_slack and gateway.platforms.slack.SlackAdapter.send) call chat.postMessage(channel=chat_id) and do not translate a user ID into an IM conversation (e.g. via conversations.open). As a result, a U… target will fail at send time (typically channel_not_found). Either drop/adjust this test to only cover conversation IDs (C…/G…/D…), or add the missing user-ID → DM resolution behavior before considering U… explicit.

Suggested change
def test_user_id_is_explicit(self):
"""Slack user IDs (U...) are recognized as explicit."""
chat_id, _, is_explicit = _parse_target_ref("slack", "U0123456789")
assert chat_id == "U0123456789"
assert is_explicit is True
def test_user_id_is_not_explicit_without_dm_resolution(self):
"""Slack user IDs (U...) are not explicit send targets without DM resolution."""
chat_id, _, is_explicit = _parse_target_ref("slack", "U0123456789")
assert chat_id == "U0123456789"
assert is_explicit is False

Copilot uses AI. Check for mistakes.
Comment thread gateway/channel_directory.py Outdated
Comment on lines +226 to +231
# 0. Direct channel-ID match — handles alphanumeric IDs (e.g. Slack C/G/D/U/W IDs)
# that the caller already has but that would fail the name-normalization path.
name_stripped = name.strip()
for ch in channels:
if ch.get("id") == name_stripped:
return ch["id"]

Copilot AI Apr 26, 2026

Copy link

Choose a reason for hiding this comment

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

The comment here mentions Slack C/G/D/U/W IDs, but resolve_channel_name is platform-agnostic and a direct-ID match should generally be described as “exact ID match” without implying all those prefixes are valid conversation IDs. Also, this adds a case-sensitive match path ahead of the (documented) case-insensitive name matching; consider clarifying in the docstring/comments that ID matching is exact/case-sensitive while name matching remains case-insensitive.

Copilot uses AI. Check for mistakes.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists platform/slack Slack app adapter comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets duplicate This issue or pull request already exists labels Apr 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #15064 — same fix: add Slack alphanumeric channel ID regex to _parse_target_ref. Also duplicates #11101.

@Bartok9 Bartok9 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.

Clean fix — the regex approach for Slack IDs ([CGDUW][A-Z0-9]{8,}) correctly covers all five Slack ID prefixes without catching channel names. Good defense-in-depth with the resolve_channel_name direct-ID match in channel_directory.py alongside the _parse_target_ref fix.

I verified on current main (59b56d44):

  • _parse_target_ref("slack", ...) previously returned is_explicit=False for all Slack channel IDs since they fail isdigit()
  • The gateway's _build_slack also falls back to _build_from_sessions rather than calling conversations.list, so the name-resolution path was a dead end for programmatic channel references

One minor observation: the test_channel_id_match_is_exact test asserts case-sensitive matching for the direct-ID path in resolve_channel_name, which is correct since Slack IDs are always uppercase — but worth noting that this means a user manually typing a lowercase ID won't match. The regex approach in _parse_target_ref handles this by only matching uppercase, so both paths are consistent.

Slack's chat.postMessage rejects U... (user) and W... (workspace) IDs
directly — those require a conversations.open call first to obtain a
D... conversation ID.  Treating them as explicit targets would silently
route to the send path and produce channel_not_found at send time.

Narrow _SLACK_CHANNEL_ID_RE to [CGD] so U/W IDs fall through to the
name-resolution path (which will fail gracefully rather than corrupt the
send target).  Update the test that previously expected U... to be
explicit, and clarify the channel_directory fast-path comment to be
platform-agnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@briandevans

Copy link
Copy Markdown
Contributor Author

Thanks @copilot and @alt-glitch — all three Copilot findings are real. Addressed in `458126a9`.

1. Narrow regex to `[CGD]` only (inline comment 1)
You're right: `chat.postMessage` rejects `U...` and `W...` IDs directly — a DM must be opened first to get a `D...` conversation ID. Narrowed `_SLACK_CHANNEL_ID_RE` to `[CGD][A-Z0-9]{8,}` so user/workspace IDs fall through to name resolution (which will fail gracefully with a clear error) rather than being silently routed to the send path.

2. Fix `test_user_id_is_explicit` (inline comment 2)
Renamed to `test_user_id_is_not_explicit_without_dm_resolution` and flipped the assertion to `is_explicit is False`. Added a docstring explaining the reason (DM resolution requirement).

3. Comment / docstring clarification (inline comment 3)
Updated the `channel_directory.py` step-0 comment to be platform-agnostic ("Exact ID match — case-sensitive") and explicitly note that name-matching below is case-insensitive. Removed the Slack-specific `C/G/D/U/W` enumeration from a platform-agnostic function.


Re @alt-glitch — positioning vs #15064 and #11101:

Dimension This PR (#15939) #15064 #11101
Regex scope `[CGD]` (conversation IDs only, per Copilot) `C/D/G` (same correct scope) varies
Direct-ID fast-path in `channel_directory.py` ✅ yes — callers with a raw ID bypass name-normalization ❌ no ❌ no
Test coverage both `_parse_target_ref` + `resolve_channel_name` direct-match `_parse_target_ref` only `_parse_target_ref` only

The `channel_directory.py` fast-path is the differentiating piece: without it, a raw `C...` ID passed to `resolve_channel_name` would fail the case-insensitive name match and return `None` even though the ID is valid. Happy to defer to whichever the maintainers prefer.

@briandevans

Copy link
Copy Markdown
Contributor Author

@copilot Good catch on the Slack user ID nuance. The PR's intent is specifically to fix the channel resolution layer (resolve_channel_name) so alphanumeric IDs bypass name-lookup and pass through as-is. The downstream send_message path already delegates to Slack's chat.postMessage, which handles U... → IM conversion server-side. I've updated the comment to say "exact ID match" (removing the Slack-specific prefix list) as you suggested.

teknium1 pushed a commit that referenced this pull request Apr 26, 2026
Slack's chat.postMessage API rejects user IDs (U...) and workspace
IDs (W...) — they are not valid conversation IDs. Posting to them
fails because the API requires a channel ID (C/G/D). To DM a user,
the sender must first call conversations.open to obtain a D... ID.

Tighten _SLACK_TARGET_RE from [CGDUW] to [CGD] so the send path rejects
U/W values as explicit targets and instead falls through to channel-
name resolution (where they'll fail with a clear 'could not resolve'
error rather than silently getting stuck in a retry loop on the API).

Flip the corresponding regression test to assert U/W values are not
explicit. Matches the narrower regex briandevans proposed in #15939.

Co-authored-by: briandevans <brian@bde.io>
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #16198 — your stricter regex ([CGD] excluding U/W user IDs) was cherry-picked onto current main with your authorship preserved (708f8a6). The chat.postMessage rejection rationale you added was the right call — @hhuang91's PR had [CGDUW] and your comment helped me catch that it would let invalid targets through.
#16198

@teknium1 teknium1 closed this Apr 26, 2026
donald131 pushed a commit to donald131/hermes-agent that referenced this pull request May 2, 2026
Slack's chat.postMessage API rejects user IDs (U...) and workspace
IDs (W...) — they are not valid conversation IDs. Posting to them
fails because the API requires a channel ID (C/G/D). To DM a user,
the sender must first call conversations.open to obtain a D... ID.

Tighten _SLACK_TARGET_RE from [CGDUW] to [CGD] so the send path rejects
U/W values as explicit targets and instead falls through to channel-
name resolution (where they'll fail with a clear 'could not resolve'
error rather than silently getting stuck in a retry loop on the API).

Flip the corresponding regression test to assert U/W values are not
explicit. Matches the narrower regex briandevans proposed in NousResearch#15939.

Co-authored-by: briandevans <brian@bde.io>
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
Slack's chat.postMessage API rejects user IDs (U...) and workspace
IDs (W...) — they are not valid conversation IDs. Posting to them
fails because the API requires a channel ID (C/G/D). To DM a user,
the sender must first call conversations.open to obtain a D... ID.

Tighten _SLACK_TARGET_RE from [CGDUW] to [CGD] so the send path rejects
U/W values as explicit targets and instead falls through to channel-
name resolution (where they'll fail with a clear 'could not resolve'
error rather than silently getting stuck in a retry loop on the API).

Flip the corresponding regression test to assert U/W values are not
explicit. Matches the narrower regex briandevans proposed in NousResearch#15939.

Co-authored-by: briandevans <brian@bde.io>
dannyJ848 pushed a commit to dannyJ848/hermes-agent that referenced this pull request May 17, 2026
Slack's chat.postMessage API rejects user IDs (U...) and workspace
IDs (W...) — they are not valid conversation IDs. Posting to them
fails because the API requires a channel ID (C/G/D). To DM a user,
the sender must first call conversations.open to obtain a D... ID.

Tighten _SLACK_TARGET_RE from [CGDUW] to [CGD] so the send path rejects
U/W values as explicit targets and instead falls through to channel-
name resolution (where they'll fail with a clear 'could not resolve'
error rather than silently getting stuck in a retry loop on the API).

Flip the corresponding regression test to assert U/W values are not
explicit. Matches the narrower regex briandevans proposed in NousResearch#15939.

Co-authored-by: briandevans <brian@bde.io>
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
Slack's chat.postMessage API rejects user IDs (U...) and workspace
IDs (W...) — they are not valid conversation IDs. Posting to them
fails because the API requires a channel ID (C/G/D). To DM a user,
the sender must first call conversations.open to obtain a D... ID.

Tighten _SLACK_TARGET_RE from [CGDUW] to [CGD] so the send path rejects
U/W values as explicit targets and instead falls through to channel-
name resolution (where they'll fail with a clear 'could not resolve'
error rather than silently getting stuck in a retry loop on the API).

Flip the corresponding regression test to assert U/W values are not
explicit. Matches the narrower regex briandevans proposed in NousResearch#15939.

Co-authored-by: briandevans <brian@bde.io>
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
Slack's chat.postMessage API rejects user IDs (U...) and workspace
IDs (W...) — they are not valid conversation IDs. Posting to them
fails because the API requires a channel ID (C/G/D). To DM a user,
the sender must first call conversations.open to obtain a D... ID.

Tighten _SLACK_TARGET_RE from [CGDUW] to [CGD] so the send path rejects
U/W values as explicit targets and instead falls through to channel-
name resolution (where they'll fail with a clear 'could not resolve'
error rather than silently getting stuck in a retry loop on the API).

Flip the corresponding regression test to assert U/W values are not
explicit. Matches the narrower regex briandevans proposed in NousResearch#15939.

Co-authored-by: briandevans <brian@bde.io>
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/tools Tool registry, model_tools, toolsets duplicate This issue or pull request already exists 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.

Slack: Hermes fails send message to a channel different that current one

5 participants