Skip to content

Commit 32af1c0

Browse files
authored
Control Telegram group history context (#89547)
* fix(telegram): control group history context * fix(telegram): keep history mode type local * fix(telegram): respect history mode during forum recovery
1 parent b3128ba commit 32af1c0

16 files changed

Lines changed: 448 additions & 19 deletions

docs/channels/telegram.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,23 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
274274
},
275275
},
276276
}
277+
```
278+
279+
Group history context defaults to `mention-only`: prior group messages are
280+
included only when they were addressed to the bot, are replies to the bot,
281+
or are the bot's own messages. Set `includeGroupHistoryContext: "recent"` to
282+
include recent room history for trusted groups. Set
283+
`includeGroupHistoryContext: "none"` to send no prior Telegram group history
284+
with the next turn.
285+
286+
```json5
287+
{
288+
channels: {
289+
telegram: {
290+
includeGroupHistoryContext: "recent",
291+
},
292+
},
293+
}
277294
```
278295

279296
Getting the group chat ID:

extensions/telegram/src/bot-handlers.runtime.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ import {
123123
evaluateTelegramGroupBaseAccess,
124124
evaluateTelegramGroupPolicyAccess,
125125
} from "./group-access.js";
126+
import { resolveTelegramGroupHistoryContextMode } from "./group-history-context.js";
126127
import { migrateTelegramGroupConfig } from "./group-migration.js";
127128
import {
128129
resolveTelegramCommandIngressAuthorization,
@@ -1173,12 +1174,76 @@ export const registerTelegramHandlers = ({
11731174
is_reply_target: flags?.replyTarget === true ? true : undefined,
11741175
});
11751176

1177+
const buildMentionOnlyGroupHistoryPredicate = (params: {
1178+
ctx: TelegramContext;
1179+
msg: Message;
1180+
threadId?: number;
1181+
}): ((node: TelegramCachedMessageNode) => boolean) => {
1182+
const runtimeCfg = telegramDeps.getRuntimeConfig();
1183+
const isForum =
1184+
params.msg.chat.type === "supergroup" &&
1185+
Boolean(params.msg.chat.is_forum || params.msg.is_topic_message);
1186+
const senderId = params.msg.from?.id != null ? String(params.msg.from.id) : undefined;
1187+
const sessionState = resolveTelegramSessionState({
1188+
chatId: params.msg.chat.id,
1189+
isGroup: true,
1190+
isForum,
1191+
messageThreadId: params.msg.message_thread_id,
1192+
resolvedThreadId: params.threadId,
1193+
senderId,
1194+
runtimeCfg,
1195+
});
1196+
const conversationId = buildTelegramGroupPeerId(params.msg.chat.id, params.threadId);
1197+
const mentionRegexes = buildMentionRegexes(runtimeCfg, sessionState.agentId, {
1198+
provider: "telegram",
1199+
conversationId,
1200+
providerPolicy: telegramCfg.mentionPatterns,
1201+
});
1202+
const botUsername = params.ctx.me?.username?.trim().toLowerCase();
1203+
const botId = params.ctx.me?.id;
1204+
return (node) => {
1205+
if (botId != null && node.sourceMessage.from?.id === botId) {
1206+
return true;
1207+
}
1208+
const replyFromId = node.sourceMessage.reply_to_message?.from?.id;
1209+
if (
1210+
botId != null &&
1211+
replyFromId === botId &&
1212+
!isTelegramForumServiceMessage(node.sourceMessage.reply_to_message)
1213+
) {
1214+
return true;
1215+
}
1216+
const messageTextParts = getTelegramTextParts(node.sourceMessage);
1217+
const hasAnyMention = messageTextParts.entities.some((ent) => ent.type === "mention");
1218+
const explicitlyMentioned = botUsername
1219+
? hasBotMention(node.sourceMessage, botUsername)
1220+
: false;
1221+
return matchesMentionWithExplicit({
1222+
text: messageTextParts.text,
1223+
mentionRegexes,
1224+
explicit: {
1225+
hasAnyMention,
1226+
isExplicitlyMentioned: explicitlyMentioned,
1227+
canResolveExplicit: Boolean(botUsername),
1228+
},
1229+
});
1230+
};
1231+
};
1232+
11761233
const buildPromptContextForMessage = async (
1234+
ctx: TelegramContext,
11771235
msg: Message,
11781236
replyChainNodes: TelegramCachedMessageNode[],
11791237
options?: TelegramMessageContextOptions,
11801238
mediaByMessageId?: ReadonlyMap<string, TelegramMediaRef>,
11811239
): Promise<TelegramPromptContextEntry[]> => {
1240+
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
1241+
const groupHistoryContextMode = isGroup
1242+
? resolveTelegramGroupHistoryContextMode(telegramCfg)
1243+
: "recent";
1244+
if (isGroup && groupHistoryContextMode === "none") {
1245+
return [];
1246+
}
11821247
const messageId = typeof msg.message_id === "number" ? String(msg.message_id) : undefined;
11831248
const currentNode = await messageCache.get({
11841249
accountId,
@@ -1198,6 +1263,9 @@ export const registerTelegramHandlers = ({
11981263
...(options?.promptContextMinTimestampMs !== undefined
11991264
? { minTimestampMs: options.promptContextMinTimestampMs }
12001265
: {}),
1266+
...(isGroup && groupHistoryContextMode === "mention-only"
1267+
? { includeNode: buildMentionOnlyGroupHistoryPredicate({ ctx, msg, threadId }) }
1268+
: {}),
12011269
});
12021270
return conversationContext.length > 0
12031271
? [
@@ -1314,6 +1382,7 @@ export const registerTelegramHandlers = ({
13141382
}
13151383
}
13161384
const promptContext = await buildPromptContextForMessage(
1385+
params.ctx,
13171386
params.msg,
13181387
replyChainNodes,
13191388
params.options,

extensions/telegram/src/bot-message-context.require-mention.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,12 @@ describe("buildTelegramMessageContext requireMention precedence", () => {
108108

109109
it("keeps room events as context for the next direct group request", async () => {
110110
const groupHistories = new Map();
111+
const cfg = {
112+
channels: { telegram: { includeGroupHistoryContext: "recent" } },
113+
messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } },
114+
};
111115
await buildTelegramMessageContextForTest({
112-
cfg: { messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } } },
116+
cfg,
113117
message: { ...buildForumMessage(99), text: "side chatter" },
114118
historyLimit: 10,
115119
groupHistories,
@@ -122,6 +126,7 @@ describe("buildTelegramMessageContext requireMention precedence", () => {
122126
});
123127

124128
const ctx = await buildTelegramMessageContextForTest({
129+
cfg,
125130
message: {
126131
...buildForumMessage(99),
127132
message_id: 2,
@@ -147,6 +152,52 @@ describe("buildTelegramMessageContext requireMention precedence", () => {
147152
expect(ctx?.ctxPayload.Body).toContain("side chatter");
148153
});
149154

155+
it("omits pending group room events from default body context", async () => {
156+
const groupHistories = new Map();
157+
const cfg = {
158+
messages: { groupChat: { unmentionedInbound: "room_event", mentionPatterns: [] } },
159+
};
160+
await buildTelegramMessageContextForTest({
161+
cfg,
162+
message: { ...buildForumMessage(99), text: "side chatter" },
163+
historyLimit: 10,
164+
groupHistories,
165+
resolveGroupActivation: () => false,
166+
resolveGroupRequireMention: () => false,
167+
resolveTelegramGroupConfig: () => ({
168+
groupConfig: { requireMention: false },
169+
topicConfig: undefined,
170+
}),
171+
});
172+
173+
const ctx = await buildTelegramMessageContextForTest({
174+
cfg,
175+
message: {
176+
...buildForumMessage(99),
177+
message_id: 2,
178+
text: "replying directly",
179+
reply_to_message: {
180+
message_id: 10,
181+
chat: { id: -1001234567890, type: "supergroup", title: "Forum", is_forum: true },
182+
from: { id: 7, first_name: "Bot", username: "bot", is_bot: true },
183+
text: "previous bot message",
184+
},
185+
},
186+
historyLimit: 10,
187+
groupHistories,
188+
resolveGroupActivation: () => false,
189+
resolveGroupRequireMention: () => false,
190+
resolveTelegramGroupConfig: () => ({
191+
groupConfig: { requireMention: false },
192+
topicConfig: undefined,
193+
}),
194+
});
195+
196+
expect(ctx?.ctxPayload.InboundEventKind).toBe("user_request");
197+
expect(ctx?.ctxPayload.Body).not.toContain("side chatter");
198+
expect(ctx?.ctxPayload.InboundHistory).toBeUndefined();
199+
});
200+
150201
it("lets explicit topic requireMention=false override mention activation", async () => {
151202
const resolveGroupActivation = vi.fn(() => true);
152203

extensions/telegram/src/bot-message-context.session.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ import {
4646
} from "./bot/helpers.js";
4747
import type { TelegramContext } from "./bot/types.js";
4848
import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js";
49+
import {
50+
type TelegramGroupHistoryContextMode,
51+
includesRecentTelegramGroupHistoryContext,
52+
resolveTelegramGroupHistoryContextModeForAccount,
53+
} from "./group-history-context.js";
4954
import type { TelegramReplyChainEntry } from "./message-cache.js";
5055

5156
export type TelegramInboundContextPayload = BuiltChannelInboundEventContext & {
@@ -211,6 +216,7 @@ export async function buildTelegramInboundContextPayload(params: {
211216
historyKey?: string;
212217
historyLimit: number;
213218
groupHistories: Map<string, HistoryEntry[]>;
219+
groupHistoryContextMode?: TelegramGroupHistoryContextMode;
214220
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
215221
topicConfig?: TelegramTopicConfig;
216222
effectiveWasMentioned: boolean;
@@ -260,6 +266,7 @@ export async function buildTelegramInboundContextPayload(params: {
260266
historyKey,
261267
historyLimit,
262268
groupHistories,
269+
groupHistoryContextMode,
263270
groupConfig,
264271
topicConfig,
265272
effectiveWasMentioned,
@@ -412,8 +419,17 @@ export async function buildTelegramInboundContextPayload(params: {
412419
envelope: envelopeOptions,
413420
});
414421
const channelHistory = createChannelHistoryWindow({ historyMap: groupHistories });
422+
const includeRecentGroupHistoryContext =
423+
isGroup &&
424+
includesRecentTelegramGroupHistoryContext(
425+
groupHistoryContextMode ??
426+
resolveTelegramGroupHistoryContextModeForAccount({
427+
cfg,
428+
accountId: route.accountId,
429+
}),
430+
);
415431
let combinedBody = body;
416-
if (isGroup && historyKey && historyLimit > 0) {
432+
if (includeRecentGroupHistoryContext && historyKey && historyLimit > 0) {
417433
combinedBody = channelHistory.buildPendingContext({
418434
historyKey,
419435
limit: historyLimit,
@@ -439,7 +455,7 @@ export async function buildTelegramInboundContextPayload(params: {
439455
botUsername: normalizeOptionalLowercaseString(primaryCtx.me?.username),
440456
});
441457
const inboundHistory =
442-
isGroup && historyKey && historyLimit > 0
458+
includeRecentGroupHistoryContext && historyKey && historyLimit > 0
443459
? channelHistory.buildInboundHistory({
444460
historyKey,
445461
limit: historyLimit,

extensions/telegram/src/bot-message-context.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ import {
4545
} from "./conversation-route.js";
4646
import { enforceTelegramDmAccess } from "./dm-access.js";
4747
import { evaluateTelegramGroupBaseAccess } from "./group-access.js";
48+
import {
49+
resolveTelegramGroupHistoryContextModeForAccount,
50+
type TelegramGroupHistoryContextMode,
51+
} from "./group-history-context.js";
4852
import {
4953
buildTelegramStatusReactionVariants,
5054
type TelegramReactionEmoji,
@@ -106,6 +110,7 @@ export type TelegramMessageContext = {
106110
historyKey?: string;
107111
historyLimit: BuildTelegramMessageContextParams["historyLimit"];
108112
groupHistories: BuildTelegramMessageContextParams["groupHistories"];
113+
groupHistoryContextMode?: TelegramGroupHistoryContextMode;
109114
route: ReturnType<typeof resolveTelegramConversationRoute>["route"];
110115
skillFilter: TelegramMessageContextPayload["skillFilter"];
111116
sendTyping: () => Promise<void>;
@@ -482,6 +487,13 @@ export const buildTelegramMessageContext = async ({
482487
return null;
483488
}
484489

490+
const groupHistoryContextMode = isGroup
491+
? resolveTelegramGroupHistoryContextModeForAccount({
492+
cfg,
493+
accountId: route.accountId,
494+
})
495+
: undefined;
496+
485497
if (!(await ensureConfiguredBindingReady())) {
486498
return null;
487499
}
@@ -517,6 +529,7 @@ export const buildTelegramMessageContext = async ({
517529
historyKey: bodyResult.historyKey ?? "",
518530
historyLimit,
519531
groupHistories,
532+
groupHistoryContextMode,
520533
groupConfig,
521534
topicConfig,
522535
effectiveWasMentioned: bodyResult.effectiveWasMentioned,
@@ -653,6 +666,7 @@ export const buildTelegramMessageContext = async ({
653666
historyKey: bodyResult.historyKey ?? "",
654667
historyLimit,
655668
groupHistories,
669+
groupHistoryContextMode,
656670
route,
657671
skillFilter,
658672
sendTyping,

0 commit comments

Comments
 (0)