fix(telegram): keep room events fully quiet

This commit is contained in:
Ayaan Zaidi
2026-05-13 13:27:08 +05:30
committed by Peter Steinberger
parent c2e659472a
commit fb0f29b9cc
4 changed files with 114 additions and 46 deletions

View File

@@ -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();

View File

@@ -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,

View File

@@ -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: "<think>ambient reasoning</think>" });
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);
});

View File

@@ -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)