feat(telegram): inject pinned messages into group context
- Add pinned-context.ts with fetchPinnedMessage() and cache logic
- Modify bot-message-context.ts to prepend pinned content to combinedBody
- 5-minute cache TTL to avoid API spam
- Format: [📌 Pinned by X (date)] content [/Pinned]
Pinned messages in Telegram groups/topics now persist in agent context
even after conversation history compacts.
This commit is contained in:
parent
26f51b8307
commit
b05c075c06
@ -1,6 +1,7 @@
|
||||
import type { Bot } from "grammy";
|
||||
|
||||
import { resolveAckReaction } from "../agents/identity.js";
|
||||
import { getPinnedContextString } from "./pinned-context.js";
|
||||
import {
|
||||
findModelInCatalog,
|
||||
loadModelCatalog,
|
||||
@ -541,6 +542,18 @@ export const buildTelegramMessageContext = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Inject pinned message context for groups
|
||||
if (isGroup) {
|
||||
try {
|
||||
const pinnedContext = await getPinnedContextString(bot, chatId, resolvedThreadId);
|
||||
if (pinnedContext) {
|
||||
combinedBody = `${pinnedContext}\n\n${combinedBody}`;
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(`[telegram] Failed to fetch pinned context: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const skillFilter = firstDefined(topicConfig?.skills, groupConfig?.skills);
|
||||
const systemPromptParts = [
|
||||
groupConfig?.systemPrompt?.trim() || null,
|
||||
|
||||
101
src/telegram/pinned-context.ts
Normal file
101
src/telegram/pinned-context.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import type { Bot } from "grammy";
|
||||
import { logVerbose } from "../globals.js";
|
||||
|
||||
type PinnedMessageCache = {
|
||||
text: string;
|
||||
from?: string;
|
||||
date?: number;
|
||||
fetchedAt: number;
|
||||
};
|
||||
|
||||
// Cache pinned messages per chat/topic
|
||||
// Key format: "chatId" or "chatId:topicId"
|
||||
const pinnedCache = new Map<string, PinnedMessageCache | null>();
|
||||
|
||||
// Cache TTL: 5 minutes
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
function buildCacheKey(chatId: string | number, topicId?: number): string {
|
||||
const base = String(chatId);
|
||||
return topicId != null && topicId !== 1 ? `${base}:${topicId}` : base;
|
||||
}
|
||||
|
||||
export function invalidatePinnedCache(chatId: string | number, topicId?: number): void {
|
||||
const key = buildCacheKey(chatId, topicId);
|
||||
pinnedCache.delete(key);
|
||||
logVerbose(`[pinned-context] Invalidated cache for ${key}`);
|
||||
}
|
||||
|
||||
export async function fetchPinnedMessage(
|
||||
bot: Bot,
|
||||
chatId: string | number,
|
||||
topicId?: number,
|
||||
): Promise<PinnedMessageCache | null> {
|
||||
const key = buildCacheKey(chatId, topicId);
|
||||
|
||||
// Check cache
|
||||
const cached = pinnedCache.get(key);
|
||||
if (cached !== undefined) {
|
||||
const age = Date.now() - (cached?.fetchedAt ?? 0);
|
||||
if (age < CACHE_TTL_MS) {
|
||||
logVerbose(`[pinned-context] Cache hit for ${key}`);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const chat = await bot.api.getChat(chatId);
|
||||
const pinned = chat.pinned_message;
|
||||
|
||||
if (!pinned) {
|
||||
pinnedCache.set(key, null);
|
||||
logVerbose(`[pinned-context] No pinned message for ${key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract text content
|
||||
const text = pinned.text ?? pinned.caption ?? "";
|
||||
if (!text.trim()) {
|
||||
pinnedCache.set(key, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const entry: PinnedMessageCache = {
|
||||
text: text.trim(),
|
||||
from: pinned.from?.first_name ?? pinned.from?.username,
|
||||
date: pinned.date,
|
||||
fetchedAt: Date.now(),
|
||||
};
|
||||
|
||||
pinnedCache.set(key, entry);
|
||||
logVerbose(`[pinned-context] Fetched pinned message for ${key}: "${text.slice(0, 50)}..."`);
|
||||
return entry;
|
||||
} catch (err) {
|
||||
logVerbose(`[pinned-context] Failed to fetch pinned for ${key}: ${String(err)}`);
|
||||
// Cache the failure briefly to avoid hammering the API
|
||||
pinnedCache.set(key, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPinnedContext(pinned: PinnedMessageCache): string {
|
||||
const datePart = pinned.date
|
||||
? ` (pinned ${new Date(pinned.date * 1000).toLocaleDateString()})`
|
||||
: "";
|
||||
const fromPart = pinned.from ? ` by ${pinned.from}` : "";
|
||||
return `[📌 Pinned${fromPart}${datePart}]\n${pinned.text}\n[/Pinned]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted pinned message context for injection.
|
||||
* Returns empty string if no pinned message.
|
||||
*/
|
||||
export async function getPinnedContextString(
|
||||
bot: Bot,
|
||||
chatId: string | number,
|
||||
topicId?: number,
|
||||
): Promise<string> {
|
||||
const pinned = await fetchPinnedMessage(bot, chatId, topicId);
|
||||
if (!pinned) return "";
|
||||
return formatPinnedContext(pinned);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user