Skip to content

Commit aac83e0

Browse files
authored
fix: Slack inbound thread session routing (#72498)
Normalize actionable Slack thread roots and follow-up replies onto the same thread parent session key.
1 parent 93ac2ce commit aac83e0

5 files changed

Lines changed: 741 additions & 43 deletions

File tree

extensions/slack/src/monitor/message-handler/prepare-routing.ts

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type SlackRoutingContextDeps = {
2020
export type SlackRoutingContext = {
2121
route: ReturnType<typeof resolveAgentRoute>;
2222
runtimeBinding: RuntimeConversationBindingRouteResult["bindingRecord"];
23+
runtimeBoundSessionKey: string | undefined;
2324
chatType: "direct" | "group" | "channel";
2425
replyToMode: ReturnType<typeof resolveSlackReplyToMode>;
2526
threadContext: ReturnType<typeof resolveSlackThreadContext>;
@@ -39,6 +40,25 @@ function resolveSlackBaseConversationId(params: {
3940
: params.message.channel;
4041
}
4142

43+
function resolveSlackInitialAgentRoute(params: {
44+
ctx: SlackRoutingContextDeps;
45+
account: ResolvedSlackAccount;
46+
message: SlackMessageEvent;
47+
isDirectMessage: boolean;
48+
isRoom: boolean;
49+
}) {
50+
return resolveAgentRoute({
51+
cfg: params.ctx.cfg,
52+
channel: "slack",
53+
accountId: params.account.accountId,
54+
teamId: params.ctx.teamId || undefined,
55+
peer: {
56+
kind: params.isDirectMessage ? "direct" : params.isRoom ? "channel" : "group",
57+
id: params.isDirectMessage ? (params.message.user ?? "unknown") : params.message.channel,
58+
},
59+
});
60+
}
61+
4262
export function resolveSlackRoutingContext(params: {
4363
ctx: SlackRoutingContextDeps;
4464
account: ResolvedSlackAccount;
@@ -47,17 +67,24 @@ export function resolveSlackRoutingContext(params: {
4767
isGroupDm: boolean;
4868
isRoom: boolean;
4969
isRoomish: boolean;
70+
seedTopLevelRoomThread?: boolean;
5071
}): SlackRoutingContext {
51-
const { ctx, account, message, isDirectMessage, isGroupDm, isRoom, isRoomish } = params;
52-
let route = resolveAgentRoute({
53-
cfg: ctx.cfg,
54-
channel: "slack",
55-
accountId: account.accountId,
56-
teamId: ctx.teamId || undefined,
57-
peer: {
58-
kind: isDirectMessage ? "direct" : isRoom ? "channel" : "group",
59-
id: isDirectMessage ? (message.user ?? "unknown") : message.channel,
60-
},
72+
const {
73+
ctx,
74+
account,
75+
message,
76+
isDirectMessage,
77+
isGroupDm,
78+
isRoom,
79+
isRoomish,
80+
seedTopLevelRoomThread,
81+
} = params;
82+
let route = resolveSlackInitialAgentRoute({
83+
ctx,
84+
account,
85+
message,
86+
isDirectMessage,
87+
isRoom,
6188
});
6289

6390
const chatType = isDirectMessage ? "direct" : isGroupDm ? "group" : "channel";
@@ -72,21 +99,32 @@ export function resolveSlackRoutingContext(params: {
7299
!isThreadReply && replyToMode === "all" && threadContext.messageTs
73100
? threadContext.messageTs
74101
: undefined;
75-
// Only fork channel/group messages into thread-specific sessions when they are
76-
// actual thread replies (thread_ts present, different from message ts).
77-
// Top-level channel messages must stay on the per-channel session for continuity.
78-
// Before this fix, every channel message used its own ts as threadId, creating
79-
// isolated sessions per message (regression from #10686).
102+
// Keep ordinary top-level room messages on the per-channel session for
103+
// continuity, but preserve Slack thread identity when the event already has
104+
// one or when an actionable app mention will seed a reply thread.
105+
// This keeps a thread root and its later replies on one parent session
106+
// without returning to the old "every channel message is its own thread"
107+
// behavior (regression from #10686).
108+
const seedCandidateThreadId = threadContext.incomingThreadTs ?? threadContext.messageTs;
109+
const seededRoomThreadId =
110+
!isThreadReply &&
111+
isRoom &&
112+
seedTopLevelRoomThread &&
113+
replyToMode !== "off" &&
114+
seedCandidateThreadId
115+
? seedCandidateThreadId
116+
: undefined;
80117
const roomThreadId = isThreadReply && threadTs ? threadTs : undefined;
81118
const canonicalThreadId = isRoomish ? roomThreadId : isThreadReply ? threadTs : autoThreadId;
119+
const routedThreadId = canonicalThreadId ?? (isRoomish ? seededRoomThreadId : undefined);
82120
const baseConversationId = resolveSlackBaseConversationId({ message, isDirectMessage });
83-
const boundThreadRoute = canonicalThreadId
121+
const boundThreadRoute = routedThreadId
84122
? resolveRuntimeConversationBindingRoute({
85123
route,
86124
conversation: {
87125
channel: "slack",
88126
accountId: account.accountId,
89-
conversationId: canonicalThreadId,
127+
conversationId: routedThreadId,
90128
parentConversationId: baseConversationId,
91129
},
92130
})
@@ -107,9 +145,8 @@ export function resolveSlackRoutingContext(params: {
107145
? { sessionKey: route.sessionKey, parentSessionKey: undefined }
108146
: resolveThreadSessionKeys({
109147
baseSessionKey: route.sessionKey,
110-
threadId: canonicalThreadId,
111-
parentSessionKey:
112-
canonicalThreadId && ctx.threadInheritParent ? route.sessionKey : undefined,
148+
threadId: routedThreadId,
149+
parentSessionKey: routedThreadId && ctx.threadInheritParent ? route.sessionKey : undefined,
113150
});
114151
const sessionKey = threadKeys.sessionKey;
115152
const historyKey =
@@ -118,6 +155,7 @@ export function resolveSlackRoutingContext(params: {
118155
return {
119156
route,
120157
runtimeBinding: runtimeRoute.bindingRecord,
158+
runtimeBoundSessionKey: runtimeRoute.boundSessionKey,
121159
chatType,
122160
replyToMode,
123161
threadContext,

extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function createInboundSlackTestContext(params: {
1212
cfg: OpenClawConfig;
1313
appClient?: App["client"];
1414
defaultRequireMention?: boolean;
15-
replyToMode?: "off" | "all" | "first";
15+
replyToMode?: "off" | "all" | "first" | "batched";
1616
channelsConfig?: SlackChannelConfigEntries;
1717
threadRequireExplicitMention?: boolean;
1818
}) {

0 commit comments

Comments
 (0)