Skip to content

fix(slack): guard Slack API calls against empty threadTs to fix invalid_thread_ts#292

Merged
dancer merged 2 commits into
vercel:mainfrom
chechunhsu:fix/slack-dm-thread-ts
Apr 17, 2026
Merged

fix(slack): guard Slack API calls against empty threadTs to fix invalid_thread_ts#292
dancer merged 2 commits into
vercel:mainfrom
chechunhsu:fix/slack-dm-thread-ts

Conversation

@chechunhsu
Copy link
Copy Markdown
Contributor

@chechunhsu chechunhsu commented Mar 24, 2026

Summary

Fixes #268

The empty threadTs for top-level DMs was intentionally added in #39 so that openDM() subscriptions match incoming DM messages. The previous approach of changing the threadTs logic to event.thread_ts || event.ts fixed the API error but broke subscription matching — DMs routed to onNewMention() instead of onSubscribedMessage().

This PR takes the two-part approach suggested in review feedback: preserve the empty threadTs for DM subscription matching, and guard Slack API calls so empty strings don't cause invalid_thread_ts errors.

What changed

Each method that calls Slack APIs (postMessage, postEphemeral, scheduleMessage, stream) now normalizes threadTs at entry:

const { channel, threadTs: rawThreadTs } = this.decodeThreadId(threadId);
const threadTs = rawThreadTs || undefined;

This follows the same pattern postEphemeral already used (thread_ts: threadTs || undefined), but applied once at method entry instead of at each call site.

Result

  • openDM() subscription matching continues to work (empty threadTs in threadId)
  • Slack API calls receive undefined instead of "" → no invalid_thread_ts errors
  • No behavioral change for channel messages or DM thread replies

Test plan

  • pnpm test --filter=@chat-adapter/slack — 297 tests pass
  • pnpm test --filter=@chat-adapter/integration-testsreplay-dm.test.ts 17 tests pass (DM subscription matching verified)
  • pnpm turbo build --filter=@chat-adapter/slack — builds cleanly with no type errors

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 24, 2026

@chechunhsu is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

@vercel vercel Bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

Test description claims "top-level DM messages use empty threadTs (matches openDM subscriptions)" but the test assertion expects a non-empty threadTs using event.ts, contradicting the description.

Fix on Vercel

@bensabic
Copy link
Copy Markdown
Contributor

Thanks for the fix, the invalid_thread_ts bug is definitely real and needs to be addressed.

However, the empty threadTs for top-level DMs was intentional (added in #39) so that openDM() subscriptions would match incoming DM messages. This PR fixes the API error but breaks that subscription matching, DMs now route to onNewMention() instead of onSubscribedMessage(), which is a regression for apps using openDM().

The issue author actually suggested a two-part fix: use event.ts as the fallback and guard the API calls with thread_ts: threadTs || undefined (like postEphemeral already does). Could we take that approach instead? That way postMessage/chatStream won't choke on empty threadTs, and openDM() subscription matching continues to work.

@chechunhsu chechunhsu force-pushed the fix/slack-dm-thread-ts branch from 30d48d8 to a6601ca Compare April 1, 2026 02:20
@chechunhsu chechunhsu changed the title fix(slack): use event.ts as threadTs fallback for DMs fix(slack): guard Slack API calls against empty threadTs to fix invalid_thread_ts Apr 1, 2026
@chechunhsu
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed feedback! You're right — the empty threadTs for top-level DMs is intentional for openDM() subscription matching, and my original approach broke that.

I've updated the PR to take the two-part approach you suggested: keep the existing DM threadTs logic unchanged, and guard the Slack API calls against empty strings instead.

Specifically, each method that calls Slack APIs (postMessage, postEphemeral, scheduleMessage, stream) now normalizes threadTs at entry:

const { channel, threadTs: rawThreadTs } = this.decodeThreadId(threadId);
const threadTs = rawThreadTs || undefined;

This follows the pattern postEphemeral already used, but applied once per method rather than at each call site. All existing tests pass, including the DM subscription matching tests in replay-dm.test.ts.

@bensabic
Copy link
Copy Markdown
Contributor

bensabic commented Apr 1, 2026

Thanks for the updated approach, guarding with rawThreadTs || undefined instead of changing how threadTs is computed is the right call. It preserves openDM() subscription matching while fixing the API errors.

Two things on the as string casts:

  1. startTyping (line 2898): This one is fine since there's already an early return on if (!threadTs) above it. The cast is redundant though, you could drop it.

  2. chatStream (line 2944): This one is unsafe. There's no guard, so if threadTs is undefined, it passes undefined to chat.startStream which requires thread_ts: string. That's exactly the invalid_arguments error from DMs broken: empty threadTs causes invalid_thread_ts and invalid_arguments #268. It probably can't happen in practice today (streaming is triggered from incoming messages which always have a valid event.ts), but the cast silences TypeScript rather than handling the case. Could you add the same guard that startTyping has, early return or fallback when threadTs is undefined?

…id_thread_ts

Preserve the intentional empty threadTs for top-level DMs (added in vercel#39
for openDM() subscription matching) while preventing Slack API errors.

Instead of changing the threadTs logic, normalize empty threadTs to
undefined at the entry of each method that calls Slack APIs (postMessage,
postEphemeral, scheduleMessage, stream). This way:

- openDM() subscription matching continues to work (empty threadTs)
- Slack API calls receive undefined instead of "" (no invalid_thread_ts)

The stream method throws ValidationError on empty threadTs (matching
startTyping's early-return pattern) so TypeScript narrows correctly
without any `as string` casts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@chechunhsu chechunhsu force-pushed the fix/slack-dm-thread-ts branch from a6601ca to 6570e06 Compare April 1, 2026 08:45
@chechunhsu
Copy link
Copy Markdown
Contributor Author

Updated:

  1. startTyping: Removed the redundant as string cast — TypeScript already narrows after the if (!threadTs) guard.

  2. stream: Added a ValidationError guard when threadTs is empty (matching startTyping's pattern), so chatStream receives a properly narrowed string — no more as string cast.

Zero as string casts remaining.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview, Comment, Open in v0 Apr 6, 2026 6:14pm
chat-sdk-nextjs-chat Ready Ready Preview, Comment, Open in v0 Apr 6, 2026 6:14pm

@dancer dancer merged commit 53c6b68 into vercel:main Apr 17, 2026
9 checks passed
grundmanise pushed a commit to grundmanise/vercel-chat-sdk that referenced this pull request Apr 22, 2026
…id_thread_ts (vercel#292)

* fix(slack): guard Slack API calls against empty threadTs to fix invalid_thread_ts

Preserve the intentional empty threadTs for top-level DMs (added in vercel#39
for openDM() subscription matching) while preventing Slack API errors.

Instead of changing the threadTs logic, normalize empty threadTs to
undefined at the entry of each method that calls Slack APIs (postMessage,
postEphemeral, scheduleMessage, stream). This way:

- openDM() subscription matching continues to work (empty threadTs)
- Slack API calls receive undefined instead of "" (no invalid_thread_ts)

The stream method throws ValidationError on empty threadTs (matching
startTyping's early-return pattern) so TypeScript narrows correctly
without any `as string` casts.

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

* Add tests for empty threadTs normalization in stream, scheduleMessage, and postEphemeral

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
dancer pushed a commit that referenced this pull request Apr 30, 2026
…id_thread_ts (#292)

* fix(slack): guard Slack API calls against empty threadTs to fix invalid_thread_ts

Preserve the intentional empty threadTs for top-level DMs (added in #39
for openDM() subscription matching) while preventing Slack API errors.

Instead of changing the threadTs logic, normalize empty threadTs to
undefined at the entry of each method that calls Slack APIs (postMessage,
postEphemeral, scheduleMessage, stream). This way:

- openDM() subscription matching continues to work (empty threadTs)
- Slack API calls receive undefined instead of "" (no invalid_thread_ts)

The stream method throws ValidationError on empty threadTs (matching
startTyping's early-return pattern) so TypeScript narrows correctly
without any `as string` casts.

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

* Add tests for empty threadTs normalization in stream, scheduleMessage, and postEphemeral

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
patrick-chinchill pushed a commit to Chinchill-AI/chat-sdk-python that referenced this pull request May 15, 2026
Bundles 5 small upstream bug fixes into one PR. Each is independent and
covered by a regression test.

- vercel/chat#394 (slack): preserve email addresses in @mention regex.
  ``user@domain.com`` no longer extracts ``@domain`` as a mention.
- vercel/chat#292 (slack): guard Slack API calls against empty
  ``thread_ts`` to fix ``invalid_thread_ts`` errors.
- vercel/chat#256 (discord): remove duplicate text when posting card
  messages. ``content`` is omitted on the create path and explicitly
  cleared on the edit path (Discord PATCH preserves omitted fields).
- vercel/chat#395 (slack): enrich link previews with unfurl metadata
  from attachments. Routes ``message_changed`` events through a new
  ``_handle_message_changed`` so the message handler sees unfurled
  links instead of bare URLs. New cache + poll window.
- vercel/chat#407 (telegram): rewrite format converter to emit
  MarkdownV2 (``*bold*`` etc.) instead of legacy ``Markdown``. Adds
  proper escaping for the 18 special characters MarkdownV2 reserves
  in normal text, the narrower set inside code blocks, and the
  parens/backslash escape inside link URLs.

https://claude.ai/code/session_01FyMxQn2BEAzmwKS1GZczKj
patrick-chinchill pushed a commit to Chinchill-AI/chat-sdk-python that referenced this pull request May 16, 2026
#94)

The vercel/chat#292 empty-thread_ts guard correctly avoided
chat.startStream's invalid_thread_ts rejection, but raising
ValidationError silently dropped top-level Slack DM streaming
replies because Thread._handle_stream does not catch and
_fallback_stream only triggers when the adapter has no stream
method at all.

Replace the raise with the production-tested accumulate-and-post
path Tony described in chat-sdk-python#94: degrade DMs to a single
non-streamed post_message call (Slack accepts an empty thread_ts
on chat.postMessage) and keep native streaming for channels.

This lets chinchill-api retire its MultiTenantSlackAdapter
subclass override.
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.

DMs broken: empty threadTs causes invalid_thread_ts and invalid_arguments

3 participants