From e607ad4ab0dff6a94ede4b085ba00ede19b3fb3f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 12:47:00 +0100 Subject: [PATCH] fix(telegram): route bound group native commands --- CHANGELOG.md | 1 + ...ot-native-commands.fixture-test-support.ts | 24 +++++++++++ .../bot-native-commands.session-meta.test.ts | 35 ++++++++++++++++ extensions/telegram/src/conversation-route.ts | 42 +++++++++---------- 4 files changed, 79 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9da92d3e4dd..696b50df1b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Telegram: honor runtime conversation bindings for native slash commands in bound top-level groups, so commands like `/status@bot` route to the active non-`main` session instead of falling back to the default route. Fixes #75405; supersedes #75558. Thanks @ziptbm and @yfge. - Models CLI: restore `openclaw models list --provider ` catalog and registry fallback rows for unconfigured providers, so provider-specific verification commands no longer report "No models found." Fixes #75517; supersedes #75615. Thanks @lotsoftick and @koshaji. - Gateway/macOS: write LaunchAgent services with a canonical system PATH and stop preserving old plist PATH entries, so Volta, asdf, fnm, and pnpm shell paths no longer affect gateway child-process Node resolution. Fixes #75233; supersedes #75246. Thanks @nphyde2. - Slack/hooks: preserve bot alert attachment text in message-received hook content when command text is blank. Fixes #76035; refs #76036. Thanks @amsminn. diff --git a/extensions/telegram/src/bot-native-commands.fixture-test-support.ts b/extensions/telegram/src/bot-native-commands.fixture-test-support.ts index 7a67628e259..fb10ec7f79d 100644 --- a/extensions/telegram/src/bot-native-commands.fixture-test-support.ts +++ b/extensions/telegram/src/bot-native-commands.fixture-test-support.ts @@ -82,6 +82,30 @@ export function createTelegramPrivateCommandContext(params?: { }; } +export function createTelegramGroupCommandContext(params?: { + match?: string; + messageId?: number; + date?: number; + chatId?: number; + title?: string; + userId?: number; + username?: string; +}) { + return { + match: params?.match ?? "", + message: { + message_id: params?.messageId ?? 2, + date: params?.date ?? Math.floor(Date.now() / 1000), + chat: { + id: params?.chatId ?? -1001234567890, + type: "supergroup" as const, + title: params?.title ?? "OpenClaw", + }, + from: { id: params?.userId ?? 200, username: params?.username ?? "bob" }, + }, + }; +} + export function createTelegramTopicCommandContext(params?: { match?: string; messageId?: number; diff --git a/extensions/telegram/src/bot-native-commands.session-meta.test.ts b/extensions/telegram/src/bot-native-commands.session-meta.test.ts index 44d7c14426d..1f211a56378 100644 --- a/extensions/telegram/src/bot-native-commands.session-meta.test.ts +++ b/extensions/telegram/src/bot-native-commands.session-meta.test.ts @@ -4,6 +4,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { TelegramNativeCommandDeps } from "./bot-native-command-deps.runtime.js"; import { createDeferred, + createTelegramGroupCommandContext, createNativeCommandTestParams, createTelegramPrivateCommandContext, createTelegramTopicCommandContext, @@ -873,6 +874,40 @@ describe("registerTelegramNativeCommands — session metadata", () => { ); }); + it("routes Telegram native commands through bound top-level group sessions", async () => { + sessionBindingMocks.resolveByConversation.mockReturnValue({ + bindingId: "default:-1001234567890", + targetSessionKey: "agent:codex-acp:session-group", + }); + + const { handler } = registerAndResolveStatusHandler({ + cfg: {}, + allowFrom: ["200"], + groupAllowFrom: ["200"], + }); + await handler(createTelegramGroupCommandContext()); + + expect(sessionBindingMocks.resolveByConversation).toHaveBeenCalledWith({ + channel: "telegram", + accountId: "default", + conversationId: "-1001234567890", + }); + const dispatchCall = ( + replyMocks.dispatchReplyWithBufferedBlockDispatcher.mock.calls as unknown as Array< + [{ ctx?: { CommandTargetSessionKey?: string; OriginatingTo?: string } }] + > + )[0]?.[0]; + expect(dispatchCall?.ctx?.CommandTargetSessionKey).toBe("agent:codex-acp:session-group"); + expect(dispatchCall?.ctx?.OriginatingTo).toBe("telegram:-1001234567890"); + const sessionMetaCall = ( + sessionMocks.recordSessionMetaFromInbound.mock.calls as unknown as Array< + [{ sessionKey?: string }] + > + )[0]?.[0]; + expect(sessionMetaCall?.sessionKey).toBe("agent:codex-acp:session-group"); + expect(sessionBindingMocks.touch).toHaveBeenCalledWith("default:-1001234567890", undefined); + }); + it.each(["new", "reset"] as const)( "preserves the topic-qualified origin target for native /%s in forum topics", async (commandName) => { diff --git a/extensions/telegram/src/conversation-route.ts b/extensions/telegram/src/conversation-route.ts index 0d6c25b7434..757a6130f27 100644 --- a/extensions/telegram/src/conversation-route.ts +++ b/extensions/telegram/src/conversation-route.ts @@ -105,31 +105,27 @@ export function resolveTelegramConversationRoute(params: { let configuredBindingSessionKey = configuredRoute.boundSessionKey ?? ""; route = configuredRoute.route; - const threadBindingConversationId = + const runtimeBindingConversationId = params.replyThreadId != null ? `${params.chatId}:topic:${params.replyThreadId}` - : !params.isGroup - ? String(params.chatId) - : undefined; - if (threadBindingConversationId) { - const runtimeRoute = resolveRuntimeConversationBindingRoute({ - route, - conversation: { - channel: "telegram", - accountId: params.accountId, - conversationId: threadBindingConversationId, - }, - }); - route = runtimeRoute.route; - if (runtimeRoute.bindingRecord) { - configuredBinding = null; - configuredBindingSessionKey = ""; - logVerbose( - runtimeRoute.boundSessionKey - ? `telegram: routed via bound conversation ${threadBindingConversationId} -> ${runtimeRoute.boundSessionKey}` - : `telegram: plugin-bound conversation ${threadBindingConversationId}`, - ); - } + : String(params.chatId); + const runtimeRoute = resolveRuntimeConversationBindingRoute({ + route, + conversation: { + channel: "telegram", + accountId: params.accountId, + conversationId: runtimeBindingConversationId, + }, + }); + route = runtimeRoute.route; + if (runtimeRoute.bindingRecord) { + configuredBinding = null; + configuredBindingSessionKey = ""; + logVerbose( + runtimeRoute.boundSessionKey + ? `telegram: routed via bound conversation ${runtimeBindingConversationId} -> ${runtimeRoute.boundSessionKey}` + : `telegram: plugin-bound conversation ${runtimeBindingConversationId}`, + ); } return {