Skip to content

[Bug]: Slack thread_ts lost during collect queue drain — typeof === "number" check drops string thread IDs #4380

@spartakb

Description

@spartakb

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

  1. Configure messages.queue.mode: "steer" (or collect) with replyToMode: "all" on a Slack channel
  2. Send a message in a Slack channel while the agent is busy processing another request
  3. The message gets queued and eventually drained via scheduleFollowupDrain
  4. 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.jsreplies.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions