Skip to content

Commit d964488

Browse files
committed
fix(slack): keep top-level dms on stable session
1 parent 9c307a3 commit d964488

4 files changed

Lines changed: 45 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
3535
- Providers/configure: preserve the existing default model when adding or reauthing a provider whose plugin returns a default-model config patch. Fixes #50268. Thanks @rixcorp-oc.
3636
- Slack/message actions: send media before the follow-up Block Kit message when Slack `send` includes a file plus presentation or interactive controls, so file attachments are no longer rejected. Fixes #51458. Thanks @HirokiKobayashi-R.
3737
- Slack/DMs: honor `dmHistoryLimit` for fresh 1:1 Slack DM sessions by backfilling recent conversation history before the current reply. Fixes #64427. Thanks @brantley-creator.
38+
- Slack/DMs: keep top-level direct messages on the stable DM session even when `replyToMode` targets Slack thread replies, preserving context across DM turns. Fixes #58832. Thanks @daye-jjeong.
3839
- Slack/mentions: resolve `<!subteam^...>` user-group mentions through Slack `usergroups.users.list` and treat them as explicit mentions only when the bot user is a member, so mention-gated agent channels wake for real user-group mentions without config-only allowlists. Fixes #73827. Thanks @CG-Intelligence-Agent-Jack.
3940
- Slack/message tool: let `read` fetch an exact Slack message timestamp, including a specific thread reply when paired with `threadId`, instead of returning only the parent thread or recent channel history. Fixes #53943. Thanks @zomars.
4041
- Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97.

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ export function resolveSlackRoutingContext(params: {
9292
const threadContext = resolveSlackThreadContext({ message, replyToMode });
9393
const threadTs = threadContext.incomingThreadTs;
9494
const isThreadReply = threadContext.isThreadReply;
95-
// Keep true thread replies thread-scoped, but preserve channel-level sessions
96-
// for top-level room turns when replyToMode is off.
97-
// For DMs, preserve existing auto-thread behavior when replyToMode="all".
95+
// Keep true thread replies thread-scoped, while top-level DMs keep their
96+
// stable direct-message session even when reply delivery targets a Slack UI
97+
// thread.
9898
const autoThreadId =
9999
!isThreadReply && replyToMode === "all" && threadContext.messageTs
100100
? threadContext.messageTs
@@ -115,7 +115,15 @@ export function resolveSlackRoutingContext(params: {
115115
? seedCandidateThreadId
116116
: undefined;
117117
const roomThreadId = isThreadReply && threadTs ? threadTs : undefined;
118-
const canonicalThreadId = isRoomish ? roomThreadId : isThreadReply ? threadTs : autoThreadId;
118+
const canonicalThreadId = isDirectMessage
119+
? isThreadReply
120+
? threadTs
121+
: undefined
122+
: isRoomish
123+
? roomThreadId
124+
: isThreadReply
125+
? threadTs
126+
: autoThreadId;
119127
const routedThreadId = canonicalThreadId ?? (isRoomish ? seededRoomThreadId : undefined);
120128
const baseConversationId = resolveSlackBaseConversationId({ message, isDirectMessage });
121129
const boundThreadRoute = routedThreadId

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,11 +1073,11 @@ describe("slack prepareSlackMessage inbound contract", () => {
10731073
expect(prepared!.ctxPayload.Body).not.toContain("parent_user_id");
10741074
});
10751075

1076-
it("creates thread session for top-level DM when replyToMode=all", async () => {
1076+
it("keeps top-level DM session stable when replyToMode=all", async () => {
10771077
const { storePath } = storeFixture.makeTmpStorePath();
10781078
const slackCtx = createInboundSlackCtx({
10791079
cfg: {
1080-
session: { store: storePath },
1080+
session: { store: storePath, dmScope: "per-channel-peer" },
10811081
channels: { slack: { enabled: true, replyToMode: "all" } },
10821082
} as OpenClawConfig,
10831083
replyToMode: "all",
@@ -1092,9 +1092,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
10921092
);
10931093

10941094
expect(prepared).toBeTruthy();
1095-
// Session key should include :thread:500.000 for the auto-threaded message
1096-
expect(prepared!.ctxPayload.SessionKey).toContain(":thread:500.000");
1097-
// MessageThreadId should be set for the reply
1095+
expect(prepared!.ctxPayload.SessionKey).toBe("agent:main:slack:direct:u1");
10981096
expect(prepared!.ctxPayload.MessageThreadId).toBe("500.000");
10991097
});
11001098

extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import type { ResolvedSlackAccount } from "../../accounts.js";
44
import type { SlackMessageEvent } from "../../types.js";
55
import { resolveSlackRoutingContext, type SlackRoutingContextDeps } from "./prepare-routing.js";
66

7-
function buildCtx(overrides?: { replyToMode?: "all" | "first" | "off" | "batched" }) {
7+
function buildCtx(overrides?: {
8+
replyToMode?: "all" | "first" | "off" | "batched";
9+
dmScope?: "main" | "per-sender" | "per-channel-peer";
10+
}) {
811
const replyToMode = overrides?.replyToMode ?? "all";
912
return {
1013
cfg: {
14+
session: { dmScope: overrides?.dmScope },
1115
channels: {
1216
slack: { enabled: true, replyToMode },
1317
},
@@ -321,4 +325,28 @@ describe("thread-level session keys", () => {
321325
const sessionKey = routing.sessionKey;
322326
expect(sessionKey).not.toContain(":thread:");
323327
});
328+
329+
it("keeps top-level DMs on the direct session when replyToMode=all", () => {
330+
const ctx = buildCtx({ replyToMode: "all", dmScope: "per-channel-peer" });
331+
const account = buildAccount("all");
332+
333+
const routing = resolveSlackRoutingContext({
334+
ctx,
335+
account,
336+
message: {
337+
channel: "D456",
338+
channel_type: "im",
339+
user: "U3",
340+
text: "dm message",
341+
ts: "1770408530.000000",
342+
} as SlackMessageEvent,
343+
isDirectMessage: true,
344+
isGroupDm: false,
345+
isRoom: false,
346+
isRoomish: false,
347+
});
348+
349+
expect(routing.sessionKey).toBe("agent:main:slack:direct:u3");
350+
expect(routing.threadContext.messageThreadId).toBe("1770408530.000000");
351+
});
324352
});

0 commit comments

Comments
 (0)