-
-
Notifications
You must be signed in to change notification settings - Fork 69.2k
[Bug]: Slack thread_ts lost during collect queue drain — typeof === "number" check drops string thread IDs #4380
Description
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"(orcollect) withreplyToMode: "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 identical3. 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 threadWhy This Is Wrong
Slack timestamps (ts, thread_ts) are always strings per Slack API docs:
Provide a
thread_tsvalue 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, orsteer-backlogqueue modes - Not affected: Direct (non-queued) replies, which use the Slack dispatch path in
dispatch.js→replies.jsthat correctly passes stringmessageTs - 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