diff --git a/CHANGELOG.md b/CHANGELOG.md index a6330ba84..593b2f183 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai - BlueBubbles: fix webhook auth bypass via loopback proxy trust. (#13787) Thanks @coygeek. - Slack: change default replyToMode from "off" to "all". (#14364) Thanks @nm-de. - Slack: detect control commands when channel messages start with bot mention prefixes (for example, `@Bot /new`). (#14142) Thanks @beefiker. +- Slack: include thread reply metadata in inbound message footer context (`thread_ts`, `parent_user_id`) while keeping top-level `thread_ts == ts` events unthreaded. (#14625) Thanks @bennewton999. - Signal: enforce E.164 validation for the Signal bot account prompt so mistyped numbers are caught early. (#15063) Thanks @Duartemartins. - Discord: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr. - Discord: treat Administrator as full permissions in channel permission checks. Thanks @thewilloftheshadow. diff --git a/src/slack/monitor/message-handler/prepare.inbound-contract.test.ts b/src/slack/monitor/message-handler/prepare.inbound-contract.test.ts index e5080d22e..9f438c1d2 100644 --- a/src/slack/monitor/message-handler/prepare.inbound-contract.test.ts +++ b/src/slack/monitor/message-handler/prepare.inbound-contract.test.ts @@ -306,7 +306,7 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared).toBeTruthy(); // Verify thread metadata is in the message footer expect(prepared!.ctxPayload.Body).toMatch( - /\[slack message id: 1\.002 channel: D123 thread_ts: 1\.000 parent_user: U2\]/, + /\[slack message id: 1\.002 channel: D123 thread_ts: 1\.000 parent_user_id: U2\]/, ); }); @@ -380,4 +380,76 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared!.ctxPayload.Body).toMatch(/\[slack message id: 1\.000 channel: D123\]$/); expect(prepared!.ctxPayload.Body).not.toContain("thread_ts"); }); + + it("excludes thread metadata when thread_ts equals ts without parent_user_id", async () => { + const slackCtx = createSlackMonitorContext({ + cfg: { + channels: { slack: { enabled: true } }, + } as OpenClawConfig, + accountId: "default", + botToken: "token", + app: { client: {} } as App, + runtime: {} as RuntimeEnv, + botUserId: "B1", + teamId: "T1", + apiAppId: "A1", + historyLimit: 0, + sessionScope: "per-sender", + mainKey: "main", + dmEnabled: true, + dmPolicy: "open", + allowFrom: [], + groupDmEnabled: true, + groupDmChannels: [], + defaultRequireMention: true, + groupPolicy: "open", + useAccessGroups: false, + reactionMode: "off", + reactionAllowlist: [], + replyToMode: "off", + threadHistoryScope: "thread", + threadInheritParent: false, + slashCommand: { + enabled: false, + name: "openclaw", + sessionPrefix: "slack:slash", + ephemeral: true, + }, + textLimit: 4000, + ackReactionScope: "group-mentions", + mediaMaxBytes: 1024, + removeAckAfterReply: false, + }); + // oxlint-disable-next-line typescript/no-explicit-any + slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any; + + const account: ResolvedSlackAccount = { + accountId: "default", + enabled: true, + botTokenSource: "config", + appTokenSource: "config", + config: {}, + }; + + const message: SlackMessageEvent = { + channel: "D123", + channel_type: "im", + user: "U1", + text: "top level", + ts: "1.000", + thread_ts: "1.000", + } as SlackMessageEvent; + + const prepared = await prepareSlackMessage({ + ctx: slackCtx, + account, + message, + opts: { source: "message" }, + }); + + expect(prepared).toBeTruthy(); + expect(prepared!.ctxPayload.Body).toMatch(/\[slack message id: 1\.000 channel: D123\]$/); + expect(prepared!.ctxPayload.Body).not.toContain("thread_ts"); + expect(prepared!.ctxPayload.Body).not.toContain("parent_user_id"); + }); }); diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 52fc509c3..438b9862c 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -399,9 +399,10 @@ export async function prepareSlackMessage(params: { GroupSubject: isRoomish ? roomLabel : undefined, From: slackFrom, }) ?? (isDirectMessage ? senderName : roomLabel); - const threadInfo = message.thread_ts - ? ` thread_ts: ${message.thread_ts}${message.parent_user_id ? ` parent_user: ${message.parent_user_id}` : ""}` - : ""; + const threadInfo = + isThreadReply && threadTs + ? ` thread_ts: ${threadTs}${message.parent_user_id ? ` parent_user_id: ${message.parent_user_id}` : ""}` + : ""; const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}${threadInfo}]`; const storePath = resolveStorePath(ctx.cfg.session?.store, { agentId: route.agentId,