Skip to content

fix(adapters): Slack streaming team_id + Teams DM Graph conversation IDs (vercel/chat#330, #403)#85

Draft
patrick-chinchill wants to merge 3 commits intomainfrom
claude/port-slack-team-id-teams-dm-graph-J7S7H
Draft

fix(adapters): Slack streaming team_id + Teams DM Graph conversation IDs (vercel/chat#330, #403)#85
patrick-chinchill wants to merge 3 commits intomainfrom
claude/port-slack-team-id-teams-dm-graph-J7S7H

Conversation

@patrick-chinchill
Copy link
Copy Markdown
Collaborator

Summary

Two small upstream bug-fix ports bundled into one PR. Both touch adapter dispatch code and have new regression tests that fail before the fix.

fix(slack): interactive-payload team_id through streaming context — vercel/chat#330

Slack carries the workspace ID in different shapes depending on the webhook envelope:

  • Message events (message, app_mention): top-level team_id / team (string).
  • Interactive payloads (block_actions, view_submission, …): nested team.id (object), with user.team_id as a final fallback.

The old extraction (raw.get("team_id") or raw.get("team")) returned the entire team dict for block_actions, which then traveled to the Slack adapter as recipient_team_id and either crashed Slack streaming API calls or routed them to the wrong workspace.

Moved the extraction into a dedicated _extract_slack_recipient_team_id helper in src/chat_sdk/thread.py that walks each shape in order and returns None when no string ID is found.

fix(teams): canonical DM conversation ID for Microsoft Graph API — vercel/chat#403

Bot Framework hands out opaque DM conversation IDs (e.g. a:1xWhatever) which Graph's /chats/{chat-id}/messages endpoint rejects with 404. The canonical Graph chat ID for a 1:1 DM is 19:{userAadId}_{botId}@unq.gbl.spaces.

  • Cache the user's AAD object ID from incoming activities (from.aadObjectId) into a new TeamsDmContext keyed by base conversation ID.
  • Add discriminated union TeamsGraphContext = TeamsChannelContext | TeamsDmContext.
  • Rename _get_channel_context_get_graph_context and add _chat_id_from_context() dispatch helper.
  • Update fetch_messages, fetch_channel_messages, and fetch_channel_info to dispatch on context type.
  • Backwards-compatible: cached entries written before #403 lack a type discriminator and are treated as channel.

Tests

  • tests/test_thread_faithful.py:
    • Parametrized test_should_pass_stream_options_from_current_message_context over all four Slack payload shapes (team_id, team string, team.id object, user.team_id fallback).
    • New test_concurrent_block_actions_team_ids_do_not_cross_contaminate covering hazard chore: bump to 0.0.1a3 #6 (no team_id leak across concurrent requests).
    • New test_should_forward_structured_stream_chunks_to_adapter_stream_from_an_action_created_thread (port of upstream's #330 test; restores 0-missing on thread.test.ts fidelity).
  • tests/test_teams_coverage.py: new TestGraphDmConversationIdResolution class with 8 cases covering _chat_id_from_context (DM / channel / no-context branches), _cache_user_context (DM cached / channel skipped / no-aad skipped / DM-like channel adversarial), and end-to-end fetch_messages (DM resolves to 19:{aadId}_{botId}@unq.gbl.spaces, group chat falls back to raw ID).
  • Each new test docstring carries a "What to fix if this fails:" line.

Adversarial checks ran per docs/SELF_REVIEW.md: pass-interaction (concurrent team_ids) and the "DM-like channel" misclassification both have explicit tests.

Test plan

  • uv run ruff check src/ tests/ scripts/ — clean
  • uv run ruff format --check src/ tests/ scripts/ — clean
  • uv run python scripts/audit_test_quality.py — 0 hard failures (39 pre-existing warnings unchanged)
  • TS_ROOT=/tmp/vercel-chat uv run python scripts/verify_test_fidelity.pythread.test.ts now reports 0 missing
  • uv run pytest tests/ --tb=short -q3681 passed, 2 skipped, 1 failed
    • Only failure: pre-existing tests/test_github_webhook.py::TestGitHubAdapterConstructor::test_throws_when_no_auth (called out as ignorable in the task brief)

Upstream refs

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj


Generated by Claude Code

claude added 3 commits May 8, 2026 02:52
vercel/chat#330)

Slack carries the workspace ID in different shapes depending on the
webhook envelope. Block_actions / view_submission payloads use a nested
``team.id`` (object) with ``user.team_id`` as a fallback, while message
events use the top-level ``team_id`` / ``team`` (string). The previous
``raw.get("team_id") or raw.get("team")`` extraction returned the entire
``team`` dict for block_actions, causing Slack streaming API calls to
fail or hit the wrong workspace.

Move the extraction into a dedicated helper that walks each shape in
order and returns ``None`` when no string ID is found.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
…est (vercel/chat#330)

Upstream's chat@4.27.0 added a thread.test.ts case verifying that a
block_actions-created thread can stream structured chunks (text +
TaskUpdateChunk) through ``adapter.stream`` with the resolved
``recipient_team_id``. Port it to keep test_fidelity at 0 missing for
the thread.test.ts mapping.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
…l/chat#403)

Bot Framework hands out opaque DM conversation IDs (e.g.
``a:1xWhatever``) which Microsoft Graph's
``/chats/{chat-id}/messages`` endpoint rejects with 404. The canonical
Graph chat ID for a 1:1 DM is ``19:{userAadId}_{botId}@unq.gbl.spaces``.

Cache the user's AAD object ID from incoming activities and resolve
the Graph chat ID before issuing Graph calls. Add a discriminated
union ``TeamsGraphContext`` (channel | DM) and dispatch on context
type from ``fetch_messages``, ``fetch_channel_messages``, and
``fetch_channel_info``. Group chats (no cached context) keep falling
back to the raw conversation ID, which works as-is with Graph.

Backwards-compatible with the pre-#403 cache shape: entries without a
``type`` discriminator are treated as ``channel``.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b734869-90cc-4a5e-8d86-42e04c0427fc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/port-slack-team-id-teams-dm-graph-J7S7H

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements canonical Microsoft Graph chat ID resolution for Teams DMs to prevent 404 errors and improves Slack workspace ID extraction from interactive payloads. The Teams adapter now caches and utilizes a broader Graph context for both channels and direct messages, ensuring the correct IDs are used for Graph API calls. Additionally, a new helper function in thread.py ensures correct Slack team ID parsing across different webhook shapes. Feedback was provided regarding duplicated pagination logic in the Teams adapter, suggesting a refactor to improve maintainability.

Comment on lines +1212 to +1225
chat_id = self._chat_id_from_context(graph_context, base_conversation_id)
if direction == "forward":
params = {"$top": limit, "$orderby": "createdDateTime asc"}
if options.cursor:
params["$filter"] = f"createdDateTime gt {options.cursor}"
graph_messages = await self._graph_list_chat_messages(chat_id, params)
has_more = len(graph_messages) >= limit
else:
params = {"$top": limit, "$orderby": "createdDateTime desc"}
if options.cursor:
params["$filter"] = f"createdDateTime lt {options.cursor}"
graph_messages = await self._graph_list_chat_messages(chat_id, params)
graph_messages.reverse()
has_more = len(graph_messages) >= limit
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The logic for handling forward and backward pagination is duplicated here and in fetch_messages (lines 1092-1110). To improve maintainability and reduce code duplication, you could refactor this block and apply a similar change in fetch_messages.

Here's a suggested refactoring for this block:

Suggested change
chat_id = self._chat_id_from_context(graph_context, base_conversation_id)
if direction == "forward":
params = {"$top": limit, "$orderby": "createdDateTime asc"}
if options.cursor:
params["$filter"] = f"createdDateTime gt {options.cursor}"
graph_messages = await self._graph_list_chat_messages(chat_id, params)
has_more = len(graph_messages) >= limit
else:
params = {"$top": limit, "$orderby": "createdDateTime desc"}
if options.cursor:
params["$filter"] = f"createdDateTime lt {options.cursor}"
graph_messages = await self._graph_list_chat_messages(chat_id, params)
graph_messages.reverse()
has_more = len(graph_messages) >= limit
chat_id = self._chat_id_from_context(graph_context, base_conversation_id)
order_by = "createdDateTime asc"
filter_op = "gt"
if direction != "forward":
order_by = "createdDateTime desc"
filter_op = "lt"
params = {"$top": limit, "$orderby": order_by}
if options.cursor:
params["$filter"] = f"createdDateTime {filter_op} {options.cursor}"
graph_messages = await self._graph_list_chat_messages(chat_id, params)
has_more = len(graph_messages) >= limit
if direction != "forward":
graph_messages.reverse()

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants