diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cf82bf78ea..2c894b54165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai - Telegram/documents: sanitize binary reply context and ZIP-like archive extraction so `.epub` and `.mobi` uploads can no longer leak raw binary into prompt context through reply metadata or archive-to-`text/plain` coercion. (#66877) Thanks @martinfrancois. - Telegram/native commands: restore plugin-registry-backed auto defaults for native commands and native skills so Telegram slash commands keep registering when `commands.native` and `commands.nativeSkills` stay on `auto`. (#66843) Thanks @kashevk0. - fix(bluebubbles): replay missed webhook messages after gateway restart via a persistent per-account cursor and `/api/v1/message/query?after=` pass, so messages delivered while the gateway was down no longer disappear. Uses the existing `processMessage` path and is deduped by #66816's inbound GUID cache. (#66857, #66721) Thanks @omarshahine. +- Telegram/native commands: keep Telegram command-sync cache process-local so gateway restarts re-register the menu instead of trusting stale on-disk sync state after Telegram cleared commands out-of-band. (#66730) Thanks @nightq. ## 2026.4.14 diff --git a/extensions/telegram/src/bot-native-command-menu.ts b/extensions/telegram/src/bot-native-command-menu.ts index 82f1fe4fab1..e9f914d2435 100644 --- a/extensions/telegram/src/bot-native-command-menu.ts +++ b/extensions/telegram/src/bot-native-command-menu.ts @@ -1,11 +1,7 @@ import { createHash } from "node:crypto"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; import type { Bot } from "grammy"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; -import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; import { normalizeOptionalString, readStringValue } from "openclaw/plugin-sdk/text-runtime"; import { withTelegramApiErrorLogging } from "./api-logging.js"; import { normalizeTelegramCommandName, TELEGRAM_COMMAND_NAME_PATTERN } from "./command-config.js"; @@ -215,45 +211,25 @@ export function hashCommandList(commands: TelegramMenuCommand[]): string { return createHash("sha256").update(JSON.stringify(sorted)).digest("hex").slice(0, 16); } -function hashBotIdentity(botIdentity?: string): string { - const normalized = botIdentity?.trim(); - if (!normalized) { - return "no-bot"; - } - return createHash("sha256").update(normalized).digest("hex").slice(0, 16); +// Keep the sync cache process-local so restarts always re-register commands. +const syncedCommandHashes = new Map(); + +function getCommandHashKey(accountId?: string, botIdentity?: string): string { + return `${accountId ?? "default"}:${botIdentity ?? ""}`; } -function resolveCommandHashPath(accountId?: string, botIdentity?: string): string { - const stateDir = resolveStateDir(process.env, os.homedir); - const normalizedAccount = accountId?.trim().replace(/[^a-z0-9._-]+/gi, "_") || "default"; - const botHash = hashBotIdentity(botIdentity); - return path.join(stateDir, "telegram", `command-hash-${normalizedAccount}-${botHash}.txt`); +function readCachedCommandHash(accountId?: string, botIdentity?: string): string | null { + const key = getCommandHashKey(accountId, botIdentity); + return syncedCommandHashes.get(key) ?? null; } -async function readCachedCommandHash( - accountId?: string, - botIdentity?: string, -): Promise { - try { - return (await fs.readFile(resolveCommandHashPath(accountId, botIdentity), "utf-8")).trim(); - } catch { - return null; - } -} - -async function writeCachedCommandHash( +function writeCachedCommandHash( accountId: string | undefined, botIdentity: string | undefined, hash: string, -): Promise { - const filePath = resolveCommandHashPath(accountId, botIdentity); - try { - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, hash, "utf-8"); - } catch { - // Best-effort: failing to cache the hash just means the next restart - // will sync commands again, which is the pre-fix behaviour. - } +): void { + const key = getCommandHashKey(accountId, botIdentity); + syncedCommandHashes.set(key, hash); } export function syncTelegramMenuCommands(params: { @@ -270,7 +246,7 @@ export function syncTelegramMenuCommands(params: { // is restarted several times in quick succession. // See: openclaw/openclaw#32017 const currentHash = hashCommandList(commandsToRegister); - const cachedHash = await readCachedCommandHash(accountId, botIdentity); + const cachedHash = readCachedCommandHash(accountId, botIdentity); if (cachedHash === currentHash) { logVerbose("telegram: command menu unchanged; skipping sync"); return; @@ -293,7 +269,7 @@ export function syncTelegramMenuCommands(params: { runtime.log?.("telegram: deleteMyCommands failed; skipping empty-menu hash cache write"); return; } - await writeCachedCommandHash(accountId, botIdentity, currentHash); + writeCachedCommandHash(accountId, botIdentity, currentHash); return; } @@ -315,7 +291,7 @@ export function syncTelegramMenuCommands(params: { }), ); } - await writeCachedCommandHash(accountId, botIdentity, currentHash); + writeCachedCommandHash(accountId, botIdentity, currentHash); return; } catch (err) { if (!isBotCommandsTooMuchError(err)) {