Bug Description
When messages are queued in steer or collect mode and then drained, Slack originatingThreadId values are silently dropped because the drain code checks typeof originatingThreadId === "number". Slack thread timestamps are always strings (e.g., "1769742846.264069"), so the check never matches.
This causes collected/batched replies to lose their thread context and post as new top-level channel messages instead of replying within the originating thread.
Steps to Reproduce
- Configure
messages.queue.mode: "steer" (or collect) with replyToMode: "all" on a Slack channel
- Send a message in a Slack channel while the agent is busy processing another request
- The message gets queued and eventually drained via
scheduleFollowupDrain
- The reply appears as a new top-level channel message instead of threading under the user's message
Root Cause
In src/auto-reply/reply/queue/drain.ts (compiled to dist/auto-reply/reply/queue/drain.js), three locations use typeof === "number" to check for thread IDs:
1. Cross-channel detection (line ~36)
if (!channel && !to && !accountId && typeof threadId !== "number") {
return {}; // treats string threadId same as no threadId
}
2. Thread key for routing (line ~41)
const threadKey = typeof threadId === "number" ? String(threadId) : "";
// Slack thread "1769742846.264069" → "" → all threads look identical
3. Preserving threadId in collected batch (line ~62)
const originatingThreadId = items.find(
(i) => typeof i.originatingThreadId === "number"
)?.originatingThreadId;
// Always undefined for Slack → collected reply loses its thread
Why This Is Wrong
Slack timestamps (ts, thread_ts) are always strings per Slack API docs:
Provide a thread_ts value for the posted message to act as a reply to a parent message.
{
"channel": "YOUR_CHANNEL_ID",
"thread_ts": "PARENT_MESSAGE_TS",
"text": "Hello again!"
}
The originatingThreadId for Slack messages is set from messageThreadId in threading.ts, which comes from message.ts or message.thread_ts — both strings.
Suggested Fix
Change all three checks from typeof === "number" to != null:
- if (!channel && !to && !accountId && typeof threadId !== "number") {
+ if (!channel && !to && !accountId && threadId == null) {
- const threadKey = typeof threadId === "number" ? String(threadId) : "";
+ const threadKey = threadId != null ? String(threadId) : "";
- const originatingThreadId = items.find((i) => typeof i.originatingThreadId === "number")?.originatingThreadId;
+ const originatingThreadId = items.find((i) => i.originatingThreadId != null)?.originatingThreadId;
This preserves the behavior for numeric thread IDs (if any platform uses them) while also handling string thread IDs (Slack, and potentially others).
Impact
- Affected: All Slack channels using
steer, collect, followup, or steer-backlog queue modes
- Not affected: Direct (non-queued) replies, which use the Slack dispatch path in
dispatch.js → replies.js that correctly passes string messageTs
- Severity: Medium — replies go to the wrong place (top-level instead of thread), breaking conversation continuity
Environment
- Clawdbot version: 2026.1.24-3
- Channel: Slack (Socket Mode)
- Queue mode:
steer
replyToMode: all
Bug Description
When messages are queued in
steerorcollectmode and then drained, SlackoriginatingThreadIdvalues are silently dropped because the drain code checkstypeof originatingThreadId === "number". Slack thread timestamps are always strings (e.g.,"1769742846.264069"), so the check never matches.This causes collected/batched replies to lose their thread context and post as new top-level channel messages instead of replying within the originating thread.
Steps to Reproduce
messages.queue.mode: "steer"(orcollect) withreplyToMode: "all"on a Slack channelscheduleFollowupDrainRoot Cause
In
src/auto-reply/reply/queue/drain.ts(compiled todist/auto-reply/reply/queue/drain.js), three locations usetypeof === "number"to check for thread IDs:1. Cross-channel detection (line ~36)
2. Thread key for routing (line ~41)
3. Preserving threadId in collected batch (line ~62)
Why This Is Wrong
Slack timestamps (
ts,thread_ts) are always strings per Slack API docs:{ "channel": "YOUR_CHANNEL_ID", "thread_ts": "PARENT_MESSAGE_TS", "text": "Hello again!" }The
originatingThreadIdfor Slack messages is set frommessageThreadIdinthreading.ts, which comes frommessage.tsormessage.thread_ts— both strings.Suggested Fix
Change all three checks from
typeof === "number"to!= null:This preserves the behavior for numeric thread IDs (if any platform uses them) while also handling string thread IDs (Slack, and potentially others).
Impact
steer,collect,followup, orsteer-backlogqueue modesdispatch.js→replies.jsthat correctly passes stringmessageTsEnvironment
steerreplyToMode:all