diff --git a/CHANGELOG.md b/CHANGELOG.md index a2a484c6534..28b6c06f191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai - Security/audit account handling: avoid prototype-chain account IDs in audit validation by using own-property checks for `accounts`. (#34982) Thanks @HOYALIM. - Cron/restart catch-up semantics: replay interrupted recurring jobs and missed immediate cron slots on startup without replaying interrupted one-shot jobs, with guarded missed-slot probing to avoid malformed-schedule startup aborts and duplicate-trigger drift after restart. (from #34466, #34896, #34625, #33206) Thanks @dunamismax, @dsantoreis, @Octane0411, and @Sid-Qin. - Agents/session usage tracking: preserve accumulated usage metadata on embedded Pi runner error exits so failed turns still update session `totalTokens` from real usage instead of stale prior values. (#34275) thanks @RealKai42. +- Slack/reaction thread context routing: carry Slack native DM channel IDs through inbound context and threading tool resolution so reaction targets resolve consistently for DM `To=user:*` sessions (including `toolContext.currentChannelId` fallback behavior). (from #34831; overlaps #34440, #34502, #34483, #32754) Thanks @dunamismax. - Nodes/system.run approval hardening: use explicit argv-mutation signaling when regenerating prepared `rawCommand`, and cover the `system.run.prepare -> system.run` handoff so direct PATH-based `nodes.run` commands no longer fail with `rawCommand does not match command`. (#33137) thanks @Sid-Qin. - Models/custom provider headers: propagate `models.providers..headers` across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin. - Ollama/custom provider headers: forward resolved model headers into native Ollama stream requests so header-authenticated Ollama proxies receive configured request headers. (#24337) thanks @echoVic. diff --git a/src/auto-reply/reply/agent-runner-utils.ts b/src/auto-reply/reply/agent-runner-utils.ts index ace68914e18..daa9b548fb2 100644 --- a/src/auto-reply/reply/agent-runner-utils.ts +++ b/src/auto-reply/reply/agent-runner-utils.ts @@ -58,6 +58,7 @@ export function buildThreadingToolContext(params: { ReplyToId: sessionCtx.ReplyToId, ThreadLabel: sessionCtx.ThreadLabel, MessageThreadId: sessionCtx.MessageThreadId, + NativeChannelId: sessionCtx.NativeChannelId, }, hasRepliedRef, }) ?? {}; diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index c0ab459bfe9..9c9a7f4d430 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -142,6 +142,8 @@ export type MsgContext = { GatewayClientScopes?: string[]; /** Thread identifier (Telegram topic id or Matrix thread event id). */ MessageThreadId?: string | number; + /** Platform-native channel/conversation id (e.g. Slack DM channel "D…" id). */ + NativeChannelId?: string; /** Telegram forum supergroup marker. */ IsForum?: boolean; /** Warning: DM has topics enabled but this message is not in a topic. */ diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index 379c6b8c89e..af8a2eca955 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -257,6 +257,8 @@ export type ChannelThreadingContext = { ReplyToIdFull?: string; ThreadLabel?: string; MessageThreadId?: string | number; + /** Platform-native channel/conversation id (e.g. Slack DM channel "D…" id). */ + NativeChannelId?: string; }; export type ChannelThreadingToolContext = { diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 4d66c73e40d..564dce16fea 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -727,6 +727,7 @@ export async function prepareSlackMessage(params: { CommandAuthorized: commandAuthorized, OriginatingChannel: "slack" as const, OriginatingTo: slackTo, + NativeChannelId: message.channel, }) satisfies FinalizedMsgContext; const pinnedMainDmOwner = isDirectMessage ? resolvePinnedMainDmOwnerFromAllowlist({ diff --git a/src/slack/threading-tool-context.test.ts b/src/slack/threading-tool-context.test.ts index c4be6ef2d77..69f4cf0e0dd 100644 --- a/src/slack/threading-tool-context.test.ts +++ b/src/slack/threading-tool-context.test.ts @@ -144,4 +144,35 @@ describe("buildSlackThreadingToolContext", () => { }); expect(result.replyToMode).toBe("off"); }); + + it("extracts currentChannelId from channel: prefixed To", () => { + const result = buildSlackThreadingToolContext({ + cfg: emptyCfg, + accountId: null, + context: { ChatType: "channel", To: "channel:C1234ABC" }, + }); + expect(result.currentChannelId).toBe("C1234ABC"); + }); + + it("uses NativeChannelId for DM when To is user-prefixed", () => { + const result = buildSlackThreadingToolContext({ + cfg: emptyCfg, + accountId: null, + context: { + ChatType: "direct", + To: "user:U8SUVSVGS", + NativeChannelId: "D8SRXRDNF", + }, + }); + expect(result.currentChannelId).toBe("D8SRXRDNF"); + }); + + it("returns undefined currentChannelId when neither channel: To nor NativeChannelId is set", () => { + const result = buildSlackThreadingToolContext({ + cfg: emptyCfg, + accountId: null, + context: { ChatType: "direct", To: "user:U8SUVSVGS" }, + }); + expect(result.currentChannelId).toBeUndefined(); + }); }); diff --git a/src/slack/threading-tool-context.ts b/src/slack/threading-tool-context.ts index 6841972d3e0..11860f78636 100644 --- a/src/slack/threading-tool-context.ts +++ b/src/slack/threading-tool-context.ts @@ -19,10 +19,14 @@ export function buildSlackThreadingToolContext(params: { const hasExplicitThreadTarget = params.context.MessageThreadId != null; const effectiveReplyToMode = hasExplicitThreadTarget ? "all" : configuredReplyToMode; const threadId = params.context.MessageThreadId ?? params.context.ReplyToId; + // For channel messages, To is "channel:C…" — extract the bare ID. + // For DMs, To is "user:U…" which can't be used for reactions; fall back + // to NativeChannelId (the raw Slack channel id, e.g. "D…"). + const currentChannelId = params.context.To?.startsWith("channel:") + ? params.context.To.slice("channel:".length) + : params.context.NativeChannelId?.trim() || undefined; return { - currentChannelId: params.context.To?.startsWith("channel:") - ? params.context.To.slice("channel:".length) - : undefined, + currentChannelId, currentThreadTs: threadId != null ? String(threadId) : undefined, replyToMode: effectiveReplyToMode, hasRepliedRef: params.hasRepliedRef,