fix(slack): keep top-level dms on stable session · openclaw/openclaw@d964488
steipete
·
2026-05-02
·
via Recent Commits to openclaw:main
File tree
extensions/slack/src/monitor/message-handler
| Original file line number | Diff line number | Diff line change |
|---|
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
35 | 35 | - 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. |
36 | 36 | - 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. |
37 | 37 | - 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. |
38 | 39 | - 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. |
39 | 40 | - 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. |
40 | 41 | - Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -92,9 +92,9 @@ export function resolveSlackRoutingContext(params: {
|
92 | 92 | const threadContext = resolveSlackThreadContext({ message, replyToMode }); |
93 | 93 | const threadTs = threadContext.incomingThreadTs; |
94 | 94 | 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. |
98 | 98 | const autoThreadId = |
99 | 99 | !isThreadReply && replyToMode === "all" && threadContext.messageTs |
100 | 100 | ? threadContext.messageTs |
@@ -115,7 +115,15 @@ export function resolveSlackRoutingContext(params: {
|
115 | 115 | ? seedCandidateThreadId |
116 | 116 | : undefined; |
117 | 117 | 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; |
119 | 127 | const routedThreadId = canonicalThreadId ?? (isRoomish ? seededRoomThreadId : undefined); |
120 | 128 | const baseConversationId = resolveSlackBaseConversationId({ message, isDirectMessage }); |
121 | 129 | const boundThreadRoute = routedThreadId |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1073,11 +1073,11 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
1073 | 1073 | expect(prepared!.ctxPayload.Body).not.toContain("parent_user_id"); |
1074 | 1074 | }); |
1075 | 1075 | |
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 () => { |
1077 | 1077 | const { storePath } = storeFixture.makeTmpStorePath(); |
1078 | 1078 | const slackCtx = createInboundSlackCtx({ |
1079 | 1079 | cfg: { |
1080 | | -session: { store: storePath }, |
| 1080 | +session: { store: storePath, dmScope: "per-channel-peer" }, |
1081 | 1081 | channels: { slack: { enabled: true, replyToMode: "all" } }, |
1082 | 1082 | } as OpenClawConfig, |
1083 | 1083 | replyToMode: "all", |
@@ -1092,9 +1092,7 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
1092 | 1092 | ); |
1093 | 1093 | |
1094 | 1094 | 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"); |
1098 | 1096 | expect(prepared!.ctxPayload.MessageThreadId).toBe("500.000"); |
1099 | 1097 | }); |
1100 | 1098 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -4,10 +4,14 @@ import type { ResolvedSlackAccount } from "../../accounts.js";
|
4 | 4 | import type { SlackMessageEvent } from "../../types.js"; |
5 | 5 | import { resolveSlackRoutingContext, type SlackRoutingContextDeps } from "./prepare-routing.js"; |
6 | 6 | |
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 | +}) { |
8 | 11 | const replyToMode = overrides?.replyToMode ?? "all"; |
9 | 12 | return { |
10 | 13 | cfg: { |
| 14 | +session: { dmScope: overrides?.dmScope }, |
11 | 15 | channels: { |
12 | 16 | slack: { enabled: true, replyToMode }, |
13 | 17 | }, |
@@ -321,4 +325,28 @@ describe("thread-level session keys", () => {
|
321 | 325 | const sessionKey = routing.sessionKey; |
322 | 326 | expect(sessionKey).not.toContain(":thread:"); |
323 | 327 | }); |
| 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 | +}); |
324 | 352 | }); |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。