diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e80a5d7..9d4cb83b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -246,6 +246,7 @@ Docs: https://docs.openclaw.ai - Slack/Threading: resolve `replyToMode` per incoming message using chat-type-aware account config (`replyToModeByChatType` and legacy `dm.replyToMode`) so DM/channel reply threading honors overrides instead of always using monitor startup defaults. (#24717) Thanks @dbachelder. - Slack/Threading: track bot participation in message threads (per account/channel/thread) so follow-up messages in those threads can be handled without requiring repeated @mentions, while preserving mention-gating behavior for unrelated threads. (#29165) Thanks @luijoc. - Slack/Threading: stop forcing tool-call reply mode to `all` based on `ThreadLabel` alone; now force thread reply mode only when an explicit thread target exists (`MessageThreadId`/`ReplyToId`), so DM `replyToModeByChatType.direct` overrides are honored outside real thread replies. (#26251) Thanks @dbachelder. +- Slack/Threading: when `replyToMode="all"` auto-threads top-level Slack DMs, seed the thread session key from the message `ts` so the initial message and later replies share the same isolated `:thread:` session instead of falling back to base DM context. (#26849) Thanks @calder-sandy. - Agents/Subagents delivery: refactor subagent completion announce dispatch into an explicit queue/direct/fallback state machine, recover outbound channel-plugin resolution in cold/stale plugin-registry states across announce/message/gateway send paths, finalize cleanup bookkeeping when announce flow rejects, and treat Telegram sends without `message_id` as delivery failures (instead of false-success `"unknown"` IDs). (#26867, #25961, #26803, #25069, #26741) Thanks @SmithLabsLLC and @docaohieu2808. - Telegram/Webhook: pre-initialize webhook bots, switch webhook processing to callback-mode JSON handling, and preserve full near-limit payload reads under delayed handlers to prevent webhook request hangs and dropped updates. (#26156) - Slack/Session threads: prevent oversized parent-session inheritance from silently bricking new thread sessions, surface embedded context-overflow empty-result failures to users, and add configurable `session.parentForkMaxTokens` (default `100000`, `0` disables). (#26912) Thanks @markshields-tl. diff --git a/src/slack/monitor/message-handler/prepare.test.ts b/src/slack/monitor/message-handler/prepare.test.ts index f40e00631..c41f821c0 100644 --- a/src/slack/monitor/message-handler/prepare.test.ts +++ b/src/slack/monitor/message-handler/prepare.test.ts @@ -712,6 +712,32 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared!.ctxPayload.Body).not.toContain("thread_ts"); expect(prepared!.ctxPayload.Body).not.toContain("parent_user_id"); }); + + it("creates thread session for top-level DM when replyToMode=all", async () => { + const { storePath } = makeTmpStorePath(); + const slackCtx = createInboundSlackCtx({ + cfg: { + session: { store: storePath }, + channels: { slack: { enabled: true, replyToMode: "all" } }, + } as OpenClawConfig, + replyToMode: "all", + }); + // oxlint-disable-next-line typescript/no-explicit-any + slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any; + + const message = createSlackMessage({ ts: "500.000" }); + const prepared = await prepareMessageWith( + slackCtx, + createSlackAccount({ replyToMode: "all" }), + message, + ); + + expect(prepared).toBeTruthy(); + // Session key should include :thread:500.000 for the auto-threaded message + expect(prepared!.ctxPayload.SessionKey).toContain(":thread:500.000"); + // MessageThreadId should be set for the reply + expect(prepared!.ctxPayload.MessageThreadId).toBe("500.000"); + }); }); describe("prepareSlackMessage sender prefix", () => { diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 484e64ca4..79fd7957f 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -181,10 +181,19 @@ export async function prepareSlackMessage(params: { const threadContext = resolveSlackThreadContext({ message, replyToMode }); const threadTs = threadContext.incomingThreadTs; const isThreadReply = threadContext.isThreadReply; + // When replyToMode="all", every top-level message starts a new thread. + // Use its own ts as threadId so the initial message AND subsequent replies + // in that thread share an isolated session (instead of falling back to the + // base DM/channel session for the first message). + const autoThreadId = + !isThreadReply && replyToMode === "all" && threadContext.messageTs + ? threadContext.messageTs + : undefined; const threadKeys = resolveThreadSessionKeys({ baseSessionKey, - threadId: isThreadReply ? threadTs : undefined, - parentSessionKey: isThreadReply && ctx.threadInheritParent ? baseSessionKey : undefined, + threadId: isThreadReply ? threadTs : autoThreadId, + parentSessionKey: + (isThreadReply || autoThreadId) && ctx.threadInheritParent ? baseSessionKey : undefined, }); const sessionKey = threadKeys.sessionKey; const historyKey =