diff --git a/CHANGELOG.md b/CHANGELOG.md index bb52cc8e6..f3998d892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,11 +73,14 @@ Status: beta. - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). ### Fixes +- Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R. - Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald. - Agents: align MiniMax base URL test expectation with default provider config. (#3131) Thanks @bonald. - Agents: prevent retries on oversized image errors and surface size limits. (#2871) Thanks @Suksham-sharma. - Agents: inherit provider baseUrl/api for inline models. (#2740) Thanks @lploc94. - Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355. +- Telegram: include AccountId in native command context for multi-agent routing. (#2942) Thanks @Chloe-VP. +- Telegram: handle video note attachments in media extraction. (#2905) Thanks @mylukin. - TTS: read OPENAI_TTS_BASE_URL at runtime instead of module load to honor config.env. (#3341) Thanks @hclsys. - macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee. - Web UI: auto-expand the chat compose textarea while typing (with sensible max height). (#2950) Thanks @shivamraut101. diff --git a/README.md b/README.md index 02a0938a6..4da25759b 100644 --- a/README.md +++ b/README.md @@ -514,36 +514,38 @@ Thanks to all clawtributors:

steipete plum-dawg bohdanpodvirnyi iHildy jaydenfyi joaohlisboa mneves75 MatthieuBizien MaudeBot Glucksberg - rahthakor vrknetha radek-paclt Tobias Bischoff joshp123 vignesh07 czekaj mukhtharcm sebslight maxsumrall - xadenryan rodrigouroz juanpablodlc hsrvc magimetal zerone0x meaningfool tyler6204 patelhiren NicholasSpisak - jonisjongithub abhisekbasu1 jamesgroat claude JustYannicc Hyaxia dantelex SocialNerd42069 daveonkels google-labs-jules[bot] - lc0rp mousberg adam91holt hougangdev gumadeiras mteam88 hirefrank joeynyc orlyjamie dbhurley - Mariano Belinky Eng. Juan Combetto TSavo julianengel bradleypriest benithors rohannagpal timolins f-trycua benostein - nachx639 shakkernerd pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino Vasanth Rao Naik Sabavat petter-b - cpojer scald thewilloftheshadow andranik-sahakyan davidguttman sleontenko denysvitali sircrumpet peschee rafaelreis-r - dominicnunez ratulsarna lutr0 danielz1z AdeboyeDN Alg0rix papago2355 emanuelst KristijanJovanovski rdev - rhuanssauro joshrad-dev kiranjd osolmaz adityashaw2 CashWilliams sheeek artuskg Takhoffman onutc - pauloportella neooriginal manuelhettich minghinmatthewlam myfunc travisirby buddyh connorshea kyleok obviyus - mcinteerj dependabot[bot] John-Rood timkrase uos-status gerardward2007 roshanasingh4 tosh-hamburg azade-c dlauer - JonUleis bjesuiter cheeeee robbyczgw-cla Josh Phillips YuriNachos pookNast Whoaa512 chriseidhof ngutman - ysqander aj47 kennyklee superman32432432 Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] damoahdominic - dan-dr HeimdallStrategy imfing jalehman jarvis-medmatic kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse - dougvk erikpr1994 fal3 Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr - neist sibbl chrisrodz Friederike Seiler gabriel-trigo iamadig Jonathan D. Rhyne (DJ-D) Joshua Mitchell Kit koala73 - manmal ogulcancelik pasogott petradonka rubyrunsstuff siddhantjain suminhthanh svkozak VACInc wes-davis - zats 24601 ameno- Chris Taylor dguido Django Navarro evalexpr henrino3 humanwritten larlyssa - odysseus0 oswalpalash pcty-nextgen-service-account rmorse Syhids Aaron Konyer aaronveklabs andreabadesso Andrii cash-echo-bot - Clawd ClawdFx EnzeD erik-agens Evizero fcatuhe itsjaydesu ivancasco ivanrvpereira jayhickey - jeffersonwarrior jeffersonwarrior jverdi longmaba mickahouan mjrussell odnxe p6l-richard philipp-spiess Pocket Clawd - robaxelsen Sash Catanzarite T5-AndyML travisp VAC william arzt zknicker 0oAstro abhaymundhara alejandro maza - Alex-Alaniz alexstyl andrewting19 anpoirier arthyn Asleep123 bolismauro chenyuan99 Clawdbot Maintainers conhecendoia - dasilva333 David-Marsh-Photo Developer Dimitrios Ploutarchos Drake Thomsen Felix Krause foeken ganghyun kim grrowl gtsifrikas - HazAT hrdwdmrbl hugobarauna Jamie Openshaw Jane Jarvis Jefferson Nunn jogi47 kentaro Kevin Lin - kitze Kiwitwitter levifig Lloyd longjos loukotal louzhixian martinpucik Matt mini mertcicekci0 - Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman nexty5870 Noctivoro ppamment prathamdby ptn1411 - reeltimeapps RLTCmpe Rolf Fredheim Rony Kelner Samrat Jha senoldogann Seredeep sergical shiv19 shiyuanhai - siraht snopoke Suksham-sharma techboss testingabc321 The Admiral thesash Ubuntu voidserf Vultr-Clawd Admin - Wimmie wolfred wstock yazinsai YiWang24 ymat19 Zach Knickerbocker 0xJonHoldsCrypto aaronn Alphonse-arianee - atalovesyou Azade carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder - Quentin Randy Torres rhjoh ronak-guliani William Stock + rahthakor vrknetha radek-paclt vignesh07 Tobias Bischoff joshp123 czekaj mukhtharcm sebslight maxsumrall + xadenryan rodrigouroz juanpablodlc hsrvc magimetal zerone0x tyler6204 meaningfool patelhiren NicholasSpisak + jonisjongithub abhisekbasu1 jamesgroat claude JustYannicc Mariano Belinky Hyaxia dantelex SocialNerd42069 daveonkels + google-labs-jules[bot] lc0rp mousberg adam91holt hougangdev shakkernerd gumadeiras mteam88 hirefrank joeynyc + orlyjamie dbhurley Eng. Juan Combetto TSavo julianengel bradleypriest benithors rohannagpal timolins f-trycua + benostein elliotsecops nachx639 pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino Vasanth Rao Naik Sabavat + petter-b thewilloftheshadow cpojer scald andranik-sahakyan davidguttman sleontenko denysvitali sircrumpet peschee + nonggialiang rafaelreis-r dominicnunez lploc94 ratulsarna lutr0 danielz1z AdeboyeDN Alg0rix papago2355 + emanuelst KristijanJovanovski rdev rhuanssauro joshrad-dev kiranjd osolmaz adityashaw2 CashWilliams sheeek + ryancontent artuskg Takhoffman onutc pauloportella neooriginal manuelhettich minghinmatthewlam myfunc travisirby + obviyus buddyh connorshea kyleok mcinteerj dependabot[bot] John-Rood timkrase uos-status gerardward2007 + roshanasingh4 tosh-hamburg azade-c dlauer JonUleis shivamraut101 bjesuiter cheeeee robbyczgw-cla Josh Phillips + YuriNachos pookNast Whoaa512 chriseidhof ngutman ysqander aj47 kennyklee superman32432432 Yurii Chukhlib + grp06 antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy imfing jalehman jarvis-medmatic + kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse dougvk erikpr1994 fal3 Ghost jonasjancarik + Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist sibbl chrisrodz Friederike Seiler gabriel-trigo + iamadig Jonathan D. Rhyne (DJ-D) Joshua Mitchell Kit koala73 manmal ogulcancelik pasogott petradonka rubyrunsstuff + siddhantjain suminhthanh svkozak VACInc wes-davis zats 24601 ameno- Chris Taylor dguido + Django Navarro evalexpr henrino3 humanwritten larlyssa Lukavyi odysseus0 oswalpalash pcty-nextgen-service-account pi0 + rmorse Roopak Nijhara Syhids Aaron Konyer aaronveklabs andreabadesso Andrii cash-echo-bot Clawd ClawdFx + EnzeD erik-agens Evizero fcatuhe itsjaydesu ivancasco ivanrvpereira Jarvis jayhickey jeffersonwarrior + jeffersonwarrior jverdi longmaba MarvinCui mickahouan mjrussell odnxe p6l-richard philipp-spiess Pocket Clawd + robaxelsen Sash Catanzarite Suksham-sharma T5-AndyML tewatia travisp VAC william arzt zknicker 0oAstro + abhaymundhara aduk059 alejandro maza Alex-Alaniz alexstyl andrewting19 anpoirier araa47 arthyn Asleep123 + bguidolim bolismauro chenyuan99 Clawdbot Maintainers conhecendoia dasilva333 David-Marsh-Photo Developer Dimitrios Ploutarchos Drake Thomsen + dylanneve1 Felix Krause foeken frankekn ganghyun kim grrowl gtsifrikas HazAT hrdwdmrbl hugobarauna + Jamie Openshaw Jane Jarvis Deploy Jefferson Nunn jogi47 kentaro Kevin Lin kira-ariaki kitze Kiwitwitter + levifig Lloyd longjos loukotal louzhixian martinpucik Matt mini mertcicekci0 Miles mrdbstn + MSch Mustafa Tag Eldeen ndraiman nexty5870 Noctivoro ppamment prathamdby ptn1411 reeltimeapps RLTCmpe + Rolf Fredheim Rony Kelner Samrat Jha senoldogann sergical shiv19 shiyuanhai siraht snopoke techboss + testingabc321 The Admiral thesash Ubuntu voidserf Vultr-Clawd Admin Wimmie wolfred wstock YangHuang2280 + yazinsai YiWang24 ymat19 Zach Knickerbocker zackerthescar 0xJonHoldsCrypto aaronn Alphonse-arianee atalovesyou Azade + carlulsoe ddyo Erik latitudeki5223 Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres + rhjoh ronak-guliani William Stock

diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index c080ef55f..2604038ec 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -138,7 +138,7 @@ describe("dispatchReplyFromConfig", () => { ); }); - it("does not provide onToolResult when routing cross-provider", async () => { + it("provides onToolResult in DM sessions", async () => { mocks.tryFastAbortFromMessage.mockResolvedValue({ handled: false, aborted: false, @@ -147,9 +147,34 @@ describe("dispatchReplyFromConfig", () => { const cfg = {} as MoltbotConfig; const dispatcher = createDispatcher(); const ctx = buildTestCtx({ - Provider: "slack", - OriginatingChannel: "telegram", - OriginatingTo: "telegram:999", + Provider: "telegram", + ChatType: "direct", + }); + + const replyResolver = async ( + _ctx: MsgContext, + opts: GetReplyOptions | undefined, + _cfg: ClawdbotConfig, + ) => { + expect(opts?.onToolResult).toBeDefined(); + expect(typeof opts?.onToolResult).toBe("function"); + return { text: "hi" } satisfies ReplyPayload; + }; + + await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); + expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1); + }); + + it("does not provide onToolResult in group sessions", async () => { + mocks.tryFastAbortFromMessage.mockResolvedValue({ + handled: false, + aborted: false, + }); + const cfg = {} as ClawdbotConfig; + const dispatcher = createDispatcher(); + const ctx = buildTestCtx({ + Provider: "telegram", + ChatType: "group", }); const replyResolver = async ( @@ -162,12 +187,62 @@ describe("dispatchReplyFromConfig", () => { }; await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); + expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1); + }); - expect(mocks.routeReply).toHaveBeenCalledWith( - expect.objectContaining({ - payload: expect.objectContaining({ text: "hi" }), - }), + it("sends tool results via dispatcher in DM sessions", async () => { + mocks.tryFastAbortFromMessage.mockResolvedValue({ + handled: false, + aborted: false, + }); + const cfg = {} as ClawdbotConfig; + const dispatcher = createDispatcher(); + const ctx = buildTestCtx({ + Provider: "telegram", + ChatType: "direct", + }); + + const replyResolver = async ( + _ctx: MsgContext, + opts: GetReplyOptions | undefined, + _cfg: ClawdbotConfig, + ) => { + // Simulate tool result emission + await opts?.onToolResult?.({ text: "🔧 exec: ls" }); + return { text: "done" } satisfies ReplyPayload; + }; + + await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); + expect(dispatcher.sendToolResult).toHaveBeenCalledWith( + expect.objectContaining({ text: "🔧 exec: ls" }), ); + expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1); + }); + + it("does not provide onToolResult for native slash commands", async () => { + mocks.tryFastAbortFromMessage.mockResolvedValue({ + handled: false, + aborted: false, + }); + const cfg = {} as ClawdbotConfig; + const dispatcher = createDispatcher(); + const ctx = buildTestCtx({ + Provider: "telegram", + ChatType: "direct", + CommandSource: "native", + }); + + const replyResolver = async ( + _ctx: MsgContext, + opts: GetReplyOptions | undefined, + _cfg: ClawdbotConfig, + ) => { + expect(opts?.onToolResult).toBeUndefined(); + return { text: "hi" } satisfies ReplyPayload; + }; + + await dispatchReplyFromConfig({ ctx, cfg, dispatcher, replyResolver }); + expect(dispatcher.sendFinalReply).toHaveBeenCalledTimes(1); }); it("fast-aborts without calling the reply resolver", async () => { diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 58d5d71b5..c85e654de 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -276,6 +276,27 @@ export async function dispatchReplyFromConfig(params: { ctx, { ...params.replyOptions, + onToolResult: + ctx.ChatType !== "group" && ctx.CommandSource !== "native" + ? (payload: ReplyPayload) => { + const run = async () => { + const ttsPayload = await maybeApplyTtsToPayload({ + payload, + cfg, + channel: ttsChannel, + kind: "tool", + inboundAudio, + ttsAuto: sessionTtsAuto, + }); + if (shouldRouteToOriginating) { + await sendPayloadAsync(ttsPayload, undefined, false); + } else { + dispatcher.sendToolResult(ttsPayload); + } + }; + return run(); + } + : undefined, onBlockReply: (payload: ReplyPayload, context) => { const run = async () => { // Accumulate block text for TTS generation after streaming diff --git a/src/auto-reply/reply/mentions.test.ts b/src/auto-reply/reply/mentions.test.ts index 7742b4f30..07cff23a9 100644 --- a/src/auto-reply/reply/mentions.test.ts +++ b/src/auto-reply/reply/mentions.test.ts @@ -4,7 +4,7 @@ import { matchesMentionWithExplicit } from "./mentions.js"; describe("matchesMentionWithExplicit", () => { const mentionRegexes = [/\bclawd\b/i]; - it("prefers explicit mentions when other mentions are present", () => { + it("checks mentionPatterns even when explicit mention is available", () => { const result = matchesMentionWithExplicit({ text: "@clawd hello", mentionRegexes, @@ -14,6 +14,19 @@ describe("matchesMentionWithExplicit", () => { canResolveExplicit: true, }, }); + expect(result).toBe(true); + }); + + it("returns false when explicit is false and no regex match", () => { + const result = matchesMentionWithExplicit({ + text: "<@999999> hello", + mentionRegexes, + explicit: { + hasAnyMention: true, + isExplicitlyMentioned: false, + canResolveExplicit: true, + }, + }); expect(result).toBe(false); }); diff --git a/src/auto-reply/reply/mentions.ts b/src/auto-reply/reply/mentions.ts index 71964ac5f..9554a3c7b 100644 --- a/src/auto-reply/reply/mentions.ts +++ b/src/auto-reply/reply/mentions.ts @@ -90,7 +90,9 @@ export function matchesMentionWithExplicit(params: { const explicit = params.explicit?.isExplicitlyMentioned === true; const explicitAvailable = params.explicit?.canResolveExplicit === true; const hasAnyMention = params.explicit?.hasAnyMention === true; - if (hasAnyMention && explicitAvailable) return explicit; + if (hasAnyMention && explicitAvailable) { + return explicit || params.mentionRegexes.some((re) => re.test(cleaned)); + } if (!cleaned) return explicit; return explicit || params.mentionRegexes.some((re) => re.test(cleaned)); } diff --git a/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts b/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts index 80bb5ff8f..bd4ec38ca 100644 --- a/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts +++ b/src/discord/monitor.tool-result.accepts-guild-messages-mentionpatterns-match.test.ts @@ -135,7 +135,7 @@ describe("discord tool result dispatch", () => { expect(sendMock).toHaveBeenCalledTimes(1); }, 20_000); - it("skips guild messages when another user is explicitly mentioned", async () => { + it("accepts guild messages when mentionPatterns match even if another user is mentioned", async () => { const { createDiscordMessageHandler } = await import("./monitor.js"); const cfg = { agents: { @@ -211,8 +211,8 @@ describe("discord tool result dispatch", () => { client, ); - expect(dispatchMock).not.toHaveBeenCalled(); - expect(sendMock).not.toHaveBeenCalled(); + expect(dispatchMock).toHaveBeenCalledTimes(1); + expect(sendMock).toHaveBeenCalledTimes(1); }, 20_000); it("accepts guild reply-to-bot messages as implicit mentions", async () => { diff --git a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts index ce7015399..4481d7589 100644 --- a/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts +++ b/src/slack/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts @@ -392,7 +392,7 @@ describe("monitorSlackProvider tool results", () => { expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); }); - it("skips channel messages when another user is explicitly mentioned", async () => { + it("accepts channel messages when mentionPatterns match even if another user is mentioned", async () => { slackTestState.config = { messages: { responsePrefix: "PFX", @@ -433,8 +433,8 @@ describe("monitorSlackProvider tool results", () => { controller.abort(); await run; - expect(replyMock).not.toHaveBeenCalled(); - expect(sendMock).not.toHaveBeenCalled(); + expect(replyMock).toHaveBeenCalledTimes(1); + expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true); }); it("treats replies to bot threads as implicit mentions", async () => { diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 832a4413d..abd06cdef 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -335,6 +335,7 @@ export const buildTelegramMessageContext = async ({ let placeholder = ""; if (msg.photo) placeholder = ""; else if (msg.video) placeholder = ""; + else if (msg.video_note) placeholder = ""; else if (msg.audio || msg.voice) placeholder = ""; else if (msg.document) placeholder = ""; else if (msg.sticker) placeholder = ""; diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index 3415ea927..73a5a148a 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -468,6 +468,7 @@ export const registerTelegramNativeCommands = ({ CommandAuthorized: commandAuthorized, CommandSource: "native" as const, SessionKey: `telegram:slash:${senderId || chatId}`, + AccountId: route.accountId, CommandTargetSessionKey: sessionKey, MessageThreadId: threadIdForSend, IsForum: isForum, diff --git a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts index 8bfe1fdd3..03aaeebd7 100644 --- a/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts +++ b/src/telegram/bot.create-telegram-bot.accepts-group-messages-mentionpatterns-match-without-botusername.test.ts @@ -212,7 +212,7 @@ describe("createTelegramBot", () => { ); }); - it("skips group messages when another user is explicitly mentioned", async () => { + it("accepts group messages when mentionPatterns match even if another user is mentioned", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType; replySpy.mockReset(); @@ -249,7 +249,8 @@ describe("createTelegramBot", () => { getFile: async () => ({ download: async () => new Uint8Array() }), }); - expect(replySpy).not.toHaveBeenCalled(); + expect(replySpy).toHaveBeenCalledTimes(1); + expect(replySpy.mock.calls[0][0].WasMentioned).toBe(true); }); it("keeps group envelope headers stable (sender identity is separate)", async () => { diff --git a/src/telegram/bot/delivery.ts b/src/telegram/bot/delivery.ts index c6d731d2e..8c1e74b73 100644 --- a/src/telegram/bot/delivery.ts +++ b/src/telegram/bot/delivery.ts @@ -368,7 +368,12 @@ export async function resolveMedia( } const m = - msg.photo?.[msg.photo.length - 1] ?? msg.video ?? msg.document ?? msg.audio ?? msg.voice; + msg.photo?.[msg.photo.length - 1] ?? + msg.video ?? + msg.video_note ?? + msg.document ?? + msg.audio ?? + msg.voice; if (!m?.file_id) return null; const file = await ctx.getFile(); if (!file.file_path) { @@ -395,6 +400,7 @@ export async function resolveMedia( let placeholder = ""; if (msg.photo) placeholder = ""; else if (msg.video) placeholder = ""; + else if (msg.video_note) placeholder = ""; else if (msg.audio || msg.voice) placeholder = ""; return { path: saved.path, contentType: saved.contentType, placeholder }; }