Skip to content

fix(slack): don't thread block-action responses in DMs#133

Closed
tony-chinchill-ai wants to merge 1 commit into
Chinchill-AI:mainfrom
tony-chinchill-ai:fix/slack-dm-block-action-threading
Closed

fix(slack): don't thread block-action responses in DMs#133
tony-chinchill-ai wants to merge 1 commit into
Chinchill-AI:mainfrom
tony-chinchill-ai:fix/slack-dm-block-action-threading

Conversation

@tony-chinchill-ai

@tony-chinchill-ai tony-chinchill-ai commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Problem

When a HITL approval button is clicked in a Slack DM, anything the handler posts via event.thread.post(...) (e.g. an approval result card) becomes a phantom "1 reply" thread in the DM. DMs should never have reply threads.

Root cause

_handle_block_actions set thread_ts by falling back to the clicked message's own ts, with no DM special-casing — unlike its sibling _handle_message_event, which already sets thread_ts="" for top-level DM messages. The two had diverged.

Fix

Mirror _handle_message_event's DM handling in _handle_block_actions: detect DMs (channel starts with D) and never fall back to message_ts for a top-level DM click. A real in-DM thread_ts (a click inside an existing DM thread) is still preserved; channel behavior is unchanged.

Tests (tests/test_slack_webhook.py)

  • test_dm_block_action_does_not_thread — regression: a DM click resolves to a DM-root thread_id (empty thread_ts); fails when the fix is reverted.
  • test_channel_block_action_threads_under_clicked_message — guards that channel threading is preserved.

🤖 Generated with Claude Code

Summary by CodeRabbit

Bug Fixes

  • Fixed an issue where clicking buttons in Slack direct messages would incorrectly create phantom threads appearing as "1 reply".
  • Enhanced thread handling for Slack interactive actions to correctly differentiate between direct message and channel message contexts, ensuring proper thread ID encoding.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tony-chinchill-ai, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 42 minutes and 50 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e45c53c-bf4a-44fa-b75e-e6802acb0778

📥 Commits

Reviewing files that changed from the base of the PR and between 1a5697a and 32d99f5.

⛔ Files ignored due to path filters (3)
  • .DS_Store is excluded by !**/.DS_Store
  • src/.DS_Store is excluded by !**/.DS_Store
  • src/chat_sdk/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (2)
  • src/chat_sdk/adapters/slack/adapter.py
  • tests/test_slack_webhook.py
📝 Walkthrough

Walkthrough

Slack adapter's block_actions handler now detects direct message channels and prevents phantom threaded replies by avoiding fallback to the clicked message timestamp for DM thread context. Thread ID encoding logic simplified to use computed thread_ts directly.

Changes

DM-aware thread ID computation

Layer / File(s) Summary
Block actions DM-aware thread computation
src/chat_sdk/adapters/slack/adapter.py
block_actions adds is_dm detection and adjusts thread_ts selection to prevent DM button clicks from creating phantom threaded replies. Thread ID encoding passes computed thread_ts directly without fallback to message_ts.
Thread ID behavior regression tests
tests/test_slack_webhook.py
Two new async test cases validate that DM top-level clicks produce empty thread_ts in decoded thread ID, and channel top-level clicks preserve thread_ts matching the clicked message.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A rabbit hops through threads so neat,
DMs now keep their beat—
No phantom replies in sight,
Thread IDs encoded right! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(slack): don't thread block-action responses in DMs' directly and concisely describes the main change: preventing threading of block-action responses in Slack direct messages.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 modifies the Slack adapter to prevent block actions on top-level Direct Messages (DMs) from creating phantom reply threads, as DMs do not support threading. It introduces a check to identify DM channels and ensures that the thread timestamp does not fall back to the message timestamp in those cases, while preserving the existing threading behavior for regular channels. Unit tests have been added to verify both behaviors. No review comments were provided, so there is no additional feedback to address.

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.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/chat_sdk/adapters/slack/adapter.py`:
- Line 1627: The value thread_ts may be None and violates the
SlackThreadId.thread_ts: str contract; before constructing SlackThreadId in the
encode_thread_id call, normalize thread_ts to a guaranteed string (e.g., convert
non-None to str and provide a safe default like an empty string when None) so
SlackThreadId(thread_ts=...) always receives a str; update the code path that
assigns thread_id (the line calling encode_thread_id and SlackThreadId) to use
this normalized thread_ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a67aea4-47be-4797-b98c-2a491619672f

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd86e7 and 1a5697a.

📒 Files selected for processing (2)
  • src/chat_sdk/adapters/slack/adapter.py
  • tests/test_slack_webhook.py

thread_id = ""
if channel and (thread_ts or message_ts):
thread_id = self.encode_thread_id(SlackThreadId(channel=channel, thread_ts=thread_ts or message_ts or ""))
thread_id = self.encode_thread_id(SlackThreadId(channel=channel, thread_ts=thread_ts))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize thread_ts to a guaranteed str before SlackThreadId construction.

CI is failing here because thread_ts is inferred as possibly None. This breaks the SlackThreadId.thread_ts: str contract and blocks type checks.

Suggested fix
-        thread_ts = (
+        thread_ts_raw = (
             (payload.get("message") or {}).get("thread_ts")
             or (payload.get("container") or {}).get("thread_ts")
             or ("" if is_dm else message_ts)
         )
+        thread_ts = str(thread_ts_raw) if thread_ts_raw is not None else ""
 
         is_view_action = (payload.get("container") or {}).get("type") == "view"
 
         if not (is_view_action or channel):
             self._logger.warn("Missing channel in block_actions", {"channel": channel})
             return
@@
         thread_id = ""
         if channel and (thread_ts or message_ts):
             thread_id = self.encode_thread_id(SlackThreadId(channel=channel, thread_ts=thread_ts))
🧰 Tools
🪛 GitHub Actions: Lint & Type Check / 0_Lint & Type Check.txt

[error] 1627-1627: Type checking failed (pyrefly). Argument Literal[''] | Unknown | None is not assignable to parameter thread_ts with type str in chat_sdk.adapters.slack.types.SlackThreadId.__init__ [bad-argument-type].

🪛 GitHub Actions: Lint & Type Check / Lint & Type Check

[error] 1627-1627: pyrefly type check failed with [bad-argument-type]: Argument Literal[''] | Unknown | None is not assignable to parameter thread_ts with type str in chat_sdk.adapters.slack.types.SlackThreadId.__init__.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/chat_sdk/adapters/slack/adapter.py` at line 1627, The value thread_ts may
be None and violates the SlackThreadId.thread_ts: str contract; before
constructing SlackThreadId in the encode_thread_id call, normalize thread_ts to
a guaranteed string (e.g., convert non-None to str and provide a safe default
like an empty string when None) so SlackThreadId(thread_ts=...) always receives
a str; update the code path that assigns thread_id (the line calling
encode_thread_id and SlackThreadId) to use this normalized thread_ts.

Source: Pipeline failures

_handle_block_actions fell back to the clicked message's ts for thread_ts
in DMs, so HITL approval result cards posted via event.thread.post became
phantom "1 reply" threads in the DM. Mirror _handle_message_event's DM
handling: keep a real in-DM thread_ts but never fall back to message_ts
for a top-level DM click.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tony-chinchill-ai tony-chinchill-ai force-pushed the fix/slack-dm-block-action-threading branch from 1a5697a to 32d99f5 Compare June 5, 2026 00:35
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

patrick-chinchill added a commit that referenced this pull request Jun 17, 2026
)

Upstream handleBlockActions falls back to messageTs even for DMs (phantom thread); we empty-case DMs to match upstream's own handleMessageEvent convention (index.ts:2158). Documented per the project divergence rule; cf. PR #107.
patrick-chinchill added a commit that referenced this pull request Jun 18, 2026
…) (#137)

* fix(slack): don't thread block-action responses in DMs

_handle_block_actions fell back to the clicked message's ts for thread_ts
in DMs, so HITL approval result cards posted via event.thread.post became
phantom "1 reply" threads in the DM. Mirror _handle_message_event's DM
handling: keep a real in-DM thread_ts but never fall back to message_ts
for a top-level DM click.

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

* fix(slack): restore thread_ts str-coalesce + drop committed .DS_Store

Address two defects from local review of the DM block-action threading fix:

1. pyrefly bad-argument-type regression: the block-action handler replaced the
   established `thread_ts or message_ts or ""` coalesce with bare `thread_ts`,
   letting `str | None` flow into SlackThreadId(thread_ts: str). Restore the
   coalesce as `thread_ts or ""`, which both preserves the DM empty-thread_ts
   intent and guarantees `str` (thread_ts already folds in the message_ts
   fallback for channels). pyrefly src/ back to 0 errors.

2. Remove three committed .DS_Store binaries (./, src/, src/chat_sdk/) and add
   .DS_Store to .gitignore so they cannot return.

Also add a channel-path counterpart test
(test_channel_block_action_threads_under_clicked_message) so the DM-only guard
is proven not to leak into channels, alongside the existing DM regression test.

* fix(slack): use explicit None-coalesce for DM thread_ts at encode time

Per CLAUDE.md port rule, prefer 'x if x is not None else default' over 'x or default'. Functionally identical here (only falsy value is the intended empty string) and keeps the SlackThreadId(thread_ts: str) guarantee that pyrefly checks.

* docs(sync): record Slack DM block-action threading divergence (#133/#137)

Upstream handleBlockActions falls back to messageTs even for DMs (phantom thread); we empty-case DMs to match upstream's own handleMessageEvent convention (index.ts:2158). Documented per the project divergence rule; cf. PR #107.

---------

Co-authored-by: tony-chinchill <tony@chinchill.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
patrick-chinchill added a commit that referenced this pull request Jun 19, 2026
CHANGELOG:
- Drop false gchat file-delivery bullet (PR #112 never merged; upstream
  itself does not implement gchat outbound file delivery)
- Fix Slack DM block-action citation to merged PR #137 (supersedes #133)
- Re-point 0.4.27.1 vehicle from unmerged #120 to tag v0.4.27.1 + #117
- Drop numberless Twilio 'scaffolding PR' (only #142 exists)

UPSTREAM_SYNC:
- Reframe Google Chat outbound file delivery as parity (upstream also
  logs 'not yet supported' + media.upload TODO, index.ts:1282-1289)
- Correct Teams cert-auth row: TS is not 'Supported' — config throws at
  startup in both SDKs (types.ts:31, config.ts:13); parity, issue #58
- Add Messenger get_user row: upstream has no getUser; raising stub is
  parity, Graph-API impl tracked as #132
patrick-chinchill added a commit that referenced this pull request Jun 19, 2026
…PR 4/4) (#146)

* chore(release): cut 0.4.30 — Teams SDK migration + 4.30.0 parity (#93 PR 4/4)

Final PR of the Teams SDK migration (issue #93). PRs 1-3 (inbound+auth,
outbound, native streaming) are merged; this cuts the 0.4.30 release.

- Version bump: pyproject 0.4.29 -> 0.4.30; UPSTREAM_PARITY "4.29.0" -> "4.30.0".
- Fidelity re-pin chat@4.29.0 -> chat@4.30.0 in lint.yml +
  verify_test_fidelity.py (docstring, default parity fallback, clone hint).
  packages/chat/src is byte-for-byte identical between the two tags, so zero
  new test ports: strict fidelity stays 100% (732/732, 0 missing) against
  chat@4.30.0. Baseline regenerated (ts_parity -> chat@4.30.0; the recorded
  total_ts_tests literal 731 -> 732 corrects a stale count from the merged
  adapter waves, not the re-pin — the count is identical against both tags).
- Docs: project-instructions version map + fidelity pin; README status line;
  CHANGELOG 0.4.30 entry (Twilio adapter, Telegram streaming, Slack subpaths,
  WhatsApp/Slack/gchat fixes, Teams #93 PRs 1-4); UPSTREAM_SYNC.md parity
  header + the Teams deferral row flipped to delivered.
- Version-label normalization: malformed `adapter-teams@chat@4.30.0` and loose
  `adapter-teams@4.30.0` -> `@chat-adapter/teams@4.30.0` in adapter.py (5),
  bridge.py (1), UPSTREAM_SYNC.md (4). Comment/doc-only.

Does NOT tag/publish — the release is a separate maintainer-gated step (live
Teams 429 streaming check + PyPI authorization).

* docs(release): clarify 0.4.30 core-parity wording + finish label normalization

Review fast-follow for PR 4. The CHANGELOG now states the mapped core is
content-identical *between the chat@4.29.0 and chat@4.30.0 upstream tags*
(verified: thread.ts/types.ts/thread.test.ts and the full packages/chat/src
tree are byte-identical) — the prior 'unchanged from 4.29.0' phrasing was
accurate but misread as a claim about our code. Also sweeps the two
remaining old-style `adapter-teams@chat@4.30.0` labels in the Teams test
docstrings to the canonical `@chat-adapter/teams@4.30.0` npm tag,
completing the normalization the PR's scope called for. No logic change.

* docs(release): document 0.4.30 parity-audit wave + remaining exceptions

CHANGELOG: add 'Pre-existing parity gaps closed (4.30 audit)' subsection
covering the 7 ported fixes (PRs #147-#150) and a documented-exceptions
note (Linear agent-sessions #151 deferred to 4.31 / #152; adapter-web and
the GitHub/Linear native-client + message.subject halves stay Known
Non-Parity).

UPSTREAM_SYNC:
- file-mapping table: add state-ioredis -> redis.py (IoRedisStateAdapter)
  and state-pg -> postgres.py rows (both ported + tested, were missing).
- Known Non-Parity (platform gaps): add Linear agent-sessions row (#151)
  and an adapter-web row that splits the portable server-side WebAdapter
  (deferred) from the genuinely browser-only client subpaths, correcting
  the earlier over-broad 'browser-only; no Python runtime' note.
- Correct the Google Chat file-uploads row: inbound attachment parsing is
  fully implemented (_create_attachment), so the gap is outbound file
  delivery only (post_message still logs 'not yet supported').

* docs(release): correct phantom-feature framing before PyPI cut

CHANGELOG:
- Drop false gchat file-delivery bullet (PR #112 never merged; upstream
  itself does not implement gchat outbound file delivery)
- Fix Slack DM block-action citation to merged PR #137 (supersedes #133)
- Re-point 0.4.27.1 vehicle from unmerged #120 to tag v0.4.27.1 + #117
- Drop numberless Twilio 'scaffolding PR' (only #142 exists)

UPSTREAM_SYNC:
- Reframe Google Chat outbound file delivery as parity (upstream also
  logs 'not yet supported' + media.upload TODO, index.ts:1282-1289)
- Correct Teams cert-auth row: TS is not 'Supported' — config throws at
  startup in both SDKs (types.ts:31, config.ts:13); parity, issue #58
- Add Messenger get_user row: upstream has no getUser; raising stub is
  parity, Graph-API impl tracked as #132

* docs(changelog): clarify 0.4.30 audit-gap count (8 closed + 1 deferred = 9)
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