fix(slack): resolve channel IDs to human-readable names

Previously, Slack sessions and the channel directory stored raw channel
IDs (e.g. C0ATFHY907L) as chat_name, making it impossible for operators
to identify channels in send_message listings or session data.

Changes:
- Add _channel_name_cache and _resolve_channel_name() to SlackAdapter
  that calls conversations.info (channels) or users.info (DMs) with
  in-memory caching to avoid repeated API calls
- Use resolved names in _handle_slack_message() build_source() calls
- Use cached names in _seed_assistant_thread_session()
- Enrich session-sourced entries in channel_directory._build_slack()
  by cross-referencing API results and falling back to conversations.info
  + users.info for DMs and private channels not in the bot's scope

Fixes: channel directory and session origins now show readable names
like 'general' or 'John Doe' instead of 'C0xxx' / 'D0xxx'.
This commit is contained in:
dirtyren 2026-04-30 01:42:15 +00:00 committed by Teknium
parent da131aef3a
commit 8685fea0ce
2 changed files with 105 additions and 5 deletions

View file

@ -321,10 +321,49 @@ async def _build_slack(adapter) -> List[Dict[str, Any]]:
continue
# Merge in DM/group entries discovered from session history.
# Build a lookup from API-discovered channels so we can enrich session entries.
api_name_lookup = {ch["id"]: ch["name"] for ch in channels}
for entry in await asyncio.to_thread(_build_from_sessions, "slack"):
if entry.get("id") not in seen_ids:
eid = entry.get("id")
if eid not in seen_ids:
# If the entry name is still a raw Slack ID (e.g. C0xxx / D0xxx),
# try to resolve it from the API lookup first.
if entry.get("name", "").startswith(("C0", "D0", "G0")):
if eid in api_name_lookup:
entry["name"] = api_name_lookup[eid]
channels.append(entry)
seen_ids.add(entry.get("id"))
seen_ids.add(eid)
# Resolve remaining raw-ID entries (DMs, private channels not in bot scope)
# by calling conversations.info + users.info for each.
unresolved = [ch for ch in channels if ch.get("name", "").startswith(("C0", "D0", "G0"))]
if unresolved and team_clients:
client = next(iter(team_clients.values()))
for entry in unresolved:
try:
resp = await client.conversations_info(channel=entry["id"])
if not resp.get("ok"):
continue
ch_info = resp.get("channel", {})
if ch_info.get("is_im"):
peer_user = ch_info.get("user", "")
if peer_user:
user_resp = await client.users_info(user=peer_user)
if user_resp.get("ok"):
u = user_resp["user"]
entry["name"] = (
u.get("profile", {}).get("display_name")
or u.get("real_name")
or u.get("name")
or entry["id"]
)
entry["type"] = "dm"
else:
entry["name"] = ch_info.get("name") or ch_info.get("name_normalized") or entry["id"]
except Exception as e:
logger.debug("Channel directory: failed to resolve %s: %s", entry["id"], e)
continue
return channels

View file

@ -902,6 +902,12 @@ class SlackAdapter(BasePlatformAdapter):
# tenant's display name.
self._user_name_cache: Dict[Tuple[str, str], str] = {}
self._USER_NAME_CACHE_MAX = 5000
# (team_id, channel_id) → resolved channel/DM display name. Channel
# IDs are workspace-local like user IDs, so scope by workspace too.
# Bounded like the sibling caches (grows per DM — DM channel IDs are
# per-user).
self._channel_name_cache: Dict[Tuple[str, str], str] = {}
self._CHANNEL_NAME_CACHE_MAX = 5000
# (team_id, user_id) → Slack bot identity, same workspace scoping as
# the name cache. Used to catch peer-agent posts that arrive as plain
# user messages without bot_id/subtype=bot_message markers.
@ -3825,6 +3831,53 @@ class SlackAdapter(BasePlatformAdapter):
del self._user_name_cache[old_key]
return name
async def _resolve_channel_name(
self, channel_id: str, team_id: str = ""
) -> str:
"""Resolve a Slack channel ID to a human-readable name (cached).
For public/private channels returns the channel name. For DMs (im)
returns the peer user's display name. Falls back to the raw
channel_id on any error, so logs and agent context degrade to the
current behavior rather than breaking message handling.
"""
if not channel_id:
return channel_id
team_id = str(team_id or self._channel_team.get(channel_id, ""))
cache_key = (team_id, str(channel_id))
cached = self._channel_name_cache.get(cache_key)
if cached is not None:
return cached
if not self._app:
return channel_id
try:
resp = await self._get_client(
channel_id, team_id=team_id or None
).conversations_info(channel=channel_id)
if not isinstance(resp, dict) or not resp.get("ok"):
name = channel_id
else:
ch = resp.get("channel") or {}
if ch.get("is_im"):
peer_user = ch.get("user", "")
name = (
await self._resolve_user_name(
peer_user, chat_id=channel_id, team_id=team_id
)
if peer_user
else channel_id
)
else:
name = ch.get("name") or ch.get("name_normalized") or channel_id
except Exception as e:
logger.debug("[Slack] conversations.info failed for %s: %s", channel_id, e)
name = channel_id
self._channel_name_cache[cache_key] = name
self._trim_oldest_dict_entries(
self._channel_name_cache, self._CHANNEL_NAME_CACHE_MAX
)
return name
async def _humanize_user_mentions(
self, text: str, chat_id: str = "", team_id: str = ""
) -> str:
@ -4562,7 +4615,9 @@ class SlackAdapter(BasePlatformAdapter):
source = self.build_source(
chat_id=channel_id,
chat_name=channel_id,
chat_name=self._channel_name_cache.get(
(str(metadata.get("team_id") or ""), channel_id), channel_id
),
chat_type="dm",
user_id=user_id,
thread_id=thread_ts,
@ -4599,7 +4654,9 @@ class SlackAdapter(BasePlatformAdapter):
source = self.build_source(
chat_id=channel_id,
chat_name=channel_id,
chat_name=self._channel_name_cache.get(
(str(metadata.get("team_id") or ""), channel_id), channel_id
),
chat_type="dm",
user_id=user_id,
chat_topic=metadata.get("context_channel_id") or None,
@ -6125,6 +6182,10 @@ class SlackAdapter(BasePlatformAdapter):
user_id, chat_id=channel_id, team_id=team_id
)
# Resolve channel display name (cached after first lookup) so logs
# and agent context show #channel / peer names instead of raw IDs.
channel_name = await self._resolve_channel_name(channel_id, team_id=team_id)
# Slack's AI Agent Messages tab shows visible app threads; title the
# first DM thread turn from the user's prompt when Slack AI APIs are
# available. This is best-effort and configurable via config.yaml.
@ -6139,7 +6200,7 @@ class SlackAdapter(BasePlatformAdapter):
# Build source
source = self.build_source(
chat_id=channel_id,
chat_name=channel_id, # Will be resolved later if needed
chat_name=channel_name,
chat_type="dm" if is_dm else "group",
user_id=user_id,
user_name=user_name,