-
-
Notifications
You must be signed in to change notification settings - Fork 69.3k
[Bug]: Slack DM thread attachments posted outside thread when sent via message tool #38409
Description
Summary
When an agent is in a Slack DM thread and sends an attachment (image, audio, TTS output, file) via the message tool (action=send with media or buffer), the attachment is posted outside the thread as a new top-level message instead of continuing in the existing thread.
Text auto-replies work correctly — only explicit message tool sends of media are affected.
Steps to Reproduce
- Configure OpenClaw with Slack (DM mode;
replyToModeat default"off"or unset) - Start a thread in a Slack DM by replying to an existing message
- Ask the agent something that causes it to send media via the
messagetool (e.g. TTS audio, a generated image, an uploaded file) - Observe that the attachment lands in the main DM, not in the thread
Root Cause
Two gaps in the DM thread context chain inside the message tool path:
Gap 1 — buildSlackThreadingToolContext (src/slack/threading-tool-context.ts)
currentChannelId is only populated for channel:xxx-prefixed To values. In DMs, To is user:U0AC3LBA08M, so currentChannelId is always undefined:
// current — always undefined in DMs
currentChannelId: params.context.To?.startsWith("channel:")
? params.context.To.slice("channel:".length)
: undefined,Gap 2 — resolveSlackAutoThreadId (src/infra/outbound/message-action-params.ts)
Because currentChannelId is undefined, the guard at the top of the function exits early for every DM:
if (!context?.currentThreadTs || !context.currentChannelId) {
return undefined; // ← always exits here for DMs
}Even if currentChannelId were set, the next guard rejects DM (user:) targets outright:
if (!parsedTarget || parsedTarget.kind !== "channel") {
return undefined; // ← user: targets have kind="user", not "channel"
}The auto-reply path is unaffected — deliverReplies uses replyPlan.nextThreadTs() which applies the isThreadReply → "all" override directly, bypassing resolveSlackAutoThreadId entirely. Only explicit message tool sends are broken.
Fix
src/slack/threading-tool-context.ts — preserve user:xxx as currentChannelId for DM sessions:
const to = params.context.To;
const currentChannelId = to?.startsWith("channel:")
? to.slice("channel:".length)
: to?.startsWith("user:")
? to // "user:U0AC3LBA08M" — preserved for DM thread matching
: undefined;src/infra/outbound/message-action-params.ts — build a canonical address for comparison instead of requiring kind === "channel":
const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
if (!parsedTarget) return undefined;
// channel: compare raw ID; user/DM: compare as "user:<id>"
const targetAddress =
parsedTarget.kind === "channel" ? parsedTarget.id : `user:${parsedTarget.id}`;
if (targetAddress.toLowerCase() !== context.currentChannelId.toLowerCase()) return undefined;Tests
21 new assertions across two test files:
src/slack/threading-tool-context.test.ts— 3 new cases covering channel prefix stripping, DMuser:preservation, and absentTosrc/infra/outbound/message-action-params.test.ts(new file) — 11 cases covering channel threads, DM threads, cross-target isolation,replyToModegating, and missing-context guards