mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-21 05:32:53 +00:00
feat(slack): add thread.requireExplicitMention config option (#58276)
* feat(slack): add thread.requireExplicitMention config option When requireMention is true in a Slack channel, replying inside a thread where the bot previously participated currently bypasses mention gating via implicit mention detection. This makes the bot respond to every thread message even without an explicit @mention. Add channels.slack.thread.requireExplicitMention (default: false) which, when set to true, suppresses implicit thread mentions. Only explicit @bot mentions will trigger replies inside threads. Closes #34389 Closes #49972 * slack: refresh changelog and generated config artifacts * slack: restore bundled channel metadata generation --------- Co-authored-by: praktika-devops <devops@praktika.ai> Co-authored-by: George Pickett <gpickett00@gmail.com>
This commit is contained in:
committed by
GitHub
parent
98b76d83ea
commit
b8c8139138
@@ -45,6 +45,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Tools/web search/Exa: show Exa Search in onboarding and configure provider pickers again by marking the bundled Exa provider as setup-visible. Thanks @vincentkoc.
|
||||
- Docs/i18n: relocalize final localized-page links after translation and remove the zh-CN homepage redirect override so localized Mintlify pages resolve to the correct language roots again. (#61796) Thanks @hxy91819.
|
||||
- Plugins/provider hooks: stop recursive provider snapshot loads from overflowing the stack during plugin initialization, while still preserving cached nested provider-hook results. (#61922, #61938, #61946, #61951)
|
||||
- Exec/runtime events: mark background `notifyOnExit` summaries and ACP parent-stream relays as untrusted system events so lower-trust runtime output no longer re-enters later turns as trusted `System:` text.
|
||||
- Hooks/wake: queue direct and mapped wake-hook payloads as untrusted system events so external wake content no longer enters the main session as trusted input. (#62003)
|
||||
- Slack/thread mentions: add `channels.slack.thread.requireExplicitMention` so Slack channels that already require mentions can also require explicit `@bot` mentions inside bot-participated threads. (#58276) Thanks @praktika-engineer.
|
||||
|
||||
## 2026.4.5
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
afae767b9ec71e2566ec92bd3760408a2b339210ddac66c4d02c5f25d0089d31 config-baseline.json
|
||||
e742d7392b2d9b90a6cd9cdf0fb2878951474f17fdeb2a58d4b44271a83f2153 config-baseline.core.json
|
||||
66edc86a9d16db1b9e9e7dd99b7032e2d9bcfb9ff210256a21f4b4f088cb3dc1 config-baseline.channel.json
|
||||
f13d08dc73c57307c847643c6b4a4dda899e44f95d97e15ff44dc0f8e7387675 config-baseline.plugin.json
|
||||
6ad199bff1771839d1ab1129c2bb27ff583cf5a2e60a5603fa87b8a34c0856d0 config-baseline.json
|
||||
cd556f8c976e535c710b5273c895bc5763650d67090e30dedc82cf227b2034d6 config-baseline.core.json
|
||||
d22f4414b79ee03d896e58d875c80523bcc12303cbacb1700261e6ec73945187 config-baseline.channel.json
|
||||
1891bcb68d80ab8b7546a2946b5a9d82b18c3e92ffd2c834d15928e73fa11564 config-baseline.plugin.json
|
||||
|
||||
@@ -399,7 +399,7 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
|
||||
|
||||
- explicit app mention (`<@botId>`)
|
||||
- mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`)
|
||||
- implicit reply-to-bot thread behavior
|
||||
- implicit reply-to-bot thread behavior (disabled when `thread.requireExplicitMention` is `true`)
|
||||
|
||||
Per-channel controls (`channels.slack.channels.<id>`; names only via startup resolution or `dangerouslyAllowNameMatching`):
|
||||
|
||||
@@ -423,6 +423,7 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
|
||||
- Thread replies can create thread session suffixes (`:thread:<threadTs>`) when applicable.
|
||||
- `channels.slack.thread.historyScope` default is `thread`; `thread.inheritParent` default is `false`.
|
||||
- `channels.slack.thread.initialHistoryLimit` controls how many existing thread messages are fetched when a new thread session starts (default `20`; set `0` to disable).
|
||||
- `channels.slack.thread.requireExplicitMention` (default `false`): when `true`, suppress implicit thread mentions so the bot only responds to explicit `@bot` mentions inside threads, even when the bot already participated in the thread. Without this, replies in a bot-participated thread bypass `requireMention` gating.
|
||||
|
||||
Reply threading controls:
|
||||
|
||||
|
||||
1
extensions/mattermost/src/config-schema.ts
Normal file
1
extensions/mattermost/src/config-schema.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MattermostChannelConfigSchema } from "./config-surface.js";
|
||||
@@ -109,4 +109,8 @@ export const slackChannelConfigUiHints = {
|
||||
label: "Slack Thread Initial History Limit",
|
||||
help: "Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable).",
|
||||
},
|
||||
"thread.requireExplicitMention": {
|
||||
label: "Slack Thread Require Explicit Mention",
|
||||
help: "If true, require an explicit @mention even inside threads where the bot has participated. Suppresses implicit thread mention behavior so the bot only responds to explicit @bot mentions in threads (default: false).",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
|
||||
@@ -34,6 +34,7 @@ function createTestContext() {
|
||||
replyToMode: "off",
|
||||
threadHistoryScope: "thread",
|
||||
threadInheritParent: false,
|
||||
threadRequireExplicitMention: false,
|
||||
slashCommand: {
|
||||
enabled: true,
|
||||
name: "openclaw",
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SlackMonitorContext = {
|
||||
replyToMode: "off" | "first" | "all" | "batched";
|
||||
threadHistoryScope: "thread" | "channel";
|
||||
threadInheritParent: boolean;
|
||||
threadRequireExplicitMention: boolean;
|
||||
slashCommand: Required<import("openclaw/plugin-sdk/config-runtime").SlackSlashCommandConfig>;
|
||||
textLimit: number;
|
||||
ackReactionScope: string;
|
||||
@@ -118,6 +119,7 @@ export function createSlackMonitorContext(params: {
|
||||
replyToMode: SlackMonitorContext["replyToMode"];
|
||||
threadHistoryScope: SlackMonitorContext["threadHistoryScope"];
|
||||
threadInheritParent: SlackMonitorContext["threadInheritParent"];
|
||||
threadRequireExplicitMention: SlackMonitorContext["threadRequireExplicitMention"];
|
||||
slashCommand: SlackMonitorContext["slashCommand"];
|
||||
textLimit: number;
|
||||
ackReactionScope: string;
|
||||
@@ -418,6 +420,7 @@ export function createSlackMonitorContext(params: {
|
||||
replyToMode: params.replyToMode,
|
||||
threadHistoryScope: params.threadHistoryScope,
|
||||
threadInheritParent: params.threadInheritParent,
|
||||
threadRequireExplicitMention: params.threadRequireExplicitMention,
|
||||
slashCommand: params.slashCommand,
|
||||
textLimit: params.textLimit,
|
||||
ackReactionScope: params.ackReactionScope,
|
||||
|
||||
@@ -11,6 +11,7 @@ export function createInboundSlackTestContext(params: {
|
||||
defaultRequireMention?: boolean;
|
||||
replyToMode?: "off" | "all" | "first";
|
||||
channelsConfig?: SlackChannelConfigEntries;
|
||||
threadRequireExplicitMention?: boolean;
|
||||
}) {
|
||||
return createSlackMonitorContext({
|
||||
cfg: params.cfg,
|
||||
@@ -39,6 +40,7 @@ export function createInboundSlackTestContext(params: {
|
||||
replyToMode: params.replyToMode ?? "off",
|
||||
threadHistoryScope: "thread",
|
||||
threadInheritParent: false,
|
||||
threadRequireExplicitMention: params.threadRequireExplicitMention ?? false,
|
||||
slashCommand: {
|
||||
enabled: false,
|
||||
name: "openclaw",
|
||||
|
||||
@@ -645,6 +645,7 @@ describe("prepareSlackMessage sender prefix", () => {
|
||||
replyToMode: "off",
|
||||
threadHistoryScope: "channel",
|
||||
threadInheritParent: false,
|
||||
threadRequireExplicitMention: false,
|
||||
slashCommand: params.slashCommand,
|
||||
textLimit: 2000,
|
||||
ackReactionScope: "off",
|
||||
@@ -710,3 +711,121 @@ describe("prepareSlackMessage sender prefix", () => {
|
||||
expect(result?.ctxPayload.CommandAuthorized).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("slack thread.requireExplicitMention", () => {
|
||||
let fixtureRoot = "";
|
||||
let caseId = 0;
|
||||
|
||||
function makeTmpStorePath() {
|
||||
if (!fixtureRoot) {
|
||||
throw new Error("fixtureRoot missing");
|
||||
}
|
||||
const dir = path.join(fixtureRoot, `require-explicit-${caseId++}`);
|
||||
fs.mkdirSync(dir);
|
||||
return { dir, storePath: path.join(dir, "sessions.json") };
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-explicit-mention-"));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fixtureRoot) {
|
||||
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
fixtureRoot = "";
|
||||
}
|
||||
});
|
||||
|
||||
function createCtxWithExplicitMention(requireExplicitMention: boolean) {
|
||||
const ctx = createInboundSlackTestContext({
|
||||
cfg: {
|
||||
channels: { slack: { enabled: true } },
|
||||
session: {},
|
||||
} as OpenClawConfig,
|
||||
threadRequireExplicitMention: requireExplicitMention,
|
||||
});
|
||||
ctx.resolveUserName = async () => ({ name: "Alice" }) as any;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
it("drops thread reply without explicit mention when requireExplicitMention is true", async () => {
|
||||
const ctx = createCtxWithExplicitMention(true);
|
||||
const { storePath } = makeTmpStorePath();
|
||||
vi.spyOn(
|
||||
await import("openclaw/plugin-sdk/config-runtime"),
|
||||
"resolveStorePath",
|
||||
).mockReturnValue(storePath);
|
||||
const account = createSlackTestAccount();
|
||||
const message: SlackMessageEvent = {
|
||||
type: "message",
|
||||
channel: "C123",
|
||||
channel_type: "channel",
|
||||
user: "U1",
|
||||
text: "hello",
|
||||
ts: "1700000001.000001",
|
||||
thread_ts: "1700000000.000000",
|
||||
parent_user_id: "B1", // bot is thread parent
|
||||
};
|
||||
const result = await prepareSlackMessage({
|
||||
ctx,
|
||||
account,
|
||||
message,
|
||||
opts: { source: "message" },
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("allows thread reply with explicit @mention when requireExplicitMention is true", async () => {
|
||||
const ctx = createCtxWithExplicitMention(true);
|
||||
const { storePath } = makeTmpStorePath();
|
||||
vi.spyOn(
|
||||
await import("openclaw/plugin-sdk/config-runtime"),
|
||||
"resolveStorePath",
|
||||
).mockReturnValue(storePath);
|
||||
const account = createSlackTestAccount();
|
||||
const message: SlackMessageEvent = {
|
||||
type: "message",
|
||||
channel: "C123",
|
||||
channel_type: "channel",
|
||||
user: "U1",
|
||||
text: "<@B1> hello",
|
||||
ts: "1700000001.000002",
|
||||
thread_ts: "1700000000.000000",
|
||||
parent_user_id: "B1",
|
||||
};
|
||||
const result = await prepareSlackMessage({
|
||||
ctx,
|
||||
account,
|
||||
message,
|
||||
opts: { source: "message" },
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("allows thread reply without explicit mention when requireExplicitMention is false (default)", async () => {
|
||||
const ctx = createCtxWithExplicitMention(false);
|
||||
const { storePath } = makeTmpStorePath();
|
||||
vi.spyOn(
|
||||
await import("openclaw/plugin-sdk/config-runtime"),
|
||||
"resolveStorePath",
|
||||
).mockReturnValue(storePath);
|
||||
const account = createSlackTestAccount();
|
||||
const message: SlackMessageEvent = {
|
||||
type: "message",
|
||||
channel: "C123",
|
||||
channel_type: "channel",
|
||||
user: "U1",
|
||||
text: "hello",
|
||||
ts: "1700000001.000003",
|
||||
thread_ts: "1700000000.000000",
|
||||
parent_user_id: "B1",
|
||||
};
|
||||
const result = await prepareSlackMessage({
|
||||
ctx,
|
||||
account,
|
||||
message,
|
||||
opts: { source: "message" },
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -384,6 +384,7 @@ export async function prepareSlackMessage(params: {
|
||||
},
|
||||
}));
|
||||
const implicitMention = Boolean(
|
||||
!ctx.threadRequireExplicitMention &&
|
||||
!isDirectMessage &&
|
||||
ctx.botUserId &&
|
||||
message.thread_ts &&
|
||||
|
||||
@@ -142,6 +142,7 @@ const baseParams = () => ({
|
||||
mediaMaxBytes: 1,
|
||||
threadHistoryScope: "thread" as const,
|
||||
threadInheritParent: false,
|
||||
threadRequireExplicitMention: false,
|
||||
removeAckAfterReply: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -312,6 +312,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const replyToMode = slackCfg.replyToMode ?? "off";
|
||||
const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
|
||||
const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
|
||||
const threadRequireExplicitMention = slackCfg.thread?.requireExplicitMention ?? false;
|
||||
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
|
||||
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId, {
|
||||
fallbackLimit: SLACK_TEXT_LIMIT,
|
||||
@@ -423,6 +424,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
replyToMode,
|
||||
threadHistoryScope,
|
||||
threadInheritParent,
|
||||
threadRequireExplicitMention,
|
||||
slashCommand,
|
||||
textLimit,
|
||||
ackReactionScope,
|
||||
|
||||
@@ -10840,6 +10840,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
minimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
requireExplicitMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -11746,6 +11749,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
minimum: 0,
|
||||
maximum: 9007199254740991,
|
||||
},
|
||||
requireExplicitMention: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
@@ -12130,6 +12136,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "Slack Thread Initial History Limit",
|
||||
help: "Maximum number of existing Slack thread messages to fetch when starting a new thread session (default: 20, set to 0 to disable).",
|
||||
},
|
||||
"thread.requireExplicitMention": {
|
||||
label: "Slack Thread Require Explicit Mention",
|
||||
help: "If true, require an explicit @mention even inside threads where the bot has participated. Suppresses implicit thread mention behavior so the bot only responds to explicit @bot mentions in threads (default: false).",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -96,6 +96,14 @@ export type SlackThreadConfig = {
|
||||
inheritParent?: boolean;
|
||||
/** Maximum number of thread messages to fetch as context when starting a new thread session (default: 20). Set to 0 to disable thread history fetching. */
|
||||
initialHistoryLimit?: number;
|
||||
/**
|
||||
* If true, require explicit @mention even inside threads where the bot has
|
||||
* previously participated. By default (false), replying in a thread where
|
||||
* the bot is a participant counts as an implicit mention and bypasses
|
||||
* requireMention gating. Set to true to suppress implicit thread mentions
|
||||
* so only explicit @bot mentions trigger replies in threads.
|
||||
*/
|
||||
requireExplicitMention?: boolean;
|
||||
};
|
||||
|
||||
export type SlackAccountConfig = {
|
||||
|
||||
@@ -865,6 +865,7 @@ export const SlackThreadSchema = z
|
||||
historyScope: z.enum(["thread", "channel"]).optional(),
|
||||
inheritParent: z.boolean().optional(),
|
||||
initialHistoryLimit: z.number().int().min(0).optional(),
|
||||
requireExplicitMention: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user