diff --git a/CHANGELOG.md b/CHANGELOG.md index 9303094d1..aec03170b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - Docs: expand parameter descriptions for agent/wake hooks. (#532) — thanks @mcinteerj - Docs: add community showcase entries from Discord. (#476) — thanks @gupsammy - TUI: refresh status bar after think/verbose/reasoning changes. (#519) — thanks @jdrhyne +- Commands: treat mention-bypassed group command messages as mentioned so elevated directives respond. ## 2026.1.8 diff --git a/docs/tools/elevated.md b/docs/tools/elevated.md index 482341f54..746edc9cf 100644 --- a/docs/tools/elevated.md +++ b/docs/tools/elevated.md @@ -15,7 +15,7 @@ read_when: - **Global availability gate**: `agent.elevated` is global (not per-agent). If disabled or sender not allowlisted, elevated is unavailable everywhere. - **Per-session state**: `/elevated on|off` sets the elevated level for the current session key. - **Inline directive**: `/elevated on` inside a message applies to that message only. -- **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. +- **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. Command-only messages that bypass mention requirements are treated as mentioned. - **Host execution**: elevated runs `bash` on the host (bypasses sandbox). - **Unsandboxed agents**: when there is no sandbox to bypass, elevated does not change where `bash` runs. - **Tool policy still applies**: if `bash` is denied by tool policy, elevated cannot be used. diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index 224c63a70..829cb1014 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -785,6 +785,7 @@ export function createDiscordMessageHandler(params: { !hasAnyMention && commandAuthorized && hasControlCommand(baseText); + const effectiveWasMentioned = wasMentioned || shouldBypassMention; const canDetectMention = Boolean(botId) || mentionRegexes.length > 0; if (isGuildMessage && shouldRequireMention) { if (botId && !wasMentioned && !shouldBypassMention) { @@ -981,7 +982,7 @@ export function createDiscordMessageHandler(params: { : undefined, Provider: "discord" as const, Surface: "discord" as const, - WasMentioned: wasMentioned, + WasMentioned: effectiveWasMentioned, MessageSid: message.id, ParentSessionKey: threadKeys.parentSessionKey, ThreadStarterBody: threadStarterBody, diff --git a/src/imessage/monitor.ts b/src/imessage/monitor.ts index b8c0aef4a..44db2e1ba 100644 --- a/src/imessage/monitor.ts +++ b/src/imessage/monitor.ts @@ -326,6 +326,7 @@ export async function monitorIMessageProvider( !mentioned && commandAuthorized && hasControlCommand(messageText); + const effectiveWasMentioned = mentioned || shouldBypassMention; if ( isGroup && requireMention && @@ -387,7 +388,7 @@ export async function monitorIMessageProvider( MediaPath: mediaPath, MediaType: mediaType, MediaUrl: mediaPath, - WasMentioned: mentioned, + WasMentioned: effectiveWasMentioned, CommandAuthorized: commandAuthorized, // Originating channel for reply routing. OriginatingChannel: "imessage" as const, diff --git a/src/slack/monitor.tool-result.test.ts b/src/slack/monitor.tool-result.test.ts index 5c63f7468..551cb8ba8 100644 --- a/src/slack/monitor.tool-result.test.ts +++ b/src/slack/monitor.tool-result.test.ts @@ -250,6 +250,39 @@ describe("monitorSlackProvider tool results", () => { expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); }); + it("treats control commands as mentions for group bypass", async () => { + replyMock.mockResolvedValue({ text: "ok" }); + + const controller = new AbortController(); + const run = monitorSlackProvider({ + botToken: "bot-token", + appToken: "app-token", + abortSignal: controller.signal, + }); + + await waitForEvent("message"); + const handler = getSlackHandlers()?.get("message"); + if (!handler) throw new Error("Slack message handler not registered"); + + await handler({ + event: { + type: "message", + user: "U1", + text: "/elevated off", + ts: "123", + channel: "C1", + channel_type: "channel", + }, + }); + + await flush(); + controller.abort(); + await run; + + expect(replyMock).toHaveBeenCalledTimes(1); + expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); + }); + it("threads replies when incoming message is in a thread", async () => { replyMock.mockResolvedValue({ text: "thread reply" }); diff --git a/src/slack/monitor.ts b/src/slack/monitor.ts index 352370d3e..f97cd42df 100644 --- a/src/slack/monitor.ts +++ b/src/slack/monitor.ts @@ -913,6 +913,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { !hasAnyMention && commandAuthorized && hasControlCommand(message.text ?? ""); + const effectiveWasMentioned = wasMentioned || shouldBypassMention; const canDetectMention = Boolean(botUserId) || mentionRegexes.length > 0; if ( isRoom && @@ -1058,7 +1059,7 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { ThreadStarterBody: threadStarterBody, ThreadLabel: threadLabel, Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, - WasMentioned: isRoomish ? wasMentioned : undefined, + WasMentioned: isRoomish ? effectiveWasMentioned : undefined, MediaPath: media?.path, MediaType: media?.contentType, MediaUrl: media?.path, diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index f3563e3f3..80d99028d 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -486,6 +486,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { !hasAnyMention && commandAuthorized && hasControlCommand(msg.text ?? msg.caption ?? ""); + const effectiveWasMentioned = wasMentioned || shouldBypassMention; const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0; if (isGroup && requireMention && canDetectMention) { if (!wasMentioned && !shouldBypassMention) { @@ -592,7 +593,7 @@ export function createTelegramBot(opts: TelegramBotOptions) { ReplyToBody: replyTarget?.body, ReplyToSender: replyTarget?.sender, Timestamp: msg.date ? msg.date * 1000 : undefined, - WasMentioned: isGroup ? wasMentioned : undefined, + WasMentioned: isGroup ? effectiveWasMentioned : undefined, MediaPath: allMedia[0]?.path, MediaType: allMedia[0]?.contentType, MediaUrl: allMedia[0]?.path,