From 8ef26d323f2fa301c86b88db5ccc247ca3619e9f Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Wed, 15 Apr 2026 12:16:49 +0000 Subject: [PATCH] fix(v2/telegram): await pairing interceptor work to serialize DB commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Telegram pairing interceptor fired DB writes (createMessagingGroup, upsertUser, grantRole) and the pairing-success confirmation inside an unawaited `void (async () => {...})()`. Recent changes (0d3326a user privilege model, c483860 pairing confirmation) widened the work done inside this closure to include an extra two DB writes and a Telegram API round-trip, making the race between match and commit reproducible — a paired message could appear "lost" until a second send. Change onInbound to optionally return a Promise, await it in the chat-sdk-bridge dispatch callbacks, and make the pairing interceptor async so its DB writes + confirmation send complete before the handler resolves. Note: the upstream @chat-adapter/telegram SDK itself does not await processUpdate in its polling loop, so the adapter's getUpdates offset still advances before our handler resolves. A true restart-safe fix needs a corresponding change in chat-adapter. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/channels/adapter.ts | 6 +++++- src/channels/chat-sdk-bridge.ts | 6 +++--- src/channels/telegram.ts | 8 ++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/channels/adapter.ts b/src/channels/adapter.ts index de957004a..d8859c9ff 100644 --- a/src/channels/adapter.ts +++ b/src/channels/adapter.ts @@ -20,7 +20,11 @@ export interface ChannelSetup { conversations: ConversationConfig[]; /** Called when an inbound message arrives from the platform. */ - onInbound(platformId: string, threadId: string | null, message: InboundMessage): void; + onInbound( + platformId: string, + threadId: string | null, + message: InboundMessage, + ): void | Promise; /** Called when the adapter discovers metadata about a conversation. */ onMetadata(platformId: string, name?: string, isGroup?: boolean): void; diff --git a/src/channels/chat-sdk-bridge.ts b/src/channels/chat-sdk-bridge.ts index 727a94241..ed3a78175 100644 --- a/src/channels/chat-sdk-bridge.ts +++ b/src/channels/chat-sdk-bridge.ts @@ -165,20 +165,20 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter // Subscribed threads — forward all messages chat.onSubscribedMessage(async (thread, message) => { const channelId = adapter.channelIdFromThreadId(thread.id); - setupConfig.onInbound(channelId, thread.id, await messageToInbound(message)); + await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message)); }); // @mention in unsubscribed thread — forward + subscribe chat.onNewMention(async (thread, message) => { const channelId = adapter.channelIdFromThreadId(thread.id); - setupConfig.onInbound(channelId, thread.id, await messageToInbound(message)); + await setupConfig.onInbound(channelId, thread.id, await messageToInbound(message)); await thread.subscribe(); }); // DMs — always forward + subscribe chat.onDirectMessage(async (thread, message) => { const channelId = adapter.channelIdFromThreadId(thread.id); - setupConfig.onInbound(channelId, null, await messageToInbound(message)); + await setupConfig.onInbound(channelId, null, await messageToInbound(message)); await thread.subscribe(); }); diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index 72bb5c0c3..a974ca8b8 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -115,8 +115,8 @@ function createPairingInterceptor( hostOnInbound: ChannelSetup['onInbound'], token: string, ): ChannelSetup['onInbound'] { - return (platformId, threadId, message) => { - void (async () => { + return async (platformId, threadId, message) => { + try { const botUsername = await botUsernamePromise; if (!botUsername) { hostOnInbound(platformId, threadId, message); @@ -187,11 +187,11 @@ function createPairingInterceptor( }); await sendPairingConfirmation(token, platformId); - })().catch((err) => { + } catch (err) { log.error('Telegram pairing interceptor error', { err }); // Fail open: pass through so a pairing bug doesn't break normal traffic. hostOnInbound(platformId, threadId, message); - }); + } }; }