From abec3ed6453810fb355c16c12b0baa4097c29a0e Mon Sep 17 00:00:00 2001 From: nimbleenigma <129692390+nimbleenigma@users.noreply.github.com> Date: Wed, 25 Mar 2026 01:48:54 -0400 Subject: [PATCH] fix: keep Telegram native commands on runtime snapshot (#53179) (thanks @nimbleenigma) * fix(telegram): use runtime snapshot for native commands * fix: keep Telegram native commands on runtime snapshot (#53179) (thanks @nimbleenigma) --------- Co-authored-by: Ayaan Zaidi --- CHANGELOG.md | 1 + .../telegram/src/bot-native-commands.ts | 10 ++-- extensions/telegram/src/bot.test.ts | 53 +++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df49430a161..ebb09c41f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai - Plugins: enforce terminal hook decision semantics for tool/message guards (#54241) Thanks @joshavant. - Telegram/outbound errors: preserve actionable 403 membership/block/kick details and treat `bot not a member` as a permanent delivery failure so Telegram sends stop retrying doomed chats. (#53635) Thanks @w-sss. - Telegram/pairing: render pairing codes and approval commands as Telegram-only code blocks while keeping shared pairing replies plain text for other channels. (#52784) Thanks @sumukhj1219. +- Telegram/native commands: run native slash-command execution against the resolved runtime snapshot so DM commands still reply when fresh config reads surface unresolved SecretRefs. (#53179) Thanks @nimbleenigma. ## 2026.3.23 diff --git a/extensions/telegram/src/bot-native-commands.ts b/extensions/telegram/src/bot-native-commands.ts index f031dea40e7..a40faaf9a06 100644 --- a/extensions/telegram/src/bot-native-commands.ts +++ b/extensions/telegram/src/bot-native-commands.ts @@ -16,6 +16,7 @@ import { } from "openclaw/plugin-sdk/command-auth"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ChannelGroupPolicy } from "openclaw/plugin-sdk/config-runtime"; +import { getRuntimeConfigSnapshot } from "openclaw/plugin-sdk/config-runtime"; import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/config-runtime"; import { normalizeTelegramCommandName, @@ -645,6 +646,7 @@ export const registerTelegramNativeCommands = ({ } const { threadSpec, route, mediaLocalRoots, tableMode, chunkMode } = runtimeContext; const threadParams = buildTelegramThreadParams(threadSpec) ?? {}; + const executionCfg = getRuntimeConfigSnapshot() ?? cfg; const commandDefinition = findCommandByNativeName(command.name, "telegram"); const rawText = ctx.match?.trim() ?? ""; @@ -774,7 +776,7 @@ export const registerTelegramNativeCommands = ({ }); await recordInboundSessionMetaSafe({ - cfg: runtimeCfg, + cfg: executionCfg, agentId: route.agentId, sessionKey: ctxPayload.SessionKey ?? route.sessionKey, ctx: ctxPayload, @@ -794,7 +796,7 @@ export const registerTelegramNativeCommands = ({ }; const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({ - cfg: runtimeCfg, + cfg: executionCfg, agentId: route.agentId, channel: "telegram", accountId: route.accountId, @@ -802,13 +804,13 @@ export const registerTelegramNativeCommands = ({ await telegramDeps.dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, - cfg: runtimeCfg, + cfg: executionCfg, dispatcherOptions: { ...replyPipeline, deliver: async (payload, _info) => { if ( shouldSuppressLocalTelegramExecApprovalPrompt({ - cfg: runtimeCfg, + cfg: executionCfg, accountId: route.accountId, payload, }) diff --git a/extensions/telegram/src/bot.test.ts b/extensions/telegram/src/bot.test.ts index 999c67b118d..306bc397449 100644 --- a/extensions/telegram/src/bot.test.ts +++ b/extensions/telegram/src/bot.test.ts @@ -1589,6 +1589,59 @@ describe("createTelegramBot", () => { ).toBe(false); }); + it("keeps native DM commands on the startup-resolved config when fresh reads contain SecretRefs", async () => { + onSpy.mockClear(); + sendMessageSpy.mockClear(); + commandSpy.mockClear(); + replySpy.mockClear(); + replySpy.mockResolvedValue({ text: "response" }); + + const startupConfig = { + commands: { native: true }, + channels: { + telegram: { + dmPolicy: "pairing" as const, + botToken: "resolved-token", + }, + }, + }; + + createTelegramBot({ + token: "tok", + config: startupConfig, + }); + loadConfig.mockReturnValue({ + commands: { native: true }, + channels: { + telegram: { + dmPolicy: "pairing", + botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" }, + }, + }, + }); + readChannelAllowFromStore.mockResolvedValueOnce(["12345"]); + + const handler = commandSpy.mock.calls.find((call) => call[0] === "status")?.[1] as + | ((ctx: Record) => Promise) + | undefined; + if (!handler) { + throw new Error("status command handler missing"); + } + + await handler({ + message: { + chat: { id: 12345, type: "private" }, + from: { id: 12345, username: "testuser" }, + text: "/status", + date: 1736380800, + message_id: 42, + }, + match: "", + }); + + expect(replySpy).toHaveBeenCalledTimes(1); + }); + it("blocks native DM commands for unpaired users", async () => { onSpy.mockClear(); sendMessageSpy.mockClear();