From c483860cd98af3ea4001b9cfe2bebbc1e87a5885 Mon Sep 17 00:00:00 2001 From: Koshkoshinsk <163998153+Koshkoshinsk@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:59:17 +0000 Subject: [PATCH] feat(v2/telegram): send pairing-success confirmation to paired chat After a Telegram pair-code is successfully consumed, send a one-shot "Pairing success! I'm spinning up the agent now, you'll get a message from them shortly." reply to the same chat so the user knows the code was accepted before the agent's own welcome DM arrives. Best-effort: any sendMessage failure is logged but not rethrown, so a Telegram outage can't undo a successful pairing or trigger the interceptor's fail-open path. Also includes a no-op prettier reformat in chat-sdk-bridge.ts that the husky hook missed in the previous commit. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/channels/chat-sdk-bridge.ts | 4 +--- src/channels/telegram.ts | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/channels/chat-sdk-bridge.ts b/src/channels/chat-sdk-bridge.ts index c6853a9..727a942 100644 --- a/src/channels/chat-sdk-bridge.ts +++ b/src/channels/chat-sdk-bridge.ts @@ -124,9 +124,7 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter // Project chat-sdk's nested author into the flat sender fields the router // expects (see src/router.ts extractAndUpsertUser). Native adapters already // populate these directly; this brings chat-sdk adapters in line. - const author = serialized.author as - | { userId?: string; fullName?: string; userName?: string } - | undefined; + const author = serialized.author as { userId?: string; fullName?: string; userName?: string } | undefined; if (author) { const name = author.fullName ?? author.userName; serialized.senderId = author.userId; diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index 0a95f9a..72bb5c0 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -85,9 +85,35 @@ function readInboundFields(message: InboundMessage): InboundFields { * the user to owner if the instance has no owner yet, and short-circuits. * On miss: forwards to the host. */ +/** + * Send a one-shot confirmation back to the paired chat. Best-effort — failures + * are logged but never propagated, so a Telegram outage can't undo a successful + * pairing or trigger the interceptor's fail-open path. + */ +async function sendPairingConfirmation(token: string, platformId: string): Promise { + const chatId = platformId.split(':').slice(1).join(':'); + if (!chatId) return; + try { + const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + chat_id: chatId, + text: "Pairing success! I'm spinning up the agent now, you'll get a message from them shortly.", + }), + }); + if (!res.ok) { + log.warn('Telegram pairing confirmation non-OK', { status: res.status }); + } + } catch (err) { + log.warn('Telegram pairing confirmation failed', { err }); + } +} + function createPairingInterceptor( botUsernamePromise: Promise, hostOnInbound: ChannelSetup['onInbound'], + token: string, ): ChannelSetup['onInbound'] { return (platformId, threadId, message) => { void (async () => { @@ -159,6 +185,8 @@ function createPairingInterceptor( promotedToOwner, intent: consumed.intent, }); + + await sendPairingConfirmation(token, platformId); })().catch((err) => { log.error('Telegram pairing interceptor error', { err }); // Fail open: pass through so a pairing bug doesn't break normal traffic. @@ -191,7 +219,7 @@ registerChannelAdapter('telegram', { async setup(hostConfig: ChannelSetup) { const intercepted: ChannelSetup = { ...hostConfig, - onInbound: createPairingInterceptor(botUsernamePromise, hostConfig.onInbound), + onInbound: createPairingInterceptor(botUsernamePromise, hostConfig.onInbound, token), }; return withRetry(() => bridge.setup(intercepted), 'bridge.setup'); },