Skip to content

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

Merged
patrick-chinchill merged 5 commits into
mainfrom
claude/pr133-dm-threading-fix
Jun 18, 2026
Merged

fix(slack): don't thread block-action responses in DMs (supersedes #133)#137
patrick-chinchill merged 5 commits into
mainfrom
claude/pr133-dm-threading-fix

Conversation

@patrick-chinchill

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

Copy link
Copy Markdown
Collaborator

Clean re-roll of #133 with the two local-review defects fixed. #133's head is on an external fork (tony-chinchill-ai/chat-sdk-python) outside this session's push access, so the fixes land here on a main-repo branch instead. Supersedes #133 (please close #133 in favor of this).

Original change (credit @tony-chinchill-ai, from #133)

When a HITL approval button is clicked in a Slack DM, anything the handler posts via event.thread.post(...) became a phantom "1 reply" thread. _handle_block_actions fell back to the clicked message's own ts, unlike _handle_message_event which already sets thread_ts="" for top-level DMs. This mirrors the DM handling into the block-action path; channel behavior is unchanged.

Fixes applied on top (from the local review of #133)

  1. pyrefly regression fixedfix(slack): don't thread block-action responses in DMs #133 had replaced thread_ts=thread_ts or "" with bare thread_ts=thread_ts, letting None flow into SlackThreadId(thread_ts: str) (1 pyrefly bad-argument-type error). Restored thread_ts or "" — keeps the DM intent and the str guarantee.
  2. Removed committed .DS_Storefix(slack): don't thread block-action responses in DMs #133 committed 3 macOS junk files; removed all three and added .DS_Store to .gitignore.
  3. Added the missing channel testfix(slack): don't thread block-action responses in DMs #133's description claimed test_channel_block_action_threads_under_clicked_message but never included it; added it (asserts a channel click still threads under the clicked message's ts).

Validation

  • pyrefly check src/: 0 errors (regression gone)
  • pytest tests/test_slack_webhook.py: 74 passed; -k slack: 625 passed
  • ruff check + format: clean; audit_test_quality.py: 0 hard failures
  • Guard verified: reverting the DM fix makes test_dm_block_action_does_not_thread fail; restored.

Independently re-reviewed clean (CLEAN-MERGE) against the current train tip.

https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed Slack interactive button handling so direct messages no longer create an incorrect “phantom reply” thread, and channel actions thread under the correct message timestamp.
  • Tests
    • Added regression coverage for Slack block actions in both DM and channel scenarios to verify the dispatched event threading behavior.
  • Documentation
    • Documented the known Slack SDK parity difference for DM block-action threading and the resulting UX impact.
  • Chores
    • Updated .gitignore to ignore macOS .DS_Store files.

⚠️ Upstream-parity note (added after checking against upstream)

This is a deliberate divergence from upstream, not merely an internal-consistency fix as the summary above implies. Upstream's handleBlockActions (adapter-slack/src/index.ts:1455-1456,1470) computes thread_ts || container.thread_ts || messageTs and encodes threadTs || messageTs || "" — it falls back to the clicked message's ts even for DMs, so upstream also spawns the phantom DM reply thread. The inconsistency exists in upstream too: its handleMessageEvent empty-cases DMs (:2158), but handleBlockActions does not. This PR extends upstream's own DM-message convention to the block-action path.

tony-chinchill-ai and others added 2 commits June 4, 2026 17:35
_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>
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.
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c3facf0-22f0-43d8-b404-11f70f332bb0

📥 Commits

Reviewing files that changed from the base of the PR and between b290251 and c94e87c.

📒 Files selected for processing (2)
  • docs/UPSTREAM_SYNC.md
  • src/chat_sdk/adapters/slack/adapter.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/chat_sdk/adapters/slack/adapter.py

📝 Walkthrough

Walkthrough

The Slack adapter's block_actions handler adds explicit DM detection to prevent DM interactions from falling back to message_ts as thread_ts, which was creating phantom reply threads. SlackThreadId encoding now uses thread_ts or an empty string instead of defaulting to message_ts. Two regression tests cover DM and channel top-level button clicks. Documentation of the upstream non-parity is added, and .DS_Store is added to .gitignore.

Changes

DM block_actions thread_id phantom-reply fix

Layer / File(s) Summary
DM thread_ts detection and thread_id encoding fix
src/chat_sdk/adapters/slack/adapter.py
Adds is_dm detection in block_actions to suppress message_ts fallback for thread_ts in DMs; changes SlackThreadId construction to use thread_ts or "" instead of defaulting to message_ts.
Regression tests for DM and channel block_actions
tests/test_slack_webhook.py
Adds test_dm_block_action_does_not_thread (asserts empty thread_ts in decoded thread_id for top-level DM) and test_channel_block_action_threads_under_clicked_message (asserts thread_ts equals clicked message ts for top-level channel message).
Documentation and housekeeping
docs/UPSTREAM_SYNC.md, .gitignore
Adds row documenting the DM block-action threading divergence between Python and TypeScript implementations; adds .DS_Store to .gitignore.

Sequence Diagram

sequenceDiagram
  participant Slack
  participant SlackAdapter
  participant ActionEvent

  Slack->>SlackAdapter: block_actions (DM, no thread_ts)
  SlackAdapter->>SlackAdapter: detect is_dm=True, set thread_ts=""
  SlackAdapter->>ActionEvent: encode thread_id (channel, thread_ts="")
  
  Slack->>SlackAdapter: block_actions (channel, no thread_ts)
  SlackAdapter->>SlackAdapter: detect is_dm=False, set thread_ts=message_ts
  SlackAdapter->>ActionEvent: encode thread_id (channel, thread_ts=message_ts)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

  • #138: Directly addresses a downstream consequence of the DM block-action threading fix, proposing handling for empty thread_ts values in fetch paths now produced by this PR's changes.

Poem

🐰 A phantom thread haunted the DM one day,
Popping up where no reply should stay.
With a flick of is_dm and an empty string,
The ghost was banished — no more threading!
Now channels thread right, and DMs stay clean,
The tidiest burrow you've ever seen. ✨

🚥 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 directly and concisely summarizes the main change: fixing Slack block-action responses being incorrectly threaded in DMs, with clear indication this supersedes a prior attempt.
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.

✨ 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 updates the Slack adapter to prevent button clicks on top-level direct messages (DMs) from spawning phantom reply threads, ensuring they resolve to a DM-root thread ID with an empty thread timestamp. It also adds corresponding unit tests to verify this behavior for both DMs and standard channels, and updates the .gitignore file to ignore .DS_Store files. There are no review comments, and I have no feedback to provide.

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: Replace the truthiness-based fallback `thread_ts or ""` in the
encode_thread_id call with explicit None coalescing to comply with coding
guidelines. Change the expression to use `thread_ts if thread_ts is not None
else ""` instead of the `or` operator, which avoids truthiness traps by
explicitly checking for None rather than relying on truthiness evaluation. This
applies specifically to the thread_ts parameter being passed to SlackThreadId
within the encode_thread_id method call.
🪄 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: c368d0f8-a1dd-4eea-9937-06af6c4c6f95

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd86e7 and 2b6c092.

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

Comment thread src/chat_sdk/adapters/slack/adapter.py Outdated

@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: 2b6c092c1c

ℹ️ 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 thread src/chat_sdk/adapters/slack/adapter.py Outdated
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 or ""))

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 Handle empty DM thread ids before exposing action threads

When a button is clicked on a top-level DM, this now emits an action thread_id like slack:D456:. That fixes event.thread.post(...), but action handlers still receive a normal thread; if they call event.thread.refresh() or event.open_modal(), the existing Slack fetch paths decode this id and unconditionally call conversations_replies with ts=decoded.thread_ts. With the newly empty thread_ts that sends ts='', so those DM action paths fail or lose the original message context. Please either teach the fetch paths to use channel history for empty DM thread IDs or avoid exposing an unfetchable thread id here.

Useful? React with 👍 / 👎.

claude and others added 2 commits June 16, 2026 08:05
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.
)

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.
…ding-fix

# Conflicts:
#	docs/UPSTREAM_SYNC.md
@patrick-chinchill patrick-chinchill merged commit 0b7f077 into main Jun 18, 2026
8 checks passed
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)
patrick-chinchill added a commit that referenced this pull request Jun 21, 2026
…138) (#175)

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).
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.

3 participants