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) <noreply@anthropic.com>
This commit is contained in:
Koshkoshinsk
2026-04-15 07:59:17 +00:00
parent ed5dc5ea51
commit c483860cd9
2 changed files with 30 additions and 4 deletions

View File

@@ -124,9 +124,7 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
// Project chat-sdk's nested author into the flat sender fields the router // Project chat-sdk's nested author into the flat sender fields the router
// expects (see src/router.ts extractAndUpsertUser). Native adapters already // expects (see src/router.ts extractAndUpsertUser). Native adapters already
// populate these directly; this brings chat-sdk adapters in line. // populate these directly; this brings chat-sdk adapters in line.
const author = serialized.author as const author = serialized.author as { userId?: string; fullName?: string; userName?: string } | undefined;
| { userId?: string; fullName?: string; userName?: string }
| undefined;
if (author) { if (author) {
const name = author.fullName ?? author.userName; const name = author.fullName ?? author.userName;
serialized.senderId = author.userId; serialized.senderId = author.userId;

View File

@@ -85,9 +85,35 @@ function readInboundFields(message: InboundMessage): InboundFields {
* the user to owner if the instance has no owner yet, and short-circuits. * the user to owner if the instance has no owner yet, and short-circuits.
* On miss: forwards to the host. * 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<void> {
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( function createPairingInterceptor(
botUsernamePromise: Promise<string | null>, botUsernamePromise: Promise<string | null>,
hostOnInbound: ChannelSetup['onInbound'], hostOnInbound: ChannelSetup['onInbound'],
token: string,
): ChannelSetup['onInbound'] { ): ChannelSetup['onInbound'] {
return (platformId, threadId, message) => { return (platformId, threadId, message) => {
void (async () => { void (async () => {
@@ -159,6 +185,8 @@ function createPairingInterceptor(
promotedToOwner, promotedToOwner,
intent: consumed.intent, intent: consumed.intent,
}); });
await sendPairingConfirmation(token, platformId);
})().catch((err) => { })().catch((err) => {
log.error('Telegram pairing interceptor error', { err }); log.error('Telegram pairing interceptor error', { err });
// Fail open: pass through so a pairing bug doesn't break normal traffic. // Fail open: pass through so a pairing bug doesn't break normal traffic.
@@ -191,7 +219,7 @@ registerChannelAdapter('telegram', {
async setup(hostConfig: ChannelSetup) { async setup(hostConfig: ChannelSetup) {
const intercepted: ChannelSetup = { const intercepted: ChannelSetup = {
...hostConfig, ...hostConfig,
onInbound: createPairingInterceptor(botUsernamePromise, hostConfig.onInbound), onInbound: createPairingInterceptor(botUsernamePromise, hostConfig.onInbound, token),
}; };
return withRetry(() => bridge.setup(intercepted), 'bridge.setup'); return withRetry(() => bridge.setup(intercepted), 'bridge.setup');
}, },