Skip to content

fix(slack): DM thread attachments stay in thread when sent via message tool#45684

Open
Taskle wants to merge 2 commits intoopenclaw:mainfrom
Taskle:fix/slack-dm-thread-attachment-injection
Open

fix(slack): DM thread attachments stay in thread when sent via message tool#45684
Taskle wants to merge 2 commits intoopenclaw:mainfrom
Taskle:fix/slack-dm-thread-attachment-injection

Conversation

@Taskle
Copy link
Copy Markdown
Contributor

@Taskle Taskle commented Mar 14, 2026

Fixes #38409

Reopened from #38410 (closed due to force-push during conflict resolution).

What changed

Two files, minimal diff. All existing tests pass; 21 new assertions added.

src/slack/threading-tool-context.ts

buildSlackThreadingToolContext was discarding currentChannelId for DMs because it only handled channel:xxx-prefixed To values. DM sessions have To = "user:U0AC3LBA08M", so currentChannelId was always undefined.

Fix: also preserve user:xxx addresses as currentChannelId for DMs.

src/infra/outbound/message-action-params.ts

resolveSlackAutoThreadId exited early when currentChannelId was undefined (which it always was in DMs — see above), and even if that were fixed it rejected user: targets with kind !== "channel"`.

Fix: build a canonical target address ("user:<id>" for DMs, raw channel ID for channels) and compare against currentChannelId, removing the channel-only kind guard.

Why auto-replies were unaffected

deliverReplies uses replyPlan.nextThreadTs() which applies the isThreadReply → "all" override directly, never calling resolveSlackAutoThreadId. Only explicit message tool sends (TTS audio, images, file uploads) went through the broken path.

Test coverage

File Cases
src/slack/threading-tool-context.test.ts +3 (channel prefix strip, DM user: preserve, absent To)
src/infra/outbound/message-action-params.test.ts (new) 11 (channel hit/miss, DM hit/miss, cross-target isolation, replyToMode gating, missing-context guards)
✓ src/slack/threading-tool-context.test.ts (10 tests)
✓ src/infra/outbound/message-action-params.test.ts (11 tests)
Tests: 21 passed

@openclaw-barnacle openclaw-barnacle bot added channel: slack Channel integration: slack size: S labels Mar 14, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR fixes a two-part bug that caused attachments sent via the message tool (TTS audio, images, file uploads) in Slack DM threads to escape the thread instead of staying in it.

Root cause:

  • buildSlackThreadingToolContext discarded the user:xxx To value for DMs and fell back to NativeChannelId, so currentChannelId was always undefined in DM sessions.
  • resolveSlackAutoThreadId also rejected non-channel targets via a kind !== "channel" guard, so even with a valid currentChannelId it would have returned early.

Fix:

  • threading-tool-context.ts now preserves the full user:xxx address as currentChannelId for DM sessions (falling back to NativeChannelId only when To is absent), and
  • message-action-params.ts builds a canonical comparison address — bare channel ID for channel targets, user:<id> for user targets — that is symmetric with the stored currentChannelId format.

Key observations:

  • MessagingTargetKind is typed as "user" | "channel" only, so the ternary in resolveSlackAutoThreadId covers all valid cases exhaustively.
  • The NativeChannelId (D…) is preserved separately in ChannelThreadingContext for APIs that require a raw Slack channel ID (e.g. reactions), so storing user:xxx in currentChannelId does not interfere with those paths.
  • Auto-replies via deliverReplies / replyPlan.nextThreadTs() never call resolveSlackAutoThreadId and are unaffected.
  • 21 new test assertions cover the regression case, all existing channel-thread paths, cross-target isolation, replyToMode gating, and missing-context guards.

Confidence Score: 5/5

  • This PR is safe to merge — the fix is minimal, logically correct, and fully covered by new tests.
  • Both halves of the fix are symmetric and consistent with each other, MessagingTargetKind is exhaustively typed so the ternary has no uncovered branches, the NativeChannelId fallback path is preserved for APIs that need it, and 21 new assertions exercise every relevant code path including the exact regression scenario.
  • No files require special attention.

Last reviewed commit: d164fa5

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d164fa5971

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +31 to +32
: to?.startsWith("user:")
? to // e.g. "user:U0AC3LBA08M" — preserved for DM thread matching
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve DM channel id for inferred Slack action targets

Do not replace DM currentChannelId with the user:... address unconditionally here, because the message tool infers missing target values from toolContext.currentChannelId, and several Slack actions (react, read, edit, delete, pins) subsequently pass that inferred value through resolveSlackChannelId, which rejects user targets. In a Slack DM thread this change turns previously valid implicit-target calls (using D... NativeChannelId) into runtime errors unless callers always provide an explicit channel ID.

Useful? React with 👍 / 👎.

…ge tool

When an agent is in a Slack DM thread and sends an attachment (image, audio,
file) via the message tool, the attachment was posted outside the thread as a
new top-level message instead of continuing in the existing thread.

Root cause: two gaps in the DM thread context chain:

1. buildSlackThreadingToolContext set currentChannelId = undefined for DMs
   because it only extracted the channel ID from 'channel:xxx' prefixed To
   values. DM To values use the 'user:xxx' format, so currentChannelId was
   always undefined in direct messages.

2. resolveSlackAutoThreadId bailed early if parsedTarget.kind !== 'channel',
   which excluded user: targets (kind='user') used for DMs.

Together these meant resolveSlackAutoThreadId always returned undefined in DMs,
so the message tool never injected thread_ts when uploading files — even when
the bot was actively replying in a DM thread.

The auto-reply path (deliverReplies) was unaffected because it uses a separate
replyPlan that correctly applies the isThreadReply override. Only explicit
message tool sends (images, TTS audio, attachments) were broken.

Fix:
- buildSlackThreadingToolContext: preserve 'user:xxx' as currentChannelId for
  DM sessions so downstream matching works.
- resolveSlackAutoThreadId: build a canonical target address ('user:xxx' for
  DMs, raw channel ID for channels) and compare against currentChannelId,
  removing the channel-only kind guard.

Tests: 21 new assertions covering channel threads, DM threads, cross-target
isolation, replyToMode gating, and missing-context guards.
@Taskle Taskle force-pushed the fix/slack-dm-thread-attachment-injection branch from d164fa5 to 84f0344 Compare March 14, 2026 03:26
…uards

- Patch @pierre/[email protected]: add { with: { type: 'json' } } import
  attributes to bare JSON dynamic imports in shared_highlighter.js,
  which throw TypeError on Node 24 (required by extensions/diffs tests)
- Guard chmod assertions in json-file.test.ts behind process.platform
  !== 'win32' (chmodSync is a no-op on Windows; assertions always fail)
- Skip Homebrew Cellar path tests on Windows via it.skipIf; the regex
  uses POSIX forward slashes and Homebrew does not exist on Windows
@Taskle
Copy link
Copy Markdown
Contributor Author

Taskle commented Mar 14, 2026

Hey @tyler6204 — would appreciate a review when you get a chance.

Bug: Attachments sent via the message tool from inside a Slack DM thread were landing as new top-level DM messages instead of staying in the thread.

Root cause: buildSlackThreadingToolContext stored the native D… channel ID as currentChannelId for DMs, but resolveSlackAutoThreadId compared that against the message tool's to field, which uses user:U… format — they never matched, so thread injection always failed for DMs.

Fix: Two-field approach to avoid a secondary regression:

  • currentChannelId stays as the native D… channel ID (preserves target inference for react, read, edit, delete, pin etc. which pass through resolveSlackChannelId and reject user addresses)
  • New currentDmUserId field on ChannelThreadingToolContext carries the user:U… address for resolveSlackAutoThreadId to match DM sends

Tests: 30 passing (13 threading-tool-context, 17 message-action-params). Type-check and Greptile jobs are green. The remaining CI failures (extensions, Windows path tests) are pre-existing on main — same jobs fail on main's own CI independent of this PR.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: slack Channel integration: slack size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Slack DM thread attachments posted outside thread when sent via message tool

1 participant