mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 18:44:57 +00:00
feat(slack): per-thread session isolation for DM auto-threading (#26849)
* feat(slack): create thread sessions for auto-threaded DM messages When replyToMode="all", every top-level message starts a new Slack thread. Previously, only subsequent replies in that thread got an isolated session (via 🧵<threadTs> suffix). The initial message fell back to the base DM session, mixing context across unrelated conversations. Now, when replyToMode="all" and a message is not already a thread reply, the message's own ts is used as the threadId for session key resolution. This gives the initial message AND all subsequent thread replies the same isolated session. This enables per-thread session isolation for Slack DMs — each new message starts its own thread and session, keeping conversations separate. * Slack: fix auto-thread session key mode check and add changelog --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user