Skip to content

Feat: Add Thread.thread() api to guarantee threading, otherwise defaults to message scope#374

Open
xtyler wants to merge 3 commits intovercel:mainfrom
xtyler:feat/thread-api
Open

Feat: Add Thread.thread() api to guarantee threading, otherwise defaults to message scope#374
xtyler wants to merge 3 commits intovercel:mainfrom
xtyler:feat/thread-api

Conversation

@xtyler
Copy link
Copy Markdown

@xtyler xtyler commented Apr 14, 2026

Summary

Adds thread.thread(messageId?) as a means to officially support creating threads and guarantee a threaded response in channel-supporting platforms without impacting those that don't support channels.

This change reinforces Chat SDK's intended callback design where onNewMention() and onNewMessage() return a thread representing the current message scope whether it's a channel or thread. Calling thread.subscribe() or thread.post() subscribes or posts to the thread or channel of the message. For consumers that wish to always respond in a thread, thread = await thread.thread() will return always return a thread, either itself if within a thread scope, or a new/existing thread from the latest message in the case of a channel.

Without this there is tension resulting in inconsistent behavior between platform adapters, where Slack always creates new threads for every bot response, and Discord creates new threads on @mentions only. These are forced incorrect behaviors outside of a consumers control and were presumably implemented within the adapter to compensate for the lack of explicit thread creation. So we add openThread?(scopeId, messageId) as an optional method on the Adapter interface, following the openDM pattern, that returns an existing or new threadId.

Consumers can now choose when to guarantee a threaded response:

bot.onNewMention(async (thread, message) => {
const t = await thread.thread(); // will always return a thread, whether message is from channel or existing thread
await t.subscribe();
await t.post("Continuing in a thread.");
});

Warning: this change requires consumers who were previously relying on automatic thread creation for Slack and Discord to use the new API. It is a change to the behavior of these two adapters. Without this change, thread creation is automatic in these two adapters, a choice forced on the consumer. In Slack the consumer could feasibly use thread.channel.post() as a workaround (with no easy way to know whether you're in a channel or thread scope), but in Discord @mention a thread is created and sits there empty regardless of whether you respond in that thread.

Test plan

Unit tests on Chat SDK added for the new behavior. Slack and Discord adapter tests added for new openThread() and modified for the fix in behavior. Discord, Slack, and Telegram were integrated and tested with live bots using thread.post() and (await thread.thread()).post(), each working as expected.

Chat SDK

  • Test returning self when already in a thread (id !== channelId) or adapter lacks openThread
  • Test calling adapter.openThread and returning new ThreadImpl when on a channel
  • Test support for explicit messageId when provided (thread can be created on any channel message)
  • Test fallback of returning self when no messageId and no currentMessage (presumably not a possible state)

Adapters (Discord and Slack)

  • Test openThread returns new thread ID from channel scope (Slack encodes messageId as threadTs, Discord creates via API)
  • Test openThread returns existing thread ID unchanged when already in a thread (both adapters)
  • Test openThread recovers from Discord error 160004 (thread already exists) by reusing message ID
  • Test stream accumulates and posts via postMessage for channel-scope messages (chatStream not called)
  • Test stream uses chatStream with correct thread_ts for thread-scope messages
  • Test encodeThreadId/decodeThreadId handle channel-scoped IDs without trailing colon (2-part and 3-part)
  • Test handleMessage produces channel-scoped thread IDs for channel messages (thread_ts || "", no message ts fallback)
  • Test DM messages use empty threadTs at top-level and preserve parent thread_ts in thread replies
  • Test @mentions and role mentions in Discord channels no longer auto-create threads, dispatch channel-scoped

xtyler added 3 commits April 11, 2026 13:47
Adds `thread.thread(messageId?)` as a means to officially support creating threads and guarantee a threaded response in channel-supporting platforms without impacting those that don't support channels.

Adds openThread?(scopeId, messageId) as an optional method on the Adapter
interface, following the openDM pattern, that returns an existing or new threadId.
…at, teams

Add the newly supported openThread method to each adapter that supports thread creation/retrieval from a channel's messageId. GitHub, Linear, and WhatsApp omit openThread, thread() returns self.

Removes Discord's auto-thread-creation on @mention, inconsistent with every other adapter.
Removes Slack's auto-thread-creation on *all* @mentions and messages.
Streaming is used by default in thread.post() which is not supported within channels, add check for channel and accumulate stream to a single postMessage()
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 14, 2026

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

A member of the Team first needs to authorize it.

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