fix: keep Telegram command sync process-local (#66730) (thanks @nightq)

* fix: use process-scoped cache for Telegram command sync to fix missing menu after restart

Fixes openclaw#66714, openclaw#66682

Root cause: The command hash cache was persisted to disk across gateway
restarts. When the hash matched (commands unchanged), setMyCommands was
skipped entirely. But Telegram bot commands can be cleared by external
factors, so the cached state becomes stale after restart.

Fix: Replace file-based hash cache with a process-scoped Map. This preserves
the rapid-restart rate-limit protection within a single process, but ensures
commands are always re-registered after a gateway restart.

* fix(telegram): drop stale async command cache calls

* fix: keep Telegram command sync process-local (#66730) (thanks @nightq)

---------

Co-authored-by: nightq <zengwei@nightq.cn>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
Mr.NightQ
2026-04-15 10:32:23 +08:00
committed by GitHub
parent 6f1d321aab
commit b1d03b4057
2 changed files with 16 additions and 39 deletions

View File

@@ -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=<ts>` 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

View File

@@ -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<string, string>();
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<string | null> {
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<void> {
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)) {