fix(slack): thread channel ID through inbound context for reactions (#34831)

Slack reaction/thread context routing fixes via canonical synthesis of #34831.

Co-authored-by: Tak <tak@users.noreply.github.com>
This commit is contained in:
dunamismax
2026-03-05 21:47:31 -05:00
committed by GitHub
parent 909f26a26b
commit 1efa7a88c4
7 changed files with 45 additions and 3 deletions

View File

@@ -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. - 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. - 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. - 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. - 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.<name>.headers` across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin. - Models/custom provider headers: propagate `models.providers.<name>.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. - 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.

View File

@@ -58,6 +58,7 @@ export function buildThreadingToolContext(params: {
ReplyToId: sessionCtx.ReplyToId, ReplyToId: sessionCtx.ReplyToId,
ThreadLabel: sessionCtx.ThreadLabel, ThreadLabel: sessionCtx.ThreadLabel,
MessageThreadId: sessionCtx.MessageThreadId, MessageThreadId: sessionCtx.MessageThreadId,
NativeChannelId: sessionCtx.NativeChannelId,
}, },
hasRepliedRef, hasRepliedRef,
}) ?? {}; }) ?? {};

View File

@@ -142,6 +142,8 @@ export type MsgContext = {
GatewayClientScopes?: string[]; GatewayClientScopes?: string[];
/** Thread identifier (Telegram topic id or Matrix thread event id). */ /** Thread identifier (Telegram topic id or Matrix thread event id). */
MessageThreadId?: string | number; MessageThreadId?: string | number;
/** Platform-native channel/conversation id (e.g. Slack DM channel "D…" id). */
NativeChannelId?: string;
/** Telegram forum supergroup marker. */ /** Telegram forum supergroup marker. */
IsForum?: boolean; IsForum?: boolean;
/** Warning: DM has topics enabled but this message is not in a topic. */ /** Warning: DM has topics enabled but this message is not in a topic. */

View File

@@ -257,6 +257,8 @@ export type ChannelThreadingContext = {
ReplyToIdFull?: string; ReplyToIdFull?: string;
ThreadLabel?: string; ThreadLabel?: string;
MessageThreadId?: string | number; MessageThreadId?: string | number;
/** Platform-native channel/conversation id (e.g. Slack DM channel "D…" id). */
NativeChannelId?: string;
}; };
export type ChannelThreadingToolContext = { export type ChannelThreadingToolContext = {

View File

@@ -727,6 +727,7 @@ export async function prepareSlackMessage(params: {
CommandAuthorized: commandAuthorized, CommandAuthorized: commandAuthorized,
OriginatingChannel: "slack" as const, OriginatingChannel: "slack" as const,
OriginatingTo: slackTo, OriginatingTo: slackTo,
NativeChannelId: message.channel,
}) satisfies FinalizedMsgContext; }) satisfies FinalizedMsgContext;
const pinnedMainDmOwner = isDirectMessage const pinnedMainDmOwner = isDirectMessage
? resolvePinnedMainDmOwnerFromAllowlist({ ? resolvePinnedMainDmOwnerFromAllowlist({

View File

@@ -144,4 +144,35 @@ describe("buildSlackThreadingToolContext", () => {
}); });
expect(result.replyToMode).toBe("off"); 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();
});
}); });

View File

@@ -19,10 +19,14 @@ export function buildSlackThreadingToolContext(params: {
const hasExplicitThreadTarget = params.context.MessageThreadId != null; const hasExplicitThreadTarget = params.context.MessageThreadId != null;
const effectiveReplyToMode = hasExplicitThreadTarget ? "all" : configuredReplyToMode; const effectiveReplyToMode = hasExplicitThreadTarget ? "all" : configuredReplyToMode;
const threadId = params.context.MessageThreadId ?? params.context.ReplyToId; 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 { return {
currentChannelId: params.context.To?.startsWith("channel:") currentChannelId,
? params.context.To.slice("channel:".length)
: undefined,
currentThreadTs: threadId != null ? String(threadId) : undefined, currentThreadTs: threadId != null ? String(threadId) : undefined,
replyToMode: effectiveReplyToMode, replyToMode: effectiveReplyToMode,
hasRepliedRef: params.hasRepliedRef, hasRepliedRef: params.hasRepliedRef,