Merge 1b9b43771e into da71eaebd2
This commit is contained in:
commit
85ffde1882
@ -337,6 +337,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.slack.userTokenReadOnly": "Slack User Token Read Only",
|
||||
"channels.slack.thread.historyScope": "Slack Thread History Scope",
|
||||
"channels.slack.thread.inheritParent": "Slack Thread Parent Inheritance",
|
||||
"channels.slack.thread.followOnMention": "Slack Thread Follow on Mention",
|
||||
"channels.slack.thread.followIdleMinutes": "Slack Thread Follow Idle Minutes",
|
||||
"channels.mattermost.botToken": "Mattermost Bot Token",
|
||||
"channels.mattermost.baseUrl": "Mattermost Base URL",
|
||||
"channels.mattermost.chatmode": "Mattermost Chat Mode",
|
||||
@ -469,6 +471,10 @@ const FIELD_HELP: Record<string, string> = {
|
||||
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||
"channels.slack.thread.inheritParent":
|
||||
"If true, Slack thread sessions inherit the parent channel transcript (default: false).",
|
||||
"channels.slack.thread.followOnMention":
|
||||
"If true, continue responding in threads where the bot was mentioned without requiring repeated @mentions (default: false).",
|
||||
"channels.slack.thread.followIdleMinutes":
|
||||
"Minutes of inactivity before a thread is no longer considered 'active' for followOnMention (default: 60).",
|
||||
"channels.mattermost.botToken":
|
||||
"Bot token from Mattermost System Console -> Integrations -> Bot Accounts.",
|
||||
"channels.mattermost.baseUrl":
|
||||
|
||||
@ -73,6 +73,16 @@ export type SlackThreadConfig = {
|
||||
historyScope?: "thread" | "channel";
|
||||
/** If true, thread sessions inherit the parent channel transcript. Default: false. */
|
||||
inheritParent?: boolean;
|
||||
/**
|
||||
* If true, continue responding in threads where the bot was mentioned
|
||||
* without requiring repeated @mentions. Default: false.
|
||||
*/
|
||||
followOnMention?: boolean;
|
||||
/**
|
||||
* Minutes of inactivity before a thread is no longer considered "active"
|
||||
* for followOnMention. Default: 60.
|
||||
*/
|
||||
followIdleMinutes?: number;
|
||||
};
|
||||
|
||||
export type SlackAccountConfig = {
|
||||
|
||||
@ -392,6 +392,8 @@ export const SlackThreadSchema = z
|
||||
.object({
|
||||
historyScope: z.enum(["thread", "channel"]).optional(),
|
||||
inheritParent: z.boolean().optional(),
|
||||
followOnMention: z.boolean().optional(),
|
||||
followIdleMinutes: z.number().int().positive().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@ -82,6 +82,8 @@ export type SlackMonitorContext = {
|
||||
replyToMode: "off" | "first" | "all";
|
||||
threadHistoryScope: "thread" | "channel";
|
||||
threadInheritParent: boolean;
|
||||
threadFollowOnMention: boolean;
|
||||
threadFollowIdleMinutes: number;
|
||||
slashCommand: Required<import("../../config/config.js").SlackSlashCommandConfig>;
|
||||
textLimit: number;
|
||||
ackReactionScope: string;
|
||||
@ -112,6 +114,10 @@ export type SlackMonitorContext = {
|
||||
threadTs?: string;
|
||||
status: string;
|
||||
}) => Promise<void>;
|
||||
/** Check if a thread is active (bot was mentioned recently). */
|
||||
isActiveThread: (channelId: string, threadTs: string) => boolean;
|
||||
/** Mark a thread as active (called when bot is mentioned in a thread). */
|
||||
markThreadActive: (channelId: string, threadTs: string) => void;
|
||||
};
|
||||
|
||||
export function createSlackMonitorContext(params: {
|
||||
@ -143,6 +149,8 @@ export function createSlackMonitorContext(params: {
|
||||
replyToMode: SlackMonitorContext["replyToMode"];
|
||||
threadHistoryScope: SlackMonitorContext["threadHistoryScope"];
|
||||
threadInheritParent: SlackMonitorContext["threadInheritParent"];
|
||||
threadFollowOnMention?: boolean;
|
||||
threadFollowIdleMinutes?: number;
|
||||
slashCommand: SlackMonitorContext["slashCommand"];
|
||||
textLimit: number;
|
||||
ackReactionScope: string;
|
||||
@ -152,6 +160,34 @@ export function createSlackMonitorContext(params: {
|
||||
const channelHistories = new Map<string, HistoryEntry[]>();
|
||||
const logger = getChildLogger({ module: "slack-auto-reply" });
|
||||
|
||||
// Thread following: track threads where the bot was mentioned
|
||||
// Key: "channelId:threadTs", Value: timestamp when last active
|
||||
const activeThreads = new Map<string, number>();
|
||||
const threadFollowOnMention = params.threadFollowOnMention ?? false;
|
||||
const threadFollowIdleMinutes = params.threadFollowIdleMinutes ?? 60;
|
||||
const threadFollowIdleMs = threadFollowIdleMinutes * 60 * 1000;
|
||||
|
||||
const isActiveThread = (channelId: string, threadTs: string): boolean => {
|
||||
if (!threadFollowOnMention) return false;
|
||||
const key = `${channelId}:${threadTs}`;
|
||||
const lastActive = activeThreads.get(key);
|
||||
if (!lastActive) return false;
|
||||
const now = Date.now();
|
||||
if (now - lastActive > threadFollowIdleMs) {
|
||||
// Thread has gone idle, remove it
|
||||
activeThreads.delete(key);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const markThreadActive = (channelId: string, threadTs: string): void => {
|
||||
if (!threadFollowOnMention) return;
|
||||
const key = `${channelId}:${threadTs}`;
|
||||
activeThreads.set(key, Date.now());
|
||||
logVerbose(`slack: marked thread ${key} as active for followOnMention`);
|
||||
};
|
||||
|
||||
const channelCache = new Map<
|
||||
string,
|
||||
{
|
||||
@ -387,6 +423,8 @@ export function createSlackMonitorContext(params: {
|
||||
replyToMode: params.replyToMode,
|
||||
threadHistoryScope: params.threadHistoryScope,
|
||||
threadInheritParent: params.threadInheritParent,
|
||||
threadFollowOnMention,
|
||||
threadFollowIdleMinutes,
|
||||
slashCommand: params.slashCommand,
|
||||
textLimit: params.textLimit,
|
||||
ackReactionScope: params.ackReactionScope,
|
||||
@ -400,5 +438,7 @@ export function createSlackMonitorContext(params: {
|
||||
resolveChannelName,
|
||||
resolveUserName,
|
||||
setSlackThreadStatus,
|
||||
isActiveThread,
|
||||
markThreadActive,
|
||||
};
|
||||
}
|
||||
|
||||
@ -223,11 +223,16 @@ export async function prepareSlackMessage(params: {
|
||||
canResolveExplicit: Boolean(ctx.botUserId),
|
||||
},
|
||||
}));
|
||||
// Check if this is an "active thread" where the bot was previously mentioned
|
||||
const isInActiveThread = Boolean(
|
||||
!isDirectMessage && message.thread_ts && ctx.isActiveThread(message.channel, message.thread_ts),
|
||||
);
|
||||
|
||||
const implicitMention = Boolean(
|
||||
!isDirectMessage &&
|
||||
ctx.botUserId &&
|
||||
message.thread_ts &&
|
||||
message.parent_user_id === ctx.botUserId,
|
||||
(message.parent_user_id === ctx.botUserId || isInActiveThread),
|
||||
);
|
||||
|
||||
const sender = message.user ? await ctx.resolveUserName(message.user) : null;
|
||||
@ -306,6 +311,18 @@ export async function prepareSlackMessage(params: {
|
||||
commandAuthorized,
|
||||
});
|
||||
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||
|
||||
// Mark thread as active if the bot was explicitly mentioned (for followOnMention feature)
|
||||
if (effectiveWasMentioned && message.thread_ts && ctx.threadFollowOnMention) {
|
||||
ctx.markThreadActive(message.channel, message.thread_ts);
|
||||
}
|
||||
// Also mark if this is a reply that creates a thread (the reply itself starts the thread)
|
||||
if (effectiveWasMentioned && !message.thread_ts && message.ts && ctx.threadFollowOnMention) {
|
||||
// If the bot responds, it will create a thread with this ts - mark it preemptively
|
||||
// Actually, we should mark the thread when the bot responds, not here
|
||||
// For now, just handle existing threads
|
||||
}
|
||||
|
||||
if (isRoom && shouldRequireMention && mentionGate.shouldSkip) {
|
||||
ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping channel message");
|
||||
const pendingText = (message.text ?? "").trim();
|
||||
|
||||
@ -118,6 +118,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const replyToMode = slackCfg.replyToMode ?? "off";
|
||||
const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
|
||||
const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
|
||||
const threadFollowOnMention = slackCfg.thread?.followOnMention ?? false;
|
||||
const threadFollowIdleMinutes = slackCfg.thread?.followIdleMinutes ?? 60;
|
||||
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
|
||||
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId);
|
||||
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
@ -199,6 +201,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
replyToMode,
|
||||
threadHistoryScope,
|
||||
threadInheritParent,
|
||||
threadFollowOnMention,
|
||||
threadFollowIdleMinutes,
|
||||
slashCommand,
|
||||
textLimit,
|
||||
ackReactionScope,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user