Skip to content

M2: Add proactive outbound Telegram messaging primitive#6222

Merged
noanflaherty merged 1 commit into
feature/telegram-msg-gap-closurefrom
swarm/tg-msg-gap/task-2
Feb 21, 2026
Merged

M2: Add proactive outbound Telegram messaging primitive#6222
noanflaherty merged 1 commit into
feature/telegram-msg-gap-closurefrom
swarm/tg-msg-gap/task-2

Conversation

@noanflaherty
Copy link
Copy Markdown
Contributor

@noanflaherty noanflaherty commented Feb 21, 2026

Add a Telegram Bot messaging provider following the existing Slack/Gmail provider pattern. Registers in daemon lifecycle and enables proactive outbound message sending to known Telegram chat IDs via the gateway /deliver/telegram endpoint. Part of #6200.


Open with Devin

@noanflaherty noanflaherty self-assigned this Feb 21, 2026
@noanflaherty noanflaherty merged commit a46ddf9 into feature/telegram-msg-gap-closure Feb 21, 2026
@noanflaherty noanflaherty deleted the swarm/tg-msg-gap/task-2 branch February 21, 2026 21:22
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +50 to +53
export const telegramBotMessagingProvider: MessagingProvider = {
id: 'telegram',
displayName: 'Telegram',
credentialService: 'telegram',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Telegram provider unusable via messaging tools: withProviderToken throws because no access_token exists

All messaging skill tools (send, reply, auth-test, list, search, read, etc.) call withProviderToken(provider, fn) which delegates to withValidToken(provider.credentialService, fn). For the Telegram provider, credentialService is 'telegram', so withValidToken looks for credential:telegram:access_token at assistant/src/security/token-manager.ts:110. However, Telegram stores its credential as credential:telegram:bot_token (see adapter.ts:47). Since no access_token key exists, withValidToken throws a TokenExpiredError at line 116 before the callback is ever invoked.

Root Cause and Impact

The flow is:

  1. messaging-send.ts:20 calls withProviderToken(provider, async (token) => { ... })
  2. shared.ts:44-45 calls withValidToken(provider.credentialService, fn)withValidToken('telegram', fn)
  3. token-manager.ts:110 does getSecureKey('credential:telegram:access_token') → returns undefined
  4. token-manager.ts:116 throws TokenExpiredError('No access token found for "telegram"')

The Telegram adapter's sendMessage (which correctly uses the gateway bearer token, not an OAuth token) is never reached. This makes the Telegram provider completely non-functional through the standard messaging tool interface, despite being registered and appearing as "connected" via isConnected().

Impact: Every attempt to send a Telegram message via the messaging tools will fail with a misleading "No access token found" error, even when the bot token is properly configured.

Prompt for agents
The core issue is that all messaging tools use withProviderToken() which calls withValidToken(service, fn), and withValidToken looks for credential:{service}:access_token. Since Telegram uses bot_token instead of access_token, this always throws.

There are two approaches to fix this:

1. (Recommended) Update assistant/src/config/bundled-skills/messaging/tools/shared.ts to add a Telegram-aware path in withProviderToken. For example, check if the provider has an isConnected() method (indicating it manages its own auth), and if so, skip the withValidToken call and invoke the callback with an empty/dummy token string:

   In shared.ts around line 41-46, change withProviderToken to:
   export async function withProviderToken<T>(
     provider: MessagingProvider,
     fn: (token: string) => Promise<T>,
   ): Promise<T> {
     if (provider.isConnected) {
       return fn('');
     }
     return withValidToken(provider.credentialService, fn);
   }

2. (Alternative) Store the Telegram bot token under credential:telegram:access_token instead of credential:telegram:bot_token so it works with the existing OAuth flow. But this is semantically misleading and would break the isConnected() check in the adapter (assistant/src/messaging/providers/telegram-bot/adapter.ts:47).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +77 to +85
const resp = await telegram.getMe(botToken);
if (!resp.ok || !resp.result) {
return {
connected: false,
user: 'unknown',
platform: 'telegram',
metadata: { error: resp.description ?? 'getMe failed' },
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 testConnection has unreachable error handling because getMe throws on non-ok responses

The testConnection method at line 78 checks if (!resp.ok || !resp.result) to return a graceful { connected: false } response. However, telegram.getMe() in client.ts:35-39 already throws a TelegramApiError when !resp.ok, so the !resp.ok branch in testConnection is unreachable dead code.

Detailed Explanation

In client.ts:29-43, the getMe function does:

if (!resp.ok) {
  throw new TelegramApiError(resp.status, ...);
}
return resp.json();

So when the Telegram API returns an error (e.g., invalid bot token → 401), getMe throws instead of returning a response with ok: false. The adapter's testConnection at line 77-85 expects to handle this case gracefully:

const resp = await telegram.getMe(botToken);
if (!resp.ok || !resp.result) { // ← never reached for HTTP errors
  return { connected: false, ... };
}

Instead, the TelegramApiError propagates up uncaught, and the caller (messaging-auth-test.ts) catches it as a generic error, returning a raw error message instead of the structured ConnectionInfo object.

Impact: When a bot token is invalid, testConnection throws an exception instead of returning a structured { connected: false } response. The !resp.ok check on the Telegram JSON response body (which has its own ok field) is also unreachable since HTTP errors are thrown first.

Suggested change
const resp = await telegram.getMe(botToken);
if (!resp.ok || !resp.result) {
return {
connected: false,
user: 'unknown',
platform: 'telegram',
metadata: { error: resp.description ?? 'getMe failed' },
};
}
try {
const resp = await telegram.getMe(botToken);
if (!resp.ok || !resp.result) {
return {
connected: false,
user: 'unknown',
platform: 'telegram',
metadata: { error: resp.description ?? 'getMe failed' },
};
}
return {
connected: true,
user: resp.result.username ?? resp.result.first_name,
platform: 'telegram',
metadata: {
botId: resp.result.id,
botUsername: resp.result.username,
botName: resp.result.first_name,
},
};
} catch (e) {
return {
connected: false,
user: 'unknown',
platform: 'telegram',
metadata: { error: e instanceof Error ? e.message : 'getMe failed' },
};
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfbbee9c0f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/** Return all registered providers that have stored credentials. */
export function getConnectedProviders(): MessagingProvider[] {
return Array.from(providers.values()).filter((p) => {
if (p.isConnected) return p.isConnected();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Support tokenless providers before marking them connected

By short-circuiting on p.isConnected(), Telegram is now treated as a connected provider even though the messaging tool pipeline still requires withProviderToken() to load credential:${provider.credentialService}:access_token (in assistant/src/config/bundled-skills/messaging/tools/shared.ts). Telegram setup stores credential:telegram:bot_token instead, so messaging_auth_test, messaging_send, and similar calls fail with No access token found for "telegram"; in a Telegram-only setup this makes the new provider unusable.

Useful? React with 👍 / 👎.

@noanflaherty noanflaherty mentioned this pull request Feb 21, 2026
6 tasks
noanflaherty added a commit that referenced this pull request Feb 21, 2026
* fix: remove assistantId dependency from Telegram attachment delivery (#6210)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: add Telegram webhook lifecycle reconciliation (#6211)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: auto-configure gateway routing for single-assistant mode and add rejection visibility (#6212)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: add Telegram Bot messaging provider for proactive outbound sends (#6222)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: harden /deliver/telegram auth and align docs with Telegram capabilities (#6238)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: correct misleading comment in Telegram attachment download path (#6241)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: bound rejection notice cache with periodic eviction (#6242)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: support tokenless providers in withProviderToken and fix testConnection error handling (#6244)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: always reconcile webhook and normalize ingress URL (#6245)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: resolve gateway lint error and credential security allowlist for Telegram adapter (#6257)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: require webhook_secret in Telegram isConnected check (#6259)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: only default routing policy in single-assistant deployments (#6261)

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant