Files
openclaw/extensions/telegram/src/error-policy.ts
Chinar Amrutkar 74b9f22a42 fix: add Telegram error suppression controls (#51914) (thanks @chinar-amrutkar)
* feat(telegram): add error policy for suppressing repetitive error messages

Introduces per-account error policy configuration that can suppress
repetitive error messages (e.g., 429 rate limit, ECONNRESET) to
prevent noisy error floods in Telegram channels.

Closes #34498

* fix(telegram): track error cooldown per message

* fix(telegram): prune expired error cooldowns

* fix: add Telegram error suppression controls (#51914) (thanks @chinar-amrutkar)

---------

Co-authored-by: chinar-amrutkar <chinar-amrutkar@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-04-01 17:52:28 +05:30

108 lines
2.9 KiB
TypeScript

import type {
TelegramAccountConfig,
TelegramDirectConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "openclaw/plugin-sdk/config-runtime";
export type TelegramErrorPolicy = "always" | "once" | "silent";
type TelegramErrorConfig =
| TelegramAccountConfig
| TelegramDirectConfig
| TelegramGroupConfig
| TelegramTopicConfig;
const errorCooldownStore = new Map<string, Map<string, number>>();
const DEFAULT_ERROR_COOLDOWN_MS = 14400000;
function pruneExpiredCooldowns(messageStore: Map<string, number>, now: number) {
for (const [message, expiresAt] of messageStore) {
if (expiresAt <= now) {
messageStore.delete(message);
}
}
}
export function resolveTelegramErrorPolicy(params: {
accountConfig?: TelegramAccountConfig;
groupConfig?: TelegramDirectConfig | TelegramGroupConfig;
topicConfig?: TelegramTopicConfig;
}): {
policy: TelegramErrorPolicy;
cooldownMs: number;
} {
const configs: Array<TelegramErrorConfig | undefined> = [
params.accountConfig,
params.groupConfig,
params.topicConfig,
];
let policy: TelegramErrorPolicy = "always";
let cooldownMs = DEFAULT_ERROR_COOLDOWN_MS;
for (const config of configs) {
if (config?.errorPolicy) {
policy = config.errorPolicy;
}
if (typeof config?.errorCooldownMs === "number") {
cooldownMs = config.errorCooldownMs;
}
}
return { policy, cooldownMs };
}
export function buildTelegramErrorScopeKey(params: {
accountId: string;
chatId: string | number;
threadId?: string | number | null;
}): string {
const threadId = params.threadId == null ? "main" : String(params.threadId);
return `${params.accountId}:${String(params.chatId)}:${threadId}`;
}
export function shouldSuppressTelegramError(params: {
scopeKey: string;
cooldownMs: number;
errorMessage?: string;
}): boolean {
const { scopeKey, cooldownMs, errorMessage } = params;
const now = Date.now();
const messageKey = errorMessage ?? "";
const scopeStore = errorCooldownStore.get(scopeKey);
if (scopeStore) {
pruneExpiredCooldowns(scopeStore, now);
if (scopeStore.size === 0) {
errorCooldownStore.delete(scopeKey);
}
}
if (errorCooldownStore.size > 100) {
for (const [scope, messageStore] of errorCooldownStore) {
pruneExpiredCooldowns(messageStore, now);
if (messageStore.size === 0) {
errorCooldownStore.delete(scope);
}
}
}
const expiresAt = scopeStore?.get(messageKey);
if (typeof expiresAt === "number" && expiresAt > now) {
return true;
}
const nextScopeStore = scopeStore ?? new Map<string, number>();
nextScopeStore.set(messageKey, now + cooldownMs);
errorCooldownStore.set(scopeKey, nextScopeStore);
return false;
}
export function isSilentErrorPolicy(policy: TelegramErrorPolicy): boolean {
return policy === "silent";
}
export function resetTelegramErrorPolicyStoreForTest() {
errorCooldownStore.clear();
}