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 type { Bot } from "grammy";
|
||||||
|
|
||||||
import { resolveAckReaction } from "../agents/identity.js";
|
import { resolveAckReaction } from "../agents/identity.js";
|
||||||
|
import { getPinnedContextString } from "./pinned-context.js";
|
||||||
import {
|
import {
|
||||||
findModelInCatalog,
|
findModelInCatalog,
|
||||||
loadModelCatalog,
|
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 skillFilter = firstDefined(topicConfig?.skills, groupConfig?.skills);
|
||||||
const systemPromptParts = [
|
const systemPromptParts = [
|
||||||
groupConfig?.systemPrompt?.trim() || null,
|
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