feat(slack): DM-per-thread + trigger payload with reply-in-thread contract#439
Merged
Conversation
…lated DMs to the bot used to flatten into a single top-level conversation — every reply piled up at the channel root and it was hard to tell which answer belonged to which question. App-mentions in channels already started a thread under the user's message (replyTo = thread_ts ?? ts); this brings the DM flow in line. Two changes in eventHandler.ts: - handleDirectMessage now uses the user's message ts as the thread_ts for the bot's thinking message and final reply. The first DM kicks off a new thread, follow-ups inside that thread go through the existing handleThreadReply path so the conversation continues in place. - handleMessage routes DMs that arrive with thread_ts (i.e. the user replied inside an existing bot thread) to handleThreadReply instead of handleDirectMessage. Previously the isDM short-circuit ignored the thread_ts and the bot answered at the channel root again. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…story
Subscribers of slack.message.received and slack.app_mention used to get
just the bare event fields (channel_id, user_id, text, ts, thread_ts).
A trigger-driven agent that wanted to reply in-thread had to round-trip
to SLACK_GET_USER_INFO and SLACK_GET_THREAD_REPLIES before it could
even start composing an answer.
Bake both into the publisher:
- user_name: display_name -> real_name -> name, resolved via getUserInfo
- thread_messages: when the incoming event lives in a thread, the full
ordered list of {ts, user, text, is_bot} from getThreadReplies. With
the DM-per-thread change in the parent commit, every continuation
message arrives with thread_ts set, so the agent sees the whole
conversation in one shot.
publishMessageReceived now also takes an optional extras parameter so
the llm-handler fallback path can flag fallback: true while reusing the
same enrichment instead of duplicating triggers.notify inline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three changes that make Slack threads work end-to-end for both the direct LLM path and the trigger-driven agent path.
1. Start a new thread on every DM
handleDirectMessagenow uses the user's messagetsasthread_tsfor the bot's thinking message and final reply.handleMessageroutes DMs withthread_tstohandleThreadReplyinstead ofhandleDirectMessage— fixes the previousisDMshort-circuit that ignoredthread_ts.2. Enrich the trigger payload
publishMessageReceived/publishAppMentionare now async and bake in:user_name:display_name → real_name → name, resolved viagetUserInfo.thread_messages: when the event lives in a thread, the full ordered list of{ts, user, text, is_bot}fromgetThreadReplies— the agent sees the whole conversation in one shot.The fallback path in
llm-handler.tsnow reusespublishMessageReceived({ fallback: true })instead of duplicatingtriggers.notifyinline, so trigger-only mode and fallback mode deliver the same enriched payload.3. Tell the agent exactly how to reply
The previous version still left it to the subscriber to derive the right
thread_ts. For a top-level DM event,thread_tsis undefined — the agent couldn't figure out where to reply.reply_in_thread_ts = event.thread_ts ?? event.tsis computed by the publisher. For thread continuations it's the existing thread; for top-level messages it's the message's own ts, which kicks off a new thread the moment the bot replies. The agent never has to decide start-vs-continue.reply_instruction: a one-line directive spelling out the exactSLACK_REPLY_IN_THREAD(channel, thread_ts, text)tool call.trigger-store.tsso the description embedded in studio's UI tells the agent: "To respond, ALWAYS call SLACK_REPLY_IN_THREAD with channel=channel_id and thread_ts=reply_in_thread_ts".Tools the agent already has
No new tools needed. Existing slack-mcp tools cover the workflow:
SLACK_REPLY_IN_THREAD(channel, thread_ts, text)— primary action.SLACK_SEND_MESSAGE(channel, text, thread_ts?)— alternative.Test plan
triggerOnlymode: subscriber receivesreply_in_thread_tspopulated even on first message.SLACK_REPLY_IN_THREAD(channel_id, reply_in_thread_ts, ...)and the answer appears in the right thread.thread_messagescarries the full history on follow-up messages.