diff --git a/extensions/telegram/src/bot-message-context.reactions.test.ts b/extensions/telegram/src/bot-message-context.reactions.test.ts index 5e8968ef571..fbc5f3ddfaa 100644 --- a/extensions/telegram/src/bot-message-context.reactions.test.ts +++ b/extensions/telegram/src/bot-message-context.reactions.test.ts @@ -62,6 +62,52 @@ describe("buildTelegramMessageContext reactions", () => { inboundBodyMock.mockClear(); }); + it("does not create ack or status reactions for room events", async () => { + const setMessageReaction = vi.fn(async () => undefined); + const { createStatusReactionController } = createStatusReactionControllerStub(); + + const ctx = await buildTelegramMessageContextForTest({ + message: { + message_id: 12, + chat: { id: -1001234567890, type: "group", title: "Ops" }, + date: 1_700_000_000, + text: "hello", + from: { id: 42, first_name: "Alice" }, + }, + cfg: { + agents: { + defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" }, + }, + channels: { + telegram: { + groupPolicy: "open", + groups: { "*": { requireMention: false } }, + }, + }, + messages: { + ackReaction: "👀", + groupChat: { mentionPatterns: [] }, + statusReactions: { enabled: true }, + }, + }, + ackReactionScope: "all", + botApi: { setMessageReaction }, + runtime: { createStatusReactionController }, + resolveGroupActivation: () => false, + resolveGroupRequireMention: () => false, + resolveTelegramGroupConfig: () => ({ + groupConfig: { requireMention: false }, + topicConfig: undefined, + }), + }); + + expect(ctx?.ctxPayload.InboundTurnKind).toBe("room_event"); + expect(ctx?.ackReactionPromise).toBeNull(); + expect(ctx?.statusReactionController).toBeNull(); + expect(createStatusReactionController).not.toHaveBeenCalled(); + expect(setMessageReaction).not.toHaveBeenCalled(); + }); + it("does not create status reactions when the ack gate blocks an unmentioned group message", async () => { const setMessageReaction = vi.fn(async () => undefined); const { createStatusReactionController } = createStatusReactionControllerStub(); diff --git a/extensions/telegram/src/bot-message-context.ts b/extensions/telegram/src/bot-message-context.ts index 017bbb59507..a8f7eebc333 100644 --- a/extensions/telegram/src/bot-message-context.ts +++ b/extensions/telegram/src/bot-message-context.ts @@ -475,6 +475,45 @@ export const buildTelegramMessageContext = async ({ return null; } + const { ctxPayload, skillFilter, turn } = await buildTelegramInboundContextPayload({ + cfg, + primaryCtx, + msg, + allMedia, + replyMedia, + replyChain, + promptContext, + isGroup, + isForum, + chatId, + senderId, + senderUsername, + resolvedThreadId, + dmThreadId, + threadSpec, + route, + rawBody: bodyResult.rawBody, + bodyText: bodyResult.bodyText, + historyKey: bodyResult.historyKey ?? "", + historyLimit, + groupHistories, + groupConfig, + topicConfig, + stickerCacheHit: bodyResult.stickerCacheHit, + effectiveWasMentioned: bodyResult.effectiveWasMentioned, + hasControlCommand: bodyResult.hasControlCommand, + ...(bodyResult.audioTranscribedMediaIndex !== undefined + ? { audioTranscribedMediaIndex: bodyResult.audioTranscribedMediaIndex } + : {}), + locationData: bodyResult.locationData, + options, + dmAllowFrom: dmAllow.allowFrom, + effectiveGroupAllow, + commandAuthorized: bodyResult.commandAuthorized, + topicName, + sessionRuntime, + }); + const canShowStatusReaction = ctxPayload.InboundTurnKind !== "room_event"; const ackReaction = resolveAckReaction(cfg, route.agentId, { channel: "telegram", accountId: account.accountId, @@ -483,6 +522,7 @@ export const buildTelegramMessageContext = async ({ ackReaction && isTelegramSupportedReactionEmoji(ackReaction) ? ackReaction : undefined; const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false; const shouldSendAckReaction = Boolean( + canShowStatusReaction && ackReaction && shouldAckReactionGate({ scope: ackReactionScope, @@ -577,45 +617,6 @@ export const buildTelegramMessageContext = async ({ ) : null; - const { ctxPayload, skillFilter, turn } = await buildTelegramInboundContextPayload({ - cfg, - primaryCtx, - msg, - allMedia, - replyMedia, - replyChain, - promptContext, - isGroup, - isForum, - chatId, - senderId, - senderUsername, - resolvedThreadId, - dmThreadId, - threadSpec, - route, - rawBody: bodyResult.rawBody, - bodyText: bodyResult.bodyText, - historyKey: bodyResult.historyKey ?? "", - historyLimit, - groupHistories, - groupConfig, - topicConfig, - stickerCacheHit: bodyResult.stickerCacheHit, - effectiveWasMentioned: bodyResult.effectiveWasMentioned, - hasControlCommand: bodyResult.hasControlCommand, - ...(bodyResult.audioTranscribedMediaIndex !== undefined - ? { audioTranscribedMediaIndex: bodyResult.audioTranscribedMediaIndex } - : {}), - locationData: bodyResult.locationData, - options, - dmAllowFrom: dmAllow.allowFrom, - effectiveGroupAllow, - commandAuthorized: bodyResult.commandAuthorized, - topicName, - sessionRuntime, - }); - return { ctxPayload, turn, diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 06334303a55..730291979ca 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -1503,14 +1503,25 @@ describe("dispatchTelegramMessage draft streaming", () => { const groupHistories = new Map([ [historyKey, [{ sender: "Alice", body: "side chatter", timestamp: 1 }]], ]); - dispatchReplyWithBufferedBlockDispatcher.mockResolvedValue({ - queuedFinal: false, - counts: { block: 0, final: 0, tool: 0 }, - sourceReplyDeliveryMode: "message_tool_only", + const statusReactionController = createStatusReactionController(); + loadSessionStore.mockReturnValue({ + "agent:main:telegram:group:-100123": { reasoningLevel: "stream" }, + }); + dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ replyOptions }) => { + await replyOptions?.onReasoningStream?.({ text: "ambient reasoning" }); + await replyOptions?.onToolStart?.({ name: "exec", phase: "start" }); + await replyOptions?.onCompactionStart?.(); + await replyOptions?.onCompactionEnd?.(); + return { + queuedFinal: false, + counts: { block: 0, final: 0, tool: 0 }, + sourceReplyDeliveryMode: "message_tool_only", + }; }); await dispatchWithContext({ context: createContext({ + statusReactionController: statusReactionController as never, ctxPayload: { InboundTurnKind: "room_event", SessionKey: "agent:main:telegram:group:-100123", @@ -1539,6 +1550,9 @@ describe("dispatchTelegramMessage draft streaming", () => { sourceReplyDeliveryMode?: string; suppressTyping?: boolean; allowProgressCallbacksWhenSourceDeliverySuppressed?: boolean; + onReasoningStream?: unknown; + onCompactionStart?: unknown; + onCompactionEnd?: unknown; }; }; expect(dispatchParams.replyOptions?.sourceReplyDeliveryMode).toBe("message_tool_only"); @@ -1546,7 +1560,13 @@ describe("dispatchTelegramMessage draft streaming", () => { expect(dispatchParams.replyOptions?.allowProgressCallbacksWhenSourceDeliverySuppressed).toBe( false, ); + expect(dispatchParams.replyOptions?.onReasoningStream).toBeUndefined(); + expect(dispatchParams.replyOptions?.onCompactionStart).toBeUndefined(); + expect(dispatchParams.replyOptions?.onCompactionEnd).toBeUndefined(); expect(createTelegramDraftStream).not.toHaveBeenCalled(); + expect(statusReactionController.setTool).not.toHaveBeenCalled(); + expect(statusReactionController.setCompacting).not.toHaveBeenCalled(); + expect(statusReactionController.setThinking).not.toHaveBeenCalled(); expect(deliverReplies).not.toHaveBeenCalled(); expect(groupHistories.get(historyKey)).toHaveLength(1); }); diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index 6ea4043d384..4d3ffa473b0 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -403,8 +403,10 @@ export const dispatchTelegramMessage = async ({ ackReactionPromise, reactionApi, removeAckAfterReply, - statusReactionController, + statusReactionController: rawStatusReactionController, } = context; + const isRoomEvent = ctxPayload.InboundTurnKind === "room_event"; + const statusReactionController = isRoomEvent ? null : rawStatusReactionController; const statusReactionTiming = { ...DEFAULT_TIMING, ...cfg.messages?.statusReactions?.timing, @@ -480,7 +482,6 @@ export const dispatchTelegramMessage = async ({ const accountBlockStreamingEnabled = resolveChannelStreamingBlockEnabled(telegramCfg) ?? cfg.agents?.defaults?.blockStreamingDefault === "on"; - const isRoomEvent = ctxPayload.InboundTurnKind === "room_event"; const resolvedReasoningLevel = resolveTelegramReasoningLevel({ cfg, sessionKey: ctxPayload.SessionKey, @@ -542,7 +543,7 @@ export const dispatchTelegramMessage = async ({ !hasTelegramQuoteReply && !accountBlockStreamingEnabled && !forceBlockStreamingForReasoning; - const canStreamReasoningDraft = streamReasoningDraft; + const canStreamReasoningDraft = !isRoomEvent && streamReasoningDraft; const draftReplyToMessageId = replyToMode !== "off" && typeof msg.message_id === "number" ? (replyQuoteMessageId ?? msg.message_id)