refactor: unify threading contexts

This commit is contained in:
Peter Steinberger
2026-01-21 20:01:12 +00:00
parent 76600e80ba
commit 45c1ccdfcf
15 changed files with 452 additions and 481 deletions

View File

@@ -77,4 +77,72 @@ describe("slack prepareSlackMessage inbound contract", () => {
expect(prepared).toBeTruthy();
expectInboundContextContract(prepared!.ctxPayload as any);
});
it("sets MessageThreadId for top-level messages when replyToMode=all", async () => {
const slackCtx = createSlackMonitorContext({
cfg: {
channels: { slack: { enabled: true, replyToMode: "all" } },
} as ClawdbotConfig,
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: "all",
threadHistoryScope: "thread",
threadInheritParent: false,
slashCommand: {
enabled: false,
name: "clawd",
sessionPrefix: "slack:slash",
ephemeral: true,
},
textLimit: 4000,
ackReactionScope: "group-mentions",
mediaMaxBytes: 1024,
removeAckAfterReply: false,
});
slackCtx.resolveUserName = async () => ({ name: "Alice" }) as any;
const account: ResolvedSlackAccount = {
accountId: "default",
enabled: true,
botTokenSource: "config",
appTokenSource: "config",
config: { replyToMode: "all" },
};
const message: SlackMessageEvent = {
channel: "D123",
channel_type: "im",
user: "U1",
text: "hi",
ts: "1.000",
} as SlackMessageEvent;
const prepared = await prepareSlackMessage({
ctx: slackCtx,
account,
message,
opts: { source: "message" },
});
expect(prepared).toBeTruthy();
expect(prepared!.ctxPayload.MessageThreadId).toBe("1.000");
});
});

View File

@@ -33,6 +33,7 @@ import type { ResolvedSlackAccount } from "../../accounts.js";
import { reactSlackMessage } from "../../actions.js";
import { sendMessageSlack } from "../../send.js";
import type { SlackMessageEvent } from "../../types.js";
import { resolveSlackThreadContext } from "../../threading.js";
import { resolveSlackAllowListMatch, resolveSlackUserAllowed } from "../allow-list.js";
import { resolveSlackEffectiveAllowFrom } from "../auth.js";
@@ -188,9 +189,9 @@ export async function prepareSlackMessage(params: {
});
const baseSessionKey = route.sessionKey;
const threadTs = message.thread_ts;
const hasThreadTs = typeof threadTs === "string" && threadTs.length > 0;
const isThreadReply = hasThreadTs && (threadTs !== message.ts || Boolean(message.parent_user_id));
const threadContext = resolveSlackThreadContext({ message, replyToMode: ctx.replyToMode });
const threadTs = threadContext.incomingThreadTs;
const isThreadReply = threadContext.isThreadReply;
const threadKeys = resolveThreadSessionKeys({
baseSessionKey,
threadId: isThreadReply ? threadTs : undefined,
@@ -474,9 +475,9 @@ export async function prepareSlackMessage(params: {
Provider: "slack" as const,
Surface: "slack" as const,
MessageSid: message.ts,
ReplyToId: message.thread_ts ?? message.ts,
// Preserve thread context for routed tool notifications (thread replies only).
MessageThreadId: isThreadReply ? threadTs : undefined,
ReplyToId: threadContext.replyToId,
// Preserve thread context for routed tool notifications.
MessageThreadId: threadContext.messageThreadId,
ParentSessionKey: threadKeys.parentSessionKey,
ThreadStarterBody: threadStarterBody,
ThreadLabel: threadLabel,