Skip to content

fix(slack): route empty-DM thread_ts fetch to conversations.history (#138)#175

Merged
patrick-chinchill merged 1 commit into
mainfrom
fix/slack-138-empty-dm-thread-ts-fetch
Jun 21, 2026
Merged

fix(slack): route empty-DM thread_ts fetch to conversations.history (#138)#175
patrick-chinchill merged 1 commit into
mainfrom
fix/slack-138-empty-dm-thread-ts-fetch

Conversation

@patrick-chinchill

@patrick-chinchill patrick-chinchill commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes Slack issue #138 — a top-level Slack DM root legitimately encodes an empty thread_ts (slack:Dxxx:, faithful to _handle_message_event / upstream handleMessageEvent). The fetch paths previously called conversations.replies(ts="") unconditionally, which Slack answers with no replies — silently losing the DM root context for every DM history/single-message fetch (and the #137 DM block-action consumer).

Change

src/chat_sdk/adapters/slack/adapter.py:

  • fetch_messages — when thread_ts is empty/falsy (DM root), route to the existing channel-history helpers (_fetch_channel_messages_forward / _fetch_channel_messages_backward, both conversations.history) instead of the thread-reply helpers. Direction, limit, and cursor semantics are preserved. For a DM the channel is the conversation, so the channel-history mapping is correct. No conversations.history logic is duplicated.
  • fetch_message — when thread_ts is empty, fetch the single message via conversations.history(channel, latest=message_id, inclusive=True, limit=1), mirroring the inner link-preview fetch_message. Non-empty path is byte-identical.
  • Non-empty thread_ts stays byte-identical — still conversations.replies. Only the empty-thread_ts branch changes.

Divergence

This is a divergence ahead of upstream: upstream's fetchMessages (adapter-slack/src/index.ts:4135fetchMessagesForward/Backward at :4187/:4250) and fetchMessage (:4350) call conversations.replies({ ts: threadTs }) with no empty-thread_ts guard either. A new Known Non-Parity row in docs/UPSTREAM_SYNC.md documents this, notes it covers DM messages and the #137 DM block-action path uniformly (superseding the "separate follow-up" noted in the #133/#137 row), and flags it as a candidate to file upstream so it can be removed on a future sync.

Tests (tests/test_slack_api.py)

  • Empty-DM fetch_messages (forward + backward): asserts conversations.history is called for the channel and conversations.replies is never called (esp. not with ts=""); returns the DM messages.
  • Empty-thread_ts fetch_message: asserts conversations.history (latest=message_id, inclusive, limit=1) is used, not conversations.replies.
  • Non-empty thread_ts fetch_messages (forward + backward) and fetch_message: regression guards asserting conversations.replies is still used and conversations.history is not (fails if the routing over-triggers).
  • AsyncMock'd Slack client throughout.

Mutation proofs: removing the empty-guard routing → the empty-DM fetch_messages/fetch_message tests fail (conversations.replies(ts="") yields nothing). Over-triggering the guard to fire for all thread_ts → all three non-empty regression guards fail.

Gauntlet (all green)

  • ruff check / ruff format --check — pass
  • audit_test_quality.py — 0 hard failures
  • pyrefly check src/ — 0 errors
  • verify_test_fidelity.py --strict (TS_ROOT=chat@4.31.0) — 732/732 matched (100%)
  • pytest tests/ — 5252 passed, 4 skipped (pre-existing); pytest tests/test_slack*.py — 669 passed

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Slack direct message handling to prevent phantom "reply thread" creation when using button actions. Updated message-fetching logic to correctly route empty thread timestamps in DM threads to appropriate API endpoints.
  • Tests

    • Added comprehensive test coverage for DM thread root routing behavior, including backward and forward message fetching and single-message lookups.

…138)

A top-level Slack DM root encodes an empty thread_ts (slack:Dxxx:). The
fetch paths previously called conversations.replies(ts="") unconditionally,
which returns no replies and loses the DM root context for every DM.

- fetch_messages: when thread_ts is empty (falsy), route to the existing
  channel-history helpers (_fetch_channel_messages_forward /
  _fetch_channel_messages_backward, both conversations.history) instead of
  the thread-reply helpers. Direction, limit, and cursor semantics are
  preserved; for a DM the channel IS the conversation, so this is correct.
- fetch_message: when thread_ts is empty, fetch the single message via
  conversations.history(latest=message_id, inclusive=True, limit=1),
  mirroring the inner link-preview fetch_message.
- Non-empty thread_ts stays byte-identical (still conversations.replies).

This is a divergence ahead of upstream — upstream's fetchMessages /
fetchMessage call conversations.replies(ts: threadTs) with no empty-thread_ts
guard either. Documented in docs/UPSTREAM_SYNC.md as a candidate to file
upstream; it also covers the #137 DM block-action consumer uniformly.

Tests: empty-DM fetch_messages (forward + backward) and fetch_message assert
conversations.history is used and conversations.replies(ts="") is never
called; non-empty thread_ts regression guards assert conversations.replies is
still used (so the routing cannot over-trigger).
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 88a49a9f-6eb1-4d86-879b-a604558986bb

📥 Commits

Reviewing files that changed from the base of the PR and between 3d753c0 and 9d8791d.

📒 Files selected for processing (3)
  • docs/UPSTREAM_SYNC.md
  • src/chat_sdk/adapters/slack/adapter.py
  • tests/test_slack_api.py

📝 Walkthrough

Walkthrough

The Slack adapter's fetch_messages and fetch_message methods now branch on whether thread_ts is empty. DM roots (empty thread_ts) are routed to conversations_history; real threads continue using conversations_replies. Tests and upstream non-parity documentation are updated to match.

Changes

Slack DM root fetch routing

Layer / File(s) Summary
Empty thread_ts routing in fetch_messages and fetch_message
src/chat_sdk/adapters/slack/adapter.py
fetch_messages routes to channel-history helpers (forward/backward) when thread_ts is empty; fetch_message selects conversations_history for empty thread_ts and conversations_replies for non-empty thread_ts.
Routing tests for DM roots and real threads
tests/test_slack_api.py
New tests assert DM roots (slack:Dxxx:) route to conversations_history (never conversations_replies) for both directions and single-message lookup; regression tests confirm real threads continue to use conversations_replies with correct ts/oldest arguments.
Upstream non-parity doc update
docs/UPSTREAM_SYNC.md
Revises the DM block-action threading row and adds a new row documenting that empty/falsy thread_ts must be routed to conversations.history for DM roots across fetch_messages and fetch_message.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Possibly related PRs

  • Chinchill-AI/chat-sdk-python#137 — PR #137 encodes empty thread_ts in DM block_actions thread IDs; this PR adds the complementary fetch-routing logic that correctly interprets those empty-thread_ts DM root IDs.

Poem

🐇 Hoppity-hop through the DM lane,
No phantom threads shall cause us pain!
Empty thread_ts? History it goes,
conversations_history — that's how routing flows.
With tests and docs all neatly aligned,
The rabbit hops forward, leaving bugs behind! 🌟

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request addresses an issue where fetching messages for top-level Slack DMs (which have an empty thread_ts) fails because they are routed to conversations.replies instead of conversations.history. The changes route empty thread_ts fetches to conversations.history in both fetch_messages and fetch_message, accompanied by new unit tests and documentation updates. The reviewer identified a bug where helper methods for channel history fallback to using the message's own ts for the thread_id, causing a discrepancy (e.g., slack:Dxxx:ts instead of slack:Dxxx:). A code suggestion was provided to post-process the fetched messages and ensure they are assigned the correct thread_id.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +4328 to +4331
if not thread_ts:
if direction == "forward":
return await self._fetch_channel_messages_forward(channel, limit, cursor)
return await self._fetch_channel_messages_backward(channel, limit, cursor)

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.

medium

When thread_ts is empty (such as for top-level DM roots), fetch_messages routes the request to _fetch_channel_messages_forward or _fetch_channel_messages_backward. However, these helper methods dynamically construct the message thread_id using msg.get('thread_ts') or msg.get('ts', ''). For top-level DM messages, this falls back to the message's own ts, resulting in a thread ID like slack:Dxxx:ts instead of the expected slack:Dxxx: (with an empty thread_ts). This creates a discrepancy between real-time messages (which correctly use slack:Dxxx:) and fetched history messages.

We can resolve this by post-processing the fetched messages to ensure that any top-level DM messages (those without an explicit thread_ts in their raw payload) are assigned the correct thread_id.

Suggested change
if not thread_ts:
if direction == "forward":
return await self._fetch_channel_messages_forward(channel, limit, cursor)
return await self._fetch_channel_messages_backward(channel, limit, cursor)
if not thread_ts:
if direction == "forward":
result = await self._fetch_channel_messages_forward(channel, limit, cursor)
else:
result = await self._fetch_channel_messages_backward(channel, limit, cursor)
for msg in result.messages:
if msg.raw and not msg.raw.get("thread_ts"):
msg.thread_id = thread_id
return result

@patrick-chinchill patrick-chinchill marked this pull request as ready for review June 21, 2026 19:35
@patrick-chinchill patrick-chinchill merged commit 3ed4a9a into main Jun 21, 2026
8 of 9 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d8791d7f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4330 to +4331
return await self._fetch_channel_messages_forward(channel, limit, cursor)
return await self._fetch_channel_messages_backward(channel, limit, cursor)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve empty-DM thread IDs when fetching history

For an empty-DM thread like slack:D999:, routing through the channel-history helpers returns messages parsed with f"slack:{channel}:{msg.get('thread_ts') or msg.get('ts', '')}" (see the helper below), so every top-level DM message comes back with thread_id like slack:D999:<message-ts> instead of the requested slack:D999:. That is inconsistent with _handle_message_event, open_dm, and the new fetch_message branch, and any consumer that reuses a fetched message's threadId to post/stream will start addressing a Slack DM thread rather than the unthreaded DM conversation this change is trying to preserve.

Useful? React with 👍 / 👎.

patrick-chinchill added a commit that referenced this pull request Jun 21, 2026
Python-only patch on 4.31.0 (UPSTREAM_PARITY unchanged).
- Slack #138: empty-DM thread_ts fetch routes to conversations.history (PR #175)
- Slack #95: thread team_id to chat.startStream for Enterprise Grid (PR #176)
- docs: mark Linear agent-sessions mode experimental (unverified vs live tenant, #151)
- version 0.4.31 -> 0.4.31.1; CHANGELOG; CLAUDE.md mapping
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.

1 participant