Skip to content

feat(slack): DM-per-thread + trigger payload with reply-in-thread contract#439

Merged
decobot merged 2 commits into
mainfrom
slack-dm-thread-per-message
May 14, 2026
Merged

feat(slack): DM-per-thread + trigger payload with reply-in-thread contract#439
decobot merged 2 commits into
mainfrom
slack-dm-thread-per-message

Conversation

@JonasJesus42
Copy link
Copy Markdown
Contributor

@JonasJesus42 JonasJesus42 commented May 14, 2026

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

  • handleDirectMessage now uses the user's message ts as thread_ts for the bot's thinking message and final reply.
  • handleMessage routes DMs with thread_ts to handleThreadReply instead of handleDirectMessage — fixes the previous isDM short-circuit that ignored thread_ts.

2. Enrich the trigger payload

publishMessageReceived / publishAppMention are now async and bake in:

  • user_name: display_name → real_name → name, resolved via getUserInfo.
  • thread_messages: when the event lives in a thread, the full ordered list of {ts, user, text, is_bot} from getThreadReplies — the agent sees the whole conversation in one shot.

The fallback path in llm-handler.ts now reuses publishMessageReceived({ fallback: true }) instead of duplicating triggers.notify inline, 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_ts is undefined — the agent couldn't figure out where to reply.

  • New field reply_in_thread_ts = event.thread_ts ?? event.ts is 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.
  • New field reply_instruction: a one-line directive spelling out the exact SLACK_REPLY_IN_THREAD(channel, thread_ts, text) tool call.
  • Updated trigger definitions in trigger-store.ts so 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

  • Send a top-level DM → bot replies in a thread under that message.
  • Send a 2nd top-level DM → separate thread, prior one untouched.
  • Reply inside an existing bot thread → continues in same thread.
  • triggerOnly mode: subscriber receives reply_in_thread_ts populated even on first message.
  • Trigger subscriber agent uses SLACK_REPLY_IN_THREAD(channel_id, reply_in_thread_ts, ...) and the answer appears in the right thread.
  • thread_messages carries the full history on follow-up messages.
  • @mention in a channel still threads (no regression).

JonasJesus42 and others added 2 commits May 14, 2026 11:19
…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>
@JonasJesus42 JonasJesus42 changed the title feat(slack): start a new thread on every DM so each subject stays isolated feat(slack): thread per DM + enrich trigger payload with thread history May 14, 2026
@decobot decobot merged commit f4a934c into main May 14, 2026
2 checks passed
@JonasJesus42 JonasJesus42 changed the title feat(slack): thread per DM + enrich trigger payload with thread history feat(slack): DM-per-thread + trigger payload with reply-in-thread contract May 14, 2026
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.

2 participants